OpenTTD Source 20260421-master-gc2fbc6fdeb
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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
9
10#include "stdafx.h"
12#include "fileio_func.h"
14#include "debug.h"
15#include "fios.h"
16#include "string_func.h"
17#include "tar_type.h"
18#ifdef _WIN32
19#include <windows.h>
20#elif defined(__HAIKU__)
21#include <Path.h>
22#include <storage/FindDirectory.h>
23#else
24#include <unistd.h>
25#include <pwd.h>
26#endif
27#include <sys/stat.h>
28#include <filesystem>
29
30#include "safeguards.h"
31
33static bool _do_scan_working_directory = true;
34
35extern std::string _config_file;
36extern std::string _highscore_file;
37
39static const EnumClassIndexContainer<std::array<std::string_view, to_underlying(Subdirectory::End)>, Subdirectory> _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 "docs" PATHSEP,
57};
58
66std::vector<Searchpath> _valid_searchpaths;
71
78{
79 return to_underlying(sp) < _searchpaths.size() && !_searchpaths[sp].empty();
80}
81
82static void FillValidSearchPaths(bool only_local_path)
83{
84 _valid_searchpaths.clear();
85
86 std::set<std::string> seen{};
87 for (Searchpath sp = Searchpath::Begin; sp < Searchpath::End; sp++) {
89
90 if (only_local_path) {
91 switch (sp) {
92 case Searchpath::WorkingDir: // Can be influence by "-c" option.
93 case Searchpath::BinaryDir: // Most likely contains all the language files.
94 case Searchpath::AutodownloadDir: // Otherwise we cannot download in-game content.
95 break;
96
97 default:
98 continue;
99 }
100 }
101
102 if (IsValidSearchPath(sp)) {
103 if (seen.count(_searchpaths[sp]) != 0) continue;
104 seen.insert(_searchpaths[sp]);
105 _valid_searchpaths.emplace_back(sp);
106 }
107 }
108
109 /* The working-directory is special, as it is controlled by _do_scan_working_directory.
110 * Only add the search path if it isn't already in the set. To preserve the same order
111 * as the enum, insert it in the front. */
113 _valid_searchpaths.insert(_valid_searchpaths.begin(), Searchpath::WorkingDir);
114 }
115}
116
123bool FioCheckFileExists(std::string_view filename, Subdirectory subdir)
124{
125 auto f = FioFOpenFile(filename, "rb", subdir);
126 return f.has_value();
127}
128
134bool FileExists(std::string_view filename)
135{
136 std::error_code ec;
137 return std::filesystem::exists(OTTD2FS(filename), ec);
138}
139
146std::string FioFindFullPath(Subdirectory subdir, std::string_view filename)
147{
148 assert(subdir < Subdirectory::End);
149
150 for (Searchpath sp : _valid_searchpaths) {
151 std::string buf = FioGetDirectory(sp, subdir);
152 buf += filename;
153 if (FileExists(buf)) return buf;
154#if !defined(_WIN32)
155 /* Be, as opening files, aware that sometimes the filename
156 * might be in uppercase when it is in lowercase on the
157 * disk. Of course Windows doesn't care about casing. */
158 if (strtolower(buf, _searchpaths[sp].size() - 1) && FileExists(buf)) return buf;
159#endif
160 }
161
162 return {};
163}
164
165std::string FioGetDirectory(Searchpath sp, Subdirectory subdir)
166{
167 assert(subdir < Subdirectory::End);
168 assert(sp < Searchpath::End);
169
170 return fmt::format("{}{}", _searchpaths[sp], _subdirs[subdir]);
171}
172
173std::string FioFindDirectory(Subdirectory subdir)
174{
175 /* Find and return the first valid directory */
176 for (Searchpath sp : _valid_searchpaths) {
177 std::string ret = FioGetDirectory(sp, subdir);
178 if (FileExists(ret)) return ret;
179 }
180
181 /* Could not find the directory, fall back to a base path */
182 return _personal_dir;
183}
184
185static std::optional<FileHandle> FioFOpenFileSp(std::string_view filename, std::string_view mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
186{
187#if defined(_WIN32)
188 /* fopen is implemented as a define with ellipses for
189 * Unicode support (prepend an L). As we are not sending
190 * a string, but a variable, it 'renames' the variable,
191 * so make that variable to makes it compile happily */
192 wchar_t Lmode[5];
193 MultiByteToWideChar(CP_ACP, 0, mode.data(), static_cast<int>(std::size(mode)), Lmode, static_cast<int>(std::size(Lmode)));
194#endif
195 std::string buf;
196
197 if (subdir == Subdirectory::None) {
198 buf = filename;
199 } else {
200 buf = fmt::format("{}{}{}", _searchpaths[sp], _subdirs[subdir], filename);
201 }
202
203 auto f = FileHandle::Open(buf, mode);
204#if !defined(_WIN32)
205 if (!f.has_value() && strtolower(buf, subdir == Subdirectory::None ? 0 : _searchpaths[sp].size() - 1) ) {
206 f = FileHandle::Open(buf, mode);
207 }
208#endif
209 if (f.has_value() && filesize != nullptr) {
210 /* Find the size of the file */
211 fseek(*f, 0, SEEK_END);
212 *filesize = ftell(*f);
213 fseek(*f, 0, SEEK_SET);
214 }
215 return f;
216}
217
225static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
226{
227 auto f = FileHandle::Open(entry.tar_filename, "rb");
228 if (!f.has_value()) return std::nullopt;
229
230 if (fseek(*f, entry.position, SEEK_SET) < 0) {
231 return std::nullopt;
232 }
233
234 if (filesize != nullptr) *filesize = entry.size;
235 return f;
236}
237
246std::optional<FileHandle> FioFOpenFile(std::string_view filename, std::string_view mode, Subdirectory subdir, size_t *filesize)
247{
248 std::optional<FileHandle> f = std::nullopt;
249 assert(subdir < Subdirectory::End || subdir == Subdirectory::None);
250
251 for (Searchpath sp : _valid_searchpaths) {
252 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
253 if (f.has_value() || subdir == Subdirectory::None) break;
254 }
255
256 /* We can only use .tar in case of data-dir, and read-mode */
257 if (!f.has_value() && mode[0] == 'r' && subdir != Subdirectory::None) {
258 /* Filenames in tars are always forced to be lowercase */
259 std::string resolved_name{filename};
260 strtolower(resolved_name);
261
262 /* Resolve ".." */
263 std::istringstream ss(resolved_name);
264 std::vector<std::string> tokens;
265 for (std::string token; std::getline(ss, token, PATHSEPCHAR); /* nothing */) {
266 if (token == "..") {
267 if (tokens.size() < 2) return std::nullopt;
268 tokens.pop_back();
269 } else if (token == ".") {
270 /* Do nothing. "." means current folder, but you can create tar files with "." in the path.
271 * This confuses our file resolver. So, act like this folder doesn't exist. */
272 } else {
273 tokens.push_back(token);
274 }
275 }
276
277 resolved_name.clear();
278 bool first = true;
279 for (const std::string &token : tokens) {
280 if (!first) {
281 resolved_name += PATHSEP;
282 }
283 resolved_name += token;
284 first = false;
285 }
286
287 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
288 if (it != _tar_filelist[subdir].end()) {
289 f = FioFOpenFileTar(it->second, filesize);
290 }
291 }
292
293 /* Sometimes a full path is given. To support
294 * the 'subdirectory' must be 'removed'. */
295 if (!f.has_value() && subdir != Subdirectory::None) {
296 switch (subdir) {
298 f = FioFOpenFile(filename, mode, Subdirectory::OldGm, filesize);
299 if (f.has_value()) break;
300 [[fallthrough]];
302 f = FioFOpenFile(filename, mode, Subdirectory::OldData, filesize);
303 break;
304
305 default:
306 f = FioFOpenFile(filename, mode, Subdirectory::None, filesize);
307 break;
308 }
309 }
310
311 return f;
312}
313
319void FioCreateDirectory(const std::string &name)
320{
321 /* Ignore directory creation errors; they'll surface later on. */
322 std::error_code error_code;
323 std::filesystem::create_directories(OTTD2FS(name), error_code);
324}
325
331bool FioRemove(const std::string &filename)
332{
333 std::filesystem::path path = OTTD2FS(filename);
334 std::error_code error_code;
335 if (!std::filesystem::remove(path, error_code)) {
336 if (error_code) {
337 Debug(misc, 0, "Removing {} failed: {}", filename, error_code.message());
338 } else {
339 Debug(misc, 0, "Removing {} failed: file does not exist", filename);
340 }
341 return false;
342 }
343 return true;
344}
345
351void AppendPathSeparator(std::string &buf)
352{
353 if (buf.empty()) return;
354
355 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
356}
357
363static void SimplifyFileName(std::string &name)
364{
365 for (char &c : name) {
366 /* Force lowercase */
367 c = std::tolower(c);
368#if (PATHSEPCHAR != '/')
369 /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */
370 if (c == '/') c = PATHSEPCHAR;
371#endif
372 }
373}
374
381{
382 _tar_filelist[sd].clear();
383 _tar_list[sd].clear();
384 uint num = this->Scan(".tar", sd, false);
385 if (sd == Subdirectory::Baseset || sd == Subdirectory::NewGrf) num += this->Scan(".tar", Subdirectory::OldData, false);
386 return num;
387}
388
394/* static */ uint TarScanner::DoScan(TarScanner::Modes modes)
395{
396 Debug(misc, 2, "Scanning for tars");
397 TarScanner fs;
398 uint num = 0;
399 if (modes.Test(TarScanner::Mode::Baseset)) {
400 num += fs.DoScan(Subdirectory::Baseset);
401 }
402 if (modes.Test(TarScanner::Mode::NewGRF)) {
403 num += fs.DoScan(Subdirectory::NewGrf);
404 }
405 if (modes.Test(TarScanner::Mode::AI)) {
406 num += fs.DoScan(Subdirectory::Ai);
408 }
409 if (modes.Test(TarScanner::Mode::Game)) {
410 num += fs.DoScan(Subdirectory::Gs);
412 }
413 if (modes.Test(TarScanner::Mode::Scenario)) {
416 }
417 Debug(misc, 2, "Scan complete, found {} files", num);
418 return num;
419}
420
427bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
428{
429 this->subdir = sd;
430 return this->AddFile(filename, 0);
431}
432
443static std::string ExtractString(std::span<char> buffer)
444{
445 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
446}
447
448bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
449{
450 /* No tar within tar. */
451 assert(tar_filename.empty());
452
453 /* The TAR-header, repeated for every file */
454 struct TarHeader {
455 char name[100];
456 char mode[8];
457 char uid[8];
458 char gid[8];
459 char size[12];
460 char mtime[12];
461 char chksum[8];
462 char typeflag;
463 char linkname[100];
464 char magic[6];
465 char version[2];
466 char uname[32];
467 char gname[32];
468 char devmajor[8];
469 char devminor[8];
470 char prefix[155];
471
472 char unused[12];
473 };
474 static_assert(sizeof(TarHeader) == 512);
475
476 /* Check if we already seen this file */
477 TarList::iterator it = _tar_list[this->subdir].find(filename);
478 if (it != _tar_list[this->subdir].end()) return false;
479
480 auto of = FileHandle::Open(filename, "rb");
481 /* Although the file has been found there can be
482 * a number of reasons we cannot open the file.
483 * Most common case is when we simply have not
484 * been given read access. */
485 if (!of.has_value()) return false;
486 auto &f = *of;
487
488 _tar_list[this->subdir][filename] = std::string{};
489
490 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename().native());
491 SimplifyFileName(filename_base);
492
493 TarHeader th;
494 size_t num = 0, pos = 0;
495
496 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
497 size_t num_bytes_read = fread(&th, 1, sizeof(TarHeader), f);
498 if (num_bytes_read != sizeof(TarHeader)) break;
499 pos += num_bytes_read;
500
501 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
502 auto last_of_th = &th.unused[std::size(th.unused)];
503 if (std::string_view{th.magic, 5} != "ustar" && std::any_of(th.magic, last_of_th, [](auto c) { return c != 0; })) {
504 /* If we have only zeros in the block, it can be an end-of-file indicator */
505 if (std::all_of(th.name, last_of_th, [](auto c) { return c == 0; })) continue;
506
507 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
508 return false;
509 }
510
511 std::string name;
512
513 /* The prefix contains the directory-name */
514 if (th.prefix[0] != '\0') {
515 name = ExtractString(th.prefix);
516 name += PATHSEP;
517 }
518
519 /* Copy the name of the file in a safe way at the end of 'name' */
520 name += ExtractString(th.name);
521
522 /* The size of the file, for some strange reason, this is stored as a string in octals. */
523 std::string size = ExtractString(th.size);
524 size_t skip = 0;
525 if (!size.empty()) {
526 auto value = ParseInteger<size_t>(size, 8);
527 if (!value.has_value()) {
528 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
529 fclose(f);
530 return false;
531 }
532 skip = *value;
533 }
534
535 switch (th.typeflag) {
536 case '\0':
537 case '0': { // regular file
538 if (name.empty()) break;
539
540 /* Store this entry in the list */
541 TarFileListEntry entry;
542 entry.tar_filename = filename;
543 entry.size = skip;
544 entry.position = pos;
545
546 /* Convert to lowercase and our PATHSEPCHAR */
547 SimplifyFileName(name);
548
549 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
550 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
551
552 break;
553 }
554
555 case '1': // hard links
556 case '2': { // symbolic links
557 std::string link = ExtractString(th.linkname);
558
559 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
560 break;
561 }
562
563 case '5': // directory
564 /* Convert to lowercase and our PATHSEPCHAR */
565 SimplifyFileName(name);
566
567 /* Store the first directory name we detect */
568 Debug(misc, 6, "Found dir in tar: {}", name);
569 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = std::move(name);
570 break;
571
572 default:
573 /* Ignore other types */
574 break;
575 }
576
577 /* Skip to the next block.. */
578 skip = Align(skip, 512);
579 if (fseek(f, skip, SEEK_CUR) < 0) {
580 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
581 return false;
582 }
583 pos += skip;
584 }
585
586 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
587
588 return true;
589}
590
598bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
599{
600 TarList::iterator it = _tar_list[subdir].find(tar_filename);
601 /* We don't know the file. */
602 if (it == _tar_list[subdir].end()) return false;
603
604 /* The file doesn't have a sub directory! */
605 if (it->second.empty()) {
606 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
607 return false;
608 }
609
610 auto p = tar_filename.rfind(".tar");
611 /* The file's path does not have a ".tar"? */
612 if (p == std::string::npos) return false;
613
614 const std::string dirname = tar_filename.substr(0, p);
615 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, dirname);
616 FioCreateDirectory(dirname);
617
618 for (auto &it2 : _tar_filelist[subdir]) {
619 if (tar_filename != it2.second.tar_filename) continue;
620
621 /* it2.first is tarball + PATHSEPCHAR + name. */
622 std::string_view name = it2.first;
623 name.remove_prefix(name.find_last_of(PATHSEPCHAR) + 1);
624 std::string filename = fmt::format("{}{}{}", dirname, PATHSEP, name);
625
626 Debug(misc, 9, " extracting {}", filename);
627
628 /* First open the file in the .tar. */
629 size_t to_copy = 0;
630 auto in = FioFOpenFileTar(it2.second, &to_copy);
631 if (!in.has_value()) {
632 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
633 return false;
634 }
635
636 /* Now open the 'output' file. */
637 auto out = FileHandle::Open(filename, "wb");
638 if (!out.has_value()) {
639 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
640 return false;
641 }
642
643 /* Now read from the tar and write it into the file. */
644 char buffer[4096];
645 size_t read;
646 for (; to_copy != 0; to_copy -= read) {
647 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
648 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
649 }
650
651 if (to_copy != 0) {
652 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
653 return false;
654 }
655 }
656
657 Debug(misc, 9, " extraction successful");
658 return true;
659}
660
661#if defined(_WIN32)
667extern void DetermineBasePaths(std::string_view exe);
668
670char *getcwd(char *buf, size_t size);
671#else /* defined(_WIN32) */
672
681static bool ChangeWorkingDirectoryToExecutable(std::string_view exe)
682{
683 std::string path{exe};
684
685#ifdef WITH_COCOA
686 for (size_t pos = path.find_first_of('.'); pos != std::string::npos; pos = path.find_first_of('.', pos + 1)) {
687 if (StrEqualsIgnoreCase(path.substr(pos, 4), ".app")) {
688 path.erase(pos);
689 break;
690 }
691 }
692#endif /* WITH_COCOA */
693
694 size_t pos = path.find_last_of(PATHSEPCHAR);
695 if (pos == std::string::npos) return false;
696
697 path.erase(pos);
698
699 if (chdir(path.c_str()) != 0) {
700 Debug(misc, 0, "Directory with the binary does not exist?");
701 return false;
702 }
703
704 return true;
705}
706
718{
719 /* No working directory, so nothing to do. */
720 if (_searchpaths[Searchpath::WorkingDir].empty()) return false;
721
722 /* Working directory is root, so do nothing. */
723 if (_searchpaths[Searchpath::WorkingDir] == PATHSEP) return false;
724
725 /* No personal/home directory, so the working directory won't be that. */
726 if (_searchpaths[Searchpath::PersonalDir].empty()) return true;
727
728 std::string tmp = _searchpaths[Searchpath::WorkingDir] + PERSONAL_DIR;
730
732}
733
739static std::string GetHomeDir()
740{
741#ifdef __HAIKU__
742 BPath path;
743 find_directory(B_USER_SETTINGS_DIRECTORY, &path);
744 return std::string(path.Path());
745#else
746 auto home_env = GetEnv("HOME"); // Stack var, shouldn't be freed
747 if (home_env.has_value()) return std::string(*home_env);
748
749 const struct passwd *pw = getpwuid(getuid());
750 if (pw != nullptr) return std::string(pw->pw_dir);
751#endif
752 return {};
753}
754
759void DetermineBasePaths(std::string_view exe)
760{
761 std::string tmp;
762 const std::string homedir = GetHomeDir();
763#ifdef USE_XDG
764 if (auto xdg_data_home = GetEnv("XDG_DATA_HOME"); xdg_data_home.has_value()) {
765 tmp = *xdg_data_home;
766 tmp += PATHSEP;
767 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
769 _searchpaths[Searchpath::PersonalDirXdg] = tmp;
770
771 tmp += "content_download";
774 } else if (!homedir.empty()) {
775 tmp = homedir;
776 tmp += PATHSEP ".local" PATHSEP "share" PATHSEP;
777 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
779 _searchpaths[Searchpath::PersonalDirXdg] = tmp;
780
781 tmp += "content_download";
784 } else {
785 _searchpaths[Searchpath::PersonalDirXdg].clear();
787 }
788#endif
789
790#if !defined(WITH_PERSONAL_DIR)
792#else
793 if (!homedir.empty()) {
794 tmp = std::move(homedir);
795 tmp += PATHSEP;
796 tmp += PERSONAL_DIR;
799
800 tmp += "content_download";
803 } else {
806 }
807#endif
808
809#if defined(WITH_SHARED_DIR)
810 tmp = SHARED_DIR;
813#else
815#endif
816
817 char cwd[MAX_PATH];
818 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
819
820 if (_config_file.empty()) {
821 /* Get the path to working directory of OpenTTD. */
822 tmp = cwd;
825
827 } else {
828 /* Use the folder of the config file as working directory. */
829 size_t end = _config_file.find_last_of(PATHSEPCHAR);
830 if (end == std::string::npos) {
831 /* _config_file is not in a folder, so use current directory. */
832 tmp = cwd;
833 } else {
834 tmp = FS2OTTD(std::filesystem::weakly_canonical(std::filesystem::path(OTTD2FS(_config_file))).parent_path().native());
835 }
838 }
839
840 /* Change the working directory to that one of the executable */
842 char buf[MAX_PATH];
843 if (getcwd(buf, lengthof(buf)) == nullptr) {
844 tmp.clear();
845 } else {
846 tmp = buf;
847 }
850 } else {
852 }
853
854 if (cwd[0] != '\0') {
855 /* Go back to the current working directory. */
856 if (chdir(cwd) != 0) {
857 Debug(misc, 0, "Failed to return to working directory!");
858 }
859 }
860
861#if !defined(GLOBAL_DATA_DIR)
863#else
864 tmp = GLOBAL_DATA_DIR;
867#endif
868#ifdef WITH_COCOA
869extern void CocoaSetApplicationBundleDir();
870 CocoaSetApplicationBundleDir();
871#else
873#endif
874}
875#endif /* defined(_WIN32) */
876
877std::string _personal_dir;
878
886void DeterminePaths(std::string_view exe, bool only_local_path)
887{
889 FillValidSearchPaths(only_local_path);
890
891#ifdef USE_XDG
892 std::string config_home;
893 std::string homedir = GetHomeDir();
894 if (auto xdg_config_home = GetEnv("XDG_CONFIG_HOME"); xdg_config_home.has_value()) {
895 config_home = *xdg_config_home;
896 config_home += PATHSEP;
897 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
898 } else if (!homedir.empty()) {
899 /* Defaults to ~/.config */
900 config_home = std::move(homedir);
901 config_home += PATHSEP ".config" PATHSEP;
902 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
903 }
904 AppendPathSeparator(config_home);
905#endif
906
907 for (Searchpath sp : _valid_searchpaths) {
909 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
910 }
911
912 std::string config_dir;
913 if (!_config_file.empty()) {
915 } else {
916 std::string personal_dir = FioFindFullPath(Subdirectory::Base, "openttd.cfg");
917 if (!personal_dir.empty()) {
918 auto end = personal_dir.find_last_of(PATHSEPCHAR);
919 if (end != std::string::npos) personal_dir.erase(end + 1);
920 config_dir = std::move(personal_dir);
921 } else {
922#ifdef USE_XDG
923 /* No previous configuration file found. Use the configuration folder from XDG. */
924 config_dir = config_home;
925#else
926 static const Searchpath new_openttd_cfg_order[] = {
928 };
929
930 config_dir.clear();
931 for (const auto &searchpath : new_openttd_cfg_order) {
932 if (IsValidSearchPath(searchpath)) {
933 config_dir = _searchpaths[searchpath];
934 break;
935 }
936 }
937#endif
938 }
939 _config_file = config_dir + "openttd.cfg";
940 }
941
942 Debug(misc, 1, "{} found as config directory", config_dir);
943
944 _highscore_file = config_dir + "hs.dat";
945 extern std::string _hotkeys_file;
946 _hotkeys_file = config_dir + "hotkeys.cfg";
947 extern std::string _windows_file;
948 _windows_file = config_dir + "windows.cfg";
949 extern std::string _private_file;
950 _private_file = config_dir + "private.cfg";
951 extern std::string _secrets_file;
952 _secrets_file = config_dir + "secrets.cfg";
953 extern std::string _favs_file;
954 _favs_file = config_dir + "favs.cfg";
955
956#ifdef USE_XDG
957 if (config_dir == config_home) {
958 /* We are using the XDG configuration home for the config file,
959 * then store the rest in the XDG data home folder. */
960 _personal_dir = _searchpaths[Searchpath::PersonalDirXdg];
961 if (only_local_path) {
962 /* In case of XDG and we only want local paths and we detected that
963 * the user either manually indicated the XDG path or didn't use
964 * "-c" option, we change the working-dir to the XDG personal-dir,
965 * as this is most likely what the user is expecting. */
966 _searchpaths[Searchpath::WorkingDir] = _searchpaths[Searchpath::PersonalDirXdg];
967 }
968 } else
969#endif
970 {
971 _personal_dir = config_dir;
972 }
973
974 /* Make the necessary folders */
975 FioCreateDirectory(config_dir);
976#if defined(WITH_PERSONAL_DIR)
978#endif
979
980 Debug(misc, 1, "{} found as personal directory", _personal_dir);
981
982 static const Subdirectory default_subdirs[] = {
984 };
985
986 for (const auto &default_subdir : default_subdirs) {
987 FioCreateDirectory(fmt::format("{}{}", _personal_dir, _subdirs[default_subdir]));
988 }
989
990 /* If we have network we make a directory for the autodownloading of content */
991 _searchpaths[Searchpath::AutodownloadDir] = _personal_dir + "content_download" PATHSEP;
992 Debug(misc, 3, "{} added as search path", _searchpaths[Searchpath::AutodownloadDir]);
994 FillValidSearchPaths(only_local_path);
995
996 /* Create the directory for each of the types of content */
998 for (const auto &subdir : subdirs) {
999 FioCreateDirectory(FioGetDirectory(Searchpath::AutodownloadDir, subdir));
1000 }
1001
1002 extern std::string _log_file;
1003 _log_file = _personal_dir + "openttd.log";
1004}
1005
1010void SanitizeFilename(std::string &filename)
1011{
1012 for (auto &c : filename) {
1013 switch (c) {
1014 /* The following characters are not allowed in filenames
1015 * on at least one of the supported operating systems: */
1016 case ':': case '\\': case '*': case '?': case '/':
1017 case '<': case '>': case '|': case '"':
1018 c = '_';
1019 break;
1020 }
1021 }
1022}
1023
1032std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1033{
1034 auto in = FileHandle::Open(filename, "rb");
1035 if (!in.has_value()) return nullptr;
1036
1037 fseek(*in, 0, SEEK_END);
1038 size_t len = ftell(*in);
1039 fseek(*in, 0, SEEK_SET);
1040 if (len > maxsize) return nullptr;
1041
1042 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1043
1044 mem.get()[len] = 0;
1045 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1046
1047 lenp = len;
1048 return mem;
1049}
1050
1057static bool MatchesExtension(std::string_view extension, const std::string &filename)
1058{
1059 if (extension.empty()) return true;
1060 if (filename.length() < extension.length()) return false;
1061
1062 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1063 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1064}
1065
1076static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1077{
1078 uint num = 0;
1079
1080 std::error_code error_code;
1081 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1082 if (dir_entry.is_directory()) {
1083 if (!recursive) continue;
1084 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1085 } else if (dir_entry.is_regular_file()) {
1086 std::string file = FS2OTTD(dir_entry.path().native());
1087 if (!MatchesExtension(extension, file)) continue;
1088 if (fs->AddFile(file, basepath_length, {})) num++;
1089 }
1090 }
1091 if (error_code) {
1092 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1093 }
1094
1095 return num;
1096}
1097
1105static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1106{
1107 uint num = 0;
1108
1109 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1110
1111 return num;
1112}
1113
1123uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1124{
1125 this->subdir = sd;
1126
1127 uint num = 0;
1128
1129 for (Searchpath sp : _valid_searchpaths) {
1130 /* Don't search in the working directory */
1131 if (sp == Searchpath::WorkingDir && !_do_scan_working_directory) continue;
1132
1133 std::string path = FioGetDirectory(sp, sd);
1134 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1135 }
1136
1137 if (tars && sd != Subdirectory::None) {
1138 for (const auto &tar : _tar_filelist[sd]) {
1139 num += ScanTar(this, extension, tar);
1140 }
1141 }
1142
1143 switch (sd) {
1145 num += this->Scan(extension, Subdirectory::OldGm, tars, recursive);
1146 [[fallthrough]];
1148 num += this->Scan(extension, Subdirectory::OldData, tars, recursive);
1149 break;
1150
1151 default: break;
1152 }
1153
1154 return num;
1155}
1156
1165uint FileScanner::Scan(std::string_view extension, const std::string &directory, bool recursive)
1166{
1167 std::string path(directory);
1168 AppendPathSeparator(path);
1169 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1170}
1171
1172std::optional<FileHandle> FileHandle::Open(const std::string &filename, std::string_view mode)
1173{
1174#if defined(_WIN32)
1175 /* Windows also requires mode to be wchar_t. */
1176 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1177#else
1178 auto f = fopen(filename.c_str(), std::string{mode}.c_str());
1179#endif /* _WIN32 */
1180
1181 if (f == nullptr) return std::nullopt;
1182 return FileHandle(f);
1183}
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
A sort-of mixin that implements 'at(pos)' and 'operator[](pos)' only for a specific enum class.
static std::optional< FileHandle > Open(const std::string &filename, std::string_view mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1172
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:1123
virtual bool AddFile(const std::string &filename, size_t basepath_length, const std::string &tar_filename)=0
Add a file with the given filename.
Helper for scanning for files with tar as extension.
Definition fileio_func.h:59
@ AI
Scan for AIs and its libraries.
Definition fileio_func.h:66
@ Scenario
Scan for scenarios and heightmaps.
Definition fileio_func.h:67
@ Game
Scan for game scripts.
Definition fileio_func.h:68
@ Baseset
Scan for base sets.
Definition fileio_func.h:64
@ NewGRF
Scan for non-base sets.
Definition fileio_func.h:65
uint DoScan(Subdirectory sd)
Perform the scanning of a particular subdirectory.
Definition fileio.cpp:380
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:448
Functions related to debugging.
#define Debug(category, level, format_string,...)
Output 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
constexpr std::underlying_type_t< enum_type > to_underlying(enum_type e)
Implementation of std::to_underlying (from C++23).
Definition enum_type.hpp:21
static bool ChangeWorkingDirectoryToExecutable(std::string_view exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:681
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:598
void DeterminePaths(std::string_view exe, bool only_local_path)
Acquire the base paths (personal dir and game data dir), fill all other paths (save dir,...
Definition fileio.cpp:886
static bool IsValidSearchPath(Searchpath sp)
Checks whether the given search path is a valid search path.
Definition fileio.cpp:77
bool FioRemove(const std::string &filename)
Remove a file.
Definition fileio.cpp:331
void SanitizeFilename(std::string &filename)
Sanitizes a filename, i.e.
Definition fileio.cpp:1010
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:1076
void DetermineBasePaths(std::string_view exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:759
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:351
std::optional< FileHandle > FioFOpenFile(std::string_view filename, std::string_view mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:246
std::string _personal_dir
custom directory for personal settings, saves, newgrf, etc.
Definition fileio.cpp:877
static bool _do_scan_working_directory
Whether the working directory should be scanned.
Definition fileio.cpp:33
static std::string GetHomeDir()
Gets the home directory of the user.
Definition fileio.cpp:739
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp:363
static const EnumClassIndexContainer< std::array< std::string_view, to_underlying(Subdirectory::End)>, Subdirectory > _subdirs
Subdirectory names.
Definition fileio.cpp:39
static bool MatchesExtension(std::string_view extension, const std::string &filename)
Helper to see whether a given filename matches the extension.
Definition fileio.cpp:1057
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:319
EnumClassIndexContainer< std::array< TarFileList, to_underlying(Subdirectory::End)>, Subdirectory > _tar_filelist
List of files within tar files found in each subdirectory.
Definition fileio.cpp:70
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:134
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:1105
bool FioCheckFileExists(std::string_view filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:123
static std::optional< FileHandle > FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
Opens a file from inside a tar archive.
Definition fileio.cpp:225
static std::string ExtractString(std::span< char > buffer)
Helper to extract a string for the tar header.
Definition fileio.cpp:443
EnumClassIndexContainer< std::array< std::string, to_underlying(Searchpath::End)>, Searchpath > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:65
std::string _highscore_file
The file to store the highscore data in.
Definition highscore.cpp:25
bool DoScanWorkingDirectory()
Whether we should scan the working directory.
Definition fileio.cpp:717
std::string _config_file
Configuration file of OpenTTD.
Definition settings.cpp:64
std::string FioFindFullPath(Subdirectory subdir, std::string_view filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:146
EnumClassIndexContainer< std::array< TarList, to_underlying(Subdirectory::End)>, Subdirectory > _tar_list
List of tar files found in each subdirectory.
Definition fileio.cpp:68
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1032
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:351
Searchpath
Types of searchpaths OpenTTD might use.
@ Begin
The lowest valid value.
@ WorkingDir
Search in the working directory.
@ AutodownloadDir
Search within the autodownload directory.
@ ApplicationBundleDir
Search within the application bundle.
@ InstallationDir
Search in the installation directory.
@ End
End marker.
@ AutodownloadPersonalDir
Search within the autodownload directory located in the personal directory.
@ SharedDir
Search in the shared directory, like 'Shared Files' under Windows.
@ BinaryDir
Search in the directory where the binary resides.
@ AutodownloadPersonalDirXdg
Search within the autodownload directory located in the personal directory (XDG variant).
@ PersonalDir
Search in the personal directory.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
Definition fileio_type.h:88
@ Base
Base directory for all subdirectories.
Definition fileio_type.h:89
@ Autosave
Subdirectory of save for autosaves.
Definition fileio_type.h:91
@ NewGrf
Subdirectory for all NewGRFs.
Definition fileio_type.h:97
@ Screenshot
Subdirectory for all screenshots.
@ GsLibrary
Subdirectory for all GS libraries.
@ Scenario
Base directory for all scenarios.
Definition fileio_type.h:92
@ Heightmap
Subdirectory of scenario for heightmaps.
Definition fileio_type.h:93
@ None
A path without any base directory.
@ Ai
Subdirectory for all AI files.
Definition fileio_type.h:99
@ SocialIntegration
Subdirectory for all social integration plugins.
@ End
End marker.
@ Gs
Subdirectory for all game scripts.
@ Baseset
Subdirectory for all base data (base sets, intro game).
Definition fileio_type.h:96
@ Save
Base directory for all savegames.
Definition fileio_type.h:90
@ OldData
Old subdirectory for the data.
Definition fileio_type.h:95
@ AiLibrary
Subdirectory for all AI libraries.
@ OldGm
Old subdirectory for the music.
Definition fileio_type.h:94
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:66
std::string _favs_file
Picker favourites configuration file of OpenTTD.
Definition settings.cpp:67
std::string _private_file
Private configuration file of OpenTTD.
Definition settings.cpp:65
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:271
std::optional< std::string_view > GetEnv(const char *variable)
Get the environment variable using std::getenv and when it is an empty string (or nullptr),...
Definition string.cpp:856
bool StrEqualsIgnoreCase(std::string_view str1, std::string_view str2)
Compares two string( view)s for equality, while ignoring the case of the characters.
Definition string.cpp:325
static void StrMakeValid(Builder &builder, StringConsumer &consumer, StringValidationSettings settings)
Copies the valid (UTF-8) characters from consumer to the builder.
Definition string.cpp:119
int StrCompareIgnoreCase(std::string_view str1, std::string_view str2)
Compares two string( view)s, while ignoring the case of the characters.
Definition string.cpp:312
Parse strings.
static std::optional< T > ParseInteger(std::string_view arg, int base=10, bool clamp=false)
Change a string into its number representation.
Functions related to low-level strings.
Structs, typedefs and macros used for TAR file handling.
std::map< std::string, std::string, std::less<> > TarList
Map of tar file to tar directory.
Definition tar_type.h:22
std::wstring OTTD2FS(std::string_view name)
Convert from OpenTTD's encoding to a wide string.
Definition win32.cpp:368
std::string FS2OTTD(std::wstring_view name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:352
std::string _windows_file
Config file to store WindowDesc.
Definition window.cpp:105