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