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