The Battle for Wesnoth  1.19.11+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 utils::optional<std::string> to_asset_path(const std::string& path, const std::string& addon_id, const std::string& asset_type)
1433 {
1434  // datadir is absolute path to where wesnoth's data is installed
1435  bfs::path datadir = game_config::path;
1436  bfs::path core_asset_dir = datadir / "data" / "core" / asset_type;
1437  bfs::path data_asset_dir = datadir / asset_type;
1438  bfs::path outpath = path;
1439 
1440  bool found = false;
1441 
1442  if(is_prefix(path, core_asset_dir)) {
1443  // Case 1: remove leading datadir/data/core/asset_type from given absolute path
1444  // For example: given datadir/data/core/images/misc/image.png, returns misc/image.png
1445  outpath = bfs::relative(path, core_asset_dir);
1446  found = file_exists(core_asset_dir / outpath);
1447  } else if(is_prefix(path, data_asset_dir)) {
1448  // Case 2: remove leading datadir/asset_type from given absolute path
1449  // For example: given datadir/images/misc/image.png, returns misc/image.png
1450  outpath = bfs::relative(path, data_asset_dir);
1451  found = file_exists(data_asset_dir / outpath);
1452  } else if(!addon_id.empty()) {
1453  bfs::path addon_asset_dir = get_current_editor_dir(addon_id);
1454  addon_asset_dir /= asset_type;
1455 
1456  // Case 3: remove leading addondir/asset_type from given absolute path,
1457  // where addondir is absolute path to the addon's directory
1458  // For example: given addondir/images/misc/image.png, returns misc/image.png
1459  if(is_prefix(path, addon_asset_dir)) {
1460  outpath = bfs::relative(path, addon_asset_dir);
1461  found = file_exists(addon_asset_dir / outpath);
1462  }
1463  }
1464 
1465  return found ? utils::optional<std::string>{ outpath.string() } : utils::nullopt;
1466 }
1467 
1468 /**
1469  * The paths manager is responsible for recording the various paths
1470  * that binary files may be located at.
1471  * It should be passed a config object which holds binary path information.
1472  * This is in the format
1473  *@verbatim
1474  * [binary_path]
1475  * path=<path>
1476  * [/binary_path]
1477  * Binaries will be searched for in [wesnoth-path]/data/<path>/images/
1478  *@endverbatim
1479  */
1480 namespace
1481 {
1482 std::set<std::string> binary_paths;
1483 
1484 typedef std::map<std::string, std::vector<std::string>> paths_map;
1485 paths_map binary_paths_cache;
1486 
1487 } // namespace
1488 
1489 static void init_binary_paths()
1490 {
1491  if(binary_paths.empty()) {
1492  binary_paths.insert("");
1493  }
1494 }
1495 
1497  : paths_()
1498 {
1499 }
1500 
1502  : paths_()
1503 {
1504  set_paths(cfg);
1505 }
1506 
1508 {
1509  cleanup();
1510 }
1511 
1513 {
1514  cleanup();
1516 
1517  for(const config& bp : cfg.child_range("binary_path")) {
1518  std::string path = bp["path"].str();
1519  if(path.find("..") != std::string::npos) {
1520  ERR_FS << "Invalid binary path '" << path << "'";
1521  continue;
1522  }
1523 
1524  if(!path.empty() && path.back() != '/')
1525  path += "/";
1526  if(binary_paths.count(path) == 0) {
1527  binary_paths.insert(path);
1528  paths_.push_back(path);
1529  }
1530  }
1531 }
1532 
1534 {
1535  binary_paths_cache.clear();
1536 
1537  for(const std::string& p : paths_) {
1538  binary_paths.erase(p);
1539  }
1540 }
1541 
1543 {
1544  binary_paths_cache.clear();
1545 }
1546 
1547 static bool is_legal_file(const std::string& filename_str)
1548 {
1549  DBG_FS << "Looking for '" << filename_str << "'.";
1550 
1551  if(filename_str.empty()) {
1552  LOG_FS << " invalid filename";
1553  return false;
1554  }
1555 
1556  if(filename_str.find("..") != std::string::npos) {
1557  ERR_FS << "Illegal path '" << filename_str << "' (\"..\" not allowed).";
1558  return false;
1559  }
1560 
1561  if(filename_str.find('\\') != std::string::npos) {
1562  ERR_FS << "Illegal path '" << filename_str
1563  << R"end(' ("\" not allowed, for compatibility with GNU/Linux and macOS).)end";
1564  return false;
1565  }
1566 
1567  bfs::path filepath(filename_str);
1568 
1569  if(default_blacklist.match_file(filepath.filename().string())) {
1570  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted filename).";
1571  return false;
1572  }
1573 
1574  if(std::any_of(filepath.begin(), filepath.end(),
1575  [](const bfs::path& dirname) { return default_blacklist.match_dir(dirname.string()); })) {
1576  ERR_FS << "Illegal path '" << filename_str << "' (blacklisted directory name).";
1577  return false;
1578  }
1579 
1580  return true;
1581 }
1582 
1583 /**
1584  * Returns a vector with all possible paths to a given type of binary,
1585  * e.g. 'images', 'sounds', etc,
1586  */
1587 const std::vector<std::string>& get_binary_paths(const std::string& type)
1588 {
1589  const paths_map::const_iterator itor = binary_paths_cache.find(type);
1590  if(itor != binary_paths_cache.end()) {
1591  return itor->second;
1592  }
1593 
1594  if(type.find("..") != std::string::npos) {
1595  // Not an assertion, as language.cpp is passing user data as type.
1596  ERR_FS << "Invalid WML type '" << type << "' for binary paths";
1597  static std::vector<std::string> dummy;
1598  return dummy;
1599  }
1600 
1601  std::vector<std::string>& res = binary_paths_cache[type];
1602 
1604 
1605  for(const std::string& path : binary_paths) {
1606  res.push_back(get_user_data_dir() + "/" + path + type + "/");
1607 
1608  if(!game_config::path.empty()) {
1609  res.push_back(game_config::path + "/" + path + type + "/");
1610  }
1611  }
1612 
1613  // not found in "/type" directory, try main directory
1614  res.push_back(get_user_data_dir() + "/");
1615 
1616  if(!game_config::path.empty()) {
1617  res.push_back(game_config::path + "/");
1618  }
1619 
1620  return res;
1621 }
1622 
1623 utils::optional<std::string> get_binary_file_location(const std::string& type, const std::string& filename)
1624 {
1625  // We define ".." as "remove everything before" this is needed because
1626  // on the one hand allowing ".." would be a security risk but
1627  // especially for terrains the c++ engine puts a hardcoded "terrain/" before filename
1628  // and there would be no way to "escape" from "terrain/" otherwise. This is not the
1629  // best solution but we cannot remove it without another solution (subtypes maybe?).
1630 
1631  {
1632  std::string::size_type pos = filename.rfind("../");
1633  if(pos != std::string::npos) {
1634  return get_binary_file_location(type, filename.substr(pos + 3));
1635  }
1636  }
1637 
1638  if(!is_legal_file(filename)) {
1639  return utils::nullopt;
1640  }
1641 
1642  std::string result;
1643  // fix for duplicate mainline paths on macOS for some reason
1644  // would be good for someone who uses macOS to debug the cause at some point
1645  const std::vector<std::string> temp = get_binary_paths(type);
1646  const std::set<std::string> bpaths(temp.begin(), temp.end());
1647  for(const std::string& bp : bpaths) {
1648  bfs::path bpath(bp);
1649  bpath /= filename;
1650 
1651  DBG_FS << " checking '" << bp << "'";
1652 
1653  if(file_exists(bpath)) {
1654  DBG_FS << " found at '" << bpath.string() << "'";
1655  if(result.empty()) {
1656  result = bpath.string();
1657  } else {
1658  WRN_FS << "Conflicting files in binary_path: '" << result
1659  << "' and '" << bpath.string() << "'";
1660  }
1661  }
1662  }
1663 
1664  if(result.empty()) {
1665  DBG_FS << " not found";
1666  return utils::nullopt;
1667  } else {
1668  return result;
1669  }
1670 }
1671 
1672 utils::optional<std::string> get_binary_dir_location(const std::string& type, const std::string& filename)
1673 {
1674  if(!is_legal_file(filename)) {
1675  return utils::nullopt;
1676  }
1677 
1678  for(const std::string& bp : get_binary_paths(type)) {
1679  bfs::path bpath(bp);
1680  bpath /= filename;
1681  DBG_FS << " checking '" << bp << "'";
1682  if(is_directory_internal(bpath)) {
1683  DBG_FS << " found at '" << bpath.string() << "'";
1684  return bpath.string();
1685  }
1686  }
1687 
1688  DBG_FS << " not found";
1689  return utils::nullopt;
1690 }
1691 
1692 utils::optional<std::string> get_wml_location(const std::string& path, const utils::optional<std::string>& current_dir)
1693 {
1694  if(!is_legal_file(path)) {
1695  return utils::nullopt;
1696  }
1697 
1698  bfs::path fpath(path);
1699  bfs::path result;
1700 
1701  if(path[0] == '~') {
1702  result = get_user_data_path() / "data" / path.substr(1);
1703  DBG_FS << " trying '" << result.string() << "'";
1704  } else if(*fpath.begin() == ".") {
1705  if (!current_dir) {
1706  WRN_FS << "Cannot resolve " << path << " since the current directory is unknown!";
1707  return utils::nullopt;
1708  }
1709  result = bfs::path(*current_dir) / path;
1710  error_code ec;
1711  bfs::path c = bfs::canonical(result, ec);
1712  if (!is_prefix(c, bfs::path(game_config::path) / "data") && !is_prefix(c, get_user_data_path() / "data")) {
1713  WRN_FS << "Resolved path " << c << " is outside game and user data directories!";
1714  }
1715  } else {
1716  if(game_config::path.empty()) {
1717  WRN_FS << "Cannot resolve " << path << " since the game data directory is unknown!";
1718  return utils::nullopt;
1719  }
1720  result = bfs::path(game_config::path) / "data" / path;
1721  }
1722 
1723  if(!file_exists(result)) {
1724  DBG_FS << " not found";
1725  return utils::nullopt;
1726  } else {
1727  DBG_FS << " found: '" << result.string() << "'";
1728  return result.string();
1729  }
1730 }
1731 
1732 std::string get_short_wml_path(const std::string& filename)
1733 {
1734  bfs::path full_path(filename);
1735 
1736  bfs::path partial = subtract_path(full_path, get_user_data_path() / "data");
1737  if(!partial.empty()) {
1738  return "~" + partial.generic_string();
1739  }
1740 
1741  partial = subtract_path(full_path, bfs::path(game_config::path) / "data");
1742  if(!partial.empty()) {
1743  return partial.generic_string();
1744  }
1745 
1746  return filename;
1747 }
1748 
1749 utils::optional<std::string> get_independent_binary_file_path(const std::string& type, const std::string& filename)
1750 {
1752  if(!bp) {
1753  return utils::nullopt;
1754  }
1755 
1756  bfs::path full_path{bp.value()};
1758  if(!partial.empty()) {
1759  return partial.generic_string();
1760  }
1761 
1762  partial = subtract_path(full_path, game_config::path);
1763  if(!partial.empty()) {
1764  return partial.generic_string();
1765  }
1766 
1767  return full_path.generic_string();
1768 }
1769 
1770 std::string get_program_invocation(const std::string& program_name)
1771 {
1772 #ifdef _WIN32
1773  return program_name + ".exe";
1774 #else
1775  return program_name;
1776 #endif
1777 }
1778 
1779 std::string sanitize_path(const std::string& path)
1780 {
1781 #ifdef _WIN32
1782  const char* user_name = getenv("USERNAME");
1783 #else
1784  const char* user_name = getenv("USER");
1785 #endif
1786 
1787  std::string canonicalized = filesystem::normalize_path(path, true, false);
1788  if(user_name != nullptr) {
1789  boost::replace_all(canonicalized, user_name, "USER");
1790  }
1791 
1792  return canonicalized;
1793 }
1794 
1795 // Return path to localized counterpart of the given file, if any, or empty string.
1796 // Localized counterpart may also be requested to have a suffix to base name.
1797 utils::optional<std::string> get_localized_path(const std::string& file, const std::string& suff)
1798 {
1799  std::string dir = filesystem::directory_name(file);
1800  std::string base = filesystem::base_name(file);
1801 
1802  const std::size_t pos_ext = base.rfind(".");
1803 
1804  std::string loc_base;
1805  if(pos_ext != std::string::npos) {
1806  loc_base = base.substr(0, pos_ext) + suff + base.substr(pos_ext);
1807  } else {
1808  loc_base = base + suff;
1809  }
1810 
1811  // TRANSLATORS: This is the language code which will be used
1812  // to store and fetch localized non-textual resources, such as images,
1813  // when they exist. Normally it is just the code of the PO file itself,
1814  // e.g. "de" of de.po for German. But it can also be a comma-separated
1815  // list of language codes by priority, when the localized resource
1816  // found for first of those languages will be used. This is useful when
1817  // two languages share sufficient commonality, that they can use each
1818  // other's resources rather than duplicating them. For example,
1819  // Swedish (sv) and Danish (da) are such, so Swedish translator could
1820  // translate this message as "sv,da", while Danish as "da,sv".
1821  std::vector<std::string> langs = utils::split(_("language code for localized resources^en_US"));
1822 
1823  // In case even the original image is split into base and overlay,
1824  // add en_US with lowest priority, since the message above will
1825  // not have it when translated.
1826  langs.push_back("en_US");
1827  for(const std::string& lang : langs) {
1828  std::string loc_file = dir + "/" + "l10n" + "/" + lang + "/" + loc_base;
1829  if(filesystem::file_exists(loc_file)) {
1830  return loc_file;
1831  }
1832  }
1833 
1834  return utils::nullopt;
1835 }
1836 
1837 utils::optional<std::string> get_addon_id_from_path(const std::string& location)
1838 {
1839  std::string full_path = normalize_path(location, true);
1840  std::string addons_path = normalize_path(get_addons_dir(), true);
1841 
1842  if(full_path.find(addons_path) == 0) {
1843  bfs::path path(full_path.substr(addons_path.size()+1));
1844  if(path.size() > 0) {
1845  return path.begin()->string();
1846  }
1847  }
1848 
1849  return utils::nullopt;
1850 }
1851 
1852 } // 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
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
utils::optional< std::string > to_asset_path(const std::string &path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
std::string normalize_path(const std::string &fpath, bool normalize_separators, bool resolve_dot_entries)
Returns the absolute path of a file.
void set_user_data_dir(std::string newprefdir)
Definition: filesystem.cpp: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:840
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:600
void write(std::ostream &out, const configr_of &cfg, unsigned int level)
Definition: parser.cpp:737
void move_log_file()
Move the log file to another directory.
Definition: log.cpp: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:188
rect src
Non-transparent portion of the surface to compose.
std::string filename
Filename.
std::vector< std::string > paths_
Definition: filesystem.hpp:452
void set_paths(const game_config_view &cfg)
std::chrono::system_clock::time_point modified
Definition: filesystem.hpp:317
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
mock_char c
mock_party p
static map_location::direction s
#define d
#define e
#define h
#define f