OpenTTD Source  20241121-master-g67a0fccfad
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 <http://www.gnu.org/licenses/>.
6  */
7 
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"
17 
18 #include <filesystem>
19 
20 #include "../safeguards.h"
21 
27 [[noreturn]] void FatalErrorI(const std::string &msg)
28 {
29  fmt::print(stderr, "settingsgen: FATAL: {}\n", msg);
30  exit(1);
31 }
32 
33 static const size_t OUTPUT_BLOCK_SIZE = 16000;
34 
36 class OutputBuffer {
37 public:
39  void Clear()
40  {
41  this->size = 0;
42  }
43 
50  size_t Add(const char *text, size_t length)
51  {
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;
57  }
58 
63  void Write(FILE *out_fp) const
64  {
65  if (fwrite(this->data, 1, this->size, out_fp) != this->size) {
66  FatalError("Cannot write output");
67  }
68  }
69 
74  bool HasRoom() const
75  {
76  return this->size < OUTPUT_BLOCK_SIZE;
77  }
78 
79  size_t size;
81 };
82 
84 class OutputStore {
85 public:
86  OutputStore()
87  {
88  this->Clear();
89  }
90 
92  void Clear()
93  {
94  this->output_buffer.clear();
95  }
96 
102  void Add(const char *text, size_t length = 0)
103  {
104  if (length == 0) length = strlen(text);
105 
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;
110  }
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;
117  }
118  }
119 
124  void Write(FILE *out_fp) const
125  {
126  for (const OutputBuffer &out_data : output_buffer) {
127  out_data.Write(out_fp);
128  }
129  }
130 
131 private:
136  bool BufferHasRoom() const
137  {
138  size_t num_blocks = this->output_buffer.size();
139  return num_blocks > 0 && this->output_buffer[num_blocks - 1].HasRoom();
140  }
141 
142  typedef std::vector<OutputBuffer> OutputBufferVector;
144 };
145 
146 
154  SettingsIniFile(const IniGroupNameList &list_group_names = {}, const IniGroupNameList &seq_group_names = {}) :
156  {
157  }
158 
159  std::optional<FileHandle> OpenFile(const std::string &filename, Subdirectory, size_t *size) override
160  {
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;
165 
166  fseek(*in, 0L, SEEK_END);
167  *size = ftell(*in);
168  fseek(*in, 0L, SEEK_SET); // Seek back to the start of the file.
169 
170  return in;
171  }
172 
173  void ReportFileError(const char * const pre, const char * const buffer, const char * const post) override
174  {
175  FatalError("{}{}{}", pre, buffer, post);
176  }
177 };
178 
181 
182 static const char *PREAMBLE_GROUP_NAME = "pre-amble";
183 static const char *POSTAMBLE_GROUP_NAME = "post-amble";
184 static const char *TEMPLATES_GROUP_NAME = "templates";
185 static const char *VALIDATION_GROUP_NAME = "validation";
186 static const char *DEFAULTS_GROUP_NAME = "defaults";
187 
193 static void DumpGroup(const IniLoadFile &ifile, const char * const group_name)
194 {
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);
201  }
202  }
203  }
204 }
205 
213 static const char *FindItemValue(const char *name, const IniGroup *grp, const IniGroup *defaults)
214 {
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();
219 }
220 
228 static void DumpLine(const IniItem *item, const IniGroup *grp, const IniGroup *default_grp, OutputStore &output)
229 {
230  static const int MAX_VAR_LENGTH = 64;
231 
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++;
244  }
245  }
246 
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;
254  }
255  txt++;
256  if (*txt == '$') { // Literal $
257  output.Add(txt, 1);
258  txt++;
259  continue;
260  }
261 
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++;
269  }
270  variable[i] = '\0';
271  txt += i;
272 
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);
279  }
280  }
281  output.Add("\n", 1); // \n after the expanded template.
282  while (count > 0) {
283  output.Add("#endif\n");
284  count--;
285  }
286 }
287 
292 static void DumpSections(const IniLoadFile &ifile)
293 {
295 
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;
300 
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;
305 
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);
309  }
310  DumpLine(template_item, &grp, default_grp, _stored_output);
311 
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);
316  }
317  }
318  }
319 }
320 
326 static void AppendFile(const char *fname, FILE *out_fp)
327 {
328  if (fname == nullptr) return;
329 
330  auto in_fp = FileHandle::Open(fname, "r");
331  if (!in_fp.has_value()) {
332  FatalError("Cannot open file {} for copying", fname);
333  }
334 
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");
341  }
342  } while (length == lengthof(buffer));
343 }
344 
351 static bool CompareFiles(const char *n1, const char *n2)
352 {
353  auto f2 = FileHandle::Open(n2, "rb");
354  if (!f2.has_value()) return false;
355 
356  auto f1 = FileHandle::Open(n1, "rb");
357  if (!f1.has_value()) {
358  FatalError("can't open {}", n1);
359  }
360 
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);
367 
368  if (l1 != l2 || memcmp(b1, b2, l1) != 0) {
369  return false;
370  }
371  } while (l1 != 0);
372 
373  return true;
374 }
375 
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" },
383 };
384 
405 static void ProcessIniFile(const char *fname)
406 {
407  static const IniLoadFile::IniGroupNameList seq_groups = {PREAMBLE_GROUP_NAME, POSTAMBLE_GROUP_NAME};
408 
409  SettingsIniFile ini{{}, seq_groups};
410  ini.LoadFromDisk(fname, NO_DIRECTORY);
411 
413  DumpSections(ini);
415 }
416 
422 int CDECL main(int argc, char *argv[])
423 {
424  const char *output_file = nullptr;
425  const char *before_file = nullptr;
426  const char *after_file = nullptr;
427 
428  GetOptData mgo(std::span(argv + 1, argc - 1), _opts);
429  for (;;) {
430  int i = mgo.GetOpt();
431  if (i == -1) break;
432 
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;
443 
444  case 'o':
445  output_file = mgo.opt;
446  break;
447 
448  case 'a':
449  after_file = mgo.opt;
450  break;
451 
452  case 'b':
453  before_file = mgo.opt;
454  break;
455 
456  case -2:
457  fmt::print(stderr, "Invalid arguments\n");
458  return 1;
459  }
460  }
461 
464 
465  for (auto &argument : mgo.arguments) ProcessIniFile(argument);
466 
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";
475 
476  auto fp = FileHandle::Open(tmp_output, "w");
477  if (!fp.has_value()) {
478  FatalError("Cannot open file {}", tmp_output);
479  }
480  AppendFile(before_file, *fp);
481  _stored_output.Write(*fp);
483  AppendFile(after_file, *fp);
484  fp.reset();
485 
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());
494  }
495  }
496  return 0;
497 }
498 
505 std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
506 {
507  auto f = fopen(filename.c_str(), mode.c_str());
508  if (f == nullptr) return std::nullopt;
509  return FileHandle(f);
510 }
static std::optional< FileHandle > Open(const std::string &filename, const std::string &mode)
Open an RAII file handle if possible.
Definition: fileio.cpp:1170
Output buffer for a block of data.
Definition: settingsgen.cpp:36
size_t Add(const char *text, size_t length)
Add text to the output buffer.
Definition: settingsgen.cpp:50
bool HasRoom() const
Does the block have room for more data?
Definition: settingsgen.cpp:74
void Clear()
Prepare buffer for use.
Definition: settingsgen.cpp:39
size_t size
Number of bytes stored in data.
Definition: settingsgen.cpp:79
void Write(FILE *out_fp) const
Dump buffer to the output stream.
Definition: settingsgen.cpp:63
char data[OUTPUT_BLOCK_SIZE]
Stored data.
Definition: settingsgen.cpp:80
Temporarily store output.
Definition: settingsgen.cpp:84
std::vector< OutputBuffer > OutputBufferVector
Vector type for output buffers.
OutputBufferVector output_buffer
Vector of blocks containing the stored output.
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 Add(const char *text, size_t length=0)
Add text to the output storage.
void Clear()
Clear the temporary storage.
Definition: settingsgen.cpp:92
Subdirectory
The different kinds of subdirectories OpenTTD uses.
Definition: fileio_type.h:115
@ NO_DIRECTORY
A path without any base directory.
Definition: fileio_type.h:133
@ 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
@ IGT_SEQUENCE
A list of uninterpreted lines, terminated by the next group block.
Definition: ini_type.h:19
void MemCpyT(T *destination, const T *source, size_t num=1)
Type-safe version of memcpy().
Definition: mem_func.hpp:23
static const char * VALIDATION_GROUP_NAME
Name of the group containing the validation statements.
static void AppendFile(const char *fname, FILE *out_fp)
Append a file to the output stream.
OutputStore _stored_output
Temporary storage of the output, until all processing is done.
static void DumpGroup(const IniLoadFile &ifile, const char *const group_name)
Dump a IGT_SEQUENCE group into _stored_output.
static const char * DEFAULTS_GROUP_NAME
Name of the group containing default values for the template variables.
static const char * TEMPLATES_GROUP_NAME
Name of the group containing the templates.
static const char * FindItemValue(const char *name, const IniGroup *grp, const IniGroup *defaults)
Find the value of a template variable.
static void DumpSections(const IniLoadFile &ifile)
Output all non-special sections through the template / template variable expansion system.
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 bool CompareFiles(const char *n1, const char *n2)
Compare two files for identity.
static void ProcessIniFile(const char *fname)
Process a single INI file.
void FatalErrorI(const std::string &msg)
Report a fatal error.
Definition: settingsgen.cpp:27
int CDECL main(int argc, char *argv[])
And the main program (what else?)
static const char * POSTAMBLE_GROUP_NAME
Name of the group containing the post amble.
static const char * PREAMBLE_GROUP_NAME
Name of the group containing the pre amble.
static const size_t OUTPUT_BLOCK_SIZE
Block size of the buffer in OutputBuffer.
Definition: settingsgen.cpp:33
#define lengthof(array)
Return the length of an fixed size array.
Definition: stdafx.h:280
Data storage for parsing command line options.
Definition: getoptdata.h:29
ArgumentSpan arguments
Remaining command line arguments.
Definition: getoptdata.h:33
int GetOpt()
Find the next option.
Definition: getoptdata.cpp:22
const char * opt
Option value, if available (else nullptr).
Definition: getoptdata.h:35
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:52
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
void LoadFromDisk(const std::string &filename, Subdirectory subdir)
Load the Ini file's data from the disk.
Definition: ini_load.cpp:187
IniLoadFile(const IniGroupNameList &list_group_names={}, const IniGroupNameList &seq_group_names={})
Construct a new in-memory Ini file representation.
Definition: ini_load.cpp:108
const IniGroup * GetGroup(std::string_view name) const
Get the group with the given name.
Definition: ini_load.cpp:119
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
OptionDataType type
The type of option.
Definition: getoptdata.h:22
Derived class for loading INI files without going through Fio stuff.
std::optional< FileHandle > OpenFile(const std::string &filename, Subdirectory, size_t *size) override
Open the INI file.
SettingsIniFile(const IniGroupNameList &list_group_names={}, const IniGroupNameList &seq_group_names={})
Construct a new ini loader.
void ReportFileError(const char *const pre, const char *const buffer, const char *const post) override
Report an error about the file contents.