OpenTTD Source 20260218-master-g2123fca5ea
settingsgen.cpp
Go to the documentation of this file.
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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
10#include "../stdafx.h"
12#include "../string_func.h"
13#include "../strings_type.h"
14#include "../misc/getoptdata.h"
15#include "../ini_type.h"
16#include "../error_func.h"
17
18#include <filesystem>
19#include <fstream>
20
21#include "../safeguards.h"
22
23/* Doxygen in error_func.h */
24[[noreturn]] void FatalErrorI(const std::string &str)
25{
26 fmt::print(stderr, "settingsgen: FATAL: {}\n", str);
27 exit(1);
28}
29
30static const size_t OUTPUT_BLOCK_SIZE = 16000;
31
34public:
36 void Clear()
37 {
38 this->size = 0;
39 }
40
46 size_t Add(std::string_view text)
47 {
48 size_t store_size = text.copy(this->data + this->size, OUTPUT_BLOCK_SIZE - this->size);
49 this->size += store_size;
50 return store_size;
51 }
52
57 void Write(FILE *out_fp) const
58 {
59 if (fwrite(this->data, 1, this->size, out_fp) != this->size) {
60 FatalError("Cannot write output");
61 }
62 }
63
68 bool HasRoom() const
69 {
70 return this->size < OUTPUT_BLOCK_SIZE;
71 }
72
73 size_t size;
75};
76
78class OutputStore {
79public:
80 OutputStore()
81 {
82 this->Clear();
83 }
84
86 void Clear()
87 {
88 this->output_buffer.clear();
89 }
90
95 void Add(std::string_view text)
96 {
97 if (!text.empty() && this->BufferHasRoom()) {
98 size_t stored_size = this->output_buffer[this->output_buffer.size() - 1].Add(text);
99 text.remove_prefix(stored_size);
100 }
101 while (!text.empty()) {
102 OutputBuffer &block = this->output_buffer.emplace_back();
103 block.Clear(); // Initialize the new block.
104 size_t stored_size = block.Add(text);
105 text.remove_prefix(stored_size);
106 }
107 }
108
113 void Write(FILE *out_fp) const
114 {
115 for (const OutputBuffer &out_data : output_buffer) {
116 out_data.Write(out_fp);
117 }
118 }
119
120private:
125 bool BufferHasRoom() const
126 {
127 size_t num_blocks = this->output_buffer.size();
128 return num_blocks > 0 && this->output_buffer[num_blocks - 1].HasRoom();
129 }
130
131 typedef std::vector<OutputBuffer> OutputBufferVector;
133};
134
135
143 SettingsIniFile(const IniGroupNameList &list_group_names = {}, const IniGroupNameList &seq_group_names = {}) :
145 {
146 }
147
148 std::optional<FileHandle> OpenFile(std::string_view filename, Subdirectory, size_t *size) override
149 {
150 /* Open the text file in binary mode to prevent end-of-line translations
151 * done by ftell() and friends, as defined by K&R. */
152 auto in = FileHandle::Open(filename, "rb");
153 if (!in.has_value()) return in;
154
155 fseek(*in, 0L, SEEK_END);
156 *size = ftell(*in);
157 fseek(*in, 0L, SEEK_SET); // Seek back to the start of the file.
158
159 return in;
160 }
161
162 void ReportFileError(std::string_view message) override
163 {
164 FatalError("{}", message);
165 }
166};
167
170
171static const std::string_view PREAMBLE_GROUP_NAME = "pre-amble";
172static const std::string_view POSTAMBLE_GROUP_NAME = "post-amble";
173static const std::string_view TEMPLATES_GROUP_NAME = "templates";
174static const std::string_view VALIDATION_GROUP_NAME = "validation";
175static const std::string_view DEFAULTS_GROUP_NAME = "defaults";
176
182static void DumpGroup(const IniLoadFile &ifile, std::string_view group_name)
183{
184 const IniGroup *grp = ifile.GetGroup(group_name);
185 if (grp != nullptr && grp->type == IGT_SEQUENCE) {
186 for (const IniItem &item : grp->items) {
187 if (!item.name.empty()) {
188 _stored_output.Add(item.name);
189 _stored_output.Add("\n");
190 }
191 }
192 }
193}
194
202static std::optional<std::string_view> FindItemValue(std::string_view name, const IniGroup *grp, const IniGroup *defaults)
203{
204 const IniItem *item = grp->GetItem(name);
205 if (item == nullptr && defaults != nullptr) item = defaults->GetItem(name);
206 if (item == nullptr) return std::nullopt;
207 return item->value;
208}
209
217static void DumpLine(const IniItem *item, const IniGroup *grp, const IniGroup *default_grp, OutputStore &output)
218{
219 /* Prefix with #if/#ifdef/#ifndef */
220 static const auto pp_lines = {"if", "ifdef", "ifndef"};
221 int count = 0;
222 for (const auto &name : pp_lines) {
223 auto condition = FindItemValue(name, grp, default_grp);
224 if (condition.has_value()) {
225 output.Add("#");
226 output.Add(name);
227 output.Add(" ");
228 output.Add(*condition);
229 output.Add("\n");
230 count++;
231 }
232 }
233
234 /* Output text of the template, except template variables of the form '$[_a-z0-9]+' which get replaced by their value. */
235 static const std::string_view variable_name_characters = "_abcdefghijklmnopqrstuvwxyz0123456789";
236 StringConsumer consumer{*item->value};
237 while (consumer.AnyBytesLeft()) {
238 char c = consumer.ReadChar();
239 if (c != '$' || consumer.ReadIf("$")) {
240 /* No $ or $$ (literal $). */
241 output.Add(std::string_view{&c, 1});
242 continue;
243 }
244
245 std::string_view variable = consumer.ReadUntilCharNotIn(variable_name_characters);
246 if (!variable.empty()) {
247 /* Find the text to output. */
248 auto valitem = FindItemValue(variable, grp, default_grp);
249 if (valitem.has_value()) output.Add(*valitem);
250 } else {
251 output.Add("$");
252 }
253 }
254 output.Add("\n"); // \n after the expanded template.
255 while (count > 0) {
256 output.Add("#endif\n");
257 count--;
258 }
259}
260
265static void DumpSections(const IniLoadFile &ifile)
266{
268
269 const IniGroup *default_grp = ifile.GetGroup(DEFAULTS_GROUP_NAME);
270 const IniGroup *templates_grp = ifile.GetGroup(TEMPLATES_GROUP_NAME);
271 const IniGroup *validation_grp = ifile.GetGroup(VALIDATION_GROUP_NAME);
272 if (templates_grp == nullptr) return;
273
274 /* Output every group, using its name as template name. */
275 for (const IniGroup &grp : ifile.groups) {
276 /* Exclude special group names. */
277 if (std::ranges::find(special_group_names, grp.name) != std::end(special_group_names)) continue;
278
279 const IniItem *template_item = templates_grp->GetItem(grp.name); // Find template value.
280 if (template_item == nullptr || !template_item->value.has_value()) {
281 FatalError("Cannot find template {}", grp.name);
282 }
283 DumpLine(template_item, &grp, default_grp, _stored_output);
284
285 if (validation_grp != nullptr) {
286 const IniItem *validation_item = validation_grp->GetItem(grp.name); // Find template value.
287 if (validation_item != nullptr && validation_item->value.has_value()) {
288 DumpLine(validation_item, &grp, default_grp, _post_amble_output);
289 }
290 }
291 }
292}
293
299static void AppendFile(std::optional<std::string_view> fname, FILE *out_fp)
300{
301 if (!fname.has_value()) return;
302
303 auto in_fp = FileHandle::Open(*fname, "r");
304 if (!in_fp.has_value()) {
305 FatalError("Cannot open file {} for copying", *fname);
306 }
307
308 char buffer[4096];
309 size_t length;
310 do {
311 length = fread(buffer, 1, lengthof(buffer), *in_fp);
312 if (fwrite(buffer, 1, length, out_fp) != length) {
313 FatalError("Cannot copy file");
314 }
315 } while (length == lengthof(buffer));
316}
317
324static bool CompareFiles(std::filesystem::path path1, std::filesystem::path path2)
325{
326 /* Check for equal size, but ignore the error code for cases when a file does not exist. */
327 std::error_code error_code;
328 if (std::filesystem::file_size(path1, error_code) != std::filesystem::file_size(path2, error_code)) return false;
329
330 std::ifstream stream1(path1, std::ifstream::binary);
331 std::ifstream stream2(path2, std::ifstream::binary);
332
333 return std::equal(std::istreambuf_iterator<char>(stream1.rdbuf()),
334 std::istreambuf_iterator<char>(),
335 std::istreambuf_iterator<char>(stream2.rdbuf()));
336}
337
339static const OptionData _opts[] = {
340 { .type = ODF_NO_VALUE, .id = 'h', .shortname = 'h', .longname = "--help" },
341 { .type = ODF_NO_VALUE, .id = 'h', .shortname = '?' },
342 { .type = ODF_HAS_VALUE, .id = 'o', .shortname = 'o', .longname = "--output" },
343 { .type = ODF_HAS_VALUE, .id = 'b', .shortname = 'b', .longname = "--before" },
344 { .type = ODF_HAS_VALUE, .id = 'a', .shortname = 'a', .longname = "--after" },
345};
346
367static void ProcessIniFile(std::string_view fname)
368{
369 static const IniLoadFile::IniGroupNameList seq_groups = {PREAMBLE_GROUP_NAME, POSTAMBLE_GROUP_NAME};
370
371 SettingsIniFile ini{{}, seq_groups};
372 ini.LoadFromDisk(fname, NO_DIRECTORY);
373
375 DumpSections(ini);
377}
378
384int CDECL main(int argc, char *argv[])
385{
386 std::optional<std::string_view> output_file;
387 std::optional<std::string_view> before_file;
388 std::optional<std::string_view> after_file;
389
390 std::vector<std::string_view> params;
391 for (int i = 1; i < argc; ++i) params.emplace_back(argv[i]);
392 GetOptData mgo(params, _opts);
393 for (;;) {
394 int i = mgo.GetOpt();
395 if (i == -1) break;
396
397 switch (i) {
398 case 'h':
399 fmt::print("settingsgen\n"
400 "Usage: settingsgen [options] ini-file...\n"
401 "with options:\n"
402 " -h, -?, --help Print this help message and exit\n"
403 " -b FILE, --before FILE Copy FILE before all settings\n"
404 " -a FILE, --after FILE Copy FILE after all settings\n"
405 " -o FILE, --output FILE Write output to FILE\n");
406 return 0;
407
408 case 'o':
409 output_file = mgo.opt;
410 break;
411
412 case 'a':
413 after_file = mgo.opt;
414 break;
415
416 case 'b':
417 before_file = mgo.opt;
418 break;
419
420 case -2:
421 fmt::print(stderr, "Invalid arguments\n");
422 return 1;
423 }
424 }
425
426 _stored_output.Clear();
427 _post_amble_output.Clear();
428
429 for (auto &argument : mgo.arguments) ProcessIniFile(argument);
430
431 /* Write output. */
432 if (!output_file.has_value()) {
433 AppendFile(before_file, stdout);
434 _stored_output.Write(stdout);
435 _post_amble_output.Write(stdout);
436 AppendFile(after_file, stdout);
437 } else {
438 static const std::string_view tmp_output = "tmp2.xxx";
439
440 auto fp = FileHandle::Open(tmp_output, "w");
441 if (!fp.has_value()) {
442 FatalError("Cannot open file {}", tmp_output);
443 }
444 AppendFile(before_file, *fp);
445 _stored_output.Write(*fp);
446 _post_amble_output.Write(*fp);
447 AppendFile(after_file, *fp);
448 fp.reset();
449
450 std::error_code error_code;
451 if (CompareFiles(tmp_output, *output_file)) {
452 /* Files are equal. tmp2.xxx is not needed. */
453 std::filesystem::remove(tmp_output, error_code);
454 } else {
455 /* Rename tmp2.xxx to output file. */
456 std::filesystem::rename(tmp_output, *output_file, error_code);
457 if (error_code) FatalError("rename({}, {}) failed: {}", tmp_output, *output_file, error_code.message());
458 }
459 }
460 return 0;
461}
462
463std::optional<FileHandle> FileHandle::Open(const std::string &filename, std::string_view mode)
464{
465 auto f = fopen(filename.c_str(), std::string{mode}.c_str());
466 if (f == nullptr) return std::nullopt;
467 return FileHandle(f);
468}
static std::optional< FileHandle > Open(const std::string &filename, std::string_view mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1173
Output buffer for a block of data.
bool HasRoom() const
Does the block have room for more data?
void Clear()
Prepare buffer for use.
size_t Add(std::string_view text)
Add text to the output buffer.
size_t size
Number of bytes stored in data.
void Write(FILE *out_fp) const
Dump buffer to the output stream.
char data[OUTPUT_BLOCK_SIZE]
Stored data.
Temporarily store output.
std::vector< OutputBuffer > OutputBufferVector
Vector type for output buffers.
OutputBufferVector output_buffer
Vector of blocks containing the stored output.
void Add(std::string_view text)
Add text to the output storage.
bool BufferHasRoom() const
Does the buffer have room without adding a new OutputBuffer block?
void Write(FILE *out_fp) const
Write all stored output to the output stream.
void Clear()
Clear the temporary storage.
Parse data from a string / buffer.
char ReadChar(char def='?')
Read 8-bit character, and advance reader.
bool AnyBytesLeft() const noexcept
Check whether any bytes left to read.
std::string_view ReadUntilCharNotIn(std::string_view chars)
Read 8-bit chars, while they are in 'chars', until they are not; and advance reader.
bool ReadIf(std::string_view str)
Check whether the next data matches 'str', and skip it.
Error reporting related functions.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
Definition fileio_type.h:88
@ NO_DIRECTORY
A path without any base directory.
Library for parsing command-line options.
@ ODF_NO_VALUE
A plain option (no value attached to it).
Definition getoptdata.h:15
@ ODF_HAS_VALUE
An option with a value.
Definition getoptdata.h:16
Types related to reading/writing '*.ini' files.
@ IGT_SEQUENCE
A list of uninterpreted lines, terminated by the next group block.
Definition ini_type.h:19
A number of safeguards to prevent using unsafe methods.
void FatalErrorI(const std::string &str)
Error handling for fatal non-user errors.
int main(int argc, char *argv[])
And the main program (what else?).
OutputStore _stored_output
Temporary storage of the output, until all processing is done.
static const std::string_view VALIDATION_GROUP_NAME
Name of the group containing the validation statements.
static void DumpSections(const IniLoadFile &ifile)
Output all non-special sections through the template / template variable expansion system.
static std::optional< std::string_view > FindItemValue(std::string_view name, const IniGroup *grp, const IniGroup *defaults)
Find the value of a template variable.
static void DumpLine(const IniItem *item, const IniGroup *grp, const IniGroup *default_grp, OutputStore &output)
Parse a single entry via a template and output this.
OutputStore _post_amble_output
Similar to _stored_output, but for the post amble.
static const OptionData _opts[]
Options of settingsgen.
static const std::string_view POSTAMBLE_GROUP_NAME
Name of the group containing the post amble.
static const std::string_view PREAMBLE_GROUP_NAME
Name of the group containing the pre amble.
static bool CompareFiles(std::filesystem::path path1, std::filesystem::path path2)
Compare two files for identity.
static void AppendFile(std::optional< std::string_view > fname, FILE *out_fp)
Append a file to the output stream.
static const std::string_view DEFAULTS_GROUP_NAME
Name of the group containing default values for the template variables.
static void ProcessIniFile(std::string_view fname)
Process a single INI file.
static void DumpGroup(const IniLoadFile &ifile, std::string_view group_name)
Dump a IGT_SEQUENCE group into _stored_output.
static const std::string_view TEMPLATES_GROUP_NAME
Name of the group containing the templates.
static const size_t OUTPUT_BLOCK_SIZE
Block size of the buffer in OutputBuffer.
Definition of base types and functions in a cross-platform compatible way.
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:271
Parse strings.
Functions related to low-level strings.
Types related to strings.
Data storage for parsing command line options.
Definition getoptdata.h:29
std::string_view opt
Option value, if available (else empty).
Definition getoptdata.h:35
ArgumentSpan arguments
Remaining command line arguments.
Definition getoptdata.h:33
int GetOpt()
Find the next option.
A group within an ini file.
Definition ini_type.h:34
const IniItem * GetItem(std::string_view name) const
Get the item with the given name.
Definition ini_load.cpp:50
IniGroupType type
type of group
Definition ini_type.h:36
std::string name
name of group
Definition ini_type.h:37
std::list< IniItem > items
all items in the group
Definition ini_type.h:35
A single "line" in an ini file.
Definition ini_type.h:23
std::optional< std::string > value
The value of this item.
Definition ini_type.h:25
std::string name
The name of this item.
Definition ini_type.h:24
Ini file that only supports loading.
Definition ini_type.h:50
std::list< IniGroup > groups
all groups in the ini
Definition ini_type.h:53
const IniGroupNameList seq_group_names
list of group names that are sequences.
Definition ini_type.h:56
IniLoadFile(const IniGroupNameList &list_group_names={}, const IniGroupNameList &seq_group_names={})
Construct a new in-memory Ini file representation.
Definition ini_load.cpp:106
const IniGroup * GetGroup(std::string_view name) const
Get the group with the given name.
Definition ini_load.cpp:117
void LoadFromDisk(std::string_view filename, Subdirectory subdir)
Load the Ini file's data from the disk.
Definition ini_load.cpp:184
const IniGroupNameList list_group_names
list of group names that are lists
Definition ini_type.h:55
Data of an option.
Definition getoptdata.h:21
Derived class for loading INI files without going through Fio stuff.
void ReportFileError(std::string_view message) override
Report an error about the file contents.
SettingsIniFile(const IniGroupNameList &list_group_names={}, const IniGroupNameList &seq_group_names={})
Construct a new ini loader.
std::optional< FileHandle > OpenFile(std::string_view filename, Subdirectory, size_t *size) override
Open the INI file.