OpenTTD Source 20241224-master-gf74b0cf984
fileio.cpp
Go to the documentation of this file.
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 */
7
10#include "stdafx.h"
11#include "fileio_func.h"
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>
30
31#include "safeguards.h"
32
34static bool _do_scan_working_directory = true;
35
36extern std::string _config_file;
37extern std::string _highscore_file;
38
39static 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,
56};
57static_assert(lengthof(_subdirs) == NUM_SUBDIRS);
58
65std::array<std::string, NUM_SEARCHPATHS> _searchpaths;
66std::vector<Searchpath> _valid_searchpaths;
67std::array<TarList, NUM_SUBDIRS> _tar_list;
68TarFileList _tar_filelist[NUM_SUBDIRS];
69
76{
77 return sp < _searchpaths.size() && !_searchpaths[sp].empty();
78}
79
80static void FillValidSearchPaths(bool only_local_path)
81{
82 _valid_searchpaths.clear();
83
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;
87
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;
94
95 default:
96 continue;
97 }
98 }
99
100 if (IsValidSearchPath(sp)) {
101 if (seen.count(_searchpaths[sp]) != 0) continue;
102 seen.insert(_searchpaths[sp]);
103 _valid_searchpaths.emplace_back(sp);
104 }
105 }
106
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. */
111 _valid_searchpaths.insert(_valid_searchpaths.begin(), SP_WORKING_DIR);
112 }
113}
114
121bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
122{
123 auto f = FioFOpenFile(filename, "rb", subdir);
124 return f.has_value();
125}
126
132bool FileExists(const std::string &filename)
133{
134 std::error_code ec;
135 return std::filesystem::exists(OTTD2FS(filename), ec);
136}
137
144std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
145{
146 assert(subdir < NUM_SUBDIRS);
147
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
158 }
159
160 return {};
161}
162
163std::string FioGetDirectory(Searchpath sp, Subdirectory subdir)
164{
165 assert(subdir < NUM_SUBDIRS);
166 assert(sp < NUM_SEARCHPATHS);
167
168 return _searchpaths[sp] + _subdirs[subdir];
169}
170
171std::string FioFindDirectory(Subdirectory subdir)
172{
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;
177 }
178
179 /* Could not find the directory, fall back to a base path */
180 return _personal_dir;
181}
182
183static std::optional<FileHandle> FioFOpenFileSp(const std::string &filename, const char *mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
184{
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;
194
195 if (subdir == NO_DIRECTORY) {
196 buf = filename;
197 } else {
198 buf = _searchpaths[sp] + _subdirs[subdir] + filename;
199 }
200
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);
205 }
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);
212 }
213 return f;
214}
215
223static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
224{
225 auto f = FileHandle::Open(entry.tar_filename, "rb");
226 if (!f.has_value()) return std::nullopt;
227
228 if (fseek(*f, entry.position, SEEK_SET) < 0) {
229 return std::nullopt;
230 }
231
232 if (filesize != nullptr) *filesize = entry.size;
233 return f;
234}
235
242std::optional<FileHandle> FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
243{
244 std::optional<FileHandle> f = std::nullopt;
245 assert(subdir < NUM_SUBDIRS || subdir == NO_DIRECTORY);
246
247 for (Searchpath sp : _valid_searchpaths) {
248 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
249 if (f.has_value() || subdir == NO_DIRECTORY) break;
250 }
251
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);
257
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);
271 }
272 }
273
274 resolved_name.clear();
275 bool first = true;
276 for (const std::string &token : tokens) {
277 if (!first) {
278 resolved_name += PATHSEP;
279 }
280 resolved_name += token;
281 first = false;
282 }
283
284 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
285 if (it != _tar_filelist[subdir].end()) {
286 f = FioFOpenFileTar(it->second, filesize);
287 }
288 }
289
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;
301
302 default:
303 f = FioFOpenFile(filename, mode, NO_DIRECTORY, filesize);
304 break;
305 }
306 }
307
308 return f;
309}
310
316void FioCreateDirectory(const std::string &name)
317{
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);
321}
322
328bool FioRemove(const std::string &filename)
329{
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;
336 }
337 return true;
338}
339
346void AppendPathSeparator(std::string &buf)
347{
348 if (buf.empty()) return;
349
350 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
351}
352
358static void SimplifyFileName(std::string &name)
359{
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
367 }
368}
369
376{
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;
382}
383
385{
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);
391 }
392 if (mode & TarScanner::NEWGRF) {
393 num += fs.DoScan(NEWGRF_DIR);
394 }
395 if (mode & TarScanner::AI) {
396 num += fs.DoScan(AI_DIR);
397 num += fs.DoScan(AI_LIBRARY_DIR);
398 }
399 if (mode & TarScanner::GAME) {
400 num += fs.DoScan(GAME_DIR);
401 num += fs.DoScan(GAME_LIBRARY_DIR);
402 }
403 if (mode & TarScanner::SCENARIO) {
404 num += fs.DoScan(SCENARIO_DIR);
405 num += fs.DoScan(HEIGHTMAP_DIR);
406 }
407 Debug(misc, 2, "Scan complete, found {} files", num);
408 return num;
409}
410
417bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
418{
419 this->subdir = sd;
420 return this->AddFile(filename, 0);
421}
422
433static std::string ExtractString(std::span<char> buffer)
434{
435 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
436}
437
438bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
439{
440 /* No tar within tar. */
441 assert(tar_filename.empty());
442
443 /* The TAR-header, repeated for every file */
444 struct TarHeader {
445 char name[100];
446 char mode[8];
447 char uid[8];
448 char gid[8];
449 char size[12];
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];
461
462 char unused[12];
463 };
464
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;
468
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;
476
477 _tar_list[this->subdir][filename] = std::string{};
478
479 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename());
480 SimplifyFileName(filename_base);
481
482 TarHeader th;
483 size_t num = 0, pos = 0;
484
485 /* Make a char of 512 empty bytes */
486 char empty[512];
487 memset(&empty[0], 0, sizeof(empty));
488
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;
493
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;
498
499 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
500 return false;
501 }
502
503 std::string name;
504
505 /* The prefix contains the directory-name */
506 if (th.prefix[0] != '\0') {
507 name = ExtractString(th.prefix);
508 name += PATHSEP;
509 }
510
511 /* Copy the name of the file in a safe way at the end of 'name' */
512 name += ExtractString(th.name);
513
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;
524 }
525 }
526
527 switch (th.typeflag) {
528 case '\0':
529 case '0': { // regular file
530 if (name.empty()) break;
531
532 /* Store this entry in the list */
533 TarFileListEntry entry;
534 entry.tar_filename = filename;
535 entry.size = skip;
536 entry.position = pos;
537
538 /* Convert to lowercase and our PATHSEPCHAR */
539 SimplifyFileName(name);
540
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++;
543
544 break;
545 }
546
547 case '1': // hard links
548 case '2': { // symbolic links
549 std::string link = ExtractString(th.linkname);
550
551 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
552 break;
553 }
554
555 case '5': // directory
556 /* Convert to lowercase and our PATHSEPCHAR */
557 SimplifyFileName(name);
558
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;
563
564 default:
565 /* Ignore other types */
566 break;
567 }
568
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;
574 }
575 pos += skip;
576 }
577
578 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
579
580 return true;
581}
582
590bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
591{
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;
595
596 const auto &dirname = (*it).second;
597
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;
602 }
603
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;
608
609 filename.replace(p + 1, std::string::npos, dirname);
610 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, filename);
611 FioCreateDirectory(filename);
612
613 for (auto &it2 : _tar_filelist[subdir]) {
614 if (tar_filename != it2.second.tar_filename) continue;
615
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);
620
621 Debug(misc, 9, " extracting {}", filename);
622
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;
629 }
630
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;
636 }
637
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;
644 }
645
646 if (to_copy != 0) {
647 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
648 return false;
649 }
650 }
651
652 Debug(misc, 9, " extraction successful");
653 return true;
654}
655
656#if defined(_WIN32)
662extern void DetermineBasePaths(const char *exe);
663#else /* defined(_WIN32) */
664
672static bool ChangeWorkingDirectoryToExecutable(const char *exe)
673{
674 std::string path = exe;
675
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;
681 }
682 }
683#endif /* WITH_COCOA */
684
685 size_t pos = path.find_last_of(PATHSEPCHAR);
686 if (pos == std::string::npos) return false;
687
688 path.erase(pos);
689
690 if (chdir(path.c_str()) != 0) {
691 Debug(misc, 0, "Directory with the binary does not exist?");
692 return false;
693 }
694
695 return true;
696}
697
709{
710 /* No working directory, so nothing to do. */
711 if (_searchpaths[SP_WORKING_DIR].empty()) return false;
712
713 /* Working directory is root, so do nothing. */
714 if (_searchpaths[SP_WORKING_DIR] == PATHSEP) return false;
715
716 /* No personal/home directory, so the working directory won't be that. */
717 if (_searchpaths[SP_PERSONAL_DIR].empty()) return true;
718
719 std::string tmp = _searchpaths[SP_WORKING_DIR] + PERSONAL_DIR;
721
722 return _searchpaths[SP_PERSONAL_DIR] != tmp;
723}
724
730static std::string GetHomeDir()
731{
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);
739
740 const struct passwd *pw = getpwuid(getuid());
741 if (pw != nullptr) return std::string(pw->pw_dir);
742#endif
743 return {};
744}
745
750void DetermineBasePaths(const char *exe)
751{
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;
761 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
762
763 tmp += "content_download";
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;
771 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
772
773 tmp += "content_download";
776 } else {
777 _searchpaths[SP_PERSONAL_DIR_XDG].clear();
779 }
780#endif
781
782#if !defined(WITH_PERSONAL_DIR)
784#else
785 if (!homedir.empty()) {
786 tmp = homedir;
787 tmp += PATHSEP;
788 tmp += PERSONAL_DIR;
791
792 tmp += "content_download";
795 } else {
798 }
799#endif
800
801#if defined(WITH_SHARED_DIR)
802 tmp = SHARED_DIR;
805#else
807#endif
808
809 char cwd[MAX_PATH];
810 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
811
812 if (_config_file.empty()) {
813 /* Get the path to working directory of OpenTTD. */
814 tmp = cwd;
817
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());
827 }
830 }
831
832 /* Change the working directory to that one of the executable */
834 char buf[MAX_PATH];
835 if (getcwd(buf, lengthof(buf)) == nullptr) {
836 tmp.clear();
837 } else {
838 tmp = buf;
839 }
842 } else {
844 }
845
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!");
850 }
851 }
852
853#if !defined(GLOBAL_DATA_DIR)
855#else
856 tmp = GLOBAL_DATA_DIR;
859#endif
860#ifdef WITH_COCOA
861extern void CocoaSetApplicationBundleDir();
862 CocoaSetApplicationBundleDir();
863#else
865#endif
866}
867#endif /* defined(_WIN32) */
868
869std::string _personal_dir;
870
878void DeterminePaths(const char *exe, bool only_local_path)
879{
881 FillValidSearchPaths(only_local_path);
882
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;
896 }
897 AppendPathSeparator(config_home);
898#endif
899
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]);
903 }
904
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[] = {
921 };
922
923 config_dir.clear();
924 for (const auto &searchpath : new_openttd_cfg_order) {
925 if (IsValidSearchPath(searchpath)) {
926 config_dir = _searchpaths[searchpath];
927 break;
928 }
929 }
930#endif
931 }
932 _config_file = config_dir + "openttd.cfg";
933 }
934
935 Debug(misc, 1, "{} found as config directory", config_dir);
936
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";
948
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];
960 }
961 } else
962#endif
963 {
964 _personal_dir = config_dir;
965 }
966
967 /* Make the necessary folders */
968 FioCreateDirectory(config_dir);
969#if defined(WITH_PERSONAL_DIR)
971#endif
972
973 Debug(misc, 1, "{} found as personal directory", _personal_dir);
974
975 static const Subdirectory default_subdirs[] = {
977 };
978
979 for (const auto &default_subdir : default_subdirs) {
980 FioCreateDirectory(_personal_dir + _subdirs[default_subdir]);
981 }
982
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]);
987 FillValidSearchPaths(only_local_path);
988
989 /* Create the directory for each of the types of content */
991 for (const auto &subdir : subdirs) {
992 FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, subdir));
993 }
994
995 extern std::string _log_file;
996 _log_file = _personal_dir + "openttd.log";
997}
998
1003void SanitizeFilename(std::string &filename)
1004{
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;
1013 }
1014 }
1015}
1016
1025std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1026{
1027 auto in = FileHandle::Open(filename, "rb");
1028 if (!in.has_value()) return nullptr;
1029
1030 fseek(*in, 0, SEEK_END);
1031 size_t len = ftell(*in);
1032 fseek(*in, 0, SEEK_SET);
1033 if (len > maxsize) return nullptr;
1034
1035 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1036
1037 mem.get()[len] = 0;
1038 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1039
1040 lenp = len;
1041 return mem;
1042}
1043
1050static bool MatchesExtension(std::string_view extension, const std::string &filename)
1051{
1052 if (extension.empty()) return true;
1053 if (filename.length() < extension.length()) return false;
1054
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;
1057}
1058
1068static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1069{
1070 uint num = 0;
1071
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++;
1081 }
1082 }
1083 if (error_code) {
1084 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1085 }
1086
1087 return num;
1088}
1089
1096static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1097{
1098 uint num = 0;
1099
1100 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1101
1102 return num;
1103}
1104
1114uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1115{
1116 this->subdir = sd;
1117
1118 uint num = 0;
1119
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;
1123
1124 std::string path = FioGetDirectory(sp, sd);
1125 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1126 }
1127
1128 if (tars && sd != NO_DIRECTORY) {
1129 for (const auto &tar : _tar_filelist[sd]) {
1130 num += ScanTar(this, extension, tar);
1131 }
1132 }
1133
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;
1141
1142 default: break;
1143 }
1144
1145 return num;
1146}
1147
1156uint FileScanner::Scan(const std::string_view extension, const std::string &directory, bool recursive)
1157{
1158 std::string path(directory);
1159 AppendPathSeparator(path);
1160 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1161}
1162
1170std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
1171{
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 */
1178
1179 if (f == nullptr) return std::nullopt;
1180 return FileHandle(f);
1181}
static std::optional< FileHandle > Open(const std::string &filename, const std::string &mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1170
Helper for scanning for files with a given name.
Definition fileio_func.h:37
uint Scan(std::string_view extension, Subdirectory sd, bool tars=true, bool recursive=true)
Scan for files with the given extension in the given search path.
Definition fileio.cpp:1114
virtual bool AddFile(const std::string &filename, size_t basepath_length, const std::string &tar_filename)=0
Add a file with the given filename.
Subdirectory subdir
The current sub directory we are searching through.
Definition fileio_func.h:39
Helper for scanning for files with tar as extension.
Definition fileio_func.h:59
uint DoScan(Subdirectory sd)
Perform the scanning of a particular subdirectory.
Definition fileio.cpp:375
Mode
The mode of tar scanning.
Definition fileio_func.h:63
@ NEWGRF
Scan for non-base sets.
Definition fileio_func.h:66
@ BASESET
Scan for base sets.
Definition fileio_func.h:65
@ GAME
Scan for game scripts.
Definition fileio_func.h:69
@ SCENARIO
Scan for scenarios and heightmaps.
Definition fileio_func.h:68
@ AI
Scan for AIs and its libraries.
Definition fileio_func.h:67
bool AddFile(const std::string &filename, size_t basepath_length, const std::string &tar_filename={}) override
Add a file with the given filename.
Definition fileio.cpp:438
Functions related to debugging.
#define Debug(category, level, format_string,...)
Ouptut a line of debugging information.
Definition debug.h:37
std::string _log_file
Filename to reroute output of a forked OpenTTD to.
Definition dedicated.cpp:14
std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:144
bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
Extract the tar with the given filename in the directory where the tar resides.
Definition fileio.cpp:590
void DeterminePaths(const char *exe, bool only_local_path)
Acquire the base paths (personal dir and game data dir), fill all other paths (save dir,...
Definition fileio.cpp:878
static bool IsValidSearchPath(Searchpath sp)
Checks whether the given search path is a valid search path.
Definition fileio.cpp:75
bool FioRemove(const std::string &filename)
Remove a file.
Definition fileio.cpp:328
void SanitizeFilename(std::string &filename)
Sanitizes a filename, i.e.
Definition fileio.cpp:1003
static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
Scan a single directory (and recursively its children) and add any graphics sets that are found.
Definition fileio.cpp:1068
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:346
std::string _personal_dir
custom directory for personal settings, saves, newgrf, etc.
Definition fileio.cpp:869
static bool _do_scan_working_directory
Whether the working directory should be scanned.
Definition fileio.cpp:34
static std::string GetHomeDir()
Gets the home directory of the user.
Definition fileio.cpp:730
bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:121
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp:358
static bool MatchesExtension(std::string_view extension, const std::string &filename)
Helper to see whether a given filename matches the extension.
Definition fileio.cpp:1050
void FioCreateDirectory(const std::string &name)
Create a directory with the given name If the parent directory does not exist, it will try to create ...
Definition fileio.cpp:316
static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
Scan the given tar and add graphics sets when it finds one.
Definition fileio.cpp:1096
static bool ChangeWorkingDirectoryToExecutable(const char *exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:672
bool FileExists(const std::string &filename)
Test whether the given filename exists.
Definition fileio.cpp:132
std::array< std::string, NUM_SEARCHPATHS > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:65
static std::optional< FileHandle > FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
Opens a file from inside a tar archive.
Definition fileio.cpp:223
static std::string ExtractString(std::span< char > buffer)
Helper to extract a string for the tar header.
Definition fileio.cpp:433
std::string _highscore_file
The file to store the highscore data in.
Definition highscore.cpp:24
bool DoScanWorkingDirectory()
Whether we should scan the working directory.
Definition fileio.cpp:708
std::string _config_file
Configuration file of OpenTTD.
Definition settings.cpp:60
void DetermineBasePaths(const char *exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:750
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:242
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1025
Functions for Standard In/Out file operations.
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:346
Searchpath
Types of searchpaths OpenTTD might use.
@ SP_SHARED_DIR
Search in the shared directory, like 'Shared Files' under Windows.
@ SP_INSTALLATION_DIR
Search in the installation directory.
@ SP_AUTODOWNLOAD_PERSONAL_DIR_XDG
Search within the autodownload directory located in the personal directory (XDG variant)
@ SP_BINARY_DIR
Search in the directory where the binary resides.
@ SP_AUTODOWNLOAD_PERSONAL_DIR
Search within the autodownload directory located in the personal directory.
@ SP_PERSONAL_DIR
Search in the personal directory.
@ SP_WORKING_DIR
Search in the working directory.
@ SP_APPLICATION_BUNDLE_DIR
Search within the application bundle.
@ SP_AUTODOWNLOAD_DIR
Search within the autodownload directory.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
@ OLD_DATA_DIR
Old subdirectory for the data.
@ NO_DIRECTORY
A path without any base directory.
@ AI_LIBRARY_DIR
Subdirectory for all AI libraries.
@ SCREENSHOT_DIR
Subdirectory for all screenshots.
@ SOCIAL_INTEGRATION_DIR
Subdirectory for all social integration plugins.
@ GAME_LIBRARY_DIR
Subdirectory for all GS libraries.
@ AI_DIR
Subdirectory for all AI files.
@ OLD_GM_DIR
Old subdirectory for the music.
@ SCENARIO_DIR
Base directory for all scenarios.
@ BASE_DIR
Base directory for all subdirectories.
@ SAVE_DIR
Base directory for all savegames.
@ NUM_SUBDIRS
Number of subdirectories.
@ HEIGHTMAP_DIR
Subdirectory of scenario for heightmaps.
@ NEWGRF_DIR
Subdirectory for all NewGRFs.
@ AUTOSAVE_DIR
Subdirectory of save for autosaves.
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game)
@ GAME_DIR
Subdirectory for all game scripts.
Declarations for savegames operations.
constexpr T Align(const T x, uint n)
Return the smallest multiple of n equal or greater than x.
Definition math_func.hpp:37
A number of safeguards to prevent using unsafe methods.
std::string _secrets_file
Secrets configuration file of OpenTTD.
Definition settings.cpp:62
std::string _favs_file
Picker favourites configuration file of OpenTTD.
Definition settings.cpp:63
std::string _private_file
Private configuration file of OpenTTD.
Definition settings.cpp:61
Base for loading sprites.
Definition of base types and functions in a cross-platform compatible way.
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:280
static void StrMakeValid(T &dst, const char *str, const char *last, StringValidationSettings settings)
Copies the valid (UTF-8) characters from str up to last to the dst.
Definition string.cpp:107
bool StrEqualsIgnoreCase(const std::string_view str1, const std::string_view str2)
Compares two string( view)s for equality, while ignoring the case of the characters.
Definition string.cpp:347
int StrCompareIgnoreCase(const std::string_view str1, const std::string_view str2)
Compares two string( view)s, while ignoring the case of the characters.
Definition string.cpp:334
void StrTrimInPlace(std::string &str)
Trim the spaces from given string in place, i.e.
Definition string.cpp:260
Functions related to low-level strings.
Structs, typedefs and macros used for TAR file handling.
std::wstring OTTD2FS(const std::string &name)
Convert from OpenTTD's encoding to a wide string.
Definition win32.cpp:354
std::string FS2OTTD(const std::wstring &name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:337
std::string _windows_file
Config file to store WindowDesc.
Definition window.cpp:102