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