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