The Battle for Wesnoth  1.19.24+dev
config_cache.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2025
3  by Pauli Nieminen <paniemin@cc.hut.fi>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
18 #include "config_cache.hpp"
19 #include "filesystem.hpp"
20 #include "gettext.hpp"
21 #include "log.hpp"
22 #include "hash.hpp"
24 #include "serialization/parser.hpp"
26 #include "game_version.hpp"
27 
28 #include <boost/algorithm/string/replace.hpp>
29 #include <boost/iostreams/filter/gzip.hpp>
30 
31 static lg::log_domain log_cache("cache");
32 #define ERR_CACHE LOG_STREAM(err, log_cache)
33 #define LOG_CACHE LOG_STREAM(info, log_cache)
34 #define DBG_CACHE LOG_STREAM(debug, log_cache)
35 
36 namespace game_config
37 {
38 
39 namespace
40 {
41 
42 void add_builtin_defines(preproc_map& target)
43 {
44 #ifdef __APPLE__
45  target.try_emplace("APPLE");
46 #endif
47 
48 #ifdef __ANDROID__
49  target.try_emplace("ANDROID");
50 #endif
51 
52 #if defined(MOUSE_TOUCH_EMULATION) || defined(TARGET_OS_IPHONE)
53  target.try_emplace("IPHONEOS");
54 #endif
55 
56  target.try_emplace("WESNOTH_VERSION", game_config::wesnoth_version.str());
57 
58  target.try_emplace("WESNOTH_VERSION_MAJOR", std::to_string(game_config::wesnoth_version.major_version()));
59  target.try_emplace("WESNOTH_VERSION_MINOR", std::to_string(game_config::wesnoth_version.minor_version()));
60  target.try_emplace("WESNOTH_VERSION_REVISION", std::to_string(game_config::wesnoth_version.revision_level()));
61  target.try_emplace("WESNOTH_VERSION_SPECIAL", game_config::wesnoth_version.special_version());
62 }
63 
64 }
65 
67 {
68  static config_cache cache;
69  return cache;
70 }
71 
73  : force_valid_cache_(false)
74  , use_cache_(true)
75  , fake_invalid_cache_(false)
76  , defines_map_()
77  , cache_file_prefix_("cache-v" + boost::algorithm::replace_all_copy(game_config::revision, ":", "_") + "-")
78 {
79  // To set-up initial defines map correctly
80  clear_defines();
81 }
82 
84 {
85  return defines_map_;
86 }
87 
89 {
90  LOG_CACHE << "Clearing defines map!";
91  defines_map_.clear();
92 
93  //
94  // Set-up default defines map.
95  //
96 
97  add_builtin_defines(defines_map_);
98 }
99 
100 config config_cache::get_config(const std::string& file_path, abstract_validator* validator)
101 {
102  return load_configs(file_path, validator);
103 }
104 
105 void config_cache::write_file(const std::string& file_path, const config& cfg)
106 {
109  writer.write(cfg);
110 }
111 
112 void config_cache::write_file(const std::string& file_path, const preproc_map& defines)
113 {
114  if(defines.empty()) {
115  if(filesystem::file_exists(file_path)) {
116  filesystem::delete_directory(file_path);
117  }
118  return;
119  }
120 
123 
124  // Write all defines to stream.
125  for(const preproc_map::value_type& define : defines) {
126  define.second.write(writer, define.first);
127  }
128 }
129 
130 config config_cache::read_file(const std::string& file_path)
131 {
132  return io::read_gz(*filesystem::istream_file(file_path));
133 }
134 
136 {
138  // HACK: copy_map doesn't have built-in defines in some cases (issue #1924)
139  // or they may be out of date, so brute-force them in.
140  add_builtin_defines(res);
141  return res;
142 }
143 
145 {
147 }
148 
149 std::pair<config, preproc_map> config_cache::read_configs(const std::string& file_path, abstract_validator* validator)
150 {
151  preproc_map copy_map = make_copy_map();
152  config cfg = read_configs(file_path, copy_map, validator);
153  return { std::move(cfg), std::move(copy_map) };
154 }
155 
156 config config_cache::read_configs(const std::string& file_path, preproc_map& defines_map, abstract_validator* validator)
157 {
158  return io::read(*preprocess_file(file_path, defines_map), validator);
159 }
160 
161 config config_cache::read_cache(const std::string& file_path, abstract_validator* validator)
162 {
163  static const std::string extension = ".gz";
164 
165  std::stringstream defines_string;
166  defines_string << file_path;
167 
168  bool is_valid = true;
169 
170  //
171  // Only WESNOTH_VERSION and its variants are allowed to be non-empty.
172  //
173  static const std::set<std::string> allowed_non_empty_define_keys = {
174  "WESNOTH_VERSION",
175  "WESNOTH_VERSION_MAJOR",
176  "WESNOTH_VERSION_MINOR",
177  "WESNOTH_VERSION_REVISION",
178  "WESNOTH_VERSION_SPECIAL"
179  };
180 
181  for(const auto& [key, define] : defines_map_) {
182  if((!define.value.empty() || !define.arguments.empty()) && allowed_non_empty_define_keys.find(key) == allowed_non_empty_define_keys.end()) {
183  is_valid = false;
184  ERR_CACHE << "Invalid preprocessor define: " << key;
185  break;
186  }
187 
188  defines_string << " " << key;
189  }
190 
191  // Do cache check only if define map is valid and
192  // caching is allowed.
193  const std::string& cache_path = filesystem::get_cache_dir();
194 
195  if(is_valid && !cache_path.empty()) {
196  // Use a hash for a shorter display of the defines.
197  const std::string fname = cache_path + "/" +
199  utils::md5(defines_string.str()).hex_digest();
200  const std::string fname_checksum = fname + ".checksum" + extension;
201 
202  filesystem::file_tree_checksum dir_checksum;
203 
205  try {
206  if(filesystem::file_exists(fname_checksum)) {
207  DBG_CACHE << "Reading checksum: " << fname_checksum;
208  dir_checksum = filesystem::file_tree_checksum{read_file(fname_checksum)};
209  }
210  } catch(const config::error&) {
211  ERR_CACHE << "cache checksum is corrupt";
212  } catch(const filesystem::io_exception&) {
213  ERR_CACHE << "error reading cache checksum";
214  } catch(const std::ios_base::failure&) {
215  ERR_CACHE << "error reading cache checksum";
216  }
217  }
218 
219  if(force_valid_cache_) {
220  LOG_CACHE << "skipping cache validation (forced)";
221  }
222 
223  if(filesystem::file_exists(fname + extension) && (force_valid_cache_ || (dir_checksum == filesystem::data_tree_checksum()))) {
224  LOG_CACHE << "found valid cache at '" << fname << extension << "' with defines_map " << defines_string.str();
225  log_scope("read cache");
226 
227  try {
228  config cfg = read_file(fname + extension);
229  const std::string define_file = fname + ".define" + extension;
230 
231  if(filesystem::file_exists(define_file)) {
233  }
234 
235  return cfg;
236  } catch(const config::error& e) {
237  ERR_CACHE << "cache " << fname << extension << " is corrupt. Loading from files: "<< e.message;
238  } catch(const filesystem::io_exception&) {
239  ERR_CACHE << "error reading cache " << fname << extension << ". Loading from files";
240  } catch (const boost::iostreams::gzip_error& e) {
241  //read_file -> ... -> read_gz can throw this exception.
242  ERR_CACHE << "cache " << fname << extension << " is corrupt. Error code: " << e.error();
243  }
244  }
245 
246  LOG_CACHE << "no valid cache found. Writing cache to '" << fname << extension << " with defines_map "<< defines_string.str() << "'";
247 
248  // Now we need queued defines so read them to memory
250 
251  auto [cfg, defines] = read_configs(file_path, validator);
252  add_defines_map_diff(defines);
253 
254  try {
255  write_file(fname + extension, cfg);
256  write_file(fname + ".define" + extension, defines);
257 
258  config checksum_cfg;
259 
260  filesystem::data_tree_checksum().write(checksum_cfg);
261  write_file(fname_checksum, checksum_cfg);
262  } catch(const filesystem::io_exception&) {
263  ERR_CACHE << "could not write to cache '" << fname << "'";
264  }
265 
266  return cfg;
267  }
268 
269  LOG_CACHE << "Loading plain config instead of cache";
270 
271  auto [cfg, defines] = read_configs(file_path, validator);
272  add_defines_map_diff(defines);
273  return cfg;
274 }
275 
276 void config_cache::read_defines_file(const std::string& file_path)
277 {
278  DBG_CACHE << "Reading cached defines from: " << file_path;
279  config file_cfg = read_file(file_path);
280 
281  for(const auto [key, cfg] : file_cfg.all_children_view()) {
283  }
284 }
285 
287 {
288  for(const std::string& p : config_cache_transaction::instance().get_define_files()) {
290  }
291 }
292 
293 config config_cache::load_configs(const std::string& config_path, abstract_validator* validator)
294 {
295  // Make sure that we have fake transaction if no real one is going on
297 
298  if(use_cache_) {
299  return read_cache(config_path, validator);
300  } else {
301  auto [cfg, defines] = read_configs(config_path, validator);
302  add_defines_map_diff(defines);
303  return cfg;
304  }
305 }
306 
308 {
309  fake_invalid_cache_ = force;
310 }
311 
313 {
314  use_cache_ = use;
315 }
316 
318 {
319  force_valid_cache_ = force;
320 }
321 
323 {
325 }
326 
327 void config_cache::add_define(const std::string& define)
328 {
329  DBG_CACHE << "adding define: " << define;
330  defines_map_.try_emplace(define);
331 
333  // we have to add this to active map too
335  }
336 }
337 
338 void config_cache::remove_define(const std::string& define)
339 {
340  DBG_CACHE << "removing define: " << define;
341  defines_map_.erase(define);
342 
344  // we have to remove this from active map too
346  }
347 }
348 
350 {
351  std::vector<std::string> files, dirs;
353 
354  LOG_CACHE << "clean_cache(): " << files.size() << " files, "
355  << dirs.size() << " dirs to check";
356 
357  const std::string& exclude_current = cache_file_prefix_ + "*";
358 
359  bool status = true;
360 
361  status &= delete_cache_files(files, exclude_current);
362  status &= delete_cache_files(dirs, exclude_current);
363 
364  LOG_CACHE << "clean_cache(): done";
365 
366  return status;
367 }
368 
370 {
371  std::vector<std::string> files, dirs;
373 
374  LOG_CACHE << "purge_cache(): deleting " << files.size() << " files, "
375  << dirs.size() << " dirs";
376 
377  bool status = true;
378 
379  status &= delete_cache_files(files);
380  status &= delete_cache_files(dirs);
381 
382  LOG_CACHE << "purge_cache(): done";
383  return status;
384 }
385 
386 bool config_cache::delete_cache_files(const std::vector<std::string>& paths,
387  const std::string& exclude_pattern)
388 {
389  const bool delete_everything = exclude_pattern.empty();
390  bool status = true;
391 
392  for(const std::string& file_path : paths)
393  {
394  if(!delete_everything) {
395  const std::string& fn = filesystem::base_name(file_path);
396 
397  if(utils::wildcard_string_match(fn, exclude_pattern)) {
398  LOG_CACHE << "delete_cache_files(): skipping " << file_path
399  << " excluded by '" << exclude_pattern << "'";
400  continue;
401  }
402  }
403 
404  LOG_CACHE << "delete_cache_files(): deleting " << file_path;
405  if(!filesystem::delete_directory(file_path)) {
406  ERR_CACHE << "delete_cache_files(): could not delete "
407  << file_path;
408  status = false;
409  }
410  }
411 
412  return status;
413 }
414 
416  : define_filenames_()
417  , active_map_()
418 {
419  assert(state_ == FREE);
420  state_ = NEW;
421  active_ = this;
422 }
423 
425 {
426  state_ = FREE;
427  active_ = nullptr;
428 }
429 
431 {
432  state_ = LOCKED;
433 }
434 
435 const std::vector<std::string>& config_cache_transaction::get_define_files() const
436 {
437  return define_filenames_;
438 }
439 
440 void config_cache_transaction::add_define_file(const std::string& file)
441 {
442  define_filenames_.push_back(file);
443 }
444 
446 {
447  if(active_map_.empty()) {
448  active_map_.insert(defines_map.begin(), defines_map.end());
449  if(get_state() == NEW) {
450  state_ = ACTIVE;
451  }
452  }
453 
454  return active_map_;
455 }
456 
458 {
459  switch(get_state()) {
460  case ACTIVE: {
461  preproc_map temp;
462  std::set_difference(new_map.begin(),
463  new_map.end(),
464  active_map_.begin(),
465  active_map_.end(),
466  std::insert_iterator(temp, temp.begin()));
467 
468  active_map_.insert(temp.begin(), temp.end());
469  std::swap(temp, new_map);
470  break;
471  }
472 
473  case LOCKED:
474  new_map.clear();
475  break;
476 
477  default:
478  break;
479  }
480 }
481 
482 } // namespace game_config
Used in parsing config file.
Definition: validator.hpp:38
Class for writing a config out to a file in pieces.
void write(const config &cfg, bool strong_quotes=false)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
auto all_children_view() const
In-order iteration over all children.
Definition: config.hpp:795
void add_defines_map_diff(preproc_map &defines_map)
std::vector< std::string > define_filenames_
static config_cache_transaction & instance()
const std::vector< std::string > & get_define_files() const
void lock()
Lock the transaction so no more macros are added.
preproc_map & get_active_map(const preproc_map &defines_map)
static config_cache_transaction * active_
void add_define_file(const std::string &file)
Singleton class to manage game config file caching.
void add_define(const std::string &define)
Add a entry to preproc defines map.
void recheck_filetree_checksum()
Force cache checksum validation.
config read_cache(const std::string &path, abstract_validator *validator=nullptr)
void write_file(const std::string &file, const config &cfg)
bool purge_cache()
Deletes all cache files.
void set_use_cache(bool use)
Enable/disable caching.
config load_configs(const std::string &path, abstract_validator *validator=nullptr)
static config_cache & instance()
Get reference to the singleton object.
preproc_map & make_copy_map()
const preproc_map & get_preproc_map() const
config read_file(const std::string &file)
void remove_define(const std::string &define)
Remove a entry to preproc defines map.
void clear_defines()
Clear stored defines map to default values.
bool clean_cache()
Deletes stale cache files not in use by the game.
config get_config(const std::string &path, abstract_validator *validator=nullptr)
Gets a config object from given path.
bool delete_cache_files(const std::vector< std::string > &paths, const std::string &exclude_pattern="")
std::pair< config, preproc_map > read_configs(const std::string &path, abstract_validator *validator)
void read_defines_file(const std::string &path)
void add_defines_map_diff(preproc_map &)
void set_force_valid_cache(bool force)
Enable/disable cache validation.
Holds a fake cache transaction if no real one is used.
virtual std::string hex_digest() const override
Definition: hash.cpp:119
void swap(config &lhs, config &rhs) noexcept
Implement non-member swap function for std::swap (calls config::swap).
Definition: config.cpp:1287
#define LOG_CACHE
static lg::log_domain log_cache("cache")
#define DBG_CACHE
#define ERR_CACHE
const config * cfg
Declarations for File-IO.
Interfaces for manipulating version numbers of engine, add-ons, etc.
Standard logging facilities (interface).
#define log_scope(description)
Definition: log.hpp:275
Definition: span.hpp:53
std::string get_cache_dir()
Definition: filesystem.cpp:880
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:466
std::string base_name(const std::string &file, const bool remove_extension)
Returns the base filename of a file, with directory name stripped.
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:344
bool delete_directory(const std::string &dirname, const bool keep_pbl)
const file_tree_checksum & data_tree_checksum(bool reset=false)
Get the time at which the data/ tree was last modified at.
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:53
Game configuration data as global variables.
Definition: build_info.cpp:68
const version_info wesnoth_version(VERSION)
int cache_compression_level
Definition: filesystem.cpp:118
const std::string revision
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:610
config read_gz(std::istream &file, abstract_validator *validator)
Might throw a std::ios_base::failure especially a gzip_error.
Definition: parser.cpp:666
bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
Performs pattern matching with wildcards.
fake
For describing the type of faked display, if any.
Definition: video.hpp:44
std::string to_string(const Range &range, const Func &op)
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map &defines)
Function to use the WML preprocessor on a file.
std::map< std::string, struct preproc_define > preproc_map
An exception object used when an IO error occurs.
Definition: filesystem.hpp:56
static void insert(preproc_map &, const config &)
mock_party p
#define e