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