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