Fix: Don't allow right-click to close world generation progress window. (#13084)
[openttd-github.git] / src / fileio.cpp
bloba63f74dce22275a1c74d42b2f398afc170ef2a8b
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file fileio.cpp Standard In/Out file operations */
10 #include "stdafx.h"
11 #include "fileio_func.h"
12 #include "spriteloader/spriteloader.hpp"
13 #include "debug.h"
14 #include "fios.h"
15 #include "string_func.h"
16 #include "tar_type.h"
17 #ifdef _WIN32
18 #include <windows.h>
19 #elif defined(__HAIKU__)
20 #include <Path.h>
21 #include <storage/FindDirectory.h>
22 #else
23 #include <unistd.h>
24 #include <pwd.h>
25 #endif
26 #include <charconv>
27 #include <sys/stat.h>
28 #include <sstream>
29 #include <filesystem>
31 #include "safeguards.h"
33 /** Whether the working directory should be scanned. */
34 static bool _do_scan_working_directory = true;
36 extern std::string _config_file;
37 extern std::string _highscore_file;
39 static const char * const _subdirs[] = {
40 "",
41 "save" PATHSEP,
42 "save" PATHSEP "autosave" PATHSEP,
43 "scenario" PATHSEP,
44 "scenario" PATHSEP "heightmap" PATHSEP,
45 "gm" PATHSEP,
46 "data" PATHSEP,
47 "baseset" PATHSEP,
48 "newgrf" PATHSEP,
49 "lang" PATHSEP,
50 "ai" PATHSEP,
51 "ai" PATHSEP "library" PATHSEP,
52 "game" PATHSEP,
53 "game" PATHSEP "library" PATHSEP,
54 "screenshot" PATHSEP,
55 "social_integration" PATHSEP,
57 static_assert(lengthof(_subdirs) == NUM_SUBDIRS);
59 /**
60 * The search paths OpenTTD could search through.
61 * At least one of the slots has to be filled with a path.
62 * An empty string tells that there is no such path for the
63 * current operating system.
65 std::array<std::string, NUM_SEARCHPATHS> _searchpaths;
66 std::vector<Searchpath> _valid_searchpaths;
67 std::array<TarList, NUM_SUBDIRS> _tar_list;
68 TarFileList _tar_filelist[NUM_SUBDIRS];
70 /**
71 * Checks whether the given search path is a valid search path
72 * @param sp the search path to check
73 * @return true if the search path is valid
75 static bool IsValidSearchPath(Searchpath sp)
77 return sp < _searchpaths.size() && !_searchpaths[sp].empty();
80 static void FillValidSearchPaths(bool only_local_path)
82 _valid_searchpaths.clear();
84 std::set<std::string> seen{};
85 for (Searchpath sp = SP_FIRST_DIR; sp < NUM_SEARCHPATHS; sp++) {
86 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
88 if (only_local_path) {
89 switch (sp) {
90 case SP_WORKING_DIR: // Can be influence by "-c" option.
91 case SP_BINARY_DIR: // Most likely contains all the language files.
92 case SP_AUTODOWNLOAD_DIR: // Otherwise we cannot download in-game content.
93 break;
95 default:
96 continue;
100 if (IsValidSearchPath(sp)) {
101 if (seen.count(_searchpaths[sp]) != 0) continue;
102 seen.insert(_searchpaths[sp]);
103 _valid_searchpaths.emplace_back(sp);
107 /* The working-directory is special, as it is controlled by _do_scan_working_directory.
108 * Only add the search path if it isn't already in the set. To preserve the same order
109 * as the enum, insert it in the front. */
110 if (IsValidSearchPath(SP_WORKING_DIR) && seen.count(_searchpaths[SP_WORKING_DIR]) == 0) {
111 _valid_searchpaths.insert(_valid_searchpaths.begin(), SP_WORKING_DIR);
116 * Check whether the given file exists
117 * @param filename the file to try for existence.
118 * @param subdir the subdirectory to look in
119 * @return true if and only if the file can be opened
121 bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
123 auto f = FioFOpenFile(filename, "rb", subdir);
124 return f.has_value();
128 * Test whether the given filename exists.
129 * @param filename the file to test.
130 * @return true if and only if the file exists.
132 bool FileExists(const std::string &filename)
134 std::error_code ec;
135 return std::filesystem::exists(OTTD2FS(filename), ec);
139 * Find a path to the filename in one of the search directories.
140 * @param subdir Subdirectory to try.
141 * @param filename Filename to look for.
142 * @return String containing the path if the path was found, else an empty string.
144 std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
146 assert(subdir < NUM_SUBDIRS);
148 for (Searchpath sp : _valid_searchpaths) {
149 std::string buf = FioGetDirectory(sp, subdir);
150 buf += filename;
151 if (FileExists(buf)) return buf;
152 #if !defined(_WIN32)
153 /* Be, as opening files, aware that sometimes the filename
154 * might be in uppercase when it is in lowercase on the
155 * disk. Of course Windows doesn't care about casing. */
156 if (strtolower(buf, _searchpaths[sp].size() - 1) && FileExists(buf)) return buf;
157 #endif
160 return {};
163 std::string FioGetDirectory(Searchpath sp, Subdirectory subdir)
165 assert(subdir < NUM_SUBDIRS);
166 assert(sp < NUM_SEARCHPATHS);
168 return _searchpaths[sp] + _subdirs[subdir];
171 std::string FioFindDirectory(Subdirectory subdir)
173 /* Find and return the first valid directory */
174 for (Searchpath sp : _valid_searchpaths) {
175 std::string ret = FioGetDirectory(sp, subdir);
176 if (FileExists(ret)) return ret;
179 /* Could not find the directory, fall back to a base path */
180 return _personal_dir;
183 static std::optional<FileHandle> FioFOpenFileSp(const std::string &filename, const char *mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
185 #if defined(_WIN32)
186 /* fopen is implemented as a define with ellipses for
187 * Unicode support (prepend an L). As we are not sending
188 * a string, but a variable, it 'renames' the variable,
189 * so make that variable to makes it compile happily */
190 wchar_t Lmode[5];
191 MultiByteToWideChar(CP_ACP, 0, mode, -1, Lmode, static_cast<int>(std::size(Lmode)));
192 #endif
193 std::string buf;
195 if (subdir == NO_DIRECTORY) {
196 buf = filename;
197 } else {
198 buf = _searchpaths[sp] + _subdirs[subdir] + filename;
201 auto f = FileHandle::Open(buf, mode);
202 #if !defined(_WIN32)
203 if (!f.has_value() && strtolower(buf, subdir == NO_DIRECTORY ? 0 : _searchpaths[sp].size() - 1) ) {
204 f = FileHandle::Open(buf, mode);
206 #endif
207 if (f.has_value() && filesize != nullptr) {
208 /* Find the size of the file */
209 fseek(*f, 0, SEEK_END);
210 *filesize = ftell(*f);
211 fseek(*f, 0, SEEK_SET);
213 return f;
217 * Opens a file from inside a tar archive.
218 * @param entry The entry to open.
219 * @param[out] filesize If not \c nullptr, size of the opened file.
220 * @return File handle of the opened file, or \c nullptr if the file is not available.
221 * @note The file is read from within the tar file, and may not return \c EOF after reading the whole file.
223 static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
225 auto f = FileHandle::Open(entry.tar_filename, "rb");
226 if (!f.has_value()) return std::nullopt;
228 if (fseek(*f, entry.position, SEEK_SET) < 0) {
229 return std::nullopt;
232 if (filesize != nullptr) *filesize = entry.size;
233 return f;
237 * Opens a OpenTTD file somewhere in a personal or global directory.
238 * @param filename Name of the file to open.
239 * @param subdir Subdirectory to open.
240 * @return File handle of the opened file, or \c nullptr if the file is not available.
242 std::optional<FileHandle> FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
244 std::optional<FileHandle> f = std::nullopt;
245 assert(subdir < NUM_SUBDIRS || subdir == NO_DIRECTORY);
247 for (Searchpath sp : _valid_searchpaths) {
248 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
249 if (f.has_value() || subdir == NO_DIRECTORY) break;
252 /* We can only use .tar in case of data-dir, and read-mode */
253 if (!f.has_value() && mode[0] == 'r' && subdir != NO_DIRECTORY) {
254 /* Filenames in tars are always forced to be lowercase */
255 std::string resolved_name = filename;
256 strtolower(resolved_name);
258 /* Resolve ".." */
259 std::istringstream ss(resolved_name);
260 std::vector<std::string> tokens;
261 std::string token;
262 while (std::getline(ss, token, PATHSEPCHAR)) {
263 if (token == "..") {
264 if (tokens.size() < 2) return std::nullopt;
265 tokens.pop_back();
266 } else if (token == ".") {
267 /* Do nothing. "." means current folder, but you can create tar files with "." in the path.
268 * This confuses our file resolver. So, act like this folder doesn't exist. */
269 } else {
270 tokens.push_back(token);
274 resolved_name.clear();
275 bool first = true;
276 for (const std::string &token : tokens) {
277 if (!first) {
278 resolved_name += PATHSEP;
280 resolved_name += token;
281 first = false;
284 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
285 if (it != _tar_filelist[subdir].end()) {
286 f = FioFOpenFileTar(it->second, filesize);
290 /* Sometimes a full path is given. To support
291 * the 'subdirectory' must be 'removed'. */
292 if (!f.has_value() && subdir != NO_DIRECTORY) {
293 switch (subdir) {
294 case BASESET_DIR:
295 f = FioFOpenFile(filename, mode, OLD_GM_DIR, filesize);
296 if (f.has_value()) break;
297 [[fallthrough]];
298 case NEWGRF_DIR:
299 f = FioFOpenFile(filename, mode, OLD_DATA_DIR, filesize);
300 break;
302 default:
303 f = FioFOpenFile(filename, mode, NO_DIRECTORY, filesize);
304 break;
308 return f;
312 * Create a directory with the given name
313 * If the parent directory does not exist, it will try to create that as well.
314 * @param name the new name of the directory
316 void FioCreateDirectory(const std::string &name)
318 /* Ignore directory creation errors; they'll surface later on. */
319 std::error_code error_code;
320 std::filesystem::create_directories(OTTD2FS(name), error_code);
324 * Remove a file.
325 * @param filename Filename to remove.
326 * @return true iff the file was removed.
328 bool FioRemove(const std::string &filename)
330 std::filesystem::path path = OTTD2FS(filename);
331 std::error_code error_code;
332 std::filesystem::remove(path, error_code);
333 if (error_code) {
334 Debug(misc, 0, "Removing {} failed: {}", filename, error_code.message());
335 return false;
337 return true;
341 * Appends, if necessary, the path separator character to the end of the string.
342 * It does not add the path separator to zero-sized strings.
343 * @param buf string to append the separator to
344 * @return true iff the operation succeeded
346 void AppendPathSeparator(std::string &buf)
348 if (buf.empty()) return;
350 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
354 * Simplify filenames from tars.
355 * Replace '/' by #PATHSEPCHAR, and force 'name' to lowercase.
356 * @param name Filename to process.
358 static void SimplifyFileName(std::string &name)
360 for (char &c : name) {
361 /* Force lowercase */
362 c = std::tolower(c);
363 #if (PATHSEPCHAR != '/')
364 /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */
365 if (c == '/') c = PATHSEPCHAR;
366 #endif
371 * Perform the scanning of a particular subdirectory.
372 * @param sd The subdirectory to scan.
373 * @return The number of found tar files.
375 uint TarScanner::DoScan(Subdirectory sd)
377 _tar_filelist[sd].clear();
378 _tar_list[sd].clear();
379 uint num = this->Scan(".tar", sd, false);
380 if (sd == BASESET_DIR || sd == NEWGRF_DIR) num += this->Scan(".tar", OLD_DATA_DIR, false);
381 return num;
384 /* static */ uint TarScanner::DoScan(TarScanner::Mode mode)
386 Debug(misc, 2, "Scanning for tars");
387 TarScanner fs;
388 uint num = 0;
389 if (mode & TarScanner::BASESET) {
390 num += fs.DoScan(BASESET_DIR);
392 if (mode & TarScanner::NEWGRF) {
393 num += fs.DoScan(NEWGRF_DIR);
395 if (mode & TarScanner::AI) {
396 num += fs.DoScan(AI_DIR);
397 num += fs.DoScan(AI_LIBRARY_DIR);
399 if (mode & TarScanner::GAME) {
400 num += fs.DoScan(GAME_DIR);
401 num += fs.DoScan(GAME_LIBRARY_DIR);
403 if (mode & TarScanner::SCENARIO) {
404 num += fs.DoScan(SCENARIO_DIR);
405 num += fs.DoScan(HEIGHTMAP_DIR);
407 Debug(misc, 2, "Scan complete, found {} files", num);
408 return num;
412 * Add a single file to the scanned files of a tar, circumventing the scanning code.
413 * @param sd The sub directory the file is in.
414 * @param filename The name of the file to add.
415 * @return True if the additions went correctly.
417 bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
419 this->subdir = sd;
420 return this->AddFile(filename, 0);
424 * Helper to extract a string for the tar header. We must assume that the tar
425 * header contains garbage and is malicious. So, we cannot rely on the string
426 * being properly terminated.
427 * As such, do not use strlen to determine the actual length (explicitly or
428 * implictly via the std::string constructor), but pass the buffer bounds
429 * explicitly.
430 * @param buffer The buffer to read from.
431 * @return The string data.
433 static std::string ExtractString(std::span<char> buffer)
435 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
438 bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
440 /* No tar within tar. */
441 assert(tar_filename.empty());
443 /* The TAR-header, repeated for every file */
444 struct TarHeader {
445 char name[100]; ///< Name of the file
446 char mode[8];
447 char uid[8];
448 char gid[8];
449 char size[12]; ///< Size of the file, in ASCII octals
450 char mtime[12];
451 char chksum[8];
452 char typeflag;
453 char linkname[100];
454 char magic[6];
455 char version[2];
456 char uname[32];
457 char gname[32];
458 char devmajor[8];
459 char devminor[8];
460 char prefix[155]; ///< Path of the file
462 char unused[12];
465 /* Check if we already seen this file */
466 TarList::iterator it = _tar_list[this->subdir].find(filename);
467 if (it != _tar_list[this->subdir].end()) return false;
469 auto of = FileHandle::Open(filename, "rb");
470 /* Although the file has been found there can be
471 * a number of reasons we cannot open the file.
472 * Most common case is when we simply have not
473 * been given read access. */
474 if (!of.has_value()) return false;
475 auto &f = *of;
477 _tar_list[this->subdir][filename] = std::string{};
479 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename());
480 SimplifyFileName(filename_base);
482 TarHeader th;
483 size_t num = 0, pos = 0;
485 /* Make a char of 512 empty bytes */
486 char empty[512];
487 memset(&empty[0], 0, sizeof(empty));
489 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
490 size_t num_bytes_read = fread(&th, 1, 512, f);
491 if (num_bytes_read != 512) break;
492 pos += num_bytes_read;
494 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
495 if (strncmp(th.magic, "ustar", 5) != 0 && memcmp(&th.magic, &empty[0], 512 - offsetof(TarHeader, magic)) != 0) {
496 /* If we have only zeros in the block, it can be an end-of-file indicator */
497 if (memcmp(&th, &empty[0], 512) == 0) continue;
499 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
500 return false;
503 std::string name;
505 /* The prefix contains the directory-name */
506 if (th.prefix[0] != '\0') {
507 name = ExtractString(th.prefix);
508 name += PATHSEP;
511 /* Copy the name of the file in a safe way at the end of 'name' */
512 name += ExtractString(th.name);
514 /* The size of the file, for some strange reason, this is stored as a string in octals. */
515 std::string size = ExtractString(th.size);
516 size_t skip = 0;
517 if (!size.empty()) {
518 StrTrimInPlace(size);
519 auto [_, err] = std::from_chars(size.data(), size.data() + size.size(), skip, 8);
520 if (err != std::errc()) {
521 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
522 fclose(f);
523 return false;
527 switch (th.typeflag) {
528 case '\0':
529 case '0': { // regular file
530 if (name.empty()) break;
532 /* Store this entry in the list */
533 TarFileListEntry entry;
534 entry.tar_filename = filename;
535 entry.size = skip;
536 entry.position = pos;
538 /* Convert to lowercase and our PATHSEPCHAR */
539 SimplifyFileName(name);
541 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
542 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
544 break;
547 case '1': // hard links
548 case '2': { // symbolic links
549 std::string link = ExtractString(th.linkname);
551 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
552 break;
555 case '5': // directory
556 /* Convert to lowercase and our PATHSEPCHAR */
557 SimplifyFileName(name);
559 /* Store the first directory name we detect */
560 Debug(misc, 6, "Found dir in tar: {}", name);
561 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = name;
562 break;
564 default:
565 /* Ignore other types */
566 break;
569 /* Skip to the next block.. */
570 skip = Align(skip, 512);
571 if (fseek(f, skip, SEEK_CUR) < 0) {
572 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
573 return false;
575 pos += skip;
578 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
580 return true;
584 * Extract the tar with the given filename in the directory
585 * where the tar resides.
586 * @param tar_filename the name of the tar to extract.
587 * @param subdir The sub directory the tar is in.
588 * @return false on failure.
590 bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
592 TarList::iterator it = _tar_list[subdir].find(tar_filename);
593 /* We don't know the file. */
594 if (it == _tar_list[subdir].end()) return false;
596 const auto &dirname = (*it).second;
598 /* The file doesn't have a sub directory! */
599 if (dirname.empty()) {
600 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
601 return false;
604 std::string filename = tar_filename;
605 auto p = filename.find_last_of(PATHSEPCHAR);
606 /* The file's path does not have a separator? */
607 if (p == std::string::npos) return false;
609 filename.replace(p + 1, std::string::npos, dirname);
610 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, filename);
611 FioCreateDirectory(filename);
613 for (auto &it2 : _tar_filelist[subdir]) {
614 if (tar_filename != it2.second.tar_filename) continue;
616 /* it2.first is tarball + PATHSEPCHAR + name. */
617 std::string_view name = it2.first;
618 name.remove_prefix(name.find_first_of(PATHSEPCHAR) + 1);
619 filename.replace(p + 1, std::string::npos, name);
621 Debug(misc, 9, " extracting {}", filename);
623 /* First open the file in the .tar. */
624 size_t to_copy = 0;
625 auto in = FioFOpenFileTar(it2.second, &to_copy);
626 if (!in.has_value()) {
627 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
628 return false;
631 /* Now open the 'output' file. */
632 auto out = FileHandle::Open(filename, "wb");
633 if (!out.has_value()) {
634 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
635 return false;
638 /* Now read from the tar and write it into the file. */
639 char buffer[4096];
640 size_t read;
641 for (; to_copy != 0; to_copy -= read) {
642 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
643 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
646 if (to_copy != 0) {
647 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
648 return false;
652 Debug(misc, 9, " extraction successful");
653 return true;
656 #if defined(_WIN32)
658 * Determine the base (personal dir and game data dir) paths
659 * @param exe the path from the current path to the executable
660 * @note defined in the OS related files (win32.cpp, unix.cpp etc)
662 extern void DetermineBasePaths(const char *exe);
663 #else /* defined(_WIN32) */
666 * Changes the working directory to the path of the give executable.
667 * For OSX application bundles '.app' is the required extension of the bundle,
668 * so when we crop the path to there, when can remove the name of the bundle
669 * in the same way we remove the name from the executable name.
670 * @param exe the path to the executable
672 static bool ChangeWorkingDirectoryToExecutable(const char *exe)
674 std::string path = exe;
676 #ifdef WITH_COCOA
677 for (size_t pos = path.find_first_of('.'); pos != std::string::npos; pos = path.find_first_of('.', pos + 1)) {
678 if (StrEqualsIgnoreCase(path.substr(pos, 4), ".app")) {
679 path.erase(pos);
680 break;
683 #endif /* WITH_COCOA */
685 size_t pos = path.find_last_of(PATHSEPCHAR);
686 if (pos == std::string::npos) return false;
688 path.erase(pos);
690 if (chdir(path.c_str()) != 0) {
691 Debug(misc, 0, "Directory with the binary does not exist?");
692 return false;
695 return true;
699 * Whether we should scan the working directory.
700 * It should not be scanned if it's the root or
701 * the home directory as in both cases a big data
702 * directory can cause huge amounts of unrelated
703 * files scanned. Furthermore there are nearly no
704 * use cases for the home/root directory to have
705 * OpenTTD directories.
706 * @return true if it should be scanned.
708 bool DoScanWorkingDirectory()
710 /* No working directory, so nothing to do. */
711 if (_searchpaths[SP_WORKING_DIR].empty()) return false;
713 /* Working directory is root, so do nothing. */
714 if (_searchpaths[SP_WORKING_DIR] == PATHSEP) return false;
716 /* No personal/home directory, so the working directory won't be that. */
717 if (_searchpaths[SP_PERSONAL_DIR].empty()) return true;
719 std::string tmp = _searchpaths[SP_WORKING_DIR] + PERSONAL_DIR;
720 AppendPathSeparator(tmp);
722 return _searchpaths[SP_PERSONAL_DIR] != tmp;
726 * Gets the home directory of the user.
727 * May return an empty string in the unlikely scenario that the home directory cannot be found.
728 * @return User's home directory
730 static std::string GetHomeDir()
732 #ifdef __HAIKU__
733 BPath path;
734 find_directory(B_USER_SETTINGS_DIRECTORY, &path);
735 return std::string(path.Path());
736 #else
737 const char *home_env = std::getenv("HOME"); // Stack var, shouldn't be freed
738 if (home_env != nullptr) return std::string(home_env);
740 const struct passwd *pw = getpwuid(getuid());
741 if (pw != nullptr) return std::string(pw->pw_dir);
742 #endif
743 return {};
747 * Determine the base (personal dir and game data dir) paths
748 * @param exe the path to the executable
750 void DetermineBasePaths(const char *exe)
752 std::string tmp;
753 const std::string homedir = GetHomeDir();
754 #ifdef USE_XDG
755 const char *xdg_data_home = std::getenv("XDG_DATA_HOME");
756 if (xdg_data_home != nullptr) {
757 tmp = xdg_data_home;
758 tmp += PATHSEP;
759 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
760 AppendPathSeparator(tmp);
761 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
763 tmp += "content_download";
764 AppendPathSeparator(tmp);
765 _searchpaths[SP_AUTODOWNLOAD_PERSONAL_DIR_XDG] = tmp;
766 } else if (!homedir.empty()) {
767 tmp = homedir;
768 tmp += PATHSEP ".local" PATHSEP "share" PATHSEP;
769 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
770 AppendPathSeparator(tmp);
771 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
773 tmp += "content_download";
774 AppendPathSeparator(tmp);
775 _searchpaths[SP_AUTODOWNLOAD_PERSONAL_DIR_XDG] = tmp;
776 } else {
777 _searchpaths[SP_PERSONAL_DIR_XDG].clear();
778 _searchpaths[SP_AUTODOWNLOAD_PERSONAL_DIR_XDG].clear();
780 #endif
782 #if !defined(WITH_PERSONAL_DIR)
783 _searchpaths[SP_PERSONAL_DIR].clear();
784 #else
785 if (!homedir.empty()) {
786 tmp = homedir;
787 tmp += PATHSEP;
788 tmp += PERSONAL_DIR;
789 AppendPathSeparator(tmp);
790 _searchpaths[SP_PERSONAL_DIR] = tmp;
792 tmp += "content_download";
793 AppendPathSeparator(tmp);
794 _searchpaths[SP_AUTODOWNLOAD_PERSONAL_DIR] = tmp;
795 } else {
796 _searchpaths[SP_PERSONAL_DIR].clear();
797 _searchpaths[SP_AUTODOWNLOAD_PERSONAL_DIR].clear();
799 #endif
801 #if defined(WITH_SHARED_DIR)
802 tmp = SHARED_DIR;
803 AppendPathSeparator(tmp);
804 _searchpaths[SP_SHARED_DIR] = tmp;
805 #else
806 _searchpaths[SP_SHARED_DIR].clear();
807 #endif
809 char cwd[MAX_PATH];
810 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
812 if (_config_file.empty()) {
813 /* Get the path to working directory of OpenTTD. */
814 tmp = cwd;
815 AppendPathSeparator(tmp);
816 _searchpaths[SP_WORKING_DIR] = tmp;
818 _do_scan_working_directory = DoScanWorkingDirectory();
819 } else {
820 /* Use the folder of the config file as working directory. */
821 size_t end = _config_file.find_last_of(PATHSEPCHAR);
822 if (end == std::string::npos) {
823 /* _config_file is not in a folder, so use current directory. */
824 tmp = cwd;
825 } else {
826 tmp = FS2OTTD(std::filesystem::weakly_canonical(std::filesystem::path(OTTD2FS(_config_file))).parent_path());
828 AppendPathSeparator(tmp);
829 _searchpaths[SP_WORKING_DIR] = tmp;
832 /* Change the working directory to that one of the executable */
833 if (ChangeWorkingDirectoryToExecutable(exe)) {
834 char buf[MAX_PATH];
835 if (getcwd(buf, lengthof(buf)) == nullptr) {
836 tmp.clear();
837 } else {
838 tmp = buf;
840 AppendPathSeparator(tmp);
841 _searchpaths[SP_BINARY_DIR] = tmp;
842 } else {
843 _searchpaths[SP_BINARY_DIR].clear();
846 if (cwd[0] != '\0') {
847 /* Go back to the current working directory. */
848 if (chdir(cwd) != 0) {
849 Debug(misc, 0, "Failed to return to working directory!");
853 #if !defined(GLOBAL_DATA_DIR)
854 _searchpaths[SP_INSTALLATION_DIR].clear();
855 #else
856 tmp = GLOBAL_DATA_DIR;
857 AppendPathSeparator(tmp);
858 _searchpaths[SP_INSTALLATION_DIR] = tmp;
859 #endif
860 #ifdef WITH_COCOA
861 extern void CocoaSetApplicationBundleDir();
862 CocoaSetApplicationBundleDir();
863 #else
864 _searchpaths[SP_APPLICATION_BUNDLE_DIR].clear();
865 #endif
867 #endif /* defined(_WIN32) */
869 std::string _personal_dir;
872 * Acquire the base paths (personal dir and game data dir),
873 * fill all other paths (save dir, autosave dir etc) and
874 * make the save and scenario directories.
875 * @param exe the path from the current path to the executable
876 * @param only_local_path Whether we shouldn't fill searchpaths with global folders.
878 void DeterminePaths(const char *exe, bool only_local_path)
880 DetermineBasePaths(exe);
881 FillValidSearchPaths(only_local_path);
883 #ifdef USE_XDG
884 std::string config_home;
885 const std::string homedir = GetHomeDir();
886 const char *xdg_config_home = std::getenv("XDG_CONFIG_HOME");
887 if (xdg_config_home != nullptr) {
888 config_home = xdg_config_home;
889 config_home += PATHSEP;
890 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
891 } else if (!homedir.empty()) {
892 /* Defaults to ~/.config */
893 config_home = homedir;
894 config_home += PATHSEP ".config" PATHSEP;
895 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
897 AppendPathSeparator(config_home);
898 #endif
900 for (Searchpath sp : _valid_searchpaths) {
901 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
902 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
905 std::string config_dir;
906 if (!_config_file.empty()) {
907 config_dir = _searchpaths[SP_WORKING_DIR];
908 } else {
909 std::string personal_dir = FioFindFullPath(BASE_DIR, "openttd.cfg");
910 if (!personal_dir.empty()) {
911 auto end = personal_dir.find_last_of(PATHSEPCHAR);
912 if (end != std::string::npos) personal_dir.erase(end + 1);
913 config_dir = personal_dir;
914 } else {
915 #ifdef USE_XDG
916 /* No previous configuration file found. Use the configuration folder from XDG. */
917 config_dir = config_home;
918 #else
919 static const Searchpath new_openttd_cfg_order[] = {
920 SP_PERSONAL_DIR, SP_BINARY_DIR, SP_WORKING_DIR, SP_SHARED_DIR, SP_INSTALLATION_DIR
923 config_dir.clear();
924 for (const auto &searchpath : new_openttd_cfg_order) {
925 if (IsValidSearchPath(searchpath)) {
926 config_dir = _searchpaths[searchpath];
927 break;
930 #endif
932 _config_file = config_dir + "openttd.cfg";
935 Debug(misc, 1, "{} found as config directory", config_dir);
937 _highscore_file = config_dir + "hs.dat";
938 extern std::string _hotkeys_file;
939 _hotkeys_file = config_dir + "hotkeys.cfg";
940 extern std::string _windows_file;
941 _windows_file = config_dir + "windows.cfg";
942 extern std::string _private_file;
943 _private_file = config_dir + "private.cfg";
944 extern std::string _secrets_file;
945 _secrets_file = config_dir + "secrets.cfg";
946 extern std::string _favs_file;
947 _favs_file = config_dir + "favs.cfg";
949 #ifdef USE_XDG
950 if (config_dir == config_home) {
951 /* We are using the XDG configuration home for the config file,
952 * then store the rest in the XDG data home folder. */
953 _personal_dir = _searchpaths[SP_PERSONAL_DIR_XDG];
954 if (only_local_path) {
955 /* In case of XDG and we only want local paths and we detected that
956 * the user either manually indicated the XDG path or didn't use
957 * "-c" option, we change the working-dir to the XDG personal-dir,
958 * as this is most likely what the user is expecting. */
959 _searchpaths[SP_WORKING_DIR] = _searchpaths[SP_PERSONAL_DIR_XDG];
961 } else
962 #endif
964 _personal_dir = config_dir;
967 /* Make the necessary folders */
968 FioCreateDirectory(config_dir);
969 #if defined(WITH_PERSONAL_DIR)
970 FioCreateDirectory(_personal_dir);
971 #endif
973 Debug(misc, 1, "{} found as personal directory", _personal_dir);
975 static const Subdirectory default_subdirs[] = {
976 SAVE_DIR, AUTOSAVE_DIR, SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SCREENSHOT_DIR, SOCIAL_INTEGRATION_DIR
979 for (const auto &default_subdir : default_subdirs) {
980 FioCreateDirectory(_personal_dir + _subdirs[default_subdir]);
983 /* If we have network we make a directory for the autodownloading of content */
984 _searchpaths[SP_AUTODOWNLOAD_DIR] = _personal_dir + "content_download" PATHSEP;
985 Debug(misc, 3, "{} added as search path", _searchpaths[SP_AUTODOWNLOAD_DIR]);
986 FioCreateDirectory(_searchpaths[SP_AUTODOWNLOAD_DIR]);
987 FillValidSearchPaths(only_local_path);
989 /* Create the directory for each of the types of content */
990 const Subdirectory subdirs[] = { SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SOCIAL_INTEGRATION_DIR };
991 for (const auto &subdir : subdirs) {
992 FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, subdir));
995 extern std::string _log_file;
996 _log_file = _personal_dir + "openttd.log";
1000 * Sanitizes a filename, i.e. removes all illegal characters from it.
1001 * @param filename the filename
1003 void SanitizeFilename(std::string &filename)
1005 for (auto &c : filename) {
1006 switch (c) {
1007 /* The following characters are not allowed in filenames
1008 * on at least one of the supported operating systems: */
1009 case ':': case '\\': case '*': case '?': case '/':
1010 case '<': case '>': case '|': case '"':
1011 c = '_';
1012 break;
1018 * Load a file into memory.
1019 * @param filename Name of the file to load.
1020 * @param[out] lenp Length of loaded data.
1021 * @param maxsize Maximum size to load.
1022 * @return Pointer to new memory containing the loaded data, or \c nullptr if loading failed.
1023 * @note If \a maxsize less than the length of the file, loading fails.
1025 std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1027 auto in = FileHandle::Open(filename, "rb");
1028 if (!in.has_value()) return nullptr;
1030 fseek(*in, 0, SEEK_END);
1031 size_t len = ftell(*in);
1032 fseek(*in, 0, SEEK_SET);
1033 if (len > maxsize) return nullptr;
1035 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1037 mem.get()[len] = 0;
1038 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1040 lenp = len;
1041 return mem;
1045 * Helper to see whether a given filename matches the extension.
1046 * @param extension The extension to look for.
1047 * @param filename The filename to look in for the extension.
1048 * @return True iff the extension is nullptr, or the filename ends with it.
1050 static bool MatchesExtension(std::string_view extension, const std::string &filename)
1052 if (extension.empty()) return true;
1053 if (filename.length() < extension.length()) return false;
1055 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1056 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1060 * Scan a single directory (and recursively its children) and add
1061 * any graphics sets that are found.
1062 * @param fs the file scanner to add the files to
1063 * @param extension the extension of files to search for.
1064 * @param path full path we're currently at
1065 * @param basepath_length from where in the path are we 'based' on the search path
1066 * @param recursive whether to recursively search the sub directories
1068 static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1070 uint num = 0;
1072 std::error_code error_code;
1073 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1074 if (dir_entry.is_directory()) {
1075 if (!recursive) continue;
1076 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1077 } else if (dir_entry.is_regular_file()) {
1078 std::string file = FS2OTTD(dir_entry.path());
1079 if (!MatchesExtension(extension, file)) continue;
1080 if (fs->AddFile(file, basepath_length, {})) num++;
1083 if (error_code) {
1084 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1087 return num;
1091 * Scan the given tar and add graphics sets when it finds one.
1092 * @param fs the file scanner to scan for
1093 * @param extension the extension of files to search for.
1094 * @param tar the tar to search in.
1096 static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1098 uint num = 0;
1100 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1102 return num;
1106 * Scan for files with the given extension in the given search path.
1107 * @param extension the extension of files to search for.
1108 * @param sd the sub directory to search in.
1109 * @param tars whether to search in the tars too.
1110 * @param recursive whether to search recursively
1111 * @return the number of found files, i.e. the number of times that
1112 * AddFile returned true.
1114 uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1116 this->subdir = sd;
1118 uint num = 0;
1120 for (Searchpath sp : _valid_searchpaths) {
1121 /* Don't search in the working directory */
1122 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
1124 std::string path = FioGetDirectory(sp, sd);
1125 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1128 if (tars && sd != NO_DIRECTORY) {
1129 for (const auto &tar : _tar_filelist[sd]) {
1130 num += ScanTar(this, extension, tar);
1134 switch (sd) {
1135 case BASESET_DIR:
1136 num += this->Scan(extension, OLD_GM_DIR, tars, recursive);
1137 [[fallthrough]];
1138 case NEWGRF_DIR:
1139 num += this->Scan(extension, OLD_DATA_DIR, tars, recursive);
1140 break;
1142 default: break;
1145 return num;
1149 * Scan for files with the given extension in the given search path.
1150 * @param extension the extension of files to search for.
1151 * @param directory the sub directory to search in.
1152 * @param recursive whether to search recursively
1153 * @return the number of found files, i.e. the number of times that
1154 * AddFile returned true.
1156 uint FileScanner::Scan(const std::string_view extension, const std::string &directory, bool recursive)
1158 std::string path(directory);
1159 AppendPathSeparator(path);
1160 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1164 * Open an RAII file handle if possible.
1165 * The canonical RAII-way is for FileHandle to open the file and throw an exception on failure, but we don't want that.
1166 * @param filename UTF-8 encoded filename to open.
1167 * @param mode Mode to open file.
1168 * @return FileHandle, or std::nullopt on failure.
1170 std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
1172 #if defined(_WIN32)
1173 /* Windows also requires mode to be wchar_t. */
1174 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1175 #else
1176 auto f = fopen(filename.c_str(), mode.c_str());
1177 #endif /* _WIN32 */
1179 if (f == nullptr) return std::nullopt;
1180 return FileHandle(f);