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