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