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