The Battle for Wesnoth  1.19.5+dev
manager.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2024
3  by Iris Morelle <shadowm2006@gmail.com>
4  Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #include "addon/manager.hpp"
18 
19 #include "filesystem.hpp"
20 #include "log.hpp"
21 #include "serialization/parser.hpp"
23 #include "utils/general.hpp"
24 #include "game_version.hpp"
25 #include "wml_exception.hpp"
26 
27 #include <boost/algorithm/string.hpp>
28 
29 static lg::log_domain log_config("config");
30 #define ERR_CFG LOG_STREAM(err , log_config)
31 #define LOG_CFG LOG_STREAM(info, log_config)
32 #define WRN_CFG LOG_STREAM(warn, log_config)
33 
34 static lg::log_domain log_filesystem("filesystem");
35 #define ERR_FS LOG_STREAM(err , log_filesystem)
36 
37 static lg::log_domain log_network("network");
38 #define ERR_NET LOG_STREAM(err , log_network)
39 #define LOG_NET LOG_STREAM(info, log_network)
40 
41 namespace {
42  std::string get_pbl_file_path(const std::string& addon_name)
43  {
44  const std::string& parentd = filesystem::get_addons_dir();
45  // Allow .pbl files directly in the addon dir
46  const std::string exterior = parentd + "/" + addon_name + ".pbl";
47  const std::string interior = parentd + "/" + addon_name + "/_server.pbl";
48  return filesystem::file_exists(exterior) ? exterior : interior;
49  }
50 
51  inline std::string get_info_file_path(const std::string& addon_name)
52  {
53  return filesystem::get_addons_dir() + "/" + addon_name + "/_info.cfg";
54  }
55 }
56 
57 bool have_addon_in_vcs_tree(const std::string& addon_name)
58 {
59  static const std::string parentd = filesystem::get_addons_dir();
60  return
61  filesystem::file_exists(parentd+"/"+addon_name+"/.svn") ||
62  filesystem::file_exists(parentd+"/"+addon_name+"/.git") ||
63  filesystem::file_exists(parentd+"/"+addon_name+"/.hg");
64 }
65 
66 bool have_addon_pbl_info(const std::string& addon_name)
67 {
68  return filesystem::file_exists(get_pbl_file_path(addon_name));
69 }
70 
71 config get_addon_pbl_info(const std::string& addon_name, bool do_validate)
72 {
73  config cfg;
74  const std::string& pbl_path = get_pbl_file_path(addon_name);
75  try {
77  std::unique_ptr<schema_validation::schema_validator> validator;
78  if(do_validate) {
79  validator = std::make_unique<schema_validation::schema_validator>(filesystem::get_wml_location("schema/pbl.cfg").value());
80  validator->set_create_exceptions(true);
81  }
82  read(cfg, *stream, validator.get());
83  } catch(const config::error& e) {
84  throw invalid_pbl_exception(pbl_path, e.message);
85  } catch(wml_exception& e) {
86  auto msg = e.user_message;
87  e.user_message += " in " + addon_name;
88  boost::replace_all(e.dev_message, "<unknown>", filesystem::sanitize_path(pbl_path));
89  e.show();
90  throw invalid_pbl_exception(pbl_path, msg);
91  }
92 
93  return cfg;
94 }
95 
96 void set_addon_pbl_info(const std::string& addon_name, const config& cfg)
97 {
98  filesystem::scoped_ostream stream = filesystem::ostream_file(get_pbl_file_path(addon_name));
99  write(*stream, cfg);
100 }
101 
102 bool have_addon_install_info(const std::string& addon_name)
103 {
104  return filesystem::file_exists(get_info_file_path(addon_name));
105 }
106 
107 void get_addon_install_info(const std::string& addon_name, config& cfg)
108 {
109  const std::string& info_path = get_info_file_path(addon_name);
111  try {
112  // The parser's read() API would normally do this at the start. This
113  // is a safeguard in case read() throws later
114  cfg.clear();
115  config envelope;
116  read(envelope, *stream);
117  if(auto info = envelope.optional_child("info")) {
118  cfg = std::move(*info);
119  }
120  } catch(const config::error& e) {
121  ERR_CFG << "Failed to read add-on installation information for '"
122  << addon_name << "' from " << info_path << ":\n"
123  << e.message;
124  }
125 }
126 
127 void write_addon_install_info(const std::string& addon_name, const config& cfg)
128 {
129  LOG_CFG << "Writing version info for add-on '" << addon_name << "'";
130 
131  const auto& info_path = get_info_file_path(addon_name);
132  auto out = filesystem::ostream_file(info_path);
133 
134  *out << "#\n"
135  << "# File automatically generated by Wesnoth to keep track\n"
136  << "# of version information on installed add-ons. DO NOT EDIT!\n"
137  << "#\n";
138 
139  config envelope;
140  envelope.add_child("info", cfg);
141  write(*out, envelope);
142 }
143 
144 bool remove_local_addon(const std::string& addon)
145 {
146  const std::string addon_dir = filesystem::get_addons_dir() + "/" + addon;
147 
148  LOG_CFG << "removing local add-on: " << addon;
149 
150  if(filesystem::file_exists(addon_dir) && !filesystem::delete_directory(addon_dir, true)) {
151  ERR_CFG << "Failed to delete directory/file: " << addon_dir;
152  ERR_CFG << "removal of add-on " << addon << " failed!";
153  return false;
154  }
155  return true;
156 }
157 
158 namespace {
159 
160 enum ADDON_ENUM_CRITERIA
161 {
162  ADDON_ANY,
163  ADDON_HAS_PBL,
164 };
165 
166 std::vector<std::string> enumerate_addons_internal(ADDON_ENUM_CRITERIA filter)
167 {
168  std::vector<std::string> res;
169  std::vector<std::string> addon_dirnames;
170 
171  const auto& addons_root = filesystem::get_addons_dir();
172  filesystem::get_files_in_dir(addons_root, nullptr, &addon_dirnames);
173 
174  for(const auto& addon_name : addon_dirnames) {
175  if(filesystem::file_exists(addons_root + "/" + addon_name + "/_main.cfg") &&
176  (filter != ADDON_HAS_PBL || have_addon_pbl_info(addon_name)))
177  {
178  res.emplace_back(addon_name);
179  }
180  }
181 
182  return res;
183 }
184 
185 }
186 
187 std::vector<std::string> available_addons()
188 {
189  return enumerate_addons_internal(ADDON_HAS_PBL);
190 }
191 
192 std::vector<std::string> installed_addons()
193 {
194  return enumerate_addons_internal(ADDON_ANY);
195 }
196 
197 std::map<std::string, std::string> installed_addons_and_versions()
198 {
199  std::map<std::string, std::string> addons;
200 
201  for(const std::string& addon_id : installed_addons()) {
202  if(have_addon_pbl_info(addon_id)) {
203  try {
204  // Just grabbing the version, so don't bother validating the pbl
205  addons[addon_id] = get_addon_pbl_info(addon_id, false)["version"].str();
206  } catch(const invalid_pbl_exception&) {
207  addons[addon_id] = "Invalid pbl file, version unknown";
208  }
209  } else if(filesystem::file_exists(get_info_file_path(addon_id))) {
210  config info_cfg;
211  get_addon_install_info(addon_id, info_cfg);
212  addons[addon_id] = !info_cfg.empty() ? info_cfg["version"].str() : "Unknown";
213  } else {
214  addons[addon_id] = "Unknown";
215  }
216  }
217  return addons;
218 }
219 
220 bool is_addon_installed(const std::string& addon_name)
221 {
222  const std::string namestem = filesystem::get_addons_dir() + "/" + addon_name;
223  return filesystem::file_exists(namestem + "/_main.cfg");
224 }
225 
226 static inline bool IsCR(const char& c)
227 {
228  return c == '\x0D';
229 }
230 
231 static std::string strip_cr(std::string str, bool strip)
232 {
233  if(!strip)
234  return str;
235  utils::erase_if(str, IsCR);
236  return str;
237 }
238 
239 static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string& addon_name)
240 {
241  const std::string parentd = filesystem::get_addons_dir();
242  const std::string ign_file = parentd + "/" + addon_name + "/_server.ign";
243 
245  LOG_CFG << "searching for .ign file for '" << addon_name << "'...";
246  if (!filesystem::file_exists(ign_file)) {
247  LOG_CFG << "no .ign file found for '" << addon_name << "'\n"
248  << "using default ignore patterns...";
250  }
251  LOG_CFG << "found .ign file: " << ign_file;
252  auto stream = filesystem::istream_file(ign_file);
253  std::string line;
254  while (std::getline(*stream, line)) {
255  boost::trim(line);
256  const std::size_t l = line.size();
257  // .gitignore & WML like comments
258  if (l == 0 || !line.compare(0,2,"# ")) continue;
259  if (line[l - 1] == '/') { // directory; we strip the last /
260  patterns.add_directory_pattern(line.substr(0, l - 1));
261  } else { // file
262  patterns.add_file_pattern(line);
263  }
264  }
265  return patterns;
266 }
267 
268 static void archive_file(const std::string& path, const std::string& fname, config& cfg)
269 {
270  cfg["name"] = fname;
271  cfg["contents"] = encode_binary(strip_cr(filesystem::read_file(path + '/' + fname), filesystem::is_cfg(fname)));
272 }
273 
274 static void archive_dir(const std::string& path, const std::string& dirname, config& cfg, const filesystem::blacklist_pattern_list& ignore_patterns)
275 {
276  cfg["name"] = dirname;
277  const std::string dir = path + '/' + dirname;
278 
279  std::vector<std::string> files, dirs;
280  filesystem::get_files_in_dir(dir,&files,&dirs);
281  for(const std::string& name : files) {
282  bool valid = !filesystem::looks_like_pbl(name) && !ignore_patterns.match_file(name);
283  if (valid) {
284  archive_file(dir,name,cfg.add_child("file"));
285  }
286  }
287 
288  for(const std::string& name : dirs) {
289  bool valid = !ignore_patterns.match_dir(name);
290  if (valid) {
291  archive_dir(dir,name,cfg.add_child("dir"),ignore_patterns);
292  }
293  }
294 }
295 
296 void archive_addon(const std::string& addon_name, config& cfg)
297 {
298  const std::string parentd = filesystem::get_addons_dir();
299 
300  filesystem::blacklist_pattern_list ignore_patterns(read_ignore_patterns(addon_name));
301  archive_dir(parentd, addon_name, cfg.add_child("dir"), ignore_patterns);
302 }
303 
304 static void unarchive_file(const std::string& path, const config& cfg)
305 {
306  filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
307 }
308 
309 static void unarchive_dir(const std::string& path, const config& cfg, std::function<void()> file_callback = {})
310 {
311  std::string dir;
312  if (cfg["name"].empty())
313  dir = path;
314  else
315  dir = path + '/' + cfg["name"].str();
316 
318 
319  for(const config &d : cfg.child_range("dir")) {
320  unarchive_dir(dir, d, file_callback);
321  }
322 
323  for(const config &f : cfg.child_range("file")) {
324  unarchive_file(dir, f);
325  if(file_callback) {
326  file_callback();
327  }
328  }
329 }
330 
331 static unsigned count_pack_files(const config& cfg)
332 {
333  unsigned count = 0;
334 
335  for(const config& d : cfg.child_range("dir")) {
336  count += count_pack_files(d);
337  }
338 
339  return count + cfg.child_count("file");
340 }
341 
342 void unarchive_addon(const config& cfg, std::function<void(unsigned)> progress_callback)
343 {
344  const std::string parentd = filesystem::get_addons_dir();
345  unsigned file_count = progress_callback ? count_pack_files(cfg) : 0, done = 0;
346  auto file_callback = progress_callback
347  ? [&]() { progress_callback(++done * 100.0 / file_count); }
348  : std::function<void()>{};
349  unarchive_dir(parentd, cfg, file_callback);
350 }
351 
352 static void purge_dir(const std::string& path, const config& removelist)
353 {
354  std::string dir;
355  if(removelist["name"].empty())
356  dir = path;
357  else
358  dir = path + '/' + removelist["name"].str();
359 
360  if(!filesystem::is_directory(dir)) {
361  return;
362  }
363 
364  for(const config& d : removelist.child_range("dir")) {
365  purge_dir(dir, d);
366  }
367 
368  for(const config& f : removelist.child_range("file")) {
369  filesystem::delete_file(dir + '/' + f["name"].str());
370  }
371 
372  if(filesystem::dir_size(dir) < 1) {
374  }
375 }
376 
377 void purge_addon(const config& removelist)
378 {
379  const std::string parentd = filesystem::get_addons_dir();
380  purge_dir(parentd, removelist);
381 }
382 
383 namespace {
384  std::map< std::string, version_info > version_info_cache;
385 } // end unnamed namespace 5
386 
388 {
389  version_info_cache.clear();
390 
391  LOG_CFG << "refreshing add-on versions cache";
392 
393  const std::vector<std::string>& addons = installed_addons();
394  if(addons.empty()) {
395  return;
396  }
397 
398  std::vector<std::string> addon_info_files(addons.size());
399 
400  std::transform(addons.begin(), addons.end(),
401  addon_info_files.begin(), get_info_file_path);
402 
403  for(std::size_t i = 0; i < addon_info_files.size(); ++i) {
404  assert(i < addons.size());
405 
406  const std::string& addon = addons[i];
407  const std::string& info_file = addon_info_files[i];
408 
409  if(filesystem::file_exists(info_file)) {
410  config info_cfg;
411  get_addon_install_info(addon, info_cfg);
412 
413  if(info_cfg.empty()) {
414  continue;
415  }
416 
417  const std::string& version = info_cfg["version"].str();
418  LOG_CFG << "cached add-on version: " << addon << " [" << version << "]";
419 
420  version_info_cache[addon] = version;
421  } else if (!have_addon_pbl_info(addon) && !have_addon_in_vcs_tree(addon)) {
422  // Don't print the warning if the user is clearly the author
423  WRN_CFG << "add-on '" << addon << "' has no _info.cfg; cannot read version info";
424  }
425  }
426 }
427 
428 version_info get_addon_version_info(const std::string& addon)
429 {
430  static const version_info nil;
431  std::map< std::string, version_info >::iterator entry = version_info_cache.find(addon);
432  return entry != version_info_cache.end() ? entry->second : nil;
433 }
static void archive_file(const std::string &path, const std::string &fname, config &cfg)
Definition: manager.cpp:268
bool remove_local_addon(const std::string &addon)
Removes the specified add-on, deleting its full directory structure.
Definition: manager.cpp:144
static bool IsCR(const char &c)
Definition: manager.cpp:226
config get_addon_pbl_info(const std::string &addon_name, bool do_validate)
Gets the publish information for an add-on.
Definition: manager.cpp:71
void unarchive_addon(const config &cfg, std::function< void(unsigned)> progress_callback)
Definition: manager.cpp:342
bool have_addon_in_vcs_tree(const std::string &addon_name)
Returns whether the specified add-on appears to be managed by a VCS or not.
Definition: manager.cpp:57
void set_addon_pbl_info(const std::string &addon_name, const config &cfg)
Definition: manager.cpp:96
static lg::log_domain log_filesystem("filesystem")
#define ERR_CFG
Definition: manager.cpp:30
#define LOG_CFG
Definition: manager.cpp:31
void purge_addon(const config &removelist)
Removes the listed files from the addon.
Definition: manager.cpp:377
static void unarchive_dir(const std::string &path, const config &cfg, std::function< void()> file_callback={})
Definition: manager.cpp:309
static void archive_dir(const std::string &path, const std::string &dirname, config &cfg, const filesystem::blacklist_pattern_list &ignore_patterns)
Definition: manager.cpp:274
void archive_addon(const std::string &addon_name, config &cfg)
Archives an add-on into a config object for campaignd transactions.
Definition: manager.cpp:296
static void purge_dir(const std::string &path, const config &removelist)
Definition: manager.cpp:352
void get_addon_install_info(const std::string &addon_name, config &cfg)
Gets the installation info (_info.cfg) for an add-on.
Definition: manager.cpp:107
void write_addon_install_info(const std::string &addon_name, const config &cfg)
Definition: manager.cpp:127
static lg::log_domain log_network("network")
static unsigned count_pack_files(const config &cfg)
Definition: manager.cpp:331
#define WRN_CFG
Definition: manager.cpp:32
static std::string strip_cr(std::string str, bool strip)
Definition: manager.cpp:231
std::map< std::string, std::string > installed_addons_and_versions()
Retrieves the ids and versions of all installed add-ons.
Definition: manager.cpp:197
bool have_addon_pbl_info(const std::string &addon_name)
Returns whether a .pbl file is present for the specified add-on or not.
Definition: manager.cpp:66
std::vector< std::string > available_addons()
Returns a list of local add-ons that can be published.
Definition: manager.cpp:187
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:192
bool have_addon_install_info(const std::string &addon_name)
Returns true if there is a local installation info (_info.cfg) file for the add-on.
Definition: manager.cpp:102
static void unarchive_file(const std::string &path, const config &cfg)
Definition: manager.cpp:304
void refresh_addon_version_info_cache()
Refreshes the per-session cache of add-on's version information structs.
Definition: manager.cpp:387
version_info get_addon_version_info(const std::string &addon)
Returns a particular installed add-on's version information.
Definition: manager.cpp:428
bool is_addon_installed(const std::string &addon_name)
Check whether the specified add-on is currently installed.
Definition: manager.cpp:220
static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string &addon_name)
Definition: manager.cpp:239
static lg::log_domain log_config("config")
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
std::size_t child_count(config_key_type key) const
Definition: config.cpp:296
child_itors child_range(config_key_type key)
Definition: config.cpp:272
bool empty() const
Definition: config.cpp:849
void clear()
Definition: config.cpp:828
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:384
config & add_child(config_key_type key)
Definition: config.cpp:440
bool match_file(const std::string &name) const
void add_file_pattern(const std::string &pattern)
Definition: filesystem.hpp:98
void add_directory_pattern(const std::string &pattern)
Definition: filesystem.hpp:103
bool match_dir(const std::string &name) const
Represents version numbers.
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1028
Interfaces for manipulating version numbers of engine, add-ons, etc.
Standard logging facilities (interface).
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:180
int dir_size(const std::string &pname)
Returns the sum of the sizes of the files contained in a directory.
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:444
bool is_cfg(const std::string &filename)
Returns true if the file ends with the wmlfile extension.
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:325
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
bool delete_directory(const std::string &dirname, const bool keep_pbl)
utils::optional< std::string > get_wml_location(const std::string &path, const utils::optional< std::string > &current_dir)
Returns a translated path to the actual file or directory, if it exists.
std::string read_file(const std::string &fname)
Basic disk I/O - read file.
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:53
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode)
Throws io_exception if an error occurs.
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:54
bool looks_like_pbl(const std::string &file)
bool make_directory(const std::string &dirname)
const blacklist_pattern_list default_blacklist
Definition: filesystem.cpp:264
std::string get_addons_dir()
std::string sanitize_path(const std::string &path)
Sanitizes a path to remove references to the user's name.
std::string path
Definition: filesystem.cpp:90
logger & info()
Definition: log.cpp:319
void trim(std::string_view &s)
void erase_if(Container &container, const Predicate &predicate)
Convenience wrapper for using std::remove_if on a container.
Definition: general.hpp:100
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:109
One of the realizations of serialization/validator.hpp abstract validator.
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:622
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:759
Exception thrown when the WML parser fails to read a .pbl file.
Definition: manager.hpp:45
Helper class, don't construct this directly.
mock_char c
std::string unencode_binary(const std::string &str)
Definition: validation.cpp:236
std::string encode_binary(const std::string &str)
Definition: validation.cpp:218
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define d
#define e
#define f