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