The Battle for Wesnoth  1.19.4+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 "game_version.hpp"
24 #include "wml_exception.hpp"
25 
26 #include <boost/algorithm/string.hpp>
27 
28 static lg::log_domain log_config("config");
29 #define ERR_CFG LOG_STREAM(err , log_config)
30 #define LOG_CFG LOG_STREAM(info, log_config)
31 #define WRN_CFG LOG_STREAM(warn, log_config)
32 
33 static lg::log_domain log_filesystem("filesystem");
34 #define ERR_FS LOG_STREAM(err , log_filesystem)
35 
36 static lg::log_domain log_network("network");
37 #define ERR_NET LOG_STREAM(err , log_network)
38 #define LOG_NET LOG_STREAM(info, log_network)
39 
40 namespace {
41  std::string get_pbl_file_path(const std::string& addon_name)
42  {
43  const std::string& parentd = filesystem::get_addons_dir();
44  // Allow .pbl files directly in the addon dir
45  const std::string exterior = parentd + "/" + addon_name + ".pbl";
46  const std::string interior = parentd + "/" + addon_name + "/_server.pbl";
47  return filesystem::file_exists(exterior) ? exterior : interior;
48  }
49 
50  inline std::string get_info_file_path(const std::string& addon_name)
51  {
52  return filesystem::get_addons_dir() + "/" + addon_name + "/_info.cfg";
53  }
54 }
55 
56 bool have_addon_in_vcs_tree(const std::string& addon_name)
57 {
58  static const std::string parentd = filesystem::get_addons_dir();
59  return
60  filesystem::file_exists(parentd+"/"+addon_name+"/.svn") ||
61  filesystem::file_exists(parentd+"/"+addon_name+"/.git") ||
62  filesystem::file_exists(parentd+"/"+addon_name+"/.hg");
63 }
64 
65 bool have_addon_pbl_info(const std::string& addon_name)
66 {
67  return filesystem::file_exists(get_pbl_file_path(addon_name));
68 }
69 
70 config get_addon_pbl_info(const std::string& addon_name, bool do_validate)
71 {
72  config cfg;
73  const std::string& pbl_path = get_pbl_file_path(addon_name);
74  try {
76  std::unique_ptr<schema_validation::schema_validator> validator;
77  if(do_validate) {
78  validator = std::make_unique<schema_validation::schema_validator>(filesystem::get_wml_location("schema/pbl.cfg").value());
79  validator->set_create_exceptions(true);
80  }
81  read(cfg, *stream, validator.get());
82  } catch(const config::error& e) {
83  throw invalid_pbl_exception(pbl_path, e.message);
84  } catch(wml_exception& e) {
85  auto msg = e.user_message;
86  e.user_message += " in " + addon_name;
87  boost::replace_all(e.dev_message, "<unknown>", filesystem::sanitize_path(pbl_path));
88  e.show();
89  throw invalid_pbl_exception(pbl_path, msg);
90  }
91 
92  return cfg;
93 }
94 
95 void set_addon_pbl_info(const std::string& addon_name, const config& cfg)
96 {
97  filesystem::scoped_ostream stream = filesystem::ostream_file(get_pbl_file_path(addon_name));
98  write(*stream, cfg);
99 }
100 
101 bool have_addon_install_info(const std::string& addon_name)
102 {
103  return filesystem::file_exists(get_info_file_path(addon_name));
104 }
105 
106 void get_addon_install_info(const std::string& addon_name, config& cfg)
107 {
108  const std::string& info_path = get_info_file_path(addon_name);
110  try {
111  // The parser's read() API would normally do this at the start. This
112  // is a safeguard in case read() throws later
113  cfg.clear();
114  config envelope;
115  read(envelope, *stream);
116  if(auto info = envelope.optional_child("info")) {
117  cfg = std::move(*info);
118  }
119  } catch(const config::error& e) {
120  ERR_CFG << "Failed to read add-on installation information for '"
121  << addon_name << "' from " << info_path << ":\n"
122  << e.message;
123  }
124 }
125 
126 void write_addon_install_info(const std::string& addon_name, const config& cfg)
127 {
128  LOG_CFG << "Writing version info for add-on '" << addon_name << "'";
129 
130  const auto& info_path = get_info_file_path(addon_name);
131  auto out = filesystem::ostream_file(info_path);
132 
133  *out << "#\n"
134  << "# File automatically generated by Wesnoth to keep track\n"
135  << "# of version information on installed add-ons. DO NOT EDIT!\n"
136  << "#\n";
137 
138  config envelope;
139  envelope.add_child("info", cfg);
140  write(*out, envelope);
141 }
142 
143 bool remove_local_addon(const std::string& addon)
144 {
145  const std::string addon_dir = filesystem::get_addons_dir() + "/" + addon;
146 
147  LOG_CFG << "removing local add-on: " << addon;
148 
149  if(filesystem::file_exists(addon_dir) && !filesystem::delete_directory(addon_dir, true)) {
150  ERR_CFG << "Failed to delete directory/file: " << addon_dir;
151  ERR_CFG << "removal of add-on " << addon << " failed!";
152  return false;
153  }
154  return true;
155 }
156 
157 namespace {
158 
159 enum ADDON_ENUM_CRITERIA
160 {
161  ADDON_ANY,
162  ADDON_HAS_PBL,
163 };
164 
165 std::vector<std::string> enumerate_addons_internal(ADDON_ENUM_CRITERIA filter)
166 {
167  std::vector<std::string> res;
168  std::vector<std::string> addon_dirnames;
169 
170  const auto& addons_root = filesystem::get_addons_dir();
171  filesystem::get_files_in_dir(addons_root, nullptr, &addon_dirnames);
172 
173  for(const auto& addon_name : addon_dirnames) {
174  if(filesystem::file_exists(addons_root + "/" + addon_name + "/_main.cfg") &&
175  (filter != ADDON_HAS_PBL || have_addon_pbl_info(addon_name)))
176  {
177  res.emplace_back(addon_name);
178  }
179  }
180 
181  return res;
182 }
183 
184 }
185 
186 std::vector<std::string> available_addons()
187 {
188  return enumerate_addons_internal(ADDON_HAS_PBL);
189 }
190 
191 std::vector<std::string> installed_addons()
192 {
193  return enumerate_addons_internal(ADDON_ANY);
194 }
195 
196 std::map<std::string, std::string> installed_addons_and_versions()
197 {
198  std::map<std::string, std::string> addons;
199 
200  for(const std::string& addon_id : installed_addons()) {
201  if(have_addon_pbl_info(addon_id)) {
202  try {
203  // Just grabbing the version, so don't bother validating the pbl
204  addons[addon_id] = get_addon_pbl_info(addon_id, false)["version"].str();
205  } catch(const invalid_pbl_exception&) {
206  addons[addon_id] = "Invalid pbl file, version unknown";
207  }
208  } else if(filesystem::file_exists(get_info_file_path(addon_id))) {
209  config info_cfg;
210  get_addon_install_info(addon_id, info_cfg);
211  addons[addon_id] = !info_cfg.empty() ? info_cfg["version"].str() : "Unknown";
212  } else {
213  addons[addon_id] = "Unknown";
214  }
215  }
216  return addons;
217 }
218 
219 bool is_addon_installed(const std::string& addon_name)
220 {
221  const std::string namestem = filesystem::get_addons_dir() + "/" + addon_name;
222  return filesystem::file_exists(namestem + "/_main.cfg");
223 }
224 
225 static inline bool IsCR(const char& c)
226 {
227  return c == '\x0D';
228 }
229 
230 static std::string strip_cr(std::string str, bool strip)
231 {
232  if(!strip)
233  return str;
234  std::string::iterator new_end = std::remove_if(str.begin(), str.end(), IsCR);
235  str.erase(new_end, str.end());
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:143
static bool IsCR(const char &c)
Definition: manager.cpp:225
config get_addon_pbl_info(const std::string &addon_name, bool do_validate)
Gets the publish information for an add-on.
Definition: manager.cpp:70
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:56
void set_addon_pbl_info(const std::string &addon_name, const config &cfg)
Definition: manager.cpp:95
static lg::log_domain log_filesystem("filesystem")
#define ERR_CFG
Definition: manager.cpp:29
#define LOG_CFG
Definition: manager.cpp:30
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:106
void write_addon_install_info(const std::string &addon_name, const config &cfg)
Definition: manager.cpp:126
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:31
static std::string strip_cr(std::string str, bool strip)
Definition: manager.cpp:230
std::map< std::string, std::string > installed_addons_and_versions()
Retrieves the ids and versions of all installed add-ons.
Definition: manager.cpp:196
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:65
std::vector< std::string > available_addons()
Returns a list of local add-ons that can be published.
Definition: manager.cpp:186
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:191
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:101
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:219
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:163
std::size_t child_count(config_key_type key) const
Definition: config.cpp:295
child_itors child_range(config_key_type key)
Definition: config.cpp:271
bool empty() const
Definition: config.cpp:853
void clear()
Definition: config.cpp:832
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:383
config & add_child(config_key_type key)
Definition: config.cpp:439
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:1023
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:316
void trim(std::string_view &s)
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:237
std::string encode_binary(const std::string &str)
Definition: validation.cpp:219
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