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