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