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