The Battle for Wesnoth  1.19.0-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"));
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  const bool is_cfg = (fname.size() > 4 ? (fname.substr(fname.size() - 4) == ".cfg") : false);
272  cfg["contents"] = encode_binary(strip_cr(filesystem::read_file(path + '/' + fname),is_cfg));
273 }
274 
275 static void archive_dir(const std::string& path, const std::string& dirname, config& cfg, const filesystem::blacklist_pattern_list& ignore_patterns)
276 {
277  cfg["name"] = dirname;
278  const std::string dir = path + '/' + dirname;
279 
280  std::vector<std::string> files, dirs;
281  filesystem::get_files_in_dir(dir,&files,&dirs);
282  for(const std::string& name : files) {
283  bool valid = !filesystem::looks_like_pbl(name) && !ignore_patterns.match_file(name);
284  if (valid) {
285  archive_file(dir,name,cfg.add_child("file"));
286  }
287  }
288 
289  for(const std::string& name : dirs) {
290  bool valid = !ignore_patterns.match_dir(name);
291  if (valid) {
292  archive_dir(dir,name,cfg.add_child("dir"),ignore_patterns);
293  }
294  }
295 }
296 
297 void archive_addon(const std::string& addon_name, config& cfg)
298 {
299  const std::string parentd = filesystem::get_addons_dir();
300 
301  filesystem::blacklist_pattern_list ignore_patterns(read_ignore_patterns(addon_name));
302  archive_dir(parentd, addon_name, cfg.add_child("dir"), ignore_patterns);
303 }
304 
305 static void unarchive_file(const std::string& path, const config& cfg)
306 {
307  filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
308 }
309 
310 static void unarchive_dir(const std::string& path, const config& cfg, std::function<void()> file_callback = {})
311 {
312  std::string dir;
313  if (cfg["name"].empty())
314  dir = path;
315  else
316  dir = path + '/' + cfg["name"].str();
317 
319 
320  for(const config &d : cfg.child_range("dir")) {
321  unarchive_dir(dir, d, file_callback);
322  }
323 
324  for(const config &f : cfg.child_range("file")) {
325  unarchive_file(dir, f);
326  if(file_callback) {
327  file_callback();
328  }
329  }
330 }
331 
332 static unsigned count_pack_files(const config& cfg)
333 {
334  unsigned count = 0;
335 
336  for(const config& d : cfg.child_range("dir")) {
337  count += count_pack_files(d);
338  }
339 
340  return count + cfg.child_count("file");
341 }
342 
343 void unarchive_addon(const config& cfg, std::function<void(unsigned)> progress_callback)
344 {
345  const std::string parentd = filesystem::get_addons_dir();
346  unsigned file_count = progress_callback ? count_pack_files(cfg) : 0, done = 0;
347  auto file_callback = progress_callback
348  ? [&]() { progress_callback(++done * 100.0 / file_count); }
349  : std::function<void()>{};
350  unarchive_dir(parentd, cfg, file_callback);
351 }
352 
353 static void purge_dir(const std::string& path, const config& removelist)
354 {
355  std::string dir;
356  if(removelist["name"].empty())
357  dir = path;
358  else
359  dir = path + '/' + removelist["name"].str();
360 
361  if(!filesystem::is_directory(dir)) {
362  return;
363  }
364 
365  for(const config& d : removelist.child_range("dir")) {
366  purge_dir(dir, d);
367  }
368 
369  for(const config& f : removelist.child_range("file")) {
370  filesystem::delete_file(dir + '/' + f["name"].str());
371  }
372 
373  if(filesystem::dir_size(dir) < 1) {
375  }
376 }
377 
378 void purge_addon(const config& removelist)
379 {
380  const std::string parentd = filesystem::get_addons_dir();
381  purge_dir(parentd, removelist);
382 }
383 
384 namespace {
385  std::map< std::string, version_info > version_info_cache;
386 } // end unnamed namespace 5
387 
389 {
390  version_info_cache.clear();
391 
392  LOG_CFG << "refreshing add-on versions cache";
393 
394  const std::vector<std::string>& addons = installed_addons();
395  if(addons.empty()) {
396  return;
397  }
398 
399  std::vector<std::string> addon_info_files(addons.size());
400 
401  std::transform(addons.begin(), addons.end(),
402  addon_info_files.begin(), get_info_file_path);
403 
404  for(std::size_t i = 0; i < addon_info_files.size(); ++i) {
405  assert(i < addons.size());
406 
407  const std::string& addon = addons[i];
408  const std::string& info_file = addon_info_files[i];
409 
410  if(filesystem::file_exists(info_file)) {
411  config info_cfg;
412  get_addon_install_info(addon, info_cfg);
413 
414  if(info_cfg.empty()) {
415  continue;
416  }
417 
418  const std::string& version = info_cfg["version"].str();
419  LOG_CFG << "cached add-on version: " << addon << " [" << version << "]";
420 
421  version_info_cache[addon] = version;
422  } else if (!have_addon_pbl_info(addon) && !have_addon_in_vcs_tree(addon)) {
423  // Don't print the warning if the user is clearly the author
424  WRN_CFG << "add-on '" << addon << "' has no _info.cfg; cannot read version info";
425  }
426  }
427 }
428 
429 version_info get_addon_version_info(const std::string& addon)
430 {
431  static const version_info nil;
432  std::map< std::string, version_info >::iterator entry = version_info_cache.find(addon);
433  return entry != version_info_cache.end() ? entry->second : nil;
434 }
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:343
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:378
static void unarchive_dir(const std::string &path, const config &cfg, std::function< void()> file_callback={})
Definition: manager.cpp:310
static void archive_dir(const std::string &path, const std::string &dirname, config &cfg, const filesystem::blacklist_pattern_list &ignore_patterns)
Definition: manager.cpp:275
void archive_addon(const std::string &addon_name, config &cfg)
Archives an add-on into a config object for campaignd transactions.
Definition: manager.cpp:297
static void purge_dir(const std::string &path, const config &removelist)
Definition: manager.cpp:353
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:332
#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:305
void refresh_addon_version_info_cache()
Refreshes the per-session cache of add-on's version information structs.
Definition: manager.cpp:388
version_info get_addon_version_info(const std::string &addon)
Returns a particular installed add-on's version information.
Definition: manager.cpp:429
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:159
std::size_t child_count(config_key_type key) const
Definition: config.cpp:297
child_itors child_range(config_key_type key)
Definition: config.cpp:273
bool empty() const
Definition: config.cpp:852
void clear()
Definition: config.cpp:831
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:687
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Euivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:385
config & add_child(config_key_type key)
Definition: config.cpp:441
bool match_file(const std::string &name) const
void add_file_pattern(const std::string &pattern)
Definition: filesystem.hpp:90
void add_directory_pattern(const std::string &pattern)
Definition: filesystem.hpp:95
bool match_dir(const std::string &name) const
Represents version numbers.
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
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:404
bool delete_file(const std::string &filename)
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:318
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)
std::string get_wml_location(const std::string &filename, const std::string &current_dir)
Returns a complete path to the actual WML file or directory or an empty string if the file isn't pres...
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:50
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:51
bool looks_like_pbl(const std::string &file)
bool make_directory(const std::string &dirname)
const blacklist_pattern_list default_blacklist
Definition: filesystem.cpp:257
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:83
logger & info()
Definition: log.cpp:314
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:627
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:764
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