Update: Translations from eints
[openttd-github.git] / src / settingsgen / settingsgen.cpp
blobe0b05fcc0631a51f392de9e107488be2c8f7a730
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file settingsgen.cpp Tool to create computer-readable settings. */
10 #include "../stdafx.h"
11 #include "../string_func.h"
12 #include "../strings_type.h"
13 #include "../misc/getoptdata.h"
14 #include "../ini_type.h"
15 #include "../core/mem_func.hpp"
16 #include "../error_func.h"
18 #include <filesystem>
20 #include "../safeguards.h"
22 /**
23 * Report a fatal error.
24 * @param s Format string.
25 * @note Function does not return.
27 [[noreturn]] void FatalErrorI(const std::string &msg)
29 fmt::print(stderr, "settingsgen: FATAL: {}\n", msg);
30 exit(1);
33 static const size_t OUTPUT_BLOCK_SIZE = 16000; ///< Block size of the buffer in #OutputBuffer.
35 /** Output buffer for a block of data. */
36 class OutputBuffer {
37 public:
38 /** Prepare buffer for use. */
39 void Clear()
41 this->size = 0;
44 /**
45 * Add text to the output buffer.
46 * @param text Text to store.
47 * @param length Length of the text in bytes.
48 * @return Number of bytes actually stored.
50 size_t Add(const char *text, size_t length)
52 size_t store_size = std::min(length, OUTPUT_BLOCK_SIZE - this->size);
53 assert(store_size <= OUTPUT_BLOCK_SIZE);
54 MemCpyT(this->data + this->size, text, store_size);
55 this->size += store_size;
56 return store_size;
59 /**
60 * Dump buffer to the output stream.
61 * @param out_fp Stream to write the \a data to.
63 void Write(FILE *out_fp) const
65 if (fwrite(this->data, 1, this->size, out_fp) != this->size) {
66 FatalError("Cannot write output");
70 /**
71 * Does the block have room for more data?
72 * @return \c true if room is available, else \c false.
74 bool HasRoom() const
76 return this->size < OUTPUT_BLOCK_SIZE;
79 size_t size; ///< Number of bytes stored in \a data.
80 char data[OUTPUT_BLOCK_SIZE]; ///< Stored data.
83 /** Temporarily store output. */
84 class OutputStore {
85 public:
86 OutputStore()
88 this->Clear();
91 /** Clear the temporary storage. */
92 void Clear()
94 this->output_buffer.clear();
97 /**
98 * Add text to the output storage.
99 * @param text Text to store.
100 * @param length Length of the text in bytes, \c 0 means 'length of the string'.
102 void Add(const char *text, size_t length = 0)
104 if (length == 0) length = strlen(text);
106 if (length > 0 && this->BufferHasRoom()) {
107 size_t stored_size = this->output_buffer[this->output_buffer.size() - 1].Add(text, length);
108 length -= stored_size;
109 text += stored_size;
111 while (length > 0) {
112 OutputBuffer &block = this->output_buffer.emplace_back();
113 block.Clear(); // Initialize the new block.
114 size_t stored_size = block.Add(text, length);
115 length -= stored_size;
116 text += stored_size;
121 * Write all stored output to the output stream.
122 * @param out_fp Stream to write the \a data to.
124 void Write(FILE *out_fp) const
126 for (const OutputBuffer &out_data : output_buffer) {
127 out_data.Write(out_fp);
131 private:
133 * Does the buffer have room without adding a new #OutputBuffer block?
134 * @return \c true if room is available, else \c false.
136 bool BufferHasRoom() const
138 size_t num_blocks = this->output_buffer.size();
139 return num_blocks > 0 && this->output_buffer[num_blocks - 1].HasRoom();
142 typedef std::vector<OutputBuffer> OutputBufferVector; ///< Vector type for output buffers.
143 OutputBufferVector output_buffer; ///< Vector of blocks containing the stored output.
147 /** Derived class for loading INI files without going through Fio stuff. */
148 struct SettingsIniFile : IniLoadFile {
150 * Construct a new ini loader.
151 * @param list_group_names A list with group names that should be loaded as lists instead of variables. @see IGT_LIST
152 * @param seq_group_names A list with group names that should be loaded as lists of names. @see IGT_SEQUENCE
154 SettingsIniFile(const IniGroupNameList &list_group_names = {}, const IniGroupNameList &seq_group_names = {}) :
155 IniLoadFile(list_group_names, seq_group_names)
159 std::optional<FileHandle> OpenFile(const std::string &filename, Subdirectory, size_t *size) override
161 /* Open the text file in binary mode to prevent end-of-line translations
162 * done by ftell() and friends, as defined by K&R. */
163 auto in = FileHandle::Open(filename, "rb");
164 if (!in.has_value()) return in;
166 fseek(*in, 0L, SEEK_END);
167 *size = ftell(*in);
168 fseek(*in, 0L, SEEK_SET); // Seek back to the start of the file.
170 return in;
173 void ReportFileError(const char * const pre, const char * const buffer, const char * const post) override
175 FatalError("{}{}{}", pre, buffer, post);
179 OutputStore _stored_output; ///< Temporary storage of the output, until all processing is done.
180 OutputStore _post_amble_output; ///< Similar to _stored_output, but for the post amble.
182 static const char *PREAMBLE_GROUP_NAME = "pre-amble"; ///< Name of the group containing the pre amble.
183 static const char *POSTAMBLE_GROUP_NAME = "post-amble"; ///< Name of the group containing the post amble.
184 static const char *TEMPLATES_GROUP_NAME = "templates"; ///< Name of the group containing the templates.
185 static const char *VALIDATION_GROUP_NAME = "validation"; ///< Name of the group containing the validation statements.
186 static const char *DEFAULTS_GROUP_NAME = "defaults"; ///< Name of the group containing default values for the template variables.
189 * Dump a #IGT_SEQUENCE group into #_stored_output.
190 * @param ifile Loaded INI data.
191 * @param group_name Name of the group to copy.
193 static void DumpGroup(const IniLoadFile &ifile, const char * const group_name)
195 const IniGroup *grp = ifile.GetGroup(group_name);
196 if (grp != nullptr && grp->type == IGT_SEQUENCE) {
197 for (const IniItem &item : grp->items) {
198 if (!item.name.empty()) {
199 _stored_output.Add(item.name.c_str());
200 _stored_output.Add("\n", 1);
207 * Find the value of a template variable.
208 * @param name Name of the item to find.
209 * @param grp Group currently being expanded (searched first).
210 * @param defaults Fallback group to search, \c nullptr skips the search.
211 * @return Text of the item if found, else \c nullptr.
213 static const char *FindItemValue(const char *name, const IniGroup *grp, const IniGroup *defaults)
215 const IniItem *item = grp->GetItem(name);
216 if (item == nullptr && defaults != nullptr) item = defaults->GetItem(name);
217 if (item == nullptr || !item->value.has_value()) return nullptr;
218 return item->value->c_str();
222 * Parse a single entry via a template and output this.
223 * @param item The template to use for the output.
224 * @param grp Group current being used for template rendering.
225 * @param default_grp Default values for items not set in @grp.
226 * @param output Output to use for result.
228 static void DumpLine(const IniItem *item, const IniGroup *grp, const IniGroup *default_grp, OutputStore &output)
230 static const int MAX_VAR_LENGTH = 64;
232 /* Prefix with #if/#ifdef/#ifndef */
233 static const auto pp_lines = {"if", "ifdef", "ifndef"};
234 int count = 0;
235 for (const auto &name : pp_lines) {
236 const char *condition = FindItemValue(name, grp, default_grp);
237 if (condition != nullptr) {
238 output.Add("#", 1);
239 output.Add(name);
240 output.Add(" ", 1);
241 output.Add(condition);
242 output.Add("\n", 1);
243 count++;
247 /* Output text of the template, except template variables of the form '$[_a-z0-9]+' which get replaced by their value. */
248 const char *txt = item->value->c_str();
249 while (*txt != '\0') {
250 if (*txt != '$') {
251 output.Add(txt, 1);
252 txt++;
253 continue;
255 txt++;
256 if (*txt == '$') { // Literal $
257 output.Add(txt, 1);
258 txt++;
259 continue;
262 /* Read variable. */
263 char variable[MAX_VAR_LENGTH];
264 int i = 0;
265 while (i < MAX_VAR_LENGTH - 1) {
266 if (!(txt[i] == '_' || (txt[i] >= 'a' && txt[i] <= 'z') || (txt[i] >= '0' && txt[i] <= '9'))) break;
267 variable[i] = txt[i];
268 i++;
270 variable[i] = '\0';
271 txt += i;
273 if (i > 0) {
274 /* Find the text to output. */
275 const char *valitem = FindItemValue(variable, grp, default_grp);
276 if (valitem != nullptr) output.Add(valitem);
277 } else {
278 output.Add("$", 1);
281 output.Add("\n", 1); // \n after the expanded template.
282 while (count > 0) {
283 output.Add("#endif\n");
284 count--;
289 * Output all non-special sections through the template / template variable expansion system.
290 * @param ifile Loaded INI data.
292 static void DumpSections(const IniLoadFile &ifile)
294 static const auto special_group_names = {PREAMBLE_GROUP_NAME, POSTAMBLE_GROUP_NAME, DEFAULTS_GROUP_NAME, TEMPLATES_GROUP_NAME, VALIDATION_GROUP_NAME};
296 const IniGroup *default_grp = ifile.GetGroup(DEFAULTS_GROUP_NAME);
297 const IniGroup *templates_grp = ifile.GetGroup(TEMPLATES_GROUP_NAME);
298 const IniGroup *validation_grp = ifile.GetGroup(VALIDATION_GROUP_NAME);
299 if (templates_grp == nullptr) return;
301 /* Output every group, using its name as template name. */
302 for (const IniGroup &grp : ifile.groups) {
303 /* Exclude special group names. */
304 if (std::find(std::begin(special_group_names), std::end(special_group_names), grp.name) != std::end(special_group_names)) continue;
306 const IniItem *template_item = templates_grp->GetItem(grp.name); // Find template value.
307 if (template_item == nullptr || !template_item->value.has_value()) {
308 FatalError("Cannot find template {}", grp.name);
310 DumpLine(template_item, &grp, default_grp, _stored_output);
312 if (validation_grp != nullptr) {
313 const IniItem *validation_item = validation_grp->GetItem(grp.name); // Find template value.
314 if (validation_item != nullptr && validation_item->value.has_value()) {
315 DumpLine(validation_item, &grp, default_grp, _post_amble_output);
322 * Append a file to the output stream.
323 * @param fname Filename of file to append.
324 * @param out_fp Output stream to write to.
326 static void AppendFile(const char *fname, FILE *out_fp)
328 if (fname == nullptr) return;
330 auto in_fp = FileHandle::Open(fname, "r");
331 if (!in_fp.has_value()) {
332 FatalError("Cannot open file {} for copying", fname);
335 char buffer[4096];
336 size_t length;
337 do {
338 length = fread(buffer, 1, lengthof(buffer), *in_fp);
339 if (fwrite(buffer, 1, length, out_fp) != length) {
340 FatalError("Cannot copy file");
342 } while (length == lengthof(buffer));
346 * Compare two files for identity.
347 * @param n1 First file.
348 * @param n2 Second file.
349 * @return True if both files are identical.
351 static bool CompareFiles(const char *n1, const char *n2)
353 auto f2 = FileHandle::Open(n2, "rb");
354 if (!f2.has_value()) return false;
356 auto f1 = FileHandle::Open(n1, "rb");
357 if (!f1.has_value()) {
358 FatalError("can't open {}", n1);
361 size_t l1, l2;
362 do {
363 char b1[4096];
364 char b2[4096];
365 l1 = fread(b1, 1, sizeof(b1), *f1);
366 l2 = fread(b2, 1, sizeof(b2), *f2);
368 if (l1 != l2 || memcmp(b1, b2, l1) != 0) {
369 return false;
371 } while (l1 != 0);
373 return true;
376 /** Options of settingsgen. */
377 static const OptionData _opts[] = {
378 { .type = ODF_NO_VALUE, .id = 'h', .shortname = 'h', .longname = "--help" },
379 { .type = ODF_NO_VALUE, .id = 'h', .shortname = '?' },
380 { .type = ODF_HAS_VALUE, .id = 'o', .shortname = 'o', .longname = "--output" },
381 { .type = ODF_HAS_VALUE, .id = 'b', .shortname = 'b', .longname = "--before" },
382 { .type = ODF_HAS_VALUE, .id = 'a', .shortname = 'a', .longname = "--after" },
386 * Process a single INI file.
387 * The file should have a [templates] group, where each item is one template.
388 * Variables in a template have the form '\$[_a-z0-9]+' (a literal '$' followed
389 * by one or more '_', lowercase letters, or lowercase numbers).
391 * After loading, the [pre-amble] group is copied verbatim if it exists.
393 * For every group with a name that matches a template name the template is written.
394 * It starts with a optional \c \#if line if an 'if' item exists in the group. The item
395 * value is used as condition. Similarly, \c \#ifdef and \c \#ifndef lines are also written.
396 * Below the macro processor directives, the value of the template is written
397 * at a line with its variables replaced by item values of the group being written.
398 * If the group has no item for the variable, the [defaults] group is tried as fall back.
399 * Finally, \c \#endif lines are written to match the macro processor lines.
401 * Last but not least, the [post-amble] group is copied verbatim.
403 * @param fname Ini file to process. @return Exit status of the processing.
405 static void ProcessIniFile(const char *fname)
407 static const IniLoadFile::IniGroupNameList seq_groups = {PREAMBLE_GROUP_NAME, POSTAMBLE_GROUP_NAME};
409 SettingsIniFile ini{{}, seq_groups};
410 ini.LoadFromDisk(fname, NO_DIRECTORY);
412 DumpGroup(ini, PREAMBLE_GROUP_NAME);
413 DumpSections(ini);
414 DumpGroup(ini, POSTAMBLE_GROUP_NAME);
418 * And the main program (what else?)
419 * @param argc Number of command-line arguments including the program name itself.
420 * @param argv Vector of the command-line arguments.
422 int CDECL main(int argc, char *argv[])
424 const char *output_file = nullptr;
425 const char *before_file = nullptr;
426 const char *after_file = nullptr;
428 GetOptData mgo(std::span(argv + 1, argc - 1), _opts);
429 for (;;) {
430 int i = mgo.GetOpt();
431 if (i == -1) break;
433 switch (i) {
434 case 'h':
435 fmt::print("settingsgen\n"
436 "Usage: settingsgen [options] ini-file...\n"
437 "with options:\n"
438 " -h, -?, --help Print this help message and exit\n"
439 " -b FILE, --before FILE Copy FILE before all settings\n"
440 " -a FILE, --after FILE Copy FILE after all settings\n"
441 " -o FILE, --output FILE Write output to FILE\n");
442 return 0;
444 case 'o':
445 output_file = mgo.opt;
446 break;
448 case 'a':
449 after_file = mgo.opt;
450 break;
452 case 'b':
453 before_file = mgo.opt;
454 break;
456 case -2:
457 fmt::print(stderr, "Invalid arguments\n");
458 return 1;
462 _stored_output.Clear();
463 _post_amble_output.Clear();
465 for (auto &argument : mgo.arguments) ProcessIniFile(argument);
467 /* Write output. */
468 if (output_file == nullptr) {
469 AppendFile(before_file, stdout);
470 _stored_output.Write(stdout);
471 _post_amble_output.Write(stdout);
472 AppendFile(after_file, stdout);
473 } else {
474 static const char * const tmp_output = "tmp2.xxx";
476 auto fp = FileHandle::Open(tmp_output, "w");
477 if (!fp.has_value()) {
478 FatalError("Cannot open file {}", tmp_output);
480 AppendFile(before_file, *fp);
481 _stored_output.Write(*fp);
482 _post_amble_output.Write(*fp);
483 AppendFile(after_file, *fp);
484 fp.reset();
486 std::error_code error_code;
487 if (CompareFiles(tmp_output, output_file)) {
488 /* Files are equal. tmp2.xxx is not needed. */
489 std::filesystem::remove(tmp_output, error_code);
490 } else {
491 /* Rename tmp2.xxx to output file. */
492 std::filesystem::rename(tmp_output, output_file, error_code);
493 if (error_code) FatalError("rename({}, {}) failed: {}", tmp_output, output_file, error_code.message());
496 return 0;
500 * Simplified FileHandle::Open which ignores OTTD2FS. Required as settingsgen does not include all of the fileio system.
501 * @param filename UTF-8 encoded filename to open.
502 * @param mode Mode to open file.
503 * @return FileHandle, or std::nullopt on failure.
505 std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
507 auto f = fopen(filename.c_str(), mode.c_str());
508 if (f == nullptr) return std::nullopt;
509 return FileHandle(f);