The Battle for Wesnoth  1.19.18+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 <SDL2/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 <SDL2/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(__IPHONEOS__)
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(__IPHONEOS__)
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_AndroidGetExternalStoragePath();
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)
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  // Split the given locale code: "en_GB" -> "en", "GB"
861  // If the result of split() is empty then locale_code is empty (likely using System Language)
862  // Assume en is always available as a fall-back
863  const auto& split_locale_code = utils::split(locale_code, '_');
864  const std::string& language_code = split_locale_code.empty() ? "en" : split_locale_code[0];
865  manual_path = (manual_template % language_code).str();
866 
867  if(bfs::exists(manual_path)) {
868  // If a filename like manual.en_GB.html is not found, try manual.en.html
869  return "file://" + bfs::canonical(manual_path).string();
870  }
871 
872  return {};
873 }
874 
875 std::string get_user_data_dir()
876 {
877  return get_user_data_path().string();
878 }
879 
880 std::string get_logs_dir()
881 {
882  return filesystem::get_user_data_dir() + "/logs";
883 }
884 
885 std::string get_cache_dir()
886 {
887  if(cache_dir.empty()) {
888 #if defined(_X11) && !defined(PREFERENCES_DIR)
889  char const* xdg_cache = getenv("XDG_CACHE_HOME");
890 
891  if(!xdg_cache || xdg_cache[0] == '\0') {
892  xdg_cache = getenv("HOME");
893  if(!xdg_cache) {
894  cache_dir = get_dir(get_user_data_path() / "cache");
895  return cache_dir.string();
896  }
897 
898  cache_dir = xdg_cache;
899  cache_dir /= ".cache";
900  } else {
901  cache_dir = xdg_cache;
902  }
903 
904  cache_dir /= "wesnoth";
906 #else
907  cache_dir = get_dir(get_user_data_path() / "cache");
908 #endif
909  }
910 
911  return cache_dir.string();
912 }
913 
914 std::vector<other_version_dir> find_other_version_saves_dirs()
915 {
916 #if !defined(_WIN32) && !defined(_X11) && !defined(__APPLE__)
917  // By all means, this situation doesn't make sense
918  return {};
919 #else
920  const auto& w_ver = game_config::wesnoth_version;
921  const auto& ms_ver = game_config::min_savegame_version;
922 
923  if(w_ver.major_version() != 1 || ms_ver.major_version() != 1) {
924  // Unimplemented, assuming that version 2 won't use WML-based saves
925  return {};
926  }
927 
928  std::vector<other_version_dir> result;
929 
930  // For 1.16, check for saves from all versions up to 1.20.
931  for(auto minor = w_ver.minor_version() + 4; minor >= ms_ver.minor_version(); --minor) {
932  if(minor == w_ver.minor_version())
933  continue;
934 
935  auto version = version_info{};
936  version.set_major_version(w_ver.major_version());
937  version.set_minor_version(minor);
938  auto suffix = get_version_path_suffix(version);
939 
940  bfs::path path;
941 
942  //
943  // NOTE:
944  // This is a bit of a naive approach. We assume on all platforms that
945  // get_user_data_path() will return something resembling the default
946  // configuration and that --user-data-dir wasn't used. We will get
947  // false negatives when any of these conditions don't hold true.
948  //
949 
950 #if defined(_WIN32)
951  path = get_user_data_path().parent_path() / ("Wesnoth" + suffix);
952 #elif defined(_X11)
953  path = get_user_data_path().parent_path() / suffix;
954 #elif defined(__APPLE__)
955  path = get_user_data_path().parent_path() / ("Wesnoth_" + suffix);
956 #endif
957 
958  // 1.19.2 added get_sync_dir() and changed the path to the save directory.
959  if(minor >= 19) {
960  path /= "sync";
961  }
962  path /= "saves";
963 
964  if(bfs::exists(path)) {
965  result.emplace_back(suffix, path.string());
966  }
967  }
968 
969  return result;
970 #endif
971 }
972 
973 std::string get_cwd()
974 {
975  error_code ec;
976  bfs::path cwd = bfs::current_path(ec);
977 
978  if(ec) {
979  ERR_FS << "Failed to get current directory: " << ec.message();
980  return "";
981  }
982 
983  return cwd.generic_string();
984 }
985 
986 bool set_cwd(const std::string& dir)
987 {
988  error_code ec;
989  bfs::current_path(bfs::path{dir}, ec);
990 
991  if(ec) {
992  ERR_FS << "Failed to set current directory: " << ec.message();
993  return false;
994  } else {
995  LOG_FS << "Process working directory set to " << dir;
996  }
997 
998  return true;
999 }
1000 
1001 std::string get_exe_path()
1002 {
1003 #ifdef _WIN32
1004  wchar_t process_path[MAX_PATH];
1005  SetLastError(ERROR_SUCCESS);
1006 
1007  GetModuleFileNameW(nullptr, process_path, MAX_PATH);
1008 
1009  if(GetLastError() != ERROR_SUCCESS) {
1010  return get_cwd() + "/wesnoth";
1011  }
1012 
1013  bfs::path exe(process_path);
1014  return exe.string();
1015 #elif defined(__APPLE__)
1016  std::vector<char> buffer(PATH_MAX, 0);
1017  uint32_t size = PATH_MAX;
1018  if(_NSGetExecutablePath(&buffer[0], &size) == 0) {
1019  buffer.resize(size+1);
1020  return std::string(buffer.begin(), buffer.end());
1021  } else {
1022  ERR_FS << "Path to wesnoth executable is too long";
1023  return get_cwd() + "/The Battle for Wesnoth";
1024  }
1025 #else
1026  // first check /proc
1027  if(bfs::exists("/proc/")) {
1028  bfs::path self_exe("/proc/self/exe");
1029  error_code ec;
1030  bfs::path exe = bfs::read_symlink(self_exe, ec);
1031  if(!ec) {
1032  return exe.string();
1033  }
1034  }
1035 
1036  // check the PATH for wesnoth's location
1037  // with version
1038  std::string version = std::to_string(game_config::wesnoth_version.major_version()) + "." + std::to_string(game_config::wesnoth_version.minor_version());
1039  std::string exe = filesystem::get_program_invocation("wesnoth-"+version);
1040 #if BOOST_VERSION >= 108600
1041  bfs::path search = boost::process::v2::environment::find_executable(exe);
1042 #else
1043  bfs::path search = boost::process::search_path(exe);
1044 #endif
1045  if(!search.empty()) {
1046  return search.string();
1047  }
1048 
1049  // versionless
1050  exe = filesystem::get_program_invocation("wesnoth");
1051 #if BOOST_VERSION >= 108600
1052  search = boost::process::v2::environment::find_executable(exe);
1053 #else
1054  search = boost::process::search_path(exe);
1055 #endif
1056  if(!search.empty()) {
1057  return search.string();
1058  }
1059 
1060  // return the current working directory
1061  return get_cwd() + "/wesnoth";
1062 #endif
1063 }
1064 
1065 std::string get_exe_dir()
1066 {
1068  return path.parent_path().string();
1069 }
1070 
1071 std::string get_wesnothd_name()
1072 {
1073  std::string exe_dir = get_exe_dir();
1074  std::string exe_name = base_name(get_exe_path());
1075  // macOS doesn't call the wesnoth client executable "wesnoth"
1076  // otherwise, add any suffix after the "wesnoth" part of the executable name to wesnothd's name
1077  std::string wesnothd = exe_dir + "/wesnothd" + exe_name.substr(7);
1078  if(!file_exists(wesnothd)) {
1079  return exe_dir + "/" + get_program_invocation("wesnothd");
1080  }
1081  return wesnothd;
1082 }
1083 
1084 bool make_directory(const std::string& dirname)
1085 {
1086  error_code ec;
1087  bool created = bfs::create_directory(bfs::path(dirname), ec);
1088  if(ec) {
1089  ERR_FS << "Failed to create directory " << dirname << ": " << ec.message();
1090  }
1091 
1092  return created;
1093 }
1094 
1095 bool delete_directory(const std::string& dirname, const bool keep_pbl)
1096 {
1097  bool ret = true;
1098  std::vector<std::string> files;
1099  std::vector<std::string> dirs;
1100  error_code ec;
1101 
1103 
1104  if(!files.empty()) {
1105  for(const std::string& f : files) {
1106  bfs::remove(bfs::path(f), ec);
1107  if(ec) {
1108  LOG_FS << "remove(" << f << "): " << ec.message();
1109  ret = false;
1110  }
1111  }
1112  }
1113 
1114  if(!dirs.empty()) {
1115  for(const std::string& d : dirs) {
1116  // TODO: this does not preserve any other PBL files
1117  // filesystem.cpp does this too, so this might be intentional
1118  if(!delete_directory(d))
1119  ret = false;
1120  }
1121  }
1122 
1123  if(ret) {
1124  bfs::remove(bfs::path(dirname), ec);
1125  if(ec) {
1126  LOG_FS << "remove(" << dirname << "): " << ec.message();
1127  ret = false;
1128  }
1129  }
1130 
1131  return ret;
1132 }
1133 
1134 bool delete_file(const std::string& filename)
1135 {
1136  error_code ec;
1137  bool ret = bfs::remove(bfs::path(filename), ec);
1138  if(ec) {
1139  ERR_FS << "Could not delete file " << filename << ": " << ec.message();
1140  }
1141 
1142  return ret;
1143 }
1144 
1145 std::vector<uint8_t> read_file_binary(const std::string& fname)
1146 {
1147  std::ifstream file(fname, std::ios::binary);
1148  std::vector<uint8_t> file_contents;
1149 
1150  file_contents.reserve(file_size(fname));
1151  file_contents.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
1152 
1153  return file_contents;
1154 }
1155 
1156 std::string read_file_as_data_uri(const std::string& fname)
1157 {
1158  std::vector<uint8_t> file_contents = filesystem::read_file_binary(fname);
1159  std::string name = filesystem::base_name(fname);
1160  std::string img = "";
1161 
1162  if(name.find(".") != std::string::npos) {
1163  // convert to web-safe base64, since the + symbols will get stripped out when reading this back in later
1164  img = "data:image/" + name.substr(name.find(".") + 1) + ";base64," + base64::encode(file_contents);
1165  }
1166 
1167  return img;
1168 }
1169 
1170 std::string read_file(const std::string& fname)
1171 {
1172  scoped_istream is = istream_file(fname);
1173  std::stringstream ss;
1174  ss << is->rdbuf();
1175  return ss.str();
1176 }
1177 
1178 filesystem::scoped_istream istream_file(const std::string& fname, bool treat_failure_as_error)
1179 {
1180  LOG_FS << "Streaming " << fname << " for reading.";
1181 
1182  if(fname.empty()) {
1183  ERR_FS << "Trying to open file with empty name.";
1184  filesystem::scoped_istream s(new bfs::ifstream());
1185  s->clear(std::ios_base::failbit);
1186  return s;
1187  }
1188 
1189  // mingw doesn't support std::basic_ifstream::basic_ifstream(const wchar_t* fname)
1190  // that why boost::filesystem::fstream.hpp doesn't work with mingw.
1191  try {
1192  boost::iostreams::file_descriptor_source fd(bfs::path(fname), std::ios_base::binary);
1193 
1194  // TODO: has this still use ?
1195  if(!fd.is_open() && treat_failure_as_error) {
1196  ERR_FS << "Could not open '" << fname << "' for reading.";
1197  } else if(!is_filename_case_correct(fname, fd)) {
1198  ERR_FS << "Not opening '" << fname << "' due to case mismatch.";
1199  filesystem::scoped_istream s(new bfs::ifstream());
1200  s->clear(std::ios_base::failbit);
1201  return s;
1202  }
1203 
1204  return std::make_unique<boost::iostreams::stream<boost::iostreams::file_descriptor_source>>(fd, 4096, 0);
1205  } catch(const std::exception&) {
1206  if(treat_failure_as_error) {
1207  ERR_FS << "Could not open '" << fname << "' for reading.";
1208  }
1209 
1210  filesystem::scoped_istream s(new bfs::ifstream());
1211  s->clear(std::ios_base::failbit);
1212  return s;
1213  }
1214 }
1215 
1216 filesystem::scoped_ostream ostream_file(const std::string& fname, std::ios_base::openmode mode, bool create_directory)
1217 {
1218  LOG_FS << "streaming " << fname << " for writing.";
1219  try {
1220  boost::iostreams::file_descriptor_sink fd(bfs::path(fname), mode);
1221  return std::make_unique<boost::iostreams::stream<boost::iostreams::file_descriptor_sink>>(fd, 4096, 0);
1222  } catch(const BOOST_IOSTREAMS_FAILURE& e) {
1223  // If this operation failed because the parent directory didn't exist, create the parent directory and
1224  // retry.
1225  error_code ec_unused;
1226  if(create_directory && bfs::create_directories(bfs::path(fname).parent_path(), ec_unused)) {
1227  return ostream_file(fname, mode, false);
1228  }
1229 
1230  throw filesystem::io_exception(e.what());
1231  }
1232 }
1233 
1234 // Throws io_exception if an error occurs
1235 void write_file(const std::string& fname, const std::string& data, std::ios_base::openmode mode)
1236 {
1237  scoped_ostream os = ostream_file(fname, mode);
1238  os->exceptions(std::ios_base::goodbit);
1239 
1240  const std::size_t block_size = 4096;
1241  char buf[block_size];
1242 
1243  for(std::size_t i = 0; i < data.size(); i += block_size) {
1244  const std::size_t bytes = std::min<std::size_t>(block_size, data.size() - i);
1245  std::copy(data.begin() + i, data.begin() + i + bytes, buf);
1246 
1247  os->write(buf, bytes);
1248  if(os->bad()) {
1249  throw io_exception("Error writing to file: '" + fname + "'");
1250  }
1251  }
1252 }
1253 
1254 void copy_file(const std::string& src, const std::string& dest)
1255 {
1256  write_file(dest, read_file(src));
1257 }
1258 
1259 bool create_directory_if_missing(const std::string& dirname)
1260 {
1261  return create_directory_if_missing(bfs::path(dirname));
1262 }
1263 
1264 bool create_directory_if_missing_recursive(const std::string& dirname)
1265 {
1267 }
1268 
1269 bool is_directory(const std::string& fname)
1270 {
1271  return is_directory_internal(bfs::path(fname));
1272 }
1273 
1274 bool file_exists(const std::string& name)
1275 {
1276  return file_exists(bfs::path(name));
1277 }
1278 
1279 /** @todo expose to public interface. Most string functions should take a path object */
1280 std::chrono::system_clock::time_point file_modified_time(const bfs::path& path)
1281 {
1282  error_code ec;
1283  std::time_t mtime = bfs::last_write_time(path, ec);
1284  if(ec) {
1285  LOG_FS << "Failed to read modification time of " << path.string() << ": " << ec.message();
1286  }
1287 
1288  return chrono::parse_timestamp(mtime);
1289 }
1290 
1291 std::chrono::system_clock::time_point file_modified_time(const std::string& fname)
1292 {
1293  return file_modified_time(bfs::path(fname));
1294 }
1295 
1296 bool is_map(const std::string& filename)
1297 {
1298  return bfs::path(filename).extension() == map_extension;
1299 }
1300 
1301 bool is_cfg(const std::string& filename)
1302 {
1303  return bfs::path(filename).extension() == wml_extension;
1304 }
1305 
1306 bool is_mask(const std::string& filename)
1307 {
1308  return bfs::path(filename).extension() == mask_extension;
1309 }
1310 
1311 bool is_gzip_file(const std::string& filename)
1312 {
1313  return bfs::path(filename).extension() == ".gz";
1314 }
1315 
1316 bool is_bzip2_file(const std::string& filename)
1317 {
1318  return bfs::path(filename).extension() == ".bz2";
1319 }
1320 
1321 int file_size(const std::string& fname)
1322 {
1323  error_code ec;
1324  uintmax_t size = bfs::file_size(bfs::path(fname), ec);
1325  if(ec) {
1326  LOG_FS << "Failed to read filesize of " << fname << ": " << ec.message();
1327  return -1;
1328  } else if(size > std::numeric_limits<int>::max()) {
1329  return std::numeric_limits<int>::max();
1330  } else {
1331  return size;
1332  }
1333 }
1334 
1335 int dir_size(const std::string& pname)
1336 {
1337  bfs::path p(pname);
1338  uintmax_t size_sum = 0;
1339  error_code ec;
1340  for(bfs::recursive_directory_iterator i(p), end; i != end && !ec; ++i) {
1341  if(bfs::is_regular_file(i->path())) {
1342  size_sum += bfs::file_size(i->path(), ec);
1343  }
1344  }
1345 
1346  if(ec) {
1347  LOG_FS << "Failed to read directorysize of " << pname << ": " << ec.message();
1348  return -1;
1349  } else if(size_sum > std::numeric_limits<int>::max()) {
1350  return std::numeric_limits<int>::max();
1351  } else {
1352  return size_sum;
1353  }
1354 }
1355 
1356 std::string base_name(const std::string& file, const bool remove_extension)
1357 {
1358  if(!remove_extension) {
1359  return bfs::path(file).filename().string();
1360  } else {
1361  return bfs::path(file).stem().string();
1362  }
1363 }
1364 
1365 std::string directory_name(const std::string& file)
1366 {
1367  return bfs::path(file).parent_path().string();
1368 }
1369 
1370 std::string nearest_extant_parent(const std::string& file)
1371 {
1372  if(file.empty()) {
1373  return "";
1374  }
1375 
1376  bfs::path p{file};
1377  error_code ec;
1378 
1379  do {
1380  p = p.parent_path();
1381  bfs::path q = canonical(p, ec);
1382  if(!ec) {
1383  p = q;
1384  }
1385  } while(ec && !is_root(p.string()));
1386 
1387  return ec ? "" : p.string();
1388 }
1389 
1390 bool is_path_sep(char c)
1391 {
1392  static const bfs::path sep = bfs::path("/").make_preferred();
1393  const std::string s = std::string(1, c);
1394  return sep == bfs::path(s).make_preferred();
1395 }
1396 
1398 {
1399  return bfs::path::preferred_separator;
1400 }
1401 
1402 bool is_root(const std::string& path)
1403 {
1404 #ifndef _WIN32
1405  error_code ec;
1406  const bfs::path& p = bfs::canonical(path, ec);
1407  return ec ? false : !p.has_parent_path();
1408 #else
1409  //
1410  // Boost.Filesystem is completely unreliable when it comes to detecting
1411  // whether a path refers to a drive's root directory on Windows, so we are
1412  // forced to take an alternative approach here. Instead of hand-parsing
1413  // strings we'll just call a graphical shell service.
1414  //
1415  // There are several poorly-documented ways to refer to a drive in Windows by
1416  // escaping the filesystem namespace using \\.\, \\?\, and \??\. We're just
1417  // going to ignore those here, which may yield unexpected results in places
1418  // such as the file dialog. This function really shouldn't be used for
1419  // security validation anyway, and there are virtually infinite ways to name
1420  // a drive's root using the NT object namespace so it's pretty pointless to
1421  // try to catch those there.
1422  //
1423  // (And no, shlwapi.dll's PathIsRoot() doesn't recognize \\.\C:\, \\?\C:\, or
1424  // \??\C:\ as roots either.)
1425  //
1426  // More generally, do NOT use this code in security-sensitive applications.
1427  //
1428  // See also: <https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html>
1429  //
1430  const std::wstring wpath = bfs::path{path}.make_preferred().wstring();
1431  return PathIsRootW(wpath.c_str()) == TRUE;
1432 #endif
1433 }
1434 
1435 std::string root_name(const std::string& path)
1436 {
1437  return bfs::path{path}.root_name().string();
1438 }
1439 
1440 bool is_relative(const std::string& path)
1441 {
1442  return bfs::path{path}.is_relative();
1443 }
1444 
1445 std::string normalize_path(const std::string& fpath, bool normalize_separators, bool resolve_dot_entries)
1446 {
1447  if(fpath.empty()) {
1448  return fpath;
1449  }
1450 
1451  error_code ec;
1452  bfs::path p = resolve_dot_entries ? bfs::canonical(fpath, ec) : bfs::absolute(fpath);
1453 
1454  if(ec) {
1455  return "";
1456  }
1457 
1458  if(normalize_separators) {
1459  return p.make_preferred().string();
1460  } else {
1461  return p.string();
1462  }
1463 }
1464 
1465 utils::optional<std::string> to_asset_path(const std::string& path, const std::string& addon_id, const std::string& asset_type)
1466 {
1467  // datadir is absolute path to where wesnoth's data is installed
1468  bfs::path datadir = game_config::path;
1469  bfs::path core_asset_dir = datadir / "data" / "core" / asset_type;
1470  bfs::path data_asset_dir = datadir / asset_type;
1471  bfs::path outpath = path;
1472 
1473  bool found = false;
1474 
1475  if(is_prefix(path, core_asset_dir)) {
1476  // Case 1: remove leading datadir/data/core/asset_type from given absolute path
1477  // For example: given datadir/data/core/images/misc/image.png, returns misc/image.png
1478  outpath = bfs::relative(path, core_asset_dir);
1479  found = file_exists(core_asset_dir / outpath);
1480  } else if(is_prefix(path, data_asset_dir)) {
1481  // Case 2: remove leading datadir/asset_type from given absolute path
1482  // For example: given datadir/images/misc/image.png, returns misc/image.png
1483  outpath = bfs::relative(path, data_asset_dir);
1484  found = file_exists(data_asset_dir / outpath);
1485  } else if(!addon_id.empty()) {
1486  bfs::path addon_asset_dir = get_current_editor_dir(addon_id);
1487  addon_asset_dir /= asset_type;
1488 
1489  // Case 3: remove leading addondir/asset_type from given absolute path,
1490  // where addondir is absolute path to the addon's directory
1491  // For example: given addondir/images/misc/image.png, returns misc/image.png
1492  if(is_prefix(path, addon_asset_dir)) {
1493  outpath = bfs::relative(path, addon_asset_dir);
1494  found = file_exists(addon_asset_dir / outpath);
1495  }
1496  }
1497 
1498  return found ? utils::optional<std::string>{ outpath.string() } : utils::nullopt;
1499 }
1500 
1501 /**
1502  * The paths manager is responsible for recording the various paths
1503  * that binary files may be located at.
1504  * It should be passed a config object which holds binary path information.
1505  * This is in the format
1506  *@verbatim
1507  * [binary_path]
1508  * path=<path>
1509  * [/binary_path]
1510  * Binaries will be searched for in [wesnoth-path]/data/<path>/images/
1511  *@endverbatim
1512  */
1513 namespace
1514 {
1515 std::set<std::string> binary_paths;
1516 
1517 typedef std::map<std::string, std::vector<std::string>> paths_map;
1518 paths_map binary_paths_cache;
1519 
1520 } // namespace
1521 
1522 static void init_binary_paths()
1523 {
1524  if(binary_paths.empty()) {
1525  binary_paths.insert("");
1526  }
1527 }
1528 
1529 
1531  : paths_()
1532 {
1533  set_paths(cfg);
1534 }
1535 
1537 {
1538  cleanup();
1539 }
1540 
1542 {
1543  cleanup();
1545 
1546  for(const config& bp : cfg.child_range("binary_path")) {
1547  std::string path = bp["path"].str();
1548  if(path.find("..") != std::string::npos) {
1549  ERR_FS << "Invalid binary path '" << path << "'";
1550  continue;
1551  }
1552 
1553  if(!path.empty() && path.back() != '/')
1554  path += "/";
1555  if(binary_paths.count(path) == 0) {
1556  binary_paths.insert(path);
1557  paths_.push_back(path);
1558  }
1559  }
1560 }
1561 
1563 {
1564  binary_paths_cache.clear();
1565 
1566  for(const std::string& p : paths_) {
1567  binary_paths.erase(p);
1568  }
1569 }
1570 
1572 {
1573  binary_paths_cache.clear();
1574 }
1575 
1576 static bool is_legal_file(const std::string& filename_str)
1577 {
1578  DBG_FS << "Looking for '" << filename_str << "'.";
1579 
1580  if(filename_str.empty()) {
1581  LOG_FS << " invalid filename";
1582  return false;
1583  }
1584 
1585  if(filename_str.find("..") != std::string::npos) {
1586  ERR_FS << "Illegal path '" << filename_str << "' (\"..\" not allowed).";
1587  return false;
1588  }
1589 
1590  if(filename_str.find('\\') != std::string::npos) {
1591  ERR_FS << "Illegal path '" << filename_str
1592  << R"end(' ("\" not allowed, for compatibility with GNU/Linux and macOS).)end";
1593  return false;
1594  }
1595 
1596  bfs::path filepath(filename_str);
1597 
1598  if(default_blacklist.match_file(filepath.filename().string())) {
1599  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted filename).";
1600  return false;
1601  }
1602 
1603  if(std::any_of(filepath.begin(), filepath.end(),
1604  [](const bfs::path& dirname) { return default_blacklist.match_dir(dirname.string()); })) {
1605  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted directory name).";
1606  return false;
1607  }
1608 
1609  return true;
1610 }
1611 
1612 /**
1613  * Returns a vector with all possible paths to a given type of binary,
1614  * e.g. 'images', 'sounds', etc,
1615  */
1616 const std::vector<std::string>& get_binary_paths(const std::string& type)
1617 {
1618  const paths_map::const_iterator itor = binary_paths_cache.find(type);
1619  if(itor != binary_paths_cache.end()) {
1620  return itor->second;
1621  }
1622 
1623  if(type.find("..") != std::string::npos) {
1624  // Not an assertion, as language.cpp is passing user data as type.
1625  ERR_FS << "Invalid WML type '" << type << "' for binary paths";
1626  static std::vector<std::string> dummy;
1627  return dummy;
1628  }
1629 
1630  std::vector<std::string>& res = binary_paths_cache[type];
1631 
1633 
1634  for(const std::string& path : binary_paths) {
1635  res.push_back(get_user_data_dir() + "/" + path + type + "/");
1636 
1637  if(!game_config::path.empty()) {
1638  res.push_back(game_config::path + "/" + path + type + "/");
1639  }
1640  }
1641 
1642  // not found in "/type" directory, try main directory
1643  res.push_back(get_user_data_dir() + "/");
1644 
1645  if(!game_config::path.empty()) {
1646  res.push_back(game_config::path + "/");
1647  }
1648 
1649  return res;
1650 }
1651 
1652 utils::optional<std::string> get_binary_file_location(const std::string& type, const std::string& filename)
1653 {
1654  // We define ".." as "remove everything before" this is needed because
1655  // on the one hand allowing ".." would be a security risk but
1656  // especially for terrains the c++ engine puts a hardcoded "terrain/" before filename
1657  // and there would be no way to "escape" from "terrain/" otherwise. This is not the
1658  // best solution but we cannot remove it without another solution (subtypes maybe?).
1659 
1660  {
1661  std::string::size_type pos = filename.rfind("../");
1662  if(pos != std::string::npos) {
1663  return get_binary_file_location(type, filename.substr(pos + 3));
1664  }
1665  }
1666 
1667  if(!is_legal_file(filename)) {
1668  return utils::nullopt;
1669  }
1670 
1671  std::string result;
1672  // fix for duplicate mainline paths on macOS for some reason
1673  // would be good for someone who uses macOS to debug the cause at some point
1674  const std::vector<std::string> temp = get_binary_paths(type);
1675  const std::set<std::string> bpaths(temp.begin(), temp.end());
1676  for(const std::string& bp : bpaths) {
1677  bfs::path bpath(bp);
1678  bpath /= filename;
1679 
1680  DBG_FS << " checking '" << bp << "'";
1681 
1682  if(file_exists(bpath)) {
1683  DBG_FS << " found at '" << bpath.string() << "'";
1684  if(result.empty()) {
1685  result = bpath.string();
1686  } else {
1687  WRN_FS << "Conflicting files in binary_path: '" << result
1688  << "' and '" << bpath.string() << "'";
1689  }
1690  }
1691  }
1692 
1693  if(result.empty()) {
1694  DBG_FS << " not found";
1695  return utils::nullopt;
1696  } else {
1697  return result;
1698  }
1699 }
1700 
1701 utils::optional<std::string> get_binary_dir_location(const std::string& type, const std::string& filename)
1702 {
1703  if(!is_legal_file(filename)) {
1704  return utils::nullopt;
1705  }
1706 
1707  for(const std::string& bp : get_binary_paths(type)) {
1708  bfs::path bpath(bp);
1709  bpath /= filename;
1710  DBG_FS << " checking '" << bp << "'";
1711  if(is_directory_internal(bpath)) {
1712  DBG_FS << " found at '" << bpath.string() << "'";
1713  return bpath.string();
1714  }
1715  }
1716 
1717  DBG_FS << " not found";
1718  return utils::nullopt;
1719 }
1720 
1721 utils::optional<std::string> get_wml_location(const std::string& path, const utils::optional<std::string>& current_dir)
1722 {
1723  if(!is_legal_file(path)) {
1724  return utils::nullopt;
1725  }
1726 
1727  bfs::path fpath(path);
1728  bfs::path result;
1729 
1730  if(path[0] == '~') {
1731  result = get_user_data_path() / "data" / path.substr(1);
1732  DBG_FS << " trying '" << result.string() << "'";
1733  } else if(*fpath.begin() == ".") {
1734  if (!current_dir) {
1735  WRN_FS << "Cannot resolve " << path << " since the current directory is unknown!";
1736  return utils::nullopt;
1737  }
1738  result = bfs::path(*current_dir) / path;
1739  error_code ec;
1740  bfs::path c = bfs::canonical(result, ec);
1741  if (!is_prefix(c, bfs::path(game_config::path) / "data") && !is_prefix(c, get_user_data_path() / "data")) {
1742  WRN_FS << "Resolved path " << c << " is outside game and user data directories!";
1743  }
1744  } else {
1745  if(game_config::path.empty()) {
1746  WRN_FS << "Cannot resolve " << path << " since the game data directory is unknown!";
1747  return utils::nullopt;
1748  }
1749  result = bfs::path(game_config::path) / "data" / path;
1750  }
1751 
1752  if(!file_exists(result)) {
1753  DBG_FS << " not found";
1754  return utils::nullopt;
1755  } else {
1756  DBG_FS << " found: '" << result.string() << "'";
1757  return result.string();
1758  }
1759 }
1760 
1761 std::string get_short_wml_path(const std::string& filename)
1762 {
1763  bfs::path full_path(filename);
1764 
1765  bfs::path partial = subtract_path(full_path, get_user_data_path() / "data");
1766  if(!partial.empty()) {
1767  return "~" + partial.generic_string();
1768  }
1769 
1770  partial = subtract_path(full_path, bfs::path(game_config::path) / "data");
1771  if(!partial.empty()) {
1772  return partial.generic_string();
1773  }
1774 
1775  return filename;
1776 }
1777 
1778 utils::optional<std::string> get_independent_binary_file_path(const std::string& type, const std::string& filename)
1779 {
1781  if(!bp) {
1782  return utils::nullopt;
1783  }
1784 
1785  bfs::path full_path{bp.value()};
1787  if(!partial.empty()) {
1788  return partial.generic_string();
1789  }
1790 
1791  partial = subtract_path(full_path, game_config::path);
1792  if(!partial.empty()) {
1793  return partial.generic_string();
1794  }
1795 
1796  return full_path.generic_string();
1797 }
1798 
1799 std::string get_program_invocation(const std::string& program_name)
1800 {
1801 #ifdef _WIN32
1802  return program_name + ".exe";
1803 #else
1804  return program_name;
1805 #endif
1806 }
1807 
1808 std::string sanitize_path(const std::string& path)
1809 {
1810 #ifdef _WIN32
1811  const char* user_name = getenv("USERNAME");
1812 #else
1813  const char* user_name = getenv("USER");
1814 #endif
1815 
1816  std::string canonicalized = filesystem::normalize_path(path, true, false);
1817  if(user_name != nullptr) {
1818  boost::replace_all(canonicalized, user_name, "USER");
1819  }
1820 
1821  return canonicalized;
1822 }
1823 
1824 // Return path to localized counterpart of the given file, if any, or empty string.
1825 // Localized counterpart may also be requested to have a suffix to base name.
1826 utils::optional<std::string> get_localized_path(const std::string& file, const std::string& suff)
1827 {
1828  std::string dir = filesystem::directory_name(file);
1829  std::string base = filesystem::base_name(file);
1830 
1831  const std::size_t pos_ext = base.rfind(".");
1832 
1833  std::string loc_base;
1834  if(pos_ext != std::string::npos) {
1835  loc_base = base.substr(0, pos_ext) + suff + base.substr(pos_ext);
1836  } else {
1837  loc_base = base + suff;
1838  }
1839 
1840  // TRANSLATORS: This is the language code which will be used
1841  // to store and fetch localized non-textual resources, such as images,
1842  // when they exist. Normally it is just the code of the PO file itself,
1843  // e.g. "de" of de.po for German. But it can also be a comma-separated
1844  // list of language codes by priority, when the localized resource
1845  // found for first of those languages will be used. This is useful when
1846  // two languages share sufficient commonality, that they can use each
1847  // other's resources rather than duplicating them. For example,
1848  // Swedish (sv) and Danish (da) are such, so Swedish translator could
1849  // translate this message as "sv,da", while Danish as "da,sv".
1850  std::vector<std::string> langs = utils::split(_("language code for localized resources^en_US"));
1851 
1852  // In case even the original image is split into base and overlay,
1853  // add en_US with lowest priority, since the message above will
1854  // not have it when translated.
1855  langs.push_back("en_US");
1856  for(const std::string& lang : langs) {
1857  std::string loc_file = dir + "/" + "l10n" + "/" + lang + "/" + loc_base;
1858  if(filesystem::file_exists(loc_file)) {
1859  return loc_file;
1860  }
1861  }
1862 
1863  return utils::nullopt;
1864 }
1865 
1866 utils::optional<std::string> get_localized_path(const utils::optional<std::string>& base_path)
1867 {
1868  if(base_path) {
1869  if(auto localized = get_localized_path(*base_path)) {
1870  return localized;
1871  }
1872  }
1873 
1874  return base_path;
1875 }
1876 
1877 utils::optional<std::string> get_addon_id_from_path(const std::string& location)
1878 {
1879  std::string full_path = normalize_path(location, true);
1880  std::string addons_path = normalize_path(get_addons_dir(), true);
1881 
1882  if(full_path.find(addons_path) == 0) {
1883  bfs::path path(full_path.substr(addons_path.size()+1));
1884  if(path.size() > 0) {
1885  return path.begin()->string();
1886  }
1887  }
1888 
1889  return utils::nullopt;
1890 }
1891 
1892 } // 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:1032
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:885
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:875
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()
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:53
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:880
std::unique_ptr< std::ostream > scoped_ostream
Definition: filesystem.hpp:54
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
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:849
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
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:986
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:914
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:973
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:61
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:851
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: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:36
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: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:450
void set_paths(const game_config_view &cfg)
std::chrono::system_clock::time_point modified
Definition: filesystem.hpp:316
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