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