The Battle for Wesnoth  1.19.7+dev
filesystem.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
3  by David White <dave@whitevine.net>
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 /**
17  * @file
18  * File-IO
19  */
20 #define GETTEXT_DOMAIN "wesnoth-lib"
21 
22 #include "filesystem.hpp"
23 
24 #include "config.hpp"
25 #include "gettext.hpp"
26 #include "log.hpp"
27 #include "serialization/base64.hpp"
30 #include "utils/general.hpp"
31 
32 #include <boost/algorithm/string/predicate.hpp>
33 #include <boost/filesystem.hpp>
34 #include <boost/filesystem/fstream.hpp>
35 #include <boost/iostreams/device/file_descriptor.hpp>
36 #include <boost/iostreams/stream.hpp>
37 #include <boost/process.hpp>
38 #include "game_config_view.hpp"
39 
40 #ifdef _WIN32
41 #include <boost/locale.hpp>
42 
43 #include <windows.h>
44 #include <shlobj.h>
45 #include <shlwapi.h>
46 
47 // Work around TDM-GCC not #defining this according to @newfrenchy83.
48 #ifndef VOLUME_NAME_NONE
49 #define VOLUME_NAME_NONE 0x4
50 #endif
51 
52 #endif /* !_WIN32 */
53 
54 #ifdef __APPLE__
55 #include <mach-o/dyld.h>
56 #include <limits.h>
57 #endif
58 
59 #include <algorithm>
60 #include <cstdlib>
61 #include <set>
62 #include <utility>
63 
64 // Copied from boost::predef, as it's there only since 1.55.
65 #if defined(__APPLE__) && defined(__MACH__) && defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__)
66 
67 #define WESNOTH_BOOST_OS_IOS (__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__*1000)
68 #include <SDL2/SDL_filesystem.h>
69 
70 #endif
71 
72 
73 static lg::log_domain log_filesystem("filesystem");
74 #define DBG_FS LOG_STREAM(debug, log_filesystem)
75 #define LOG_FS LOG_STREAM(info, log_filesystem)
76 #define WRN_FS LOG_STREAM(warn, log_filesystem)
77 #define ERR_FS LOG_STREAM(err, log_filesystem)
78 
79 namespace bp = boost::process;
80 namespace bfs = boost::filesystem;
81 using boost::system::error_code;
82 
83 namespace game_config
84 {
85 //
86 // Path info
87 //
88 #ifdef WESNOTH_PATH
89 std::string path = WESNOTH_PATH;
90 #else
91 std::string path = "";
92 #endif
93 
94 #ifdef DEFAULT_PREFS_PATH
95 std::string default_preferences_path = DEFAULT_PREFS_PATH;
96 #else
97 std::string default_preferences_path = "";
98 #endif
99 bool check_migration = false;
100 
101 const std::string observer_team_name = "observer";
102 
104 }
105 
106 namespace
107 {
108 // These are the filenames that get special processing
109 const std::string maincfg_filename = "_main.cfg";
110 const std::string finalcfg_filename = "_final.cfg";
111 const std::string initialcfg_filename = "_initial.cfg";
112 
113 // only used by windows but put outside the ifdef to let it check by ci build.
114 class customcodecvt : public std::codecvt<wchar_t /*intern*/, char /*extern*/, std::mbstate_t>
115 {
116 private:
117  // private static helper things
118  template<typename char_t_to>
119  struct customcodecvt_do_conversion_writer
120  {
121  customcodecvt_do_conversion_writer(char_t_to*& _to_next, char_t_to* _to_end)
122  : to_next(_to_next)
123  , to_end(_to_end)
124  {
125  }
126 
127  char_t_to*& to_next;
128  char_t_to* to_end;
129 
130  bool can_push(std::size_t count) const
131  {
132  return static_cast<std::size_t>(to_end - to_next) > count;
133  }
134 
135  void push(char_t_to val)
136  {
137  assert(to_next != to_end);
138  *to_next++ = val;
139  }
140  };
141 
142  template<typename char_t_from, typename char_t_to>
143  static void customcodecvt_do_conversion(std::mbstate_t& /*state*/,
144  const char_t_from* from,
145  const char_t_from* from_end,
146  const char_t_from*& from_next,
147  char_t_to* to,
148  char_t_to* to_end,
149  char_t_to*& to_next)
150  {
151  typedef typename ucs4_convert_impl::convert_impl<char_t_from>::type impl_type_from;
152  typedef typename ucs4_convert_impl::convert_impl<char_t_to>::type impl_type_to;
153 
154  from_next = from;
155  to_next = to;
156  customcodecvt_do_conversion_writer<char_t_to> writer(to_next, to_end);
157 
158  while(from_next != from_end) {
159  impl_type_to::write(writer, impl_type_from::read(from_next, from_end));
160  }
161  }
162 
163 public:
164  // Not used by boost filesystem
165  int do_encoding() const noexcept
166  {
167  return 0;
168  }
169 
170  // Not used by boost filesystem
171  bool do_always_noconv() const noexcept
172  {
173  return false;
174  }
175 
176  int do_length(std::mbstate_t& /*state*/, const char* /*from*/, const char* /*from_end*/, std::size_t /*max*/) const
177  {
178  // Not used by boost filesystem
179  throw "Not supported";
180  }
181 
182  std::codecvt_base::result unshift(
183  std::mbstate_t& /*state*/, char* /*to*/, char* /*to_end*/, char*& /*to_next*/) const
184  {
185  // Not used by boost filesystem
186  throw "Not supported";
187  }
188 
189  // there are still some methods which could be implemented but aren't because boost filesystem won't use them.
190  std::codecvt_base::result do_in(std::mbstate_t& state,
191  const char* from,
192  const char* from_end,
193  const char*& from_next,
194  wchar_t* to,
195  wchar_t* to_end,
196  wchar_t*& to_next) const
197  {
198  try {
199  customcodecvt_do_conversion<char, wchar_t>(state, from, from_end, from_next, to, to_end, to_next);
200  } catch(...) {
201  ERR_FS << "Invalid UTF-8 string'" << std::string(from, from_end) << "' with exception: " << utils::get_unknown_exception_type();
202  return std::codecvt_base::error;
203  }
204 
205  return std::codecvt_base::ok;
206  }
207 
208  std::codecvt_base::result do_out(std::mbstate_t& state,
209  const wchar_t* from,
210  const wchar_t* from_end,
211  const wchar_t*& from_next,
212  char* to,
213  char* to_end,
214  char*& to_next) const
215  {
216  try {
217  customcodecvt_do_conversion<wchar_t, char>(state, from, from_end, from_next, to, to_end, to_next);
218  } catch(...) {
219  ERR_FS << "Invalid UTF-16 string with exception: " << utils::get_unknown_exception_type();
220  return std::codecvt_base::error;
221  }
222 
223  return std::codecvt_base::ok;
224  }
225 };
226 
227 #ifdef _WIN32
228 class static_runner
229 {
230 public:
231  static_runner()
232  {
233  // Boost uses the current locale to generate a UTF-8 one
234  std::locale utf8_loc = boost::locale::generator().generate("");
235 
236  // use a custom locale because we want to use out log.hpp functions in case of an invalid string.
237  utf8_loc = std::locale(utf8_loc, new customcodecvt());
238 
239  boost::filesystem::path::imbue(utf8_loc);
240  }
241 };
242 
243 static static_runner static_bfs_path_imbuer;
244 
245 bool is_filename_case_correct(const std::string& fname, const boost::iostreams::file_descriptor_source& fd)
246 {
247  wchar_t real_path[MAX_PATH];
248  GetFinalPathNameByHandleW(fd.handle(), real_path, MAX_PATH - 1, VOLUME_NAME_NONE);
249 
250  std::string real_name = filesystem::base_name(unicode_cast<std::string>(std::wstring(real_path)));
251  return real_name == filesystem::base_name(fname);
252 }
253 
254 #else
255 bool is_filename_case_correct(const std::string& /*fname*/, const boost::iostreams::file_descriptor_source& /*fd*/)
256 {
257  return true;
258 }
259 #endif
260 } // namespace
261 
262 namespace filesystem
263 {
264 
266  {
267  /* Blacklist dot-files/dirs, which are hidden files in UNIX platforms */
268  ".+",
269  "#*#",
270  "*~",
271  "*-bak",
272  "*.swp",
273  "*.pbl",
274  "*.ign",
275  "_info.cfg",
276  "*.exe",
277  "*.bat",
278  "*.cmd",
279  "*.com",
280  "*.scr",
281  "*.sh",
282  "*.js",
283  "*.vbs",
284  "*.o",
285  "*.ini",
286  /* Remove junk created by certain file manager ;) */
287  "Thumbs.db",
288  /* Eclipse plugin */
289  "*.wesnoth",
290  "*.project",
291  },
292  {
293  ".+",
294  /* macOS metadata-like cruft (http://floatingsun.net/2007/02/07/whats-with-__macosx-in-zip-files/) */
295  "__MACOSX",
296  }
297 };
298 
299 static void push_if_exists(std::vector<std::string>* vec, const bfs::path& file, bool full)
300 {
301  if(vec != nullptr) {
302  if(full) {
303  vec->push_back(file.generic_string());
304  } else {
305  vec->push_back(file.filename().generic_string());
306  }
307  }
308 }
309 
310 static inline bool error_except_not_found(const error_code& ec)
311 {
312  return ec && ec != boost::system::errc::no_such_file_or_directory;
313 }
314 
315 static bool is_directory_internal(const bfs::path& fpath)
316 {
317  error_code ec;
318  bool is_dir = bfs::is_directory(fpath, ec);
319  if(error_except_not_found(ec)) {
320  LOG_FS << "Failed to check if " << fpath.string() << " is a directory: " << ec.message();
321  }
322 
323  return is_dir;
324 }
325 
326 static bool file_exists(const bfs::path& fpath)
327 {
328  error_code ec;
329  bool exists = bfs::exists(fpath, ec);
330  if(error_except_not_found(ec)) {
331  ERR_FS << "Failed to check existence of file " << fpath.string() << ": " << ec.message();
332  }
333 
334  return exists;
335 }
336 
337 static bfs::path get_dir(const bfs::path& dirpath)
338 {
339  bool is_dir = is_directory_internal(dirpath);
340  if(!is_dir) {
341  error_code ec;
342  bfs::create_directory(dirpath, ec);
343 
344  if(ec) {
345  ERR_FS << "Failed to create directory " << dirpath.string() << ": " << ec.message();
346  }
347 
348  // This is probably redundant
349  is_dir = is_directory_internal(dirpath);
350  }
351 
352  if(!is_dir) {
353  ERR_FS << "Could not open or create directory " << dirpath.string();
354  return std::string();
355  }
356 
357  return dirpath;
358 }
359 
360 static bool create_directory_if_missing(const bfs::path& dirpath)
361 {
362  error_code ec;
363  bfs::file_status fs = bfs::status(dirpath, ec);
364 
365  if(error_except_not_found(ec)) {
366  ERR_FS << "Failed to retrieve file status for " << dirpath.string() << ": " << ec.message();
367  return false;
368  } else if(bfs::is_directory(fs)) {
369  DBG_FS << "directory " << dirpath.string() << " exists, not creating";
370  return true;
371  } else if(bfs::exists(fs)) {
372  ERR_FS << "cannot create directory " << dirpath.string() << "; file exists";
373  return false;
374  }
375 
376  bool created = bfs::create_directory(dirpath, ec);
377  if(ec) {
378  ERR_FS << "Failed to create directory " << dirpath.string() << ": " << ec.message();
379  }
380 
381  return created;
382 }
383 
385 {
386  DBG_FS << "creating recursive directory: " << dirpath.string();
387 
388  if(dirpath.empty()) {
389  return false;
390  }
391 
392  error_code ec;
393  bfs::file_status fs = bfs::status(dirpath);
394 
395  if(error_except_not_found(ec)) {
396  ERR_FS << "Failed to retrieve file status for " << dirpath.string() << ": " << ec.message();
397  return false;
398  } else if(bfs::is_directory(fs)) {
399  return true;
400  } else if(bfs::exists(fs)) {
401  return false;
402  }
403 
404  if(!dirpath.has_parent_path() || create_directory_if_missing_recursive(dirpath.parent_path())) {
405  return create_directory_if_missing(dirpath);
406  } else {
407  ERR_FS << "Could not create parents to " << dirpath.string();
408  return false;
409  }
410 }
411 
412 static bool check_prefix(bfs::path::iterator& fi, const bfs::path::iterator& fe, const bfs::path& prefix)
413 {
414  bfs::path::iterator pi = prefix.begin(), pe = prefix.end();
415  while(fi != fe && pi != pe && *fi == *pi) {
416  ++fi;
417  ++pi;
418  }
419 
420  return pi == pe;
421 }
422 
423 static bool is_prefix(const bfs::path& full, const bfs::path& prefix_path)
424 {
425  bfs::path::iterator fi = full.begin();
426  return check_prefix(fi, full.end(), prefix_path);
427 }
428 
429 static bfs::path subtract_path(const bfs::path& full, const bfs::path& prefix_path)
430 {
431  bfs::path rest;
432  bfs::path::iterator fi = full.begin(), fe = full.end();
433  if(!check_prefix(fi, fe, prefix_path)) {
434  return rest;
435  }
436 
437  while(fi != fe) {
438  rest /= *fi;
439  ++fi;
440  }
441 
442  return rest;
443 }
444 
445 void get_files_in_dir(const std::string& dir,
446  std::vector<std::string>* files,
447  std::vector<std::string>* dirs,
448  name_mode mode,
449  filter_mode filter,
450  reorder_mode reorder,
451  file_tree_checksum* checksum)
452 {
453  if(bfs::path(dir).is_relative() && !game_config::path.empty()) {
454  bfs::path absolute_dir(game_config::path);
455  absolute_dir /= dir;
456 
457  if(is_directory_internal(absolute_dir)) {
458  get_files_in_dir(absolute_dir.string(), files, dirs, mode, filter, reorder, checksum);
459  return;
460  }
461  }
462 
463  const bfs::path dirpath(dir);
464 
465  if(reorder == reorder_mode::DO_REORDER) {
466  LOG_FS << "searching for _main.cfg in directory " << dir;
467  const bfs::path maincfg = dirpath / maincfg_filename;
468 
469  if(file_exists(maincfg)) {
470  LOG_FS << "_main.cfg found : " << maincfg;
471  push_if_exists(files, maincfg, mode == name_mode::ENTIRE_FILE_PATH);
472  return;
473  }
474  }
475 
476  error_code ec;
477  bfs::directory_iterator di(dirpath, ec);
478  bfs::directory_iterator end;
479 
480  // Probably not a directory, let the caller deal with it.
481  if(ec) {
482  return;
483  }
484 
485  for(; di != end; ++di) {
486  ec.clear();
487  bfs::file_status st = di->status(ec);
488  if(ec) {
489  LOG_FS << "Failed to get file status of " << di->path().string() << ": " << ec.message();
490  continue;
491  }
492 
493  if(st.type() == bfs::regular_file) {
494  {
495  std::string basename = di->path().filename().string();
496  if(filter == filter_mode::SKIP_PBL_FILES && looks_like_pbl(basename))
497  continue;
498  if(!basename.empty() && basename[0] == '.')
499  continue;
500  }
501 
502  push_if_exists(files, di->path(), mode == name_mode::ENTIRE_FILE_PATH);
503 
504  if(checksum != nullptr) {
505  std::time_t mtime = bfs::last_write_time(di->path(), ec);
506  if(ec) {
507  LOG_FS << "Failed to read modification time of " << di->path().string() << ": " << ec.message();
508  } else if(mtime > checksum->modified) {
509  checksum->modified = mtime;
510  }
511 
512  uintmax_t size = bfs::file_size(di->path(), ec);
513  if(ec) {
514  LOG_FS << "Failed to read filesize of " << di->path().string() << ": " << ec.message();
515  } else {
516  checksum->sum_size += size;
517  }
518 
519  checksum->nfiles++;
520  }
521  } else if(st.type() == bfs::directory_file) {
522  std::string basename = di->path().filename().string();
523 
524  if(!basename.empty() && basename[0] == '.') {
525  continue;
526  }
527 
528  if(filter == filter_mode::SKIP_MEDIA_DIR && (basename == "images" || basename == "sounds")) {
529  continue;
530  }
531 
532  const bfs::path inner_main(di->path() / maincfg_filename);
533  bfs::file_status main_st = bfs::status(inner_main, ec);
534 
535  if(error_except_not_found(ec)) {
536  LOG_FS << "Failed to get file status of " << inner_main.string() << ": " << ec.message();
537  } else if(reorder == reorder_mode::DO_REORDER && main_st.type() == bfs::regular_file) {
538  LOG_FS << "_main.cfg found : "
539  << (mode == name_mode::ENTIRE_FILE_PATH ? inner_main.string() : inner_main.filename().string());
540  push_if_exists(files, inner_main, mode == name_mode::ENTIRE_FILE_PATH);
541  } else {
542  push_if_exists(dirs, di->path(), mode == name_mode::ENTIRE_FILE_PATH);
543  }
544  }
545  }
546 
547  if(files != nullptr) {
548  std::sort(files->begin(), files->end());
549  }
550 
551  if(dirs != nullptr) {
552  std::sort(dirs->begin(), dirs->end());
553  }
554 
555  if(files != nullptr && reorder == reorder_mode::DO_REORDER) {
556  // move finalcfg_filename, if present, to the end of the vector
557  for(unsigned int i = 0; i < files->size(); i++) {
558  if(boost::algorithm::ends_with((*files)[i], "/" + finalcfg_filename)) {
559  files->push_back((*files)[i]);
560  files->erase(files->begin() + i);
561  break;
562  }
563  }
564 
565  // move initialcfg_filename, if present, to the beginning of the vector
566  int foundit = -1;
567  for(unsigned int i = 0; i < files->size(); i++)
568  if(boost::algorithm::ends_with((*files)[i], "/" + initialcfg_filename)) {
569  foundit = i;
570  break;
571  }
572  if(foundit > 0) {
573  std::string initialcfg = (*files)[foundit];
574  for(unsigned int i = foundit; i > 0; i--)
575  (*files)[i] = (*files)[i - 1];
576  (*files)[0] = initialcfg;
577  }
578  }
579 }
580 
581 std::string get_dir(const std::string& dir)
582 {
583  return get_dir(bfs::path(dir)).string();
584 }
585 
586 std::string get_next_filename(const std::string& name, const std::string& extension)
587 {
588  std::string next_filename;
589  int counter = 0;
590 
591  do {
592  std::stringstream filename;
593 
594  filename << name;
595  filename.width(3);
596  filename.fill('0');
597  filename.setf(std::ios_base::right);
598  filename << counter << extension;
599 
600  counter++;
601  next_filename = filename.str();
602  } while(file_exists(next_filename) && counter < 1000);
603 
604  return next_filename;
605 }
606 
608 
610 {
611  return !user_data_dir.string().empty();
612 }
613 
614 const std::string get_version_path_suffix(const version_info& version)
615 {
616  std::ostringstream s;
617  s << version.major_version() << '.' << version.minor_version();
618  return s.str();
619 }
620 
621 const std::string& get_version_path_suffix()
622 {
623  static std::string suffix;
624 
625  // We only really need to generate this once since
626  // the version number cannot change during runtime.
627 
628  if(suffix.empty()) {
630  }
631 
632  return suffix;
633 }
634 
635 #if defined(__APPLE__) && !defined(__IPHONEOS__)
636  // Starting from Wesnoth 1.14.6, we have to use sandboxing function on macOS
637  // The problem is, that only signed builds can use sandbox. Unsigned builds
638  // would use other config directory then signed ones. So if we don't want
639  // to have two separate config dirs, we have to create symlink to new config
640  // location if exists. This part of code is only required on macOS.
641  static void migrate_apple_config_directory_for_unsandboxed_builds()
642  {
643  const char* home_str = getenv("HOME");
644  bfs::path home = home_str ? home_str : ".";
645 
646  // We don't know which of the two is in PREFERENCES_DIR now.
647  boost::filesystem::path old_saves_dir = home / "Library/Application Support/Wesnoth_";
648  old_saves_dir += get_version_path_suffix();
649  boost::filesystem::path new_saves_dir = home / "Library/Containers/org.wesnoth.Wesnoth/Data/Library/Application Support/Wesnoth_";
650  new_saves_dir += get_version_path_suffix();
651 
652  if(bfs::is_directory(new_saves_dir)) {
653  if(!bfs::exists(old_saves_dir)) {
654  LOG_FS << "Apple developer's userdata migration: symlinking " << old_saves_dir.string() << " to " << new_saves_dir.string();
655  bfs::create_symlink(new_saves_dir, old_saves_dir);
656  } else if(!bfs::is_symlink(old_saves_dir)) {
657  ERR_FS << "Apple developer's userdata migration: Problem! Old (non-containerized) directory " << old_saves_dir.string() << " is not a symlink. Your savegames are scattered around 2 locations.";
658  }
659  return;
660  }
661  }
662 #endif
663 
664 
665 static void setup_user_data_dir()
666 {
667 #if defined(__APPLE__) && !defined(__IPHONEOS__)
668  migrate_apple_config_directory_for_unsandboxed_builds();
669 #endif
670  if(!file_exists(user_data_dir / "logs")) {
672  }
673 
675  ERR_FS << "could not open or create user data directory at " << user_data_dir.string();
676  return;
677  }
678  // TODO: this may not print the error message if the directory exists but we don't have the proper permissions
679 
680  // Create user data and add-on directories
690 
693  }
694 
696 }
697 
698 #ifdef _WIN32
699 /**
700  * @return the path to the My Games directory on success or an empty string on failure
701  */
702 static utils::optional<std::string> get_games_path()
703 {
704  PWSTR docs_path = nullptr;
705  HRESULT res = SHGetKnownFolderPath(FOLDERID_Documents, KF_FLAG_CREATE, nullptr, &docs_path);
706  utils::optional<std::string> path = utils::nullopt;
707 
708  if(res == S_OK) {
709  bfs::path games_path = bfs::path(docs_path) / "My Games";
710  path = games_path.string();
711  } else {
712  ERR_FS << "Could not determine path to user's Documents folder! (" << std::hex << "0x" << res << std::dec << ") "
713  << "Please report this as a bug.";
714  }
715 
716  CoTaskMemFree(docs_path);
717  return path;
718 }
719 #endif
720 
721 void set_user_data_dir(std::string newprefdir)
722 {
723 #ifdef PREFERENCES_DIR
724  if(newprefdir.empty()) {
725  newprefdir = PREFERENCES_DIR;
726  DBG_FS << "Using PREFERENCES_DIR '" << PREFERENCES_DIR << "'";
727  }
728 #endif
729 
730  // if no custom userdata directory was provided, use appropriate default
731  // next replace ~ with Documents/My Games on windows and $HOME otherwise
732  if(newprefdir.empty()) {
733 #ifdef _WIN32
734  newprefdir = "~/Wesnoth" + get_version_path_suffix();
735 #elif defined(__APPLE__)
736  newprefdir = "~/Library/Application Support/Wesnoth_"+get_version_path_suffix();
737 #elif defined(WESNOTH_BOOST_OS_IOS)
738  char* sdl_pref_path = SDL_GetPrefPath("wesnoth.org", "iWesnoth");
739  if(sdl_pref_path) {
740  newprefdir = std::string(sdl_pref_path);
741  SDL_free(sdl_pref_path);
742  } else {
743  newprefdir = "~/.wesnoth" + get_version_path_suffix();
744  }
745 #else
746  const char* h = std::getenv("HOME");
747  std::string home = h ? h : "";
748  h = std::getenv("XDG_DATA_HOME");
749  std::string xdg_data_home = h ? h : "";
750  if (!xdg_data_home.empty()) {
751  newprefdir = xdg_data_home + "/wesnoth/" + get_version_path_suffix();
752  } else if (!home.empty()) {
753  newprefdir = home + "/.local/share/wesnoth/" + get_version_path_suffix();
754  } else {
755  newprefdir = ".wesnoth" + get_version_path_suffix();
756  }
757 #endif
758  }
759 
760  bfs::path dir;
761  if(newprefdir[0] == '~') {
762 #ifdef _WIN32
763  utils::optional<std::string> games_path = get_games_path();
764  if(games_path) {
765  create_directory_if_missing(*games_path);
766  dir = *games_path;
767  } else {
768  dir = get_cwd();
769  WRN_FS << "Using current directory instead: " << dir.string();
770  }
771 #else
772  const char* h = std::getenv("HOME");
773  std::string home = h ? h : "";
774  if(!home.empty()) {
775  dir = home;
776  } else {
777  dir = get_cwd();
778  ERR_FS << "Unable to determine path to user's HOME.";
779  WRN_FS << "Using current directory instead: " << dir.string();
780  }
781 #endif
782  dir /= newprefdir.substr(1);
783  } else {
784  dir = newprefdir;
785  }
786  user_data_dir = dir;
787  DBG_FS << "userdata dir set to: " << user_data_dir.string();
788 
790  // normalize_path expects the path to exist so calling it after potentially creating it in setup_user_data_dir
791  dir = normalize_path(user_data_dir.string(), true, true);
792  if(!dir.empty()) {
793  user_data_dir = dir;
794  }
795 }
796 
797 bool rename_dir(const std::string& old_dir, const std::string& new_dir)
798 {
799  error_code ec;
800  bfs::rename(old_dir, new_dir, ec);
801 
802  if(ec) {
803  ERR_FS << "Failed to rename directory '" << old_dir << "' to '" << new_dir << "'";
804  return false;
805  }
806  return true;
807 }
808 
809 static void set_cache_path(bfs::path newcache)
810 {
811  cache_dir = std::move(newcache);
813  ERR_FS << "could not open or create cache directory at " << cache_dir.string() << '\n';
814  }
815 }
816 
817 void set_cache_dir(const std::string& newcachedir)
818 {
819  set_cache_path(newcachedir);
820 }
821 
823 {
824  assert(!user_data_dir.empty() && "Attempted to access userdata location before userdata initialization!");
825  return user_data_dir;
826 }
827 
828 std::string get_user_data_dir()
829 {
830  return get_user_data_path().string();
831 }
832 
833 std::string get_logs_dir()
834 {
835  return filesystem::get_user_data_dir() + "/logs";
836 }
837 
838 std::string get_cache_dir()
839 {
840  if(cache_dir.empty()) {
841 #if defined(_X11) && !defined(PREFERENCES_DIR)
842  char const* xdg_cache = getenv("XDG_CACHE_HOME");
843 
844  if(!xdg_cache || xdg_cache[0] == '\0') {
845  xdg_cache = getenv("HOME");
846  if(!xdg_cache) {
847  cache_dir = get_dir(get_user_data_path() / "cache");
848  return cache_dir.string();
849  }
850 
851  cache_dir = xdg_cache;
852  cache_dir /= ".cache";
853  } else {
854  cache_dir = xdg_cache;
855  }
856 
857  cache_dir /= "wesnoth";
859 #else
860  cache_dir = get_dir(get_user_data_path() / "cache");
861 #endif
862  }
863 
864  return cache_dir.string();
865 }
866 
867 std::vector<other_version_dir> find_other_version_saves_dirs()
868 {
869 #if !defined(_WIN32) && !defined(_X11) && !defined(__APPLE__)
870  // By all means, this situation doesn't make sense
871  return {};
872 #else
873  const auto& w_ver = game_config::wesnoth_version;
874  const auto& ms_ver = game_config::min_savegame_version;
875 
876  if(w_ver.major_version() != 1 || ms_ver.major_version() != 1) {
877  // Unimplemented, assuming that version 2 won't use WML-based saves
878  return {};
879  }
880 
881  std::vector<other_version_dir> result;
882 
883  // For 1.16, check for saves from all versions up to 1.20.
884  for(auto minor = w_ver.minor_version() + 4; minor >= ms_ver.minor_version(); --minor) {
885  if(minor == w_ver.minor_version())
886  continue;
887 
888  auto version = version_info{};
889  version.set_major_version(w_ver.major_version());
890  version.set_minor_version(minor);
891  auto suffix = get_version_path_suffix(version);
892 
893  bfs::path path;
894 
895  //
896  // NOTE:
897  // This is a bit of a naive approach. We assume on all platforms that
898  // get_user_data_path() will return something resembling the default
899  // configuration and that --user-data-dir wasn't used. We will get
900  // false negatives when any of these conditions don't hold true.
901  //
902 
903 #if defined(_WIN32)
904  path = get_user_data_path().parent_path() / ("Wesnoth" + suffix) / "saves";
905 #elif defined(_X11)
906  path = get_user_data_path().parent_path() / suffix / "saves";
907 #elif defined(__APPLE__)
908  path = get_user_data_path().parent_path() / ("Wesnoth_" + suffix) / "saves";
909 #endif
910 
911  if(bfs::exists(path)) {
912  result.emplace_back(suffix, path.string());
913  }
914  }
915 
916  return result;
917 #endif
918 }
919 
920 std::string get_cwd()
921 {
922  error_code ec;
923  bfs::path cwd = bfs::current_path(ec);
924 
925  if(ec) {
926  ERR_FS << "Failed to get current directory: " << ec.message();
927  return "";
928  }
929 
930  return cwd.generic_string();
931 }
932 
933 bool set_cwd(const std::string& dir)
934 {
935  error_code ec;
936  bfs::current_path(bfs::path{dir}, ec);
937 
938  if(ec) {
939  ERR_FS << "Failed to set current directory: " << ec.message();
940  return false;
941  } else {
942  LOG_FS << "Process working directory set to " << dir;
943  }
944 
945  return true;
946 }
947 
948 std::string get_exe_path()
949 {
950 #ifdef _WIN32
951  wchar_t process_path[MAX_PATH];
952  SetLastError(ERROR_SUCCESS);
953 
954  GetModuleFileNameW(nullptr, process_path, MAX_PATH);
955 
956  if(GetLastError() != ERROR_SUCCESS) {
957  return get_cwd() + "/wesnoth";
958  }
959 
960  bfs::path exe(process_path);
961  return exe.string();
962 #elif defined(__APPLE__)
963  std::vector<char> buffer(PATH_MAX, 0);
964  uint32_t size = PATH_MAX;
965  if(_NSGetExecutablePath(&buffer[0], &size) == 0) {
966  buffer.resize(size+1);
967  return std::string(buffer.begin(), buffer.end());
968  } else {
969  ERR_FS << "Path to wesnoth executable is too long";
970  return get_cwd() + "/The Battle for Wesnoth";
971  }
972 #else
973  // first check /proc
974  if(bfs::exists("/proc/")) {
975  bfs::path self_exe("/proc/self/exe");
976  error_code ec;
977  bfs::path exe = bfs::read_symlink(self_exe, ec);
978  if(!ec) {
979  return exe.string();
980  }
981  }
982 
983  // check the PATH for wesnoth's location
984  // with version
985  std::string version = std::to_string(game_config::wesnoth_version.major_version()) + "." + std::to_string(game_config::wesnoth_version.minor_version());
986  std::string exe = filesystem::get_program_invocation("wesnoth-"+version);
987  bfs::path search = bp::search_path(exe).string();
988  if(!search.string().empty()) {
989  return search.string();
990  }
991 
992  // versionless
993  exe = filesystem::get_program_invocation("wesnoth");
994  search = bp::search_path(exe).string();
995  if(!search.string().empty()) {
996  return search.string();
997  }
998 
999  // return the current working directory
1000  return get_cwd() + "/wesnoth";
1001 #endif
1002 }
1003 
1004 std::string get_exe_dir()
1005 {
1007  return path.parent_path().string();
1008 }
1009 
1010 std::string get_wesnothd_name()
1011 {
1012  std::string exe_dir = get_exe_dir();
1013  std::string exe_name = base_name(get_exe_path());
1014  // macOS doesn't call the wesnoth client executable "wesnoth"
1015  // otherwise, add any suffix after the "wesnoth" part of the executable name to wesnothd's name
1016  std::string wesnothd = exe_dir + "/wesnothd" + exe_name.substr(7);
1017  if(!file_exists(wesnothd)) {
1018  return exe_dir + "/" + get_program_invocation("wesnothd");
1019  }
1020  return wesnothd;
1021 }
1022 
1023 bool make_directory(const std::string& dirname)
1024 {
1025  error_code ec;
1026  bool created = bfs::create_directory(bfs::path(dirname), ec);
1027  if(ec) {
1028  ERR_FS << "Failed to create directory " << dirname << ": " << ec.message();
1029  }
1030 
1031  return created;
1032 }
1033 
1034 bool delete_directory(const std::string& dirname, const bool keep_pbl)
1035 {
1036  bool ret = true;
1037  std::vector<std::string> files;
1038  std::vector<std::string> dirs;
1039  error_code ec;
1040 
1042 
1043  if(!files.empty()) {
1044  for(const std::string& f : files) {
1045  bfs::remove(bfs::path(f), ec);
1046  if(ec) {
1047  LOG_FS << "remove(" << f << "): " << ec.message();
1048  ret = false;
1049  }
1050  }
1051  }
1052 
1053  if(!dirs.empty()) {
1054  for(const std::string& d : dirs) {
1055  // TODO: this does not preserve any other PBL files
1056  // filesystem.cpp does this too, so this might be intentional
1057  if(!delete_directory(d))
1058  ret = false;
1059  }
1060  }
1061 
1062  if(ret) {
1063  bfs::remove(bfs::path(dirname), ec);
1064  if(ec) {
1065  LOG_FS << "remove(" << dirname << "): " << ec.message();
1066  ret = false;
1067  }
1068  }
1069 
1070  return ret;
1071 }
1072 
1073 bool delete_file(const std::string& filename)
1074 {
1075  error_code ec;
1076  bool ret = bfs::remove(bfs::path(filename), ec);
1077  if(ec) {
1078  ERR_FS << "Could not delete file " << filename << ": " << ec.message();
1079  }
1080 
1081  return ret;
1082 }
1083 
1084 std::vector<uint8_t> read_file_binary(const std::string& fname)
1085 {
1086  std::ifstream file(fname, std::ios::binary);
1087  std::vector<uint8_t> file_contents;
1088 
1089  file_contents.reserve(file_size(fname));
1090  file_contents.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
1091 
1092  return file_contents;
1093 }
1094 
1095 std::string read_file_as_data_uri(const std::string& fname)
1096 {
1097  std::vector<uint8_t> file_contents = filesystem::read_file_binary(fname);
1098  utils::byte_string_view view = {file_contents.data(), file_contents.size()};
1099  std::string name = filesystem::base_name(fname);
1100  std::string img = "";
1101 
1102  if(name.find(".") != std::string::npos) {
1103  // convert to web-safe base64, since the + symbols will get stripped out when reading this back in later
1104  img = "data:image/"+name.substr(name.find(".")+1)+";base64,"+base64::encode(view);
1105  }
1106 
1107  return img;
1108 }
1109 
1110 std::string read_file(const std::string& fname)
1111 {
1112  scoped_istream is = istream_file(fname);
1113  std::stringstream ss;
1114  ss << is->rdbuf();
1115  return ss.str();
1116 }
1117 
1118 filesystem::scoped_istream istream_file(const std::string& fname, bool treat_failure_as_error)
1119 {
1120  LOG_FS << "Streaming " << fname << " for reading.";
1121 
1122  if(fname.empty()) {
1123  ERR_FS << "Trying to open file with empty name.";
1124  filesystem::scoped_istream s(new bfs::ifstream());
1125  s->clear(std::ios_base::failbit);
1126  return s;
1127  }
1128 
1129  // mingw doesn't support std::basic_ifstream::basic_ifstream(const wchar_t* fname)
1130  // that why boost::filesystem::fstream.hpp doesn't work with mingw.
1131  try {
1132  boost::iostreams::file_descriptor_source fd(bfs::path(fname), std::ios_base::binary);
1133 
1134  // TODO: has this still use ?
1135  if(!fd.is_open() && treat_failure_as_error) {
1136  ERR_FS << "Could not open '" << fname << "' for reading.";
1137  } else if(!is_filename_case_correct(fname, fd)) {
1138  ERR_FS << "Not opening '" << fname << "' due to case mismatch.";
1139  filesystem::scoped_istream s(new bfs::ifstream());
1140  s->clear(std::ios_base::failbit);
1141  return s;
1142  }
1143 
1144  return std::make_unique<boost::iostreams::stream<boost::iostreams::file_descriptor_source>>(fd, 4096, 0);
1145  } catch(const std::exception&) {
1146  if(treat_failure_as_error) {
1147  ERR_FS << "Could not open '" << fname << "' for reading.";
1148  }
1149 
1150  filesystem::scoped_istream s(new bfs::ifstream());
1151  s->clear(std::ios_base::failbit);
1152  return s;
1153  }
1154 }
1155 
1156 filesystem::scoped_ostream ostream_file(const std::string& fname, std::ios_base::openmode mode, bool create_directory)
1157 {
1158  LOG_FS << "streaming " << fname << " for writing.";
1159  try {
1160  boost::iostreams::file_descriptor_sink fd(bfs::path(fname), mode);
1161  return std::make_unique<boost::iostreams::stream<boost::iostreams::file_descriptor_sink>>(fd, 4096, 0);
1162  } catch(const BOOST_IOSTREAMS_FAILURE& e) {
1163  // If this operation failed because the parent directory didn't exist, create the parent directory and
1164  // retry.
1165  error_code ec_unused;
1166  if(create_directory && bfs::create_directories(bfs::path(fname).parent_path(), ec_unused)) {
1167  return ostream_file(fname, mode, false);
1168  }
1169 
1170  throw filesystem::io_exception(e.what());
1171  }
1172 }
1173 
1174 // Throws io_exception if an error occurs
1175 void write_file(const std::string& fname, const std::string& data, std::ios_base::openmode mode)
1176 {
1177  scoped_ostream os = ostream_file(fname, mode);
1178  os->exceptions(std::ios_base::goodbit);
1179 
1180  const std::size_t block_size = 4096;
1181  char buf[block_size];
1182 
1183  for(std::size_t i = 0; i < data.size(); i += block_size) {
1184  const std::size_t bytes = std::min<std::size_t>(block_size, data.size() - i);
1185  std::copy(data.begin() + i, data.begin() + i + bytes, buf);
1186 
1187  os->write(buf, bytes);
1188  if(os->bad()) {
1189  throw io_exception("Error writing to file: '" + fname + "'");
1190  }
1191  }
1192 }
1193 
1194 void copy_file(const std::string& src, const std::string& dest)
1195 {
1196  write_file(dest, read_file(src));
1197 }
1198 
1199 bool create_directory_if_missing(const std::string& dirname)
1200 {
1201  return create_directory_if_missing(bfs::path(dirname));
1202 }
1203 
1204 bool create_directory_if_missing_recursive(const std::string& dirname)
1205 {
1207 }
1208 
1209 bool is_directory(const std::string& fname)
1210 {
1211  return is_directory_internal(bfs::path(fname));
1212 }
1213 
1214 bool file_exists(const std::string& name)
1215 {
1216  return file_exists(bfs::path(name));
1217 }
1218 
1219 std::time_t file_modified_time(const std::string& fname)
1220 {
1221  error_code ec;
1222  std::time_t mtime = bfs::last_write_time(bfs::path(fname), ec);
1223  if(ec) {
1224  LOG_FS << "Failed to read modification time of " << fname << ": " << ec.message();
1225  }
1226 
1227  return mtime;
1228 }
1229 
1230 bool is_map(const std::string& filename)
1231 {
1232  return bfs::path(filename).extension() == map_extension;
1233 }
1234 
1235 bool is_cfg(const std::string& filename)
1236 {
1237  return bfs::path(filename).extension() == wml_extension;
1238 }
1239 
1240 bool is_mask(const std::string& filename)
1241 {
1242  return bfs::path(filename).extension() == mask_extension;
1243 }
1244 
1245 bool is_gzip_file(const std::string& filename)
1246 {
1247  return bfs::path(filename).extension() == ".gz";
1248 }
1249 
1250 bool is_bzip2_file(const std::string& filename)
1251 {
1252  return bfs::path(filename).extension() == ".bz2";
1253 }
1254 
1255 int file_size(const std::string& fname)
1256 {
1257  error_code ec;
1258  uintmax_t size = bfs::file_size(bfs::path(fname), ec);
1259  if(ec) {
1260  LOG_FS << "Failed to read filesize of " << fname << ": " << ec.message();
1261  return -1;
1262  } else if(size > std::numeric_limits<int>::max()) {
1263  return std::numeric_limits<int>::max();
1264  } else {
1265  return size;
1266  }
1267 }
1268 
1269 int dir_size(const std::string& pname)
1270 {
1271  bfs::path p(pname);
1272  uintmax_t size_sum = 0;
1273  error_code ec;
1274  for(bfs::recursive_directory_iterator i(p), end; i != end && !ec; ++i) {
1275  if(bfs::is_regular_file(i->path())) {
1276  size_sum += bfs::file_size(i->path(), ec);
1277  }
1278  }
1279 
1280  if(ec) {
1281  LOG_FS << "Failed to read directorysize of " << pname << ": " << ec.message();
1282  return -1;
1283  } else if(size_sum > std::numeric_limits<int>::max()) {
1284  return std::numeric_limits<int>::max();
1285  } else {
1286  return size_sum;
1287  }
1288 }
1289 
1290 std::string base_name(const std::string& file, const bool remove_extension)
1291 {
1292  if(!remove_extension) {
1293  return bfs::path(file).filename().string();
1294  } else {
1295  return bfs::path(file).stem().string();
1296  }
1297 }
1298 
1299 std::string directory_name(const std::string& file)
1300 {
1301  return bfs::path(file).parent_path().string();
1302 }
1303 
1304 std::string nearest_extant_parent(const std::string& file)
1305 {
1306  if(file.empty()) {
1307  return "";
1308  }
1309 
1310  bfs::path p{file};
1311  error_code ec;
1312 
1313  do {
1314  p = p.parent_path();
1315  bfs::path q = canonical(p, ec);
1316  if(!ec) {
1317  p = q;
1318  }
1319  } while(ec && !is_root(p.string()));
1320 
1321  return ec ? "" : p.string();
1322 }
1323 
1324 bool is_path_sep(char c)
1325 {
1326  static const bfs::path sep = bfs::path("/").make_preferred();
1327  const std::string s = std::string(1, c);
1328  return sep == bfs::path(s).make_preferred();
1329 }
1330 
1332 {
1333  return bfs::path::preferred_separator;
1334 }
1335 
1336 bool is_root(const std::string& path)
1337 {
1338 #ifndef _WIN32
1339  error_code ec;
1340  const bfs::path& p = bfs::canonical(path, ec);
1341  return ec ? false : !p.has_parent_path();
1342 #else
1343  //
1344  // Boost.Filesystem is completely unreliable when it comes to detecting
1345  // whether a path refers to a drive's root directory on Windows, so we are
1346  // forced to take an alternative approach here. Instead of hand-parsing
1347  // strings we'll just call a graphical shell service.
1348  //
1349  // There are several poorly-documented ways to refer to a drive in Windows by
1350  // escaping the filesystem namespace using \\.\, \\?\, and \??\. We're just
1351  // going to ignore those here, which may yield unexpected results in places
1352  // such as the file dialog. This function really shouldn't be used for
1353  // security validation anyway, and there are virtually infinite ways to name
1354  // a drive's root using the NT object namespace so it's pretty pointless to
1355  // try to catch those there.
1356  //
1357  // (And no, shlwapi.dll's PathIsRoot() doesn't recognize \\.\C:\, \\?\C:\, or
1358  // \??\C:\ as roots either.)
1359  //
1360  // More generally, do NOT use this code in security-sensitive applications.
1361  //
1362  // See also: <https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html>
1363  //
1364  const std::wstring& wpath = bfs::path{path}.make_preferred().wstring();
1365  return PathIsRootW(wpath.c_str()) == TRUE;
1366 #endif
1367 }
1368 
1369 std::string root_name(const std::string& path)
1370 {
1371  return bfs::path{path}.root_name().string();
1372 }
1373 
1374 bool is_relative(const std::string& path)
1375 {
1376  return bfs::path{path}.is_relative();
1377 }
1378 
1379 std::string normalize_path(const std::string& fpath, bool normalize_separators, bool resolve_dot_entries)
1380 {
1381  if(fpath.empty()) {
1382  return fpath;
1383  }
1384 
1385  error_code ec;
1386  bfs::path p = resolve_dot_entries ? bfs::canonical(fpath, ec) : bfs::absolute(fpath);
1387 
1388  if(ec) {
1389  return "";
1390  }
1391 
1392  if(normalize_separators) {
1393  return p.make_preferred().string();
1394  } else {
1395  return p.string();
1396  }
1397 }
1398 
1399 bool to_asset_path(std::string& path, const std::string& addon_id, const std::string& asset_type)
1400 {
1401  std::string rel_path = "";
1402  std::string core_asset_dir = get_dir(game_config::path + "/data/core/" + asset_type);
1403  std::string addon_asset_dir;
1404 
1405  bool found = false;
1406  bool is_in_core_dir = (path.find(core_asset_dir) != std::string::npos);
1407  bool is_in_addon_dir = false;
1408 
1409  if (is_in_core_dir) {
1410  rel_path = path.erase(0, core_asset_dir.size()+1);
1411  found = true;
1412  } else if (!addon_id.empty()) {
1413  addon_asset_dir = get_current_editor_dir(addon_id) + "/" + asset_type;
1414  is_in_addon_dir = (path.find(addon_asset_dir) != std::string::npos);
1415  if (is_in_addon_dir) {
1416  rel_path = path.erase(0, addon_asset_dir.size()+1);
1417  found = true;
1418  } else {
1419  // Not found in either core or addons dirs,
1420  // return a possible path where the asset could be copied.
1421  std::string filename = boost::filesystem::path(path).filename().string();
1422  std::string asset_path = addon_asset_dir + "/" + filename;
1423  rel_path = filename;
1424  found = false;
1425  }
1426  } else {
1427  found = false;
1428  }
1429 
1430  path = rel_path;
1431  return found;
1432 }
1433 
1434 /**
1435  * The paths manager is responsible for recording the various paths
1436  * that binary files may be located at.
1437  * It should be passed a config object which holds binary path information.
1438  * This is in the format
1439  *@verbatim
1440  * [binary_path]
1441  * path=<path>
1442  * [/binary_path]
1443  * Binaries will be searched for in [wesnoth-path]/data/<path>/images/
1444  *@endverbatim
1445  */
1446 namespace
1447 {
1448 std::set<std::string> binary_paths;
1449 
1450 typedef std::map<std::string, std::vector<std::string>> paths_map;
1451 paths_map binary_paths_cache;
1452 
1453 } // namespace
1454 
1455 static void init_binary_paths()
1456 {
1457  if(binary_paths.empty()) {
1458  binary_paths.insert("");
1459  }
1460 }
1461 
1463  : paths_()
1464 {
1465 }
1466 
1468  : paths_()
1469 {
1470  set_paths(cfg);
1471 }
1472 
1474 {
1475  cleanup();
1476 }
1477 
1479 {
1480  cleanup();
1482 
1483  for(const config& bp : cfg.child_range("binary_path")) {
1484  std::string path = bp["path"].str();
1485  if(path.find("..") != std::string::npos) {
1486  ERR_FS << "Invalid binary path '" << path << "'";
1487  continue;
1488  }
1489 
1490  if(!path.empty() && path.back() != '/')
1491  path += "/";
1492  if(binary_paths.count(path) == 0) {
1493  binary_paths.insert(path);
1494  paths_.push_back(path);
1495  }
1496  }
1497 }
1498 
1500 {
1501  binary_paths_cache.clear();
1502 
1503  for(const std::string& p : paths_) {
1504  binary_paths.erase(p);
1505  }
1506 }
1507 
1509 {
1510  binary_paths_cache.clear();
1511 }
1512 
1513 static bool is_legal_file(const std::string& filename_str)
1514 {
1515  DBG_FS << "Looking for '" << filename_str << "'.";
1516 
1517  if(filename_str.empty()) {
1518  LOG_FS << " invalid filename";
1519  return false;
1520  }
1521 
1522  if(filename_str.find("..") != std::string::npos) {
1523  ERR_FS << "Illegal path '" << filename_str << "' (\"..\" not allowed).";
1524  return false;
1525  }
1526 
1527  if(filename_str.find('\\') != std::string::npos) {
1528  ERR_FS << "Illegal path '" << filename_str
1529  << R"end(' ("\" not allowed, for compatibility with GNU/Linux and macOS).)end";
1530  return false;
1531  }
1532 
1533  bfs::path filepath(filename_str);
1534 
1535  if(default_blacklist.match_file(filepath.filename().string())) {
1536  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted filename).";
1537  return false;
1538  }
1539 
1540  if(std::any_of(filepath.begin(), filepath.end(),
1541  [](const bfs::path& dirname) { return default_blacklist.match_dir(dirname.string()); })) {
1542  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted directory name).";
1543  return false;
1544  }
1545 
1546  return true;
1547 }
1548 
1549 /**
1550  * Returns a vector with all possible paths to a given type of binary,
1551  * e.g. 'images', 'sounds', etc,
1552  */
1553 const std::vector<std::string>& get_binary_paths(const std::string& type)
1554 {
1555  const paths_map::const_iterator itor = binary_paths_cache.find(type);
1556  if(itor != binary_paths_cache.end()) {
1557  return itor->second;
1558  }
1559 
1560  if(type.find("..") != std::string::npos) {
1561  // Not an assertion, as language.cpp is passing user data as type.
1562  ERR_FS << "Invalid WML type '" << type << "' for binary paths";
1563  static std::vector<std::string> dummy;
1564  return dummy;
1565  }
1566 
1567  std::vector<std::string>& res = binary_paths_cache[type];
1568 
1570 
1571  for(const std::string& path : binary_paths) {
1572  res.push_back(get_user_data_dir() + "/" + path + type + "/");
1573 
1574  if(!game_config::path.empty()) {
1575  res.push_back(game_config::path + "/" + path + type + "/");
1576  }
1577  }
1578 
1579  // not found in "/type" directory, try main directory
1580  res.push_back(get_user_data_dir() + "/");
1581 
1582  if(!game_config::path.empty()) {
1583  res.push_back(game_config::path + "/");
1584  }
1585 
1586  return res;
1587 }
1588 
1589 utils::optional<std::string> get_binary_file_location(const std::string& type, const std::string& filename)
1590 {
1591  // We define ".." as "remove everything before" this is needed because
1592  // on the one hand allowing ".." would be a security risk but
1593  // especially for terrains the c++ engine puts a hardcoded "terrain/" before filename
1594  // and there would be no way to "escape" from "terrain/" otherwise. This is not the
1595  // best solution but we cannot remove it without another solution (subtypes maybe?).
1596 
1597  {
1598  std::string::size_type pos = filename.rfind("../");
1599  if(pos != std::string::npos) {
1600  return get_binary_file_location(type, filename.substr(pos + 3));
1601  }
1602  }
1603 
1604  if(!is_legal_file(filename)) {
1605  return utils::nullopt;
1606  }
1607 
1608  std::string result;
1609  // fix for duplicate mainline paths on macOS for some reason
1610  // would be good for someone who uses macOS to debug the cause at some point
1611  const std::vector<std::string> temp = get_binary_paths(type);
1612  const std::set<std::string> bpaths(temp.begin(), temp.end());
1613  for(const std::string& bp : bpaths) {
1614  bfs::path bpath(bp);
1615  bpath /= filename;
1616 
1617  DBG_FS << " checking '" << bp << "'";
1618 
1619  if(file_exists(bpath)) {
1620  DBG_FS << " found at '" << bpath.string() << "'";
1621  if(result.empty()) {
1622  result = bpath.string();
1623  } else {
1624  WRN_FS << "Conflicting files in binary_path: '" << result
1625  << "' and '" << bpath.string() << "'";
1626  }
1627  }
1628  }
1629 
1630  if(result.empty()) {
1631  DBG_FS << " not found";
1632  return utils::nullopt;
1633  } else {
1634  return result;
1635  }
1636 }
1637 
1638 utils::optional<std::string> get_binary_dir_location(const std::string& type, const std::string& filename)
1639 {
1640  if(!is_legal_file(filename)) {
1641  return utils::nullopt;
1642  }
1643 
1644  for(const std::string& bp : get_binary_paths(type)) {
1645  bfs::path bpath(bp);
1646  bpath /= filename;
1647  DBG_FS << " checking '" << bp << "'";
1648  if(is_directory_internal(bpath)) {
1649  DBG_FS << " found at '" << bpath.string() << "'";
1650  return bpath.string();
1651  }
1652  }
1653 
1654  DBG_FS << " not found";
1655  return utils::nullopt;
1656 }
1657 
1658 utils::optional<std::string> get_wml_location(const std::string& path, const utils::optional<std::string>& current_dir)
1659 {
1660  if(!is_legal_file(path)) {
1661  return utils::nullopt;
1662  }
1663 
1664  bfs::path fpath(path);
1665  bfs::path result;
1666 
1667  if(path[0] == '~') {
1668  result = get_user_data_path() / "data" / path.substr(1);
1669  DBG_FS << " trying '" << result.string() << "'";
1670  } else if(*fpath.begin() == ".") {
1671  if (!current_dir) {
1672  WRN_FS << "Cannot resolve " << path << " since the current directory is unknown!";
1673  return utils::nullopt;
1674  }
1675  result = bfs::path(*current_dir) / path;
1676  error_code ec;
1677  bfs::path c = bfs::canonical(result, ec);
1678  if (!is_prefix(c, bfs::path(game_config::path) / "data") && !is_prefix(c, get_user_data_path() / "data")) {
1679  WRN_FS << "Resolved path " << c << " is outside game and user data directories!";
1680  }
1681  } else {
1682  if(game_config::path.empty()) {
1683  WRN_FS << "Cannot resolve " << path << " since the game data directory is unknown!";
1684  return utils::nullopt;
1685  }
1686  result = bfs::path(game_config::path) / "data" / path;
1687  }
1688 
1689  if(!file_exists(result)) {
1690  DBG_FS << " not found";
1691  return utils::nullopt;
1692  } else {
1693  DBG_FS << " found: '" << result.string() << "'";
1694  return result.string();
1695  }
1696 }
1697 
1698 std::string get_short_wml_path(const std::string& filename)
1699 {
1700  bfs::path full_path(filename);
1701 
1702  bfs::path partial = subtract_path(full_path, get_user_data_path() / "data");
1703  if(!partial.empty()) {
1704  return "~" + partial.generic_string();
1705  }
1706 
1707  partial = subtract_path(full_path, bfs::path(game_config::path) / "data");
1708  if(!partial.empty()) {
1709  return partial.generic_string();
1710  }
1711 
1712  return filename;
1713 }
1714 
1715 utils::optional<std::string> get_independent_binary_file_path(const std::string& type, const std::string& filename)
1716 {
1718  if(!bp) {
1719  return utils::nullopt;
1720  }
1721 
1722  bfs::path full_path{bp.value()};
1724  if(!partial.empty()) {
1725  return partial.generic_string();
1726  }
1727 
1728  partial = subtract_path(full_path, game_config::path);
1729  if(!partial.empty()) {
1730  return partial.generic_string();
1731  }
1732 
1733  return full_path.generic_string();
1734 }
1735 
1736 std::string get_program_invocation(const std::string& program_name)
1737 {
1738 #ifdef _WIN32
1739  return program_name + ".exe";
1740 #else
1741  return program_name;
1742 #endif
1743 }
1744 
1745 std::string sanitize_path(const std::string& path)
1746 {
1747 #ifdef _WIN32
1748  const char* user_name = getenv("USERNAME");
1749 #else
1750  const char* user_name = getenv("USER");
1751 #endif
1752 
1753  std::string canonicalized = filesystem::normalize_path(path, true, false);
1754  if(user_name != nullptr) {
1755  boost::replace_all(canonicalized, user_name, "USER");
1756  }
1757 
1758  return canonicalized;
1759 }
1760 
1761 // Return path to localized counterpart of the given file, if any, or empty string.
1762 // Localized counterpart may also be requested to have a suffix to base name.
1763 utils::optional<std::string> get_localized_path(const std::string& file, const std::string& suff)
1764 {
1765  std::string dir = filesystem::directory_name(file);
1766  std::string base = filesystem::base_name(file);
1767 
1768  const std::size_t pos_ext = base.rfind(".");
1769 
1770  std::string loc_base;
1771  if(pos_ext != std::string::npos) {
1772  loc_base = base.substr(0, pos_ext) + suff + base.substr(pos_ext);
1773  } else {
1774  loc_base = base + suff;
1775  }
1776 
1777  // TRANSLATORS: This is the language code which will be used
1778  // to store and fetch localized non-textual resources, such as images,
1779  // when they exist. Normally it is just the code of the PO file itself,
1780  // e.g. "de" of de.po for German. But it can also be a comma-separated
1781  // list of language codes by priority, when the localized resource
1782  // found for first of those languages will be used. This is useful when
1783  // two languages share sufficient commonality, that they can use each
1784  // other's resources rather than duplicating them. For example,
1785  // Swedish (sv) and Danish (da) are such, so Swedish translator could
1786  // translate this message as "sv,da", while Danish as "da,sv".
1787  std::vector<std::string> langs = utils::split(_("language code for localized resources^en_US"));
1788 
1789  // In case even the original image is split into base and overlay,
1790  // add en_US with lowest priority, since the message above will
1791  // not have it when translated.
1792  langs.push_back("en_US");
1793  for(const std::string& lang : langs) {
1794  std::string loc_file = dir + "/" + "l10n" + "/" + lang + "/" + loc_base;
1795  if(filesystem::file_exists(loc_file)) {
1796  return loc_file;
1797  }
1798  }
1799 
1800  return utils::nullopt;
1801 }
1802 
1803 utils::optional<std::string> get_addon_id_from_path(const std::string& location)
1804 {
1805  std::string full_path = normalize_path(location, true);
1806  std::string addons_path = normalize_path(get_addons_dir(), true);
1807 
1808  if(full_path.find(addons_path) == 0) {
1809  bfs::path path(full_path.substr(addons_path.size()+1));
1810  if(path.size() > 0) {
1811  return path.begin()->string();
1812  }
1813  }
1814 
1815  return utils::nullopt;
1816 }
1817 
1818 } // namespace filesystem
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
bool match_file(const std::string &name) const
A class grating read only view to a vector of config objects, viewed as one config with all children ...
config_array_view child_range(config_key_type key) const
Represents version numbers.
void set_major_version(unsigned int)
Sets the major version number.
unsigned int minor_version() const
Retrieves the minor version number (x2 in "x1.x2.x3").
unsigned int major_version() const
Retrieves the major version number (x1 in "x1.x2.x3").
Definitions for the interface to Wesnoth Markup Language (WML).
static lg::log_domain log_filesystem("filesystem")
#define DBG_FS
Definition: filesystem.cpp:74
#define LOG_FS
Definition: filesystem.cpp:75
#define WRN_FS
Definition: filesystem.cpp:76
#define ERR_FS
Definition: filesystem.cpp:77
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1029
static auto & dummy
static std::string _(const char *str)
Definition: gettext.hpp:93
Standard logging facilities (interface).
std::string encode(utils::byte_string_view bytes)
Definition: base64.cpp:225
bool to_asset_path(std::string &path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
std::string get_legacy_editor_dir()
std::string get_cache_dir()
Definition: filesystem.cpp:838
std::time_t file_modified_time(const std::string &fname)
Get the modification time of a file.
bool is_bzip2_file(const std::string &filename)
Returns true if the file ends with '.bz2'.
int dir_size(const std::string &pname)
Returns the sum of the sizes of the files contained in a directory.
static bfs::path subtract_path(const bfs::path &full, const bfs::path &prefix_path)
Definition: filesystem.cpp:429
bool is_relative(const std::string &path)
Returns whether the path seems to be relative.
static const bfs::path & get_user_data_path()
Definition: filesystem.cpp:822
filesystem::scoped_istream istream_file(const std::string &fname, bool treat_failure_as_error)
bool is_root(const std::string &path)
Returns whether the path is the root of the file hierarchy.
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:445
const std::string wml_extension
Definition: filesystem.hpp:81
bool is_cfg(const std::string &filename)
Returns true if the file ends with the wmlfile extension.
static bfs::path get_dir(const bfs::path &dirpath)
Definition: filesystem.cpp:337
std::string get_user_data_dir()
Definition: filesystem.cpp:828
std::string base_name(const std::string &file, const bool remove_extension)
Returns the base filename of a file, with directory name stripped.
std::string get_wml_persist_dir()
bool rename_dir(const std::string &old_dir, const std::string &new_dir)
Definition: filesystem.cpp:797
static bool is_legal_file(const std::string &filename_str)
std::string get_exe_path()
Definition: filesystem.cpp:948
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
bool delete_file(const std::string &filename)
bool is_gzip_file(const std::string &filename)
Returns true if the file ends with '.gz'.
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:326
std::string get_exe_dir()
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
static bool is_directory_internal(const bfs::path &fpath)
Definition: filesystem.cpp:315
bool delete_directory(const std::string &dirname, const bool keep_pbl)
std::string get_synced_prefs_file()
location of preferences file containing preferences that are synced between computers note that wesno...
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 get_saves_dir()
std::string get_program_invocation(const std::string &program_name)
Returns the appropriate invocation for a Wesnoth-related binary, assuming that it is located in the s...
static bool is_prefix(const bfs::path &full, const bfs::path &prefix_path)
Definition: filesystem.cpp:423
std::string read_file(const std::string &fname)
Basic disk I/O - read file.
std::string get_unsynced_prefs_file()
location of preferences file containing preferences that aren't synced between computers
const std::string map_extension
Definition: filesystem.hpp:79
utils::optional< std::string > get_binary_file_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual file of a given type, if it exists.
filesystem::scoped_ostream ostream_file(const std::string &fname, std::ios_base::openmode mode, bool create_directory)
static bool create_directory_if_missing_recursive(const bfs::path &dirpath)
Definition: filesystem.cpp:384
int file_size(const std::string &fname)
Returns the size of a file, or -1 if the file doesn't exist.
bool is_mask(const std::string &filename)
Returns true if the file ends with the maskfile extension.
utils::optional< std::string > get_addon_id_from_path(const std::string &location)
Returns the add-on ID from a path.
std::string read_file_as_data_uri(const std::string &fname)
void set_cache_dir(const std::string &newcachedir)
Definition: filesystem.cpp:817
const std::string mask_extension
Definition: filesystem.hpp:80
std::unique_ptr< std::istream > scoped_istream
Definition: filesystem.hpp:53
void clear_binary_paths_cache()
static bfs::path user_data_dir
Definition: filesystem.cpp:607
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode)
Throws io_exception if an error occurs.
std::string get_short_wml_path(const std::string &filename)
Returns a short path to filename, skipping the (user) data directory.
std::string root_name(const std::string &path)
Returns the name of the root device if included in the given path.
std::string directory_name(const std::string &file)
Returns the directory name of a file, with filename stripped.
std::string get_logs_dir()
Definition: filesystem.cpp:833
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:54
static bool create_directory_if_missing(const bfs::path &dirpath)
Definition: filesystem.cpp:360
utils::optional< std::string > get_binary_dir_location(const std::string &type, const std::string &filename)
Returns a complete path to the actual directory of a given type, if it exists.
bool is_userdata_initialized()
Definition: filesystem.cpp:609
std::string nearest_extant_parent(const std::string &file)
Finds the nearest parent in existence for a file or directory.
char path_separator()
Returns the standard path separator for the current platform.
std::string get_sync_dir()
parent directory for everything that should be synced between systems.
bool looks_like_pbl(const std::string &file)
std::string get_addons_data_dir()
static void push_if_exists(std::vector< std::string > *vec, const bfs::path &file, bool full)
Definition: filesystem.cpp:299
bool is_map(const std::string &filename)
Returns true if the file ends with the mapfile extension.
bool make_directory(const std::string &dirname)
utils::optional< std::string > get_independent_binary_file_path(const std::string &type, const std::string &filename)
Returns an asset path to filename for binary path-independent use in saved games.
std::string get_wesnothd_name()
static void setup_user_data_dir()
Definition: filesystem.cpp:665
const blacklist_pattern_list default_blacklist
Definition: filesystem.cpp:265
std::string get_addons_dir()
bool set_cwd(const std::string &dir)
Definition: filesystem.cpp:933
utils::optional< std::string > get_localized_path(const std::string &file, const std::string &suff)
Returns the localized version of the given filename, if it exists.
std::vector< other_version_dir > find_other_version_saves_dirs()
Searches for directories containing saves created by other versions of Wesnoth.
Definition: filesystem.cpp:867
static bool check_prefix(bfs::path::iterator &fi, const bfs::path::iterator &fe, const bfs::path &prefix)
Definition: filesystem.cpp:412
std::string get_next_filename(const std::string &name, const std::string &extension)
Get the next free filename using "name + number (3 digits) + extension" maximum 1000 files then start...
Definition: filesystem.cpp:586
static void set_cache_path(bfs::path newcache)
Definition: filesystem.cpp:809
std::vector< uint8_t > read_file_binary(const std::string &fname)
std::string sanitize_path(const std::string &path)
Sanitizes a path to remove references to the user's name.
std::string get_current_editor_dir(const std::string &addon_id)
const std::string get_version_path_suffix(const version_info &version)
Definition: filesystem.cpp:614
static bool error_except_not_found(const error_code &ec)
Definition: filesystem.cpp:310
bool is_path_sep(char c)
Returns whether c is a path separator.
static bfs::path cache_dir
Definition: filesystem.cpp:607
std::string get_cwd()
Definition: filesystem.cpp:920
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
void set_user_data_dir(std::string newprefdir)
Definition: filesystem.cpp:721
const std::vector< std::string > & get_binary_paths(const std::string &type)
Returns a vector with all possible paths to a given type of binary, e.g.
static void init_binary_paths()
Game configuration data as global variables.
Definition: build_info.cpp:61
const version_info min_savegame_version(MIN_SAVEGAME_VERSION)
std::string path
Definition: filesystem.cpp:91
std::string default_preferences_path
Definition: filesystem.cpp:97
const version_info wesnoth_version(VERSION)
const std::string observer_team_name
observer team name used for observer team chat
Definition: filesystem.cpp:101
int cache_compression_level
Definition: filesystem.cpp:103
bool check_migration
Definition: filesystem.cpp:99
void remove()
Removes a tip.
Definition: tooltip.cpp:94
bool exists(const image::locator &i_locator)
Returns true if the given image actually exists, without loading it.
Definition: picture.cpp:818
void move_log_file()
Move the log file to another directory.
Definition: log.cpp:178
std::string img(const std::string &src, const std::string &align, const bool floating)
Definition: markup.cpp:29
rng * generator
This generator is automatically synced during synced context.
Definition: random.cpp:60
void process(int mousex, int mousey)
Definition: tooltips.cpp:340
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::basic_string_view< uint8_t > byte_string_view
Definition: base64.hpp:25
std::string get_unknown_exception_type()
Utility function for finding the type of thing caught with catch(...).
Definition: general.cpp:23
std::vector< std::string > split(const config_attribute_value &val)
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
@ partial
There are still moves and/or attacks possible, but the unit doesn't fit in the "unmoved" status.
std::string_view data
Definition: picture.cpp:178
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:629
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:766
rect src
Non-transparent portion of the surface to compose.
std::string filename
Filename.
std::vector< std::string > paths_
Definition: filesystem.hpp:451
void set_paths(const game_config_view &cfg)
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
mock_char c
mock_party p
static map_location::direction s
#define d
#define e
#define h
#define f