1 /*
2 Copyright (C) 2003 - 2012 by David White <dave@whitevine.net>
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13 */
14
15 /**
16 * @file
17 * File-IO
18 */
19
20 #include "filesystem.hpp"
21
22 #include "config.hpp"
23 #include "game_config.hpp"
24 #include "log.hpp"
25 #include "serialization/unicode.hpp"
26 #include "serialization/unicode_cast.hpp"
27 #include "game_version.hpp"
28
29 #include <boost/algorithm/string.hpp>
30 #include <boost/filesystem.hpp>
31 #include <boost/filesystem/fstream.hpp>
32 #include <boost/iostreams/device/file_descriptor.hpp>
33 #include <boost/iostreams/stream.hpp>
34 #include <boost/system/windows_error.hpp>
35
36 #ifdef _WIN32
37 #include "log_windows.hpp"
38
39 #include <boost/locale.hpp>
40
41 #include <windows.h>
42 #include <shlobj.h>
43 #include <shlwapi.h>
44
45 // Work around TDM-GCC not #defining this according to @newfrenchy83.
46 #ifndef VOLUME_NAME_NONE
47 #define VOLUME_NAME_NONE 0x4
48 #endif
49
50 #endif /* !_WIN32 */
51
52 #include <algorithm>
53 #include <set>
54
55 // Copied from boost::predef, as it's there only since 1.55.
56 #if defined(__APPLE__) && defined(__MACH__) && defined(__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__)
57
58 #define WESNOTH_BOOST_OS_IOS (__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__*1000)
59 #include <SDL2/SDL_filesystem.h>
60
61 #endif
62
63
64 static lg::log_domain log_filesystem("filesystem");
65 #define DBG_FS LOG_STREAM(debug, log_filesystem)
66 #define LOG_FS LOG_STREAM(info, log_filesystem)
67 #define WRN_FS LOG_STREAM(warn, log_filesystem)
68 #define ERR_FS LOG_STREAM(err, log_filesystem)
69
70 namespace bfs = boost::filesystem;
71 using boost::system::error_code;
72
73 namespace
74 {
75 // These are the filenames that get special processing
76 const std::string maincfg_filename = "_main.cfg";
77 const std::string finalcfg_filename = "_final.cfg";
78 const std::string initialcfg_filename = "_initial.cfg";
79
80 // only used by windows but put outside the ifdef to let it check by ci build.
81 class customcodecvt : public std::codecvt<wchar_t /*intern*/, char /*extern*/, std::mbstate_t>
82 {
83 private:
84 // private static helper things
85 template<typename char_t_to>
86 struct customcodecvt_do_conversion_writer
87 {
customcodecvt_do_conversion_writer__anon137606ae0111::customcodecvt::customcodecvt_do_conversion_writer88 customcodecvt_do_conversion_writer(char_t_to*& _to_next, char_t_to* _to_end)
89 : to_next(_to_next)
90 , to_end(_to_end)
91 {
92 }
93
94 char_t_to*& to_next;
95 char_t_to* to_end;
96
can_push__anon137606ae0111::customcodecvt::customcodecvt_do_conversion_writer97 bool can_push(size_t count) const
98 {
99 return static_cast<size_t>(to_end - to_next) > count;
100 }
101
push__anon137606ae0111::customcodecvt::customcodecvt_do_conversion_writer102 void push(char_t_to val)
103 {
104 assert(to_next != to_end);
105 *to_next++ = val;
106 }
107 };
108
109 template<typename char_t_from, typename char_t_to>
customcodecvt_do_conversion(std::mbstate_t &,const char_t_from * from,const char_t_from * from_end,const char_t_from * & from_next,char_t_to * to,char_t_to * to_end,char_t_to * & to_next)110 static void customcodecvt_do_conversion(std::mbstate_t& /*state*/,
111 const char_t_from* from,
112 const char_t_from* from_end,
113 const char_t_from*& from_next,
114 char_t_to* to,
115 char_t_to* to_end,
116 char_t_to*& to_next)
117 {
118 typedef typename ucs4_convert_impl::convert_impl<char_t_from>::type impl_type_from;
119 typedef typename ucs4_convert_impl::convert_impl<char_t_to>::type impl_type_to;
120
121 from_next = from;
122 to_next = to;
123 customcodecvt_do_conversion_writer<char_t_to> writer(to_next, to_end);
124
125 while(from_next != from_end) {
126 impl_type_to::write(writer, impl_type_from::read(from_next, from_end));
127 }
128 }
129
130 public:
131 // Not used by boost filesystem
do_encoding() const132 int do_encoding() const NOEXCEPT
133 {
134 return 0;
135 }
136
137 // Not used by boost filesystem
do_always_noconv() const138 bool do_always_noconv() const NOEXCEPT
139 {
140 return false;
141 }
142
do_length(std::mbstate_t &,const char *,const char *,std::size_t) const143 int do_length(std::mbstate_t& /*state*/, const char* /*from*/, const char* /*from_end*/, std::size_t /*max*/) const
144 {
145 // Not used by boost filesystem
146 throw "Not supported";
147 }
148
unshift(std::mbstate_t &,char *,char *,char * &) const149 std::codecvt_base::result unshift(
150 std::mbstate_t& /*state*/, char* /*to*/, char* /*to_end*/, char*& /*to_next*/) const
151 {
152 // Not used by boost filesystem
153 throw "Not supported";
154 }
155
156 // there are still some methods which could be implemented but aren't because boost filesystem won't use them.
do_in(std::mbstate_t & state,const char * from,const char * from_end,const char * & from_next,wchar_t * to,wchar_t * to_end,wchar_t * & to_next) const157 std::codecvt_base::result do_in(std::mbstate_t& state,
158 const char* from,
159 const char* from_end,
160 const char*& from_next,
161 wchar_t* to,
162 wchar_t* to_end,
163 wchar_t*& to_next) const
164 {
165 try {
166 customcodecvt_do_conversion<char, wchar_t>(state, from, from_end, from_next, to, to_end, to_next);
167 } catch(...) {
168 ERR_FS << "Invalid UTF-8 string'" << std::string(from, from_end) << "' " << std::endl;
169 return std::codecvt_base::error;
170 }
171
172 return std::codecvt_base::ok;
173 }
174
do_out(std::mbstate_t & state,const wchar_t * from,const wchar_t * from_end,const wchar_t * & from_next,char * to,char * to_end,char * & to_next) const175 std::codecvt_base::result do_out(std::mbstate_t& state,
176 const wchar_t* from,
177 const wchar_t* from_end,
178 const wchar_t*& from_next,
179 char* to,
180 char* to_end,
181 char*& to_next) const
182 {
183 try {
184 customcodecvt_do_conversion<wchar_t, char>(state, from, from_end, from_next, to, to_end, to_next);
185 } catch(...) {
186 ERR_FS << "Invalid UTF-16 string" << std::endl;
187 return std::codecvt_base::error;
188 }
189
190 return std::codecvt_base::ok;
191 }
192 };
193
194 #ifdef _WIN32
195 class static_runner
196 {
197 public:
static_runner()198 static_runner()
199 {
200 // Boost uses the current locale to generate a UTF-8 one
201 std::locale utf8_loc = boost::locale::generator().generate("");
202
203 // use a custom locale because we want to use out log.hpp functions in case of an invalid string.
204 utf8_loc = std::locale(utf8_loc, new customcodecvt());
205
206 boost::filesystem::path::imbue(utf8_loc);
207 }
208 };
209
210 static static_runner static_bfs_path_imbuer;
211
212 typedef DWORD(WINAPI* GetFinalPathNameByHandleWPtr)(HANDLE, LPWSTR, DWORD, DWORD);
213 static GetFinalPathNameByHandleWPtr dyn_GetFinalPathNameByHandle;
214
is_filename_case_correct(const std::string & fname,const boost::iostreams::file_descriptor_source & fd)215 bool is_filename_case_correct(const std::string& fname, const boost::iostreams::file_descriptor_source& fd)
216 {
217 if(dyn_GetFinalPathNameByHandle == nullptr) {
218 // Windows XP. Just assume that the case is correct.
219 return true;
220 }
221
222 wchar_t real_path[MAX_PATH];
223 dyn_GetFinalPathNameByHandle(fd.handle(), real_path, MAX_PATH - 1, VOLUME_NAME_NONE);
224
225 std::string real_name = filesystem::base_name(unicode_cast<std::string>(std::wstring(real_path)));
226 return real_name == filesystem::base_name(fname);
227 }
228
229 #else
is_filename_case_correct(const std::string &,const boost::iostreams::file_descriptor_source &)230 bool is_filename_case_correct(const std::string& /*fname*/, const boost::iostreams::file_descriptor_source& /*fd*/)
231 {
232 return true;
233 }
234 #endif
235 } // namespace
236
237 namespace filesystem
238 {
init()239 void init()
240 {
241 #ifdef _WIN32
242 HMODULE kernel32 = GetModuleHandle(TEXT("Kernel32.dll"));
243 // Note that this returns a null pointer on Windows XP!
244 dyn_GetFinalPathNameByHandle
245 = reinterpret_cast<GetFinalPathNameByHandleWPtr>(GetProcAddress(kernel32, "GetFinalPathNameByHandleW"));
246 #endif
247 }
248
push_if_exists(std::vector<std::string> * vec,const bfs::path & file,bool full)249 static void push_if_exists(std::vector<std::string>* vec, const bfs::path& file, bool full)
250 {
251 if(vec != nullptr) {
252 if(full) {
253 vec->push_back(file.generic_string());
254 } else {
255 vec->push_back(file.filename().generic_string());
256 }
257 }
258 }
259
error_except_not_found(const error_code & ec)260 static inline bool error_except_not_found(const error_code& ec)
261 {
262 return (ec && ec.value() != boost::system::errc::no_such_file_or_directory
263 #ifdef _WIN32
264 && ec.value() != boost::system::windows_error::path_not_found
265 #endif /*_WIN32*/
266 );
267 }
268
is_directory_internal(const bfs::path & fpath)269 static bool is_directory_internal(const bfs::path& fpath)
270 {
271 error_code ec;
272 bool is_dir = bfs::is_directory(fpath, ec);
273 if(error_except_not_found(ec)) {
274 LOG_FS << "Failed to check if " << fpath.string() << " is a directory: " << ec.message() << '\n';
275 }
276
277 return is_dir;
278 }
279
file_exists(const bfs::path & fpath)280 static bool file_exists(const bfs::path& fpath)
281 {
282 error_code ec;
283 bool exists = bfs::exists(fpath, ec);
284 if(error_except_not_found(ec)) {
285 ERR_FS << "Failed to check existence of file " << fpath.string() << ": " << ec.message() << '\n';
286 }
287
288 return exists;
289 }
290
get_dir(const bfs::path & dirpath)291 static bfs::path get_dir(const bfs::path& dirpath)
292 {
293 bool is_dir = is_directory_internal(dirpath);
294 if(!is_dir) {
295 error_code ec;
296 bfs::create_directory(dirpath, ec);
297
298 if(ec) {
299 ERR_FS << "Failed to create directory " << dirpath.string() << ": " << ec.message() << '\n';
300 }
301
302 // This is probably redundant
303 is_dir = is_directory_internal(dirpath);
304 }
305
306 if(!is_dir) {
307 ERR_FS << "Could not open or create directory " << dirpath.string() << '\n';
308 return std::string();
309 }
310
311 return dirpath;
312 }
313
create_directory_if_missing(const bfs::path & dirpath)314 static bool create_directory_if_missing(const bfs::path& dirpath)
315 {
316 error_code ec;
317 bfs::file_status fs = bfs::status(dirpath, ec);
318
319 if(error_except_not_found(ec)) {
320 ERR_FS << "Failed to retrieve file status for " << dirpath.string() << ": " << ec.message() << '\n';
321 return false;
322 } else if(bfs::is_directory(fs)) {
323 DBG_FS << "directory " << dirpath.string() << " exists, not creating\n";
324 return true;
325 } else if(bfs::exists(fs)) {
326 ERR_FS << "cannot create directory " << dirpath.string() << "; file exists\n";
327 return false;
328 }
329
330 bool created = bfs::create_directory(dirpath, ec);
331 if(ec) {
332 ERR_FS << "Failed to create directory " << dirpath.string() << ": " << ec.message() << '\n';
333 }
334
335 return created;
336 }
337
create_directory_if_missing_recursive(const bfs::path & dirpath)338 static bool create_directory_if_missing_recursive(const bfs::path& dirpath)
339 {
340 DBG_FS << "creating recursive directory: " << dirpath.string() << '\n';
341
342 if(dirpath.empty()) {
343 return false;
344 }
345
346 error_code ec;
347 bfs::file_status fs = bfs::status(dirpath);
348
349 if(error_except_not_found(ec)) {
350 ERR_FS << "Failed to retrieve file status for " << dirpath.string() << ": " << ec.message() << '\n';
351 return false;
352 } else if(bfs::is_directory(fs)) {
353 return true;
354 } else if(bfs::exists(fs)) {
355 return false;
356 }
357
358 if(!dirpath.has_parent_path() || create_directory_if_missing_recursive(dirpath.parent_path())) {
359 return create_directory_if_missing(dirpath);
360 } else {
361 ERR_FS << "Could not create parents to " << dirpath.string() << '\n';
362 return false;
363 }
364 }
365
get_files_in_dir(const std::string & dir,std::vector<std::string> * files,std::vector<std::string> * dirs,file_name_option mode,file_filter_option filter,file_reorder_option reorder,file_tree_checksum * checksum)366 void get_files_in_dir(const std::string& dir,
367 std::vector<std::string>* files,
368 std::vector<std::string>* dirs,
369 file_name_option mode,
370 file_filter_option filter,
371 file_reorder_option reorder,
372 file_tree_checksum* checksum)
373 {
374 if(bfs::path(dir).is_relative() && !game_config::path.empty()) {
375 bfs::path absolute_dir(game_config::path);
376 absolute_dir /= dir;
377
378 if(is_directory_internal(absolute_dir)) {
379 get_files_in_dir(absolute_dir.string(), files, dirs, mode, filter, reorder, checksum);
380 return;
381 }
382 }
383
384 const bfs::path dirpath(dir);
385
386 if(reorder == DO_REORDER) {
387 LOG_FS << "searching for _main.cfg in directory " << dir << '\n';
388 const bfs::path maincfg = dirpath / maincfg_filename;
389
390 if(file_exists(maincfg)) {
391 LOG_FS << "_main.cfg found : " << maincfg << '\n';
392 push_if_exists(files, maincfg, mode == ENTIRE_FILE_PATH);
393 return;
394 }
395 }
396
397 error_code ec;
398 bfs::directory_iterator di(dirpath, ec);
399 bfs::directory_iterator end;
400
401 // Probably not a directory, let the caller deal with it.
402 if(ec) {
403 return;
404 }
405
406 for(; di != end; ++di) {
407 bfs::file_status st = di->status(ec);
408 if(ec) {
409 LOG_FS << "Failed to get file status of " << di->path().string() << ": " << ec.message() << '\n';
410 continue;
411 }
412
413 if(st.type() == bfs::regular_file) {
414 {
415 std::string basename = di->path().filename().string();
416 if(filter == SKIP_PBL_FILES && looks_like_pbl(basename))
417 continue;
418 if(!basename.empty() && basename[0] == '.')
419 continue;
420 }
421
422 push_if_exists(files, di->path(), mode == ENTIRE_FILE_PATH);
423
424 if(checksum != nullptr) {
425 std::time_t mtime = bfs::last_write_time(di->path(), ec);
426 if(ec) {
427 LOG_FS << "Failed to read modification time of " << di->path().string() << ": " << ec.message()
428 << '\n';
429 } else if(mtime > checksum->modified) {
430 checksum->modified = mtime;
431 }
432
433 uintmax_t size = bfs::file_size(di->path(), ec);
434 if(ec) {
435 LOG_FS << "Failed to read filesize of " << di->path().string() << ": " << ec.message() << '\n';
436 } else {
437 checksum->sum_size += size;
438 }
439
440 checksum->nfiles++;
441 }
442 } else if(st.type() == bfs::directory_file) {
443 std::string basename = di->path().filename().string();
444
445 if(!basename.empty() && basename[0] == '.') {
446 continue;
447 }
448
449 if(filter == SKIP_MEDIA_DIR && (basename == "images" || basename == "sounds")) {
450 continue;
451 }
452
453 const bfs::path inner_main(di->path() / maincfg_filename);
454 bfs::file_status main_st = bfs::status(inner_main, ec);
455
456 if(error_except_not_found(ec)) {
457 LOG_FS << "Failed to get file status of " << inner_main.string() << ": " << ec.message() << '\n';
458 } else if(reorder == DO_REORDER && main_st.type() == bfs::regular_file) {
459 LOG_FS << "_main.cfg found : "
460 << (mode == ENTIRE_FILE_PATH ? inner_main.string() : inner_main.filename().string()) << '\n';
461 push_if_exists(files, inner_main, mode == ENTIRE_FILE_PATH);
462 } else {
463 push_if_exists(dirs, di->path(), mode == ENTIRE_FILE_PATH);
464 }
465 }
466 }
467
468 if(files != nullptr) {
469 std::sort(files->begin(), files->end());
470 }
471
472 if(dirs != nullptr) {
473 std::sort(dirs->begin(), dirs->end());
474 }
475
476 if(files != nullptr && reorder == DO_REORDER) {
477 // move finalcfg_filename, if present, to the end of the vector
478 for(unsigned int i = 0; i < files->size(); i++) {
479 if(ends_with((*files)[i], "/" + finalcfg_filename)) {
480 files->push_back((*files)[i]);
481 files->erase(files->begin() + i);
482 break;
483 }
484 }
485
486 // move initialcfg_filename, if present, to the beginning of the vector
487 int foundit = -1;
488 for(unsigned int i = 0; i < files->size(); i++)
489 if(ends_with((*files)[i], "/" + initialcfg_filename)) {
490 foundit = i;
491 break;
492 }
493 if(foundit > 0) {
494 std::string initialcfg = (*files)[foundit];
495 for(unsigned int i = foundit; i > 0; i--)
496 (*files)[i] = (*files)[i - 1];
497 (*files)[0] = initialcfg;
498 }
499 }
500 }
501
get_dir(const std::string & dir)502 std::string get_dir(const std::string& dir)
503 {
504 return get_dir(bfs::path(dir)).string();
505 }
506
get_next_filename(const std::string & name,const std::string & extension)507 std::string get_next_filename(const std::string& name, const std::string& extension)
508 {
509 std::string next_filename;
510 int counter = 0;
511
512 do {
513 std::stringstream filename;
514
515 filename << name;
516 filename.width(3);
517 filename.fill('0');
518 filename.setf(std::ios_base::right);
519 filename << counter << extension;
520
521 counter++;
522 next_filename = filename.str();
523 } while(file_exists(next_filename) && counter < 1000);
524
525 return next_filename;
526 }
527
528 static bfs::path user_data_dir, user_config_dir, cache_dir;
529
get_version_path_suffix()530 static const std::string& get_version_path_suffix()
531 {
532 static std::string suffix;
533
534 // We only really need to generate this once since
535 // the version number cannot change during runtime.
536
537 if(suffix.empty()) {
538 std::ostringstream s;
539 s << game_config::wesnoth_version.major_version() << '.' << game_config::wesnoth_version.minor_version();
540 suffix = s.str();
541 }
542
543 return suffix;
544 }
545
546 #if defined(__APPLE__) && !defined(__IPHONEOS__)
547 // Starting from Wesnoth 1.14.6, we have to use sandboxing function on macOS
548 // The problem is, that only signed builds can use sandbox. Unsigned builds
549 // would use other config directory then signed ones. So if we don't want
550 // to have two separate config dirs, we have to create symlink to new config
551 // location if exists. This part of code is only required on macOS.
migrate_apple_config_directory_for_unsandboxed_builds()552 static void migrate_apple_config_directory_for_unsandboxed_builds()
553 {
554 const char* home_str = getenv("HOME");
555 bfs::path home = home_str ? home_str : ".";
556
557 // We don't know which of the two is in PREFERENCES_DIR now.
558 boost::filesystem::path old_saves_dir = home / "Library/Application Support/Wesnoth_";
559 old_saves_dir += get_version_path_suffix();
560 boost::filesystem::path new_saves_dir = home / "Library/Containers/org.wesnoth.Wesnoth/Data/Library/Application Support/Wesnoth_";
561 new_saves_dir += get_version_path_suffix();
562
563 if(bfs::is_directory(new_saves_dir)) {
564 if(!bfs::exists(old_saves_dir)) {
565 std::cout << "Apple developer's userdata migration: ";
566 std::cout << "symlinking " << old_saves_dir << " to " << new_saves_dir << "\n";
567 bfs::create_symlink(new_saves_dir, old_saves_dir);
568 } else if(!bfs::symbolic_link_exists(old_saves_dir)) {
569 std::cout << "Apple developer's userdata migration: ";
570 std::cout << "Problem! Old (non-containerized) directory " << old_saves_dir << " is not a symlink. Your savegames are scattered around 2 locations.\n";
571 }
572 return;
573 }
574 }
575 #endif
576
setup_user_data_dir()577 static void setup_user_data_dir()
578 {
579 #if defined(__APPLE__) && !defined(__IPHONEOS__)
580 migrate_apple_config_directory_for_unsandboxed_builds();
581 #endif
582
583 if(!create_directory_if_missing_recursive(user_data_dir)) {
584 ERR_FS << "could not open or create user data directory at " << user_data_dir.string() << '\n';
585 return;
586 }
587 // TODO: this may not print the error message if the directory exists but we don't have the proper permissions
588
589 // Create user data and add-on directories
590 create_directory_if_missing(user_data_dir / "editor");
591 create_directory_if_missing(user_data_dir / "editor" / "maps");
592 create_directory_if_missing(user_data_dir / "editor" / "scenarios");
593 create_directory_if_missing(user_data_dir / "data");
594 create_directory_if_missing(user_data_dir / "data" / "add-ons");
595 create_directory_if_missing(user_data_dir / "saves");
596 create_directory_if_missing(user_data_dir / "persist");
597
598 #ifdef _WIN32
599 lg::finish_log_file_setup();
600 #endif
601 }
602
603 #ifdef _WIN32
604 // As a convenience for portable installs on Windows, relative paths with . or
605 // .. as the first component are considered relative to the current workdir
606 // instead of Documents/My Games.
is_path_relative_to_cwd(const std::string & str)607 static bool is_path_relative_to_cwd(const std::string& str)
608 {
609 const bfs::path p(str);
610
611 if(p.empty()) {
612 return false;
613 }
614
615 return *p.begin() == "." || *p.begin() == "..";
616 }
617 #endif
618
set_user_data_dir(std::string newprefdir)619 void set_user_data_dir(std::string newprefdir)
620 {
621 #ifdef PREFERENCES_DIR
622 if(newprefdir.empty()) {
623 newprefdir = PREFERENCES_DIR;
624 }
625 #endif
626
627 #ifdef _WIN32
628 if(newprefdir.size() > 2 && newprefdir[1] == ':') {
629 // allow absolute path override
630 user_data_dir = newprefdir;
631 } else if(is_path_relative_to_cwd(newprefdir)) {
632 // Custom directory relative to workdir (for portable installs, etc.)
633 user_data_dir = get_cwd() + "/" + newprefdir;
634 } else {
635 if(newprefdir.empty()) {
636 newprefdir = "Wesnoth" + get_version_path_suffix();
637 }
638
639 wchar_t docs_path[MAX_PATH];
640
641 HRESULT res = SHGetFolderPathW(nullptr,
642 CSIDL_PERSONAL | CSIDL_FLAG_CREATE, nullptr,
643 SHGFP_TYPE_CURRENT,
644 docs_path);
645 if(res != S_OK) {
646 //
647 // Crummy fallback path full of pain and suffering.
648 //
649 ERR_FS << "Could not determine path to user's Documents folder! (" << std::hex << "0x" << res << std::dec << ") "
650 << "User config/data directories may be unavailable for "
651 << "this session. Please report this as a bug.\n";
652 user_data_dir = bfs::path(get_cwd()) / newprefdir;
653 } else {
654 bfs::path games_path = bfs::path(docs_path) / "My Games";
655 create_directory_if_missing(games_path);
656
657 user_data_dir = games_path / newprefdir;
658 }
659 }
660
661 #else /*_WIN32*/
662
663 std::string backupprefdir = ".wesnoth" + get_version_path_suffix();
664
665 #ifdef WESNOTH_BOOST_OS_IOS
666 char *sdl_pref_path = SDL_GetPrefPath("wesnoth.org", "iWesnoth");
667 if(sdl_pref_path) {
668 backupprefdir = std::string(sdl_pref_path) + backupprefdir;
669 SDL_free(sdl_pref_path);
670 }
671 #endif
672
673 #ifdef _X11
674 const char* home_str = getenv("HOME");
675
676 if(newprefdir.empty()) {
677 char const* xdg_data = getenv("XDG_DATA_HOME");
678 if(!xdg_data || xdg_data[0] == '\0') {
679 if(!home_str) {
680 newprefdir = backupprefdir;
681 goto other;
682 }
683
684 user_data_dir = home_str;
685 user_data_dir /= ".local/share";
686 } else {
687 user_data_dir = xdg_data;
688 }
689
690 user_data_dir /= "wesnoth";
691 user_data_dir /= get_version_path_suffix();
692 } else {
693 other:
694 bfs::path home = home_str ? home_str : ".";
695
696 if(newprefdir[0] == '/') {
697 user_data_dir = newprefdir;
698 } else {
699 user_data_dir = home / newprefdir;
700 }
701 }
702 #else
703 if(newprefdir.empty()) {
704 newprefdir = backupprefdir;
705 }
706
707 const char* home_str = getenv("HOME");
708 bfs::path home = home_str ? home_str : ".";
709
710 if(newprefdir[0] == '/') {
711 user_data_dir = newprefdir;
712 } else {
713 user_data_dir = home / newprefdir;
714 }
715 #endif
716
717 #endif /*_WIN32*/
718 setup_user_data_dir();
719 user_data_dir = normalize_path(user_data_dir.string(), true, true);
720 }
721
set_user_config_path(bfs::path newconfig)722 static void set_user_config_path(bfs::path newconfig)
723 {
724 user_config_dir = newconfig;
725 if(!create_directory_if_missing_recursive(user_config_dir)) {
726 ERR_FS << "could not open or create user config directory at " << user_config_dir.string() << '\n';
727 }
728 }
729
set_user_config_dir(const std::string & newconfigdir)730 void set_user_config_dir(const std::string& newconfigdir)
731 {
732 set_user_config_path(newconfigdir);
733 }
734
get_user_data_path()735 static const bfs::path& get_user_data_path()
736 {
737 if(user_data_dir.empty()) {
738 set_user_data_dir(std::string());
739 }
740
741 return user_data_dir;
742 }
743
get_user_config_dir()744 std::string get_user_config_dir()
745 {
746 if(user_config_dir.empty()) {
747 #if defined(_X11) && !defined(PREFERENCES_DIR)
748 char const* xdg_config = getenv("XDG_CONFIG_HOME");
749
750 if(!xdg_config || xdg_config[0] == '\0') {
751 xdg_config = getenv("HOME");
752 if(!xdg_config) {
753 user_config_dir = get_user_data_path();
754 return user_config_dir.string();
755 }
756
757 user_config_dir = xdg_config;
758 user_config_dir /= ".config";
759 } else {
760 user_config_dir = xdg_config;
761 }
762
763 user_config_dir /= "wesnoth";
764 set_user_config_path(user_config_dir);
765 #else
766 user_config_dir = get_user_data_path();
767 #endif
768 }
769
770 return user_config_dir.string();
771 }
772
get_user_data_dir()773 std::string get_user_data_dir()
774 {
775 return get_user_data_path().string();
776 }
777
get_cache_dir()778 std::string get_cache_dir()
779 {
780 if(cache_dir.empty()) {
781 #if defined(_X11) && !defined(PREFERENCES_DIR)
782 char const* xdg_cache = getenv("XDG_CACHE_HOME");
783
784 if(!xdg_cache || xdg_cache[0] == '\0') {
785 xdg_cache = getenv("HOME");
786 if(!xdg_cache) {
787 cache_dir = get_dir(get_user_data_path() / "cache");
788 return cache_dir.string();
789 }
790
791 cache_dir = xdg_cache;
792 cache_dir /= ".cache";
793 } else {
794 cache_dir = xdg_cache;
795 }
796
797 cache_dir /= "wesnoth";
798 create_directory_if_missing_recursive(cache_dir);
799 #else
800 cache_dir = get_dir(get_user_data_path() / "cache");
801 #endif
802 }
803
804 return cache_dir.string();
805 }
806
get_cwd()807 std::string get_cwd()
808 {
809 error_code ec;
810 bfs::path cwd = bfs::current_path(ec);
811
812 if(ec) {
813 ERR_FS << "Failed to get current directory: " << ec.message() << '\n';
814 return "";
815 }
816
817 return cwd.generic_string();
818 }
get_exe_dir()819 std::string get_exe_dir()
820 {
821 #ifdef _WIN32
822 wchar_t process_path[MAX_PATH];
823 SetLastError(ERROR_SUCCESS);
824
825 GetModuleFileNameW(nullptr, process_path, MAX_PATH);
826
827 if(GetLastError() != ERROR_SUCCESS) {
828 return get_cwd();
829 }
830
831 bfs::path exe(process_path);
832 return exe.parent_path().string();
833 #else
834 if(bfs::exists("/proc/")) {
835 bfs::path self_exe("/proc/self/exe");
836 error_code ec;
837 bfs::path exe = bfs::read_symlink(self_exe, ec);
838 if(ec) {
839 return std::string();
840 }
841
842 return exe.parent_path().string();
843 } else {
844 return get_cwd();
845 }
846 #endif
847 }
848
make_directory(const std::string & dirname)849 bool make_directory(const std::string& dirname)
850 {
851 error_code ec;
852 bool created = bfs::create_directory(bfs::path(dirname), ec);
853 if(ec) {
854 ERR_FS << "Failed to create directory " << dirname << ": " << ec.message() << '\n';
855 }
856
857 return created;
858 }
859
delete_directory(const std::string & dirname,const bool keep_pbl)860 bool delete_directory(const std::string& dirname, const bool keep_pbl)
861 {
862 bool ret = true;
863 std::vector<std::string> files;
864 std::vector<std::string> dirs;
865 error_code ec;
866
867 get_files_in_dir(dirname, &files, &dirs, ENTIRE_FILE_PATH, keep_pbl ? SKIP_PBL_FILES : NO_FILTER);
868
869 if(!files.empty()) {
870 for(const std::string& f : files) {
871 bfs::remove(bfs::path(f), ec);
872 if(ec) {
873 LOG_FS << "remove(" << f << "): " << ec.message() << '\n';
874 ret = false;
875 }
876 }
877 }
878
879 if(!dirs.empty()) {
880 for(const std::string& d : dirs) {
881 // TODO: this does not preserve any other PBL files
882 // filesystem.cpp does this too, so this might be intentional
883 if(!delete_directory(d))
884 ret = false;
885 }
886 }
887
888 if(ret) {
889 bfs::remove(bfs::path(dirname), ec);
890 if(ec) {
891 LOG_FS << "remove(" << dirname << "): " << ec.message() << '\n';
892 ret = false;
893 }
894 }
895
896 return ret;
897 }
898
delete_file(const std::string & filename)899 bool delete_file(const std::string& filename)
900 {
901 error_code ec;
902 bool ret = bfs::remove(bfs::path(filename), ec);
903 if(ec) {
904 ERR_FS << "Could not delete file " << filename << ": " << ec.message() << '\n';
905 }
906
907 return ret;
908 }
909
read_file(const std::string & fname)910 std::string read_file(const std::string& fname)
911 {
912 scoped_istream is = istream_file(fname);
913 std::stringstream ss;
914 ss << is->rdbuf();
915 return ss.str();
916 }
917
istream_file(const std::string & fname,bool treat_failure_as_error)918 filesystem::scoped_istream istream_file(const std::string& fname, bool treat_failure_as_error)
919 {
920 LOG_FS << "Streaming " << fname << " for reading.\n";
921
922 if(fname.empty()) {
923 ERR_FS << "Trying to open file with empty name.\n";
924 filesystem::scoped_istream s(new bfs::ifstream());
925 s->clear(std::ios_base::failbit);
926 return s;
927 }
928
929 // mingw doesn't support std::basic_ifstream::basic_ifstream(const wchar_t* fname)
930 // that why boost::filesystem::fstream.hpp doesn't work with mingw.
931 try {
932 boost::iostreams::file_descriptor_source fd(bfs::path(fname), std::ios_base::binary);
933
934 // TODO: has this still use ?
935 if(!fd.is_open() && treat_failure_as_error) {
936 ERR_FS << "Could not open '" << fname << "' for reading.\n";
937 } else if(!is_filename_case_correct(fname, fd)) {
938 ERR_FS << "Not opening '" << fname << "' due to case mismatch.\n";
939 filesystem::scoped_istream s(new bfs::ifstream());
940 s->clear(std::ios_base::failbit);
941 return s;
942 }
943
944 return filesystem::scoped_istream(
945 new boost::iostreams::stream<boost::iostreams::file_descriptor_source>(fd, 4096, 0));
946 } catch(const std::exception&) {
947 if(treat_failure_as_error) {
948 ERR_FS << "Could not open '" << fname << "' for reading.\n";
949 }
950
951 filesystem::scoped_istream s(new bfs::ifstream());
952 s->clear(std::ios_base::failbit);
953 return s;
954 }
955 }
956
ostream_file(const std::string & fname,bool create_directory)957 filesystem::scoped_ostream ostream_file(const std::string& fname, bool create_directory)
958 {
959 LOG_FS << "streaming " << fname << " for writing.\n";
960 #if 1
961 try {
962 boost::iostreams::file_descriptor_sink fd(bfs::path(fname), std::ios_base::binary);
963 return filesystem::scoped_ostream(
964 new boost::iostreams::stream<boost::iostreams::file_descriptor_sink>(fd, 4096, 0));
965 } catch(const BOOST_IOSTREAMS_FAILURE& e) {
966 // If this operation failed because the parent directory didn't exist, create the parent directory and
967 // retry.
968 error_code ec_unused;
969 if(create_directory && bfs::create_directories(bfs::path(fname).parent_path(), ec_unused)) {
970 return ostream_file(fname, false);
971 }
972
973 throw filesystem::io_exception(e.what());
974 }
975 #else
976 return new bfs::ofstream(bfs::path(fname), std::ios_base::binary);
977 #endif
978 }
979
980 // Throws io_exception if an error occurs
write_file(const std::string & fname,const std::string & data)981 void write_file(const std::string& fname, const std::string& data)
982 {
983 scoped_ostream os = ostream_file(fname);
984 os->exceptions(std::ios_base::goodbit);
985
986 const size_t block_size = 4096;
987 char buf[block_size];
988
989 for(size_t i = 0; i < data.size(); i += block_size) {
990 const size_t bytes = std::min<size_t>(block_size,data.size() - i);
991 std::copy(data.begin() + i, data.begin() + i + bytes,buf);
992
993 os->write(buf, bytes);
994 if(os->bad()) {
995 throw io_exception("Error writing to file: '" + fname + "'");
996 }
997 }
998 }
999
create_directory_if_missing(const std::string & dirname)1000 bool create_directory_if_missing(const std::string& dirname)
1001 {
1002 return create_directory_if_missing(bfs::path(dirname));
1003 }
1004
create_directory_if_missing_recursive(const std::string & dirname)1005 bool create_directory_if_missing_recursive(const std::string& dirname)
1006 {
1007 return create_directory_if_missing_recursive(bfs::path(dirname));
1008 }
1009
is_directory(const std::string & fname)1010 bool is_directory(const std::string& fname)
1011 {
1012 return is_directory_internal(bfs::path(fname));
1013 }
1014
file_exists(const std::string & name)1015 bool file_exists(const std::string& name)
1016 {
1017 return file_exists(bfs::path(name));
1018 }
1019
file_modified_time(const std::string & fname)1020 time_t file_modified_time(const std::string& fname)
1021 {
1022 error_code ec;
1023 std::time_t mtime = bfs::last_write_time(bfs::path(fname), ec);
1024 if(ec) {
1025 LOG_FS << "Failed to read modification time of " << fname << ": " << ec.message() << '\n';
1026 }
1027
1028 return mtime;
1029 }
1030
is_gzip_file(const std::string & filename)1031 bool is_gzip_file(const std::string& filename)
1032 {
1033 return bfs::path(filename).extension() == ".gz";
1034 }
1035
is_bzip2_file(const std::string & filename)1036 bool is_bzip2_file(const std::string& filename)
1037 {
1038 return bfs::path(filename).extension() == ".bz2";
1039 }
1040
file_size(const std::string & fname)1041 int file_size(const std::string& fname)
1042 {
1043 error_code ec;
1044 uintmax_t size = bfs::file_size(bfs::path(fname), ec);
1045 if(ec) {
1046 LOG_FS << "Failed to read filesize of " << fname << ": " << ec.message() << '\n';
1047 return -1;
1048 } else if(size > INT_MAX) {
1049 return INT_MAX;
1050 } else {
1051 return size;
1052 }
1053 }
1054
dir_size(const std::string & pname)1055 int dir_size(const std::string& pname)
1056 {
1057 bfs::path p(pname);
1058 uintmax_t size_sum = 0;
1059 error_code ec;
1060 for(bfs::recursive_directory_iterator i(p), end; i != end && !ec; ++i) {
1061 if(bfs::is_regular_file(i->path())) {
1062 size_sum += bfs::file_size(i->path(), ec);
1063 }
1064 }
1065
1066 if(ec) {
1067 LOG_FS << "Failed to read directorysize of " << pname << ": " << ec.message() << '\n';
1068 return -1;
1069 } else if(size_sum > INT_MAX) {
1070 return INT_MAX;
1071 } else {
1072 return size_sum;
1073 }
1074 }
1075
base_name(const std::string & file,const bool remove_extension)1076 std::string base_name(const std::string& file, const bool remove_extension)
1077 {
1078 if(!remove_extension) {
1079 return bfs::path(file).filename().string();
1080 } else {
1081 return bfs::path(file).stem().string();
1082 }
1083 }
1084
directory_name(const std::string & file)1085 std::string directory_name(const std::string& file)
1086 {
1087 return bfs::path(file).parent_path().string();
1088 }
1089
nearest_extant_parent(const std::string & file)1090 std::string nearest_extant_parent(const std::string& file)
1091 {
1092 if(file.empty()) {
1093 return "";
1094 }
1095
1096 bfs::path p{file};
1097 error_code ec;
1098
1099 do {
1100 p = p.parent_path();
1101 bfs::path q = canonical(p, ec);
1102 if(!ec) {
1103 p = q;
1104 }
1105 } while(ec && !is_root(p.string()));
1106
1107 return ec ? "" : p.string();
1108 }
1109
is_path_sep(char c)1110 bool is_path_sep(char c)
1111 {
1112 static const bfs::path sep = bfs::path("/").make_preferred();
1113 const std::string s = std::string(1, c);
1114 return sep == bfs::path(s).make_preferred();
1115 }
1116
path_separator()1117 char path_separator()
1118 {
1119 return bfs::path::preferred_separator;
1120 }
1121
is_root(const std::string & path)1122 bool is_root(const std::string& path)
1123 {
1124 #ifndef _WIN32
1125 error_code ec;
1126 const bfs::path& p = bfs::canonical(path, ec);
1127 return ec ? false : !p.has_parent_path();
1128 #else
1129 //
1130 // Boost.Filesystem is completely unreliable when it comes to detecting
1131 // whether a path refers to a drive's root directory on Windows, so we are
1132 // forced to take an alternative approach here. Instead of hand-parsing
1133 // strings we'll just call a graphical shell service.
1134 //
1135 // There are several poorly-documented ways to refer to a drive in Windows by
1136 // escaping the filesystem namespace using \\.\, \\?\, and \??\. We're just
1137 // going to ignore those here, which may yield unexpected results in places
1138 // such as the file dialog. This function really shouldn't be used for
1139 // security validation anyway, and there are virtually infinite ways to name
1140 // a drive's root using the NT object namespace so it's pretty pointless to
1141 // try to catch those there.
1142 //
1143 // (And no, shlwapi.dll's PathIsRoot() doesn't recognize \\.\C:\, \\?\C:\, or
1144 // \??\C:\ as roots either.)
1145 //
1146 // More generally, do NOT use this code in security-sensitive applications.
1147 //
1148 // See also: <https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html>
1149 //
1150 const std::wstring& wpath = bfs::path{path}.make_preferred().wstring();
1151 return PathIsRootW(wpath.c_str()) == TRUE;
1152 #endif
1153 }
1154
root_name(const std::string & path)1155 std::string root_name(const std::string& path)
1156 {
1157 return bfs::path{path}.root_name().string();
1158 }
1159
is_relative(const std::string & path)1160 bool is_relative(const std::string& path)
1161 {
1162 return bfs::path{path}.is_relative();
1163 }
1164
normalize_path(const std::string & fpath,bool normalize_separators,bool resolve_dot_entries)1165 std::string normalize_path(const std::string& fpath, bool normalize_separators, bool resolve_dot_entries)
1166 {
1167 if(fpath.empty()) {
1168 return fpath;
1169 }
1170
1171 error_code ec;
1172 bfs::path p = resolve_dot_entries ? bfs::canonical(fpath, ec) : bfs::absolute(fpath);
1173
1174 if(ec) {
1175 return "";
1176 }
1177
1178 if(normalize_separators) {
1179 return p.make_preferred().string();
1180 } else {
1181 return p.string();
1182 }
1183 }
1184
1185 /**
1186 * The paths manager is responsible for recording the various paths
1187 * that binary files may be located at.
1188 * It should be passed a config object which holds binary path information.
1189 * This is in the format
1190 *@verbatim
1191 * [binary_path]
1192 * path=<path>
1193 * [/binary_path]
1194 * Binaries will be searched for in [wesnoth-path]/data/<path>/images/
1195 *@endverbatim
1196 */
1197 namespace
1198 {
1199 std::set<std::string> binary_paths;
1200
1201 typedef std::map<std::string, std::vector<std::string>> paths_map;
1202 paths_map binary_paths_cache;
1203
1204 } // namespace
1205
init_binary_paths()1206 static void init_binary_paths()
1207 {
1208 if(binary_paths.empty()) {
1209 binary_paths.insert("");
1210 }
1211 }
1212
binary_paths_manager()1213 binary_paths_manager::binary_paths_manager()
1214 : paths_()
1215 {
1216 }
1217
binary_paths_manager(const config & cfg)1218 binary_paths_manager::binary_paths_manager(const config& cfg)
1219 : paths_()
1220 {
1221 set_paths(cfg);
1222 }
1223
~binary_paths_manager()1224 binary_paths_manager::~binary_paths_manager()
1225 {
1226 cleanup();
1227 }
1228
set_paths(const config & cfg)1229 void binary_paths_manager::set_paths(const config& cfg)
1230 {
1231 cleanup();
1232 init_binary_paths();
1233
1234 for(const config& bp : cfg.child_range("binary_path")) {
1235 std::string path = bp["path"].str();
1236 if(path.find("..") != std::string::npos) {
1237 ERR_FS << "Invalid binary path '" << path << "'\n";
1238 continue;
1239 }
1240
1241 if(!path.empty() && path.back() != '/')
1242 path += "/";
1243 if(binary_paths.count(path) == 0) {
1244 binary_paths.insert(path);
1245 paths_.push_back(path);
1246 }
1247 }
1248 }
1249
cleanup()1250 void binary_paths_manager::cleanup()
1251 {
1252 binary_paths_cache.clear();
1253
1254 for(const std::string& p : paths_) {
1255 binary_paths.erase(p);
1256 }
1257 }
1258
clear_binary_paths_cache()1259 void clear_binary_paths_cache()
1260 {
1261 binary_paths_cache.clear();
1262 }
1263
is_legal_file(const std::string & filename_str)1264 static bool is_legal_file(const std::string& filename_str)
1265 {
1266 DBG_FS << "Looking for '" << filename_str << "'.\n";
1267
1268 if(filename_str.empty()) {
1269 LOG_FS << " invalid filename\n";
1270 return false;
1271 }
1272
1273 if(filename_str.find("..") != std::string::npos) {
1274 ERR_FS << "Illegal path '" << filename_str << "' (\"..\" not allowed).\n";
1275 return false;
1276 }
1277
1278 if(filename_str.find('\\') != std::string::npos) {
1279 ERR_FS << "Illegal path '" << filename_str
1280 << R"end(' ("\" not allowed, for compatibility with GNU/Linux and macOS).)end" << std::endl;
1281 return false;
1282 }
1283
1284 bfs::path filepath(filename_str);
1285
1286 if(default_blacklist.match_file(filepath.filename().string())) {
1287 ERR_FS << "Illegal path '" << filename_str << "' (blacklisted filename)." << std::endl;
1288 return false;
1289 }
1290
1291 if(std::any_of(filepath.begin(), filepath.end(),
1292 [](const bfs::path& dirname) { return default_blacklist.match_dir(dirname.string()); })) {
1293 ERR_FS << "Illegal path '" << filename_str << "' (blacklisted directory name)." << std::endl;
1294 return false;
1295 }
1296
1297 return true;
1298 }
1299
1300 /**
1301 * Returns a vector with all possible paths to a given type of binary,
1302 * e.g. 'images', 'sounds', etc,
1303 */
get_binary_paths(const std::string & type)1304 const std::vector<std::string>& get_binary_paths(const std::string& type)
1305 {
1306 const paths_map::const_iterator itor = binary_paths_cache.find(type);
1307 if(itor != binary_paths_cache.end()) {
1308 return itor->second;
1309 }
1310
1311 if(type.find("..") != std::string::npos) {
1312 // Not an assertion, as language.cpp is passing user data as type.
1313 ERR_FS << "Invalid WML type '" << type << "' for binary paths\n";
1314 static std::vector<std::string> dummy;
1315 return dummy;
1316 }
1317
1318 std::vector<std::string>& res = binary_paths_cache[type];
1319
1320 init_binary_paths();
1321
1322 for(const std::string& path : binary_paths) {
1323 res.push_back(get_user_data_dir() + "/" + path + type + "/");
1324
1325 if(!game_config::path.empty()) {
1326 res.push_back(game_config::path + "/" + path + type + "/");
1327 }
1328 }
1329
1330 // not found in "/type" directory, try main directory
1331 res.push_back(get_user_data_dir() + "/");
1332
1333 if(!game_config::path.empty()) {
1334 res.push_back(game_config::path + "/");
1335 }
1336
1337 return res;
1338 }
1339
get_binary_file_location(const std::string & type,const std::string & filename)1340 std::string get_binary_file_location(const std::string& type, const std::string& filename)
1341 {
1342 // We define ".." as "remove everything before" this is needed because
1343 // on the one hand allowing ".." would be a security risk but
1344 // especially for terrains the c++ engine puts a hardcoded "terrain/" before filename
1345 // and there would be no way to "escape" from "terrain/" otherwise. This is not the
1346 // best solution but we cannot remove it without another solution (subtypes maybe?).
1347
1348 {
1349 std::string::size_type pos = filename.rfind("../");
1350 if(pos != std::string::npos) {
1351 return get_binary_file_location(type, filename.substr(pos + 3));
1352 }
1353 }
1354
1355 if(!is_legal_file(filename)) {
1356 return std::string();
1357 }
1358
1359 std::string result;
1360 for(const std::string& bp : get_binary_paths(type)) {
1361 bfs::path bpath(bp);
1362 bpath /= filename;
1363
1364 DBG_FS << " checking '" << bp << "'\n";
1365
1366 if(file_exists(bpath)) {
1367 DBG_FS << " found at '" << bpath.string() << "'\n";
1368 if(result.empty()) {
1369 result = bpath.string();
1370 } else {
1371 WRN_FS << "Conflicting files in binary_path: '" << sanitize_path(result)
1372 << "' and '" << sanitize_path(bpath.string()) << "'\n";
1373 }
1374 }
1375 }
1376
1377 DBG_FS << " not found\n";
1378 return result;
1379 }
1380
get_binary_dir_location(const std::string & type,const std::string & filename)1381 std::string get_binary_dir_location(const std::string& type, const std::string& filename)
1382 {
1383 if(!is_legal_file(filename)) {
1384 return std::string();
1385 }
1386
1387 for(const std::string& bp : get_binary_paths(type)) {
1388 bfs::path bpath(bp);
1389 bpath /= filename;
1390 DBG_FS << " checking '" << bp << "'\n";
1391 if(is_directory_internal(bpath)) {
1392 DBG_FS << " found at '" << bpath.string() << "'\n";
1393 return bpath.string();
1394 }
1395 }
1396
1397 DBG_FS << " not found\n";
1398 return std::string();
1399 }
1400
get_wml_location(const std::string & filename,const std::string & current_dir)1401 std::string get_wml_location(const std::string& filename, const std::string& current_dir)
1402 {
1403 if(!is_legal_file(filename)) {
1404 return std::string();
1405 }
1406
1407 assert(game_config::path.empty() == false);
1408
1409 bfs::path fpath(filename);
1410 bfs::path result;
1411
1412 if(filename[0] == '~') {
1413 result /= get_user_data_path() / "data" / filename.substr(1);
1414 DBG_FS << " trying '" << result.string() << "'\n";
1415 } else if(*fpath.begin() == ".") {
1416 if(!current_dir.empty()) {
1417 result /= bfs::path(current_dir);
1418 } else {
1419 result /= bfs::path(game_config::path) / "data";
1420 }
1421
1422 result /= filename;
1423 } else if(!game_config::path.empty()) {
1424 result /= bfs::path(game_config::path) / "data" / filename;
1425 }
1426
1427 if(result.empty() || !file_exists(result)) {
1428 DBG_FS << " not found\n";
1429 result.clear();
1430 } else {
1431 DBG_FS << " found: '" << result.string() << "'\n";
1432 }
1433
1434 return result.string();
1435 }
1436
subtract_path(const bfs::path & full,const bfs::path & prefix)1437 static bfs::path subtract_path(const bfs::path& full, const bfs::path& prefix)
1438 {
1439 bfs::path::iterator fi = full.begin(), fe = full.end(), pi = prefix.begin(), pe = prefix.end();
1440 while(fi != fe && pi != pe && *fi == *pi) {
1441 ++fi;
1442 ++pi;
1443 }
1444
1445 bfs::path rest;
1446 if(pi == pe) {
1447 while(fi != fe) {
1448 rest /= *fi;
1449 ++fi;
1450 }
1451 }
1452
1453 return rest;
1454 }
1455
get_short_wml_path(const std::string & filename)1456 std::string get_short_wml_path(const std::string& filename)
1457 {
1458 bfs::path full_path(filename);
1459
1460 bfs::path partial = subtract_path(full_path, get_user_data_path() / "data");
1461 if(!partial.empty()) {
1462 return "~" + partial.generic_string();
1463 }
1464
1465 partial = subtract_path(full_path, bfs::path(game_config::path) / "data");
1466 if(!partial.empty()) {
1467 return partial.generic_string();
1468 }
1469
1470 return filename;
1471 }
1472
get_independent_image_path(const std::string & filename)1473 std::string get_independent_image_path(const std::string& filename)
1474 {
1475 bfs::path full_path(get_binary_file_location("images", filename));
1476
1477 if(full_path.empty()) {
1478 return full_path.generic_string();
1479 }
1480
1481 bfs::path partial = subtract_path(full_path, get_user_data_path());
1482 if(!partial.empty()) {
1483 return partial.generic_string();
1484 }
1485
1486 partial = subtract_path(full_path, game_config::path);
1487 if(!partial.empty()) {
1488 return partial.generic_string();
1489 }
1490
1491 return full_path.generic_string();
1492 }
1493
get_program_invocation(const std::string & program_name)1494 std::string get_program_invocation(const std::string& program_name)
1495 {
1496 const std::string real_program_name(program_name
1497 #ifdef DEBUG
1498 + "-debug"
1499 #endif
1500 #ifdef _WIN32
1501 + ".exe"
1502 #endif
1503 );
1504
1505 return (bfs::path(game_config::wesnoth_program_dir) / real_program_name).string();
1506 }
1507
sanitize_path(const std::string & path)1508 std::string sanitize_path(const std::string& path)
1509 {
1510 #ifdef _WIN32
1511 const char* user_name = getenv("USERNAME");
1512 #else
1513 const char* user_name = getenv("USER");
1514 #endif
1515
1516 std::string canonicalized = filesystem::normalize_path(path, true, false);
1517 if(user_name != nullptr) {
1518 boost::replace_all(canonicalized, user_name, "USER");
1519 }
1520
1521 return canonicalized;
1522 }
1523
1524 } // namespace filesystem
1525