OpenTTD Source 20260512-master-g20b387b91f
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
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 /* For official TTD directory, don't include the subdirectory. */
172
173 return fmt::format("{}{}", _searchpaths[sp], _subdirs[subdir]);
174}
175
176std::string FioFindDirectory(Subdirectory subdir)
177{
178 /* Find and return the first valid directory */
179 for (Searchpath sp : _valid_searchpaths) {
180 std::string ret = FioGetDirectory(sp, subdir);
181 if (FileExists(ret)) return ret;
182 }
183
184 /* Could not find the directory, fall back to a base path */
185 return _personal_dir;
186}
187
188static std::optional<FileHandle> FioFOpenFileSp(std::string_view filename, std::string_view mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
189{
190#if defined(_WIN32)
191 /* fopen is implemented as a define with ellipses for
192 * Unicode support (prepend an L). As we are not sending
193 * a string, but a variable, it 'renames' the variable,
194 * so make that variable to makes it compile happily */
195 wchar_t Lmode[5];
196 MultiByteToWideChar(CP_ACP, 0, mode.data(), static_cast<int>(std::size(mode)), Lmode, static_cast<int>(std::size(Lmode)));
197#endif
198 std::string buf;
199
200 if (subdir == Subdirectory::None) {
201 buf = filename;
202 } else {
203 buf = fmt::format("{}{}", FioGetDirectory(sp, subdir), filename);
204 }
205
206 auto f = FileHandle::Open(buf, mode);
207#if !defined(_WIN32)
208 if (!f.has_value() && strtolower(buf, subdir == Subdirectory::None ? 0 : _searchpaths[sp].size() - 1) ) {
209 f = FileHandle::Open(buf, mode);
210 }
211#endif
212 if (f.has_value() && filesize != nullptr) {
213 /* Find the size of the file */
214 fseek(*f, 0, SEEK_END);
215 *filesize = ftell(*f);
216 fseek(*f, 0, SEEK_SET);
217 }
218 return f;
219}
220
228static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
229{
230 auto f = FileHandle::Open(entry.tar_filename, "rb");
231 if (!f.has_value()) return std::nullopt;
232
233 if (fseek(*f, entry.position, SEEK_SET) < 0) {
234 return std::nullopt;
235 }
236
237 if (filesize != nullptr) *filesize = entry.size;
238 return f;
239}
240
249std::optional<FileHandle> FioFOpenFile(std::string_view filename, std::string_view mode, Subdirectory subdir, size_t *filesize)
250{
251 std::optional<FileHandle> f = std::nullopt;
252 assert(subdir < Subdirectory::End || subdir == Subdirectory::None);
253
254 for (Searchpath sp : _valid_searchpaths) {
255 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
256 if (f.has_value() || subdir == Subdirectory::None) break;
257 }
258
259 /* We can only use .tar in case of data-dir, and read-mode */
260 if (!f.has_value() && mode[0] == 'r' && subdir != Subdirectory::None) {
261 /* Filenames in tars are always forced to be lowercase */
262 std::string resolved_name{filename};
263 strtolower(resolved_name);
264
265 /* Resolve ".." */
266 std::istringstream ss(resolved_name);
267 std::vector<std::string> tokens;
268 for (std::string token; std::getline(ss, token, PATHSEPCHAR); /* nothing */) {
269 if (token == "..") {
270 if (tokens.size() < 2) return std::nullopt;
271 tokens.pop_back();
272 } else if (token == ".") {
273 /* Do nothing. "." means current folder, but you can create tar files with "." in the path.
274 * This confuses our file resolver. So, act like this folder doesn't exist. */
275 } else {
276 tokens.push_back(token);
277 }
278 }
279
280 resolved_name.clear();
281 bool first = true;
282 for (const std::string &token : tokens) {
283 if (!first) {
284 resolved_name += PATHSEP;
285 }
286 resolved_name += token;
287 first = false;
288 }
289
290 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
291 if (it != _tar_filelist[subdir].end()) {
292 f = FioFOpenFileTar(it->second, filesize);
293 }
294 }
295
296 /* Sometimes a full path is given. To support
297 * the 'subdirectory' must be 'removed'. */
298 if (!f.has_value() && subdir != Subdirectory::None) {
299 switch (subdir) {
301 f = FioFOpenFile(filename, mode, Subdirectory::OldGm, filesize);
302 if (f.has_value()) break;
303 [[fallthrough]];
305 f = FioFOpenFile(filename, mode, Subdirectory::OldData, filesize);
306 break;
307
308 default:
309 f = FioFOpenFile(filename, mode, Subdirectory::None, filesize);
310 break;
311 }
312 }
313
314 return f;
315}
316
322void FioCreateDirectory(const std::string &name)
323{
324 /* Ignore directory creation errors; they'll surface later on. */
325 std::error_code error_code;
326 std::filesystem::create_directories(OTTD2FS(name), error_code);
327}
328
334bool FioRemove(const std::string &filename)
335{
336 std::filesystem::path path = OTTD2FS(filename);
337 std::error_code error_code;
338 if (!std::filesystem::remove(path, error_code)) {
339 if (error_code) {
340 Debug(misc, 0, "Removing {} failed: {}", filename, error_code.message());
341 } else {
342 Debug(misc, 0, "Removing {} failed: file does not exist", filename);
343 }
344 return false;
345 }
346 return true;
347}
348
354void AppendPathSeparator(std::string &buf)
355{
356 if (buf.empty()) return;
357
358 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
359}
360
366static void SimplifyFileName(std::string &name)
367{
368 for (char &c : name) {
369 /* Force lowercase */
370 c = std::tolower(c);
371#if (PATHSEPCHAR != '/')
372 /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */
373 if (c == '/') c = PATHSEPCHAR;
374#endif
375 }
376}
377
384{
385 _tar_filelist[sd].clear();
386 _tar_list[sd].clear();
387 uint num = this->Scan(".tar", sd, false);
388 if (sd == Subdirectory::Baseset || sd == Subdirectory::NewGrf) num += this->Scan(".tar", Subdirectory::OldData, false);
389 return num;
390}
391
397/* static */ uint TarScanner::DoScan(TarScanner::Modes modes)
398{
399 Debug(misc, 2, "Scanning for tars");
400 TarScanner fs;
401 uint num = 0;
402 if (modes.Test(TarScanner::Mode::Baseset)) {
403 num += fs.DoScan(Subdirectory::Baseset);
404 }
405 if (modes.Test(TarScanner::Mode::NewGRF)) {
406 num += fs.DoScan(Subdirectory::NewGrf);
407 }
408 if (modes.Test(TarScanner::Mode::AI)) {
409 num += fs.DoScan(Subdirectory::Ai);
411 }
412 if (modes.Test(TarScanner::Mode::Game)) {
413 num += fs.DoScan(Subdirectory::Gs);
415 }
416 if (modes.Test(TarScanner::Mode::Scenario)) {
419 }
420 Debug(misc, 2, "Scan complete, found {} files", num);
421 return num;
422}
423
430bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
431{
432 this->subdir = sd;
433 return this->AddFile(filename, 0);
434}
435
446static std::string ExtractString(std::span<char> buffer)
447{
448 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
449}
450
451bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
452{
453 /* No tar within tar. */
454 assert(tar_filename.empty());
455
456 /* The TAR-header, repeated for every file */
457 struct TarHeader {
458 char name[100];
459 char mode[8];
460 char uid[8];
461 char gid[8];
462 char size[12];
463 char mtime[12];
464 char chksum[8];
465 char typeflag;
466 char linkname[100];
467 char magic[6];
468 char version[2];
469 char uname[32];
470 char gname[32];
471 char devmajor[8];
472 char devminor[8];
473 char prefix[155];
474
475 char unused[12];
476 };
477 static_assert(sizeof(TarHeader) == 512);
478
479 /* Check if we already seen this file */
480 TarList::iterator it = _tar_list[this->subdir].find(filename);
481 if (it != _tar_list[this->subdir].end()) return false;
482
483 auto of = FileHandle::Open(filename, "rb");
484 /* Although the file has been found there can be
485 * a number of reasons we cannot open the file.
486 * Most common case is when we simply have not
487 * been given read access. */
488 if (!of.has_value()) return false;
489 auto &f = *of;
490
491 _tar_list[this->subdir][filename] = std::string{};
492
493 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename().native());
494 SimplifyFileName(filename_base);
495
496 TarHeader th;
497 size_t num = 0, pos = 0;
498
499 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
500 size_t num_bytes_read = fread(&th, 1, sizeof(TarHeader), f);
501 if (num_bytes_read != sizeof(TarHeader)) break;
502 pos += num_bytes_read;
503
504 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
505 auto last_of_th = &th.unused[std::size(th.unused)];
506 if (std::string_view{th.magic, 5} != "ustar" && std::any_of(th.magic, last_of_th, [](auto c) { return c != 0; })) {
507 /* If we have only zeros in the block, it can be an end-of-file indicator */
508 if (std::all_of(th.name, last_of_th, [](auto c) { return c == 0; })) continue;
509
510 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
511 return false;
512 }
513
514 std::string name;
515
516 /* The prefix contains the directory-name */
517 if (th.prefix[0] != '\0') {
518 name = ExtractString(th.prefix);
519 name += PATHSEP;
520 }
521
522 /* Copy the name of the file in a safe way at the end of 'name' */
523 name += ExtractString(th.name);
524
525 /* The size of the file, for some strange reason, this is stored as a string in octals. */
526 std::string size = ExtractString(th.size);
527 size_t skip = 0;
528 if (!size.empty()) {
529 auto value = ParseInteger<size_t>(size, 8);
530 if (!value.has_value()) {
531 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
532 fclose(f);
533 return false;
534 }
535 skip = *value;
536 }
537
538 switch (th.typeflag) {
539 case '\0':
540 case '0': { // regular file
541 if (name.empty()) break;
542
543 /* Store this entry in the list */
544 TarFileListEntry entry;
545 entry.tar_filename = filename;
546 entry.size = skip;
547 entry.position = pos;
548
549 /* Convert to lowercase and our PATHSEPCHAR */
550 SimplifyFileName(name);
551
552 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
553 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
554
555 break;
556 }
557
558 case '1': // hard links
559 case '2': { // symbolic links
560 std::string link = ExtractString(th.linkname);
561
562 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
563 break;
564 }
565
566 case '5': // directory
567 /* Convert to lowercase and our PATHSEPCHAR */
568 SimplifyFileName(name);
569
570 /* Store the first directory name we detect */
571 Debug(misc, 6, "Found dir in tar: {}", name);
572 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = std::move(name);
573 break;
574
575 default:
576 /* Ignore other types */
577 break;
578 }
579
580 /* Skip to the next block.. */
581 skip = Align(skip, 512);
582 if (fseek(f, skip, SEEK_CUR) < 0) {
583 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
584 return false;
585 }
586 pos += skip;
587 }
588
589 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
590
591 return true;
592}
593
601bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
602{
603 TarList::iterator it = _tar_list[subdir].find(tar_filename);
604 /* We don't know the file. */
605 if (it == _tar_list[subdir].end()) return false;
606
607 /* The file doesn't have a sub directory! */
608 if (it->second.empty()) {
609 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
610 return false;
611 }
612
613 auto p = tar_filename.rfind(".tar");
614 /* The file's path does not have a ".tar"? */
615 if (p == std::string::npos) return false;
616
617 const std::string dirname = tar_filename.substr(0, p);
618 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, dirname);
619 FioCreateDirectory(dirname);
620
621 for (auto &it2 : _tar_filelist[subdir]) {
622 if (tar_filename != it2.second.tar_filename) continue;
623
624 /* it2.first is tarball + PATHSEPCHAR + name. */
625 std::string_view name = it2.first;
626 name.remove_prefix(name.find_last_of(PATHSEPCHAR) + 1);
627 std::string filename = fmt::format("{}{}{}", dirname, PATHSEP, name);
628
629 Debug(misc, 9, " extracting {}", filename);
630
631 /* First open the file in the .tar. */
632 size_t to_copy = 0;
633 auto in = FioFOpenFileTar(it2.second, &to_copy);
634 if (!in.has_value()) {
635 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
636 return false;
637 }
638
639 /* Now open the 'output' file. */
640 auto out = FileHandle::Open(filename, "wb");
641 if (!out.has_value()) {
642 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
643 return false;
644 }
645
646 /* Now read from the tar and write it into the file. */
647 char buffer[4096];
648 size_t read;
649 for (; to_copy != 0; to_copy -= read) {
650 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
651 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
652 }
653
654 if (to_copy != 0) {
655 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
656 return false;
657 }
658 }
659
660 Debug(misc, 9, " extraction successful");
661 return true;
662}
663
664#if defined(_WIN32)
670extern void DetermineBasePaths(std::string_view exe);
671
673char *getcwd(char *buf, size_t size);
674#else /* defined(_WIN32) */
675
684static bool ChangeWorkingDirectoryToExecutable(std::string_view exe)
685{
686 std::string path{exe};
687
688#ifdef WITH_COCOA
689 for (size_t pos = path.find_first_of('.'); pos != std::string::npos; pos = path.find_first_of('.', pos + 1)) {
690 if (StrEqualsIgnoreCase(path.substr(pos, 4), ".app")) {
691 path.erase(pos);
692 break;
693 }
694 }
695#endif /* WITH_COCOA */
696
697 size_t pos = path.find_last_of(PATHSEPCHAR);
698 if (pos == std::string::npos) return false;
699
700 path.erase(pos);
701
702 if (chdir(path.c_str()) != 0) {
703 Debug(misc, 0, "Directory with the binary does not exist?");
704 return false;
705 }
706
707 return true;
708}
709
721{
722 /* No working directory, so nothing to do. */
723 if (_searchpaths[Searchpath::WorkingDir].empty()) return false;
724
725 /* Working directory is root, so do nothing. */
726 if (_searchpaths[Searchpath::WorkingDir] == PATHSEP) return false;
727
728 /* No personal/home directory, so the working directory won't be that. */
729 if (_searchpaths[Searchpath::PersonalDir].empty()) return true;
730
731 std::string tmp = _searchpaths[Searchpath::WorkingDir] + PERSONAL_DIR;
733
735}
736
742static std::string GetHomeDir()
743{
744#ifdef __HAIKU__
745 BPath path;
746 find_directory(B_USER_SETTINGS_DIRECTORY, &path);
747 return std::string(path.Path());
748#else
749 auto home_env = GetEnv("HOME"); // Stack var, shouldn't be freed
750 if (home_env.has_value()) return std::string(*home_env);
751
752 const struct passwd *pw = getpwuid(getuid());
753 if (pw != nullptr) return std::string(pw->pw_dir);
754#endif
755 return {};
756}
757
762void DetermineBasePaths(std::string_view exe)
763{
764 std::string tmp;
765 const std::string homedir = GetHomeDir();
766#ifdef USE_XDG
767 if (auto xdg_data_home = GetEnv("XDG_DATA_HOME"); xdg_data_home.has_value()) {
768 tmp = *xdg_data_home;
769 tmp += PATHSEP;
770 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
772 _searchpaths[Searchpath::PersonalDirXdg] = tmp;
773
774 tmp += "content_download";
777 } else if (!homedir.empty()) {
778 tmp = homedir;
779 tmp += PATHSEP ".local" PATHSEP "share" PATHSEP;
780 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
782 _searchpaths[Searchpath::PersonalDirXdg] = tmp;
783
784 tmp += "content_download";
787 } else {
788 _searchpaths[Searchpath::PersonalDirXdg].clear();
790 }
791#endif
792
793#if !defined(WITH_PERSONAL_DIR)
795#else
796 if (!homedir.empty()) {
797 tmp = std::move(homedir);
798 tmp += PATHSEP;
799 tmp += PERSONAL_DIR;
802
803 tmp += "content_download";
806 } else {
809 }
810#endif
811
812#if defined(WITH_SHARED_DIR)
813 tmp = SHARED_DIR;
816#else
818#endif
819
820 char cwd[MAX_PATH];
821 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
822
823 if (_config_file.empty()) {
824 /* Get the path to working directory of OpenTTD. */
825 tmp = cwd;
828
830 } else {
831 /* Use the folder of the config file as working directory. */
832 size_t end = _config_file.find_last_of(PATHSEPCHAR);
833 if (end == std::string::npos) {
834 /* _config_file is not in a folder, so use current directory. */
835 tmp = cwd;
836 } else {
837 tmp = FS2OTTD(std::filesystem::weakly_canonical(std::filesystem::path(OTTD2FS(_config_file))).parent_path().native());
838 }
841 }
842
843 /* Change the working directory to that one of the executable */
845 char buf[MAX_PATH];
846 if (getcwd(buf, lengthof(buf)) == nullptr) {
847 tmp.clear();
848 } else {
849 tmp = buf;
850 }
853 } else {
855 }
856
857 if (cwd[0] != '\0') {
858 /* Go back to the current working directory. */
859 if (chdir(cwd) != 0) {
860 Debug(misc, 0, "Failed to return to working directory!");
861 }
862 }
863
864#if !defined(GLOBAL_DATA_DIR)
866#else
867 tmp = GLOBAL_DATA_DIR;
870#endif
871#ifdef WITH_COCOA
872extern void CocoaSetApplicationBundleDir();
873 CocoaSetApplicationBundleDir();
874#else
876#endif
877
878 /* Look for Atari release of Transport Tycoon Deluxe for original data files */
879 std::string config_file_path;
880 const std::string atari_ini_filename = "Atari/Transport Tycoon Deluxe/installpath.ini";
881
883
884#ifdef WITH_COCOA
885extern std::string CocoaGetAppSupportDir();
886 config_file_path = CocoaGetAppSupportDir();
887
888 if (!config_file_path.empty()) {
889 AppendPathSeparator(config_file_path);
890 config_file_path += atari_ini_filename;
891 }
892#else
893 config_file_path = GetHomeDir();
894
895 if (!config_file_path.empty()) {
896 AppendPathSeparator(config_file_path);
897 config_file_path += ".local/share/";
898 config_file_path += atari_ini_filename;
899 }
900#endif
901
902 if (!config_file_path.empty()) {
903 size_t installpath_len;
904 std::unique_ptr<char[]> installpath = ReadFileToMem(config_file_path, installpath_len, MAX_PATH);
905
906 if (installpath != nullptr && installpath_len > 0) {
907 std::string ttd_path = installpath.get();
908 AppendPathSeparator(ttd_path);
909
910#ifdef WITH_COCOA
911 /* The path provided is to the TTD.app/Contents/MacOS folder */
912 ttd_path += "../Resources/";
913#endif
914
915 ttd_path += "CD";
916 AppendPathSeparator(ttd_path);
917
919 }
920 }
921}
922#endif /* defined(_WIN32) */
923
924std::string _personal_dir;
925
933void DeterminePaths(std::string_view exe, bool only_local_path)
934{
936 FillValidSearchPaths(only_local_path);
937
938#ifdef USE_XDG
939 std::string config_home;
940 std::string homedir = GetHomeDir();
941 if (auto xdg_config_home = GetEnv("XDG_CONFIG_HOME"); xdg_config_home.has_value()) {
942 config_home = *xdg_config_home;
943 config_home += PATHSEP;
944 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
945 } else if (!homedir.empty()) {
946 /* Defaults to ~/.config */
947 config_home = std::move(homedir);
948 config_home += PATHSEP ".config" PATHSEP;
949 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
950 }
951 AppendPathSeparator(config_home);
952#endif
953
954 for (Searchpath sp : _valid_searchpaths) {
956 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
957 }
958
959 std::string config_dir;
960 if (!_config_file.empty()) {
962 } else {
963 std::string personal_dir = FioFindFullPath(Subdirectory::Base, "openttd.cfg");
964 if (!personal_dir.empty()) {
965 auto end = personal_dir.find_last_of(PATHSEPCHAR);
966 if (end != std::string::npos) personal_dir.erase(end + 1);
967 config_dir = std::move(personal_dir);
968 } else {
969#ifdef USE_XDG
970 /* No previous configuration file found. Use the configuration folder from XDG. */
971 config_dir = config_home;
972#else
973 static const Searchpath new_openttd_cfg_order[] = {
975 };
976
977 config_dir.clear();
978 for (const auto &searchpath : new_openttd_cfg_order) {
979 if (IsValidSearchPath(searchpath)) {
980 config_dir = _searchpaths[searchpath];
981 break;
982 }
983 }
984#endif
985 }
986 _config_file = config_dir + "openttd.cfg";
987 }
988
989 Debug(misc, 1, "{} found as config directory", config_dir);
990
991 _highscore_file = config_dir + "hs.dat";
992 extern std::string _hotkeys_file;
993 _hotkeys_file = config_dir + "hotkeys.cfg";
994 extern std::string _windows_file;
995 _windows_file = config_dir + "windows.cfg";
996 extern std::string _private_file;
997 _private_file = config_dir + "private.cfg";
998 extern std::string _secrets_file;
999 _secrets_file = config_dir + "secrets.cfg";
1000 extern std::string _favs_file;
1001 _favs_file = config_dir + "favs.cfg";
1002
1003#ifdef USE_XDG
1004 if (config_dir == config_home) {
1005 /* We are using the XDG configuration home for the config file,
1006 * then store the rest in the XDG data home folder. */
1007 _personal_dir = _searchpaths[Searchpath::PersonalDirXdg];
1008 if (only_local_path) {
1009 /* In case of XDG and we only want local paths and we detected that
1010 * the user either manually indicated the XDG path or didn't use
1011 * "-c" option, we change the working-dir to the XDG personal-dir,
1012 * as this is most likely what the user is expecting. */
1013 _searchpaths[Searchpath::WorkingDir] = _searchpaths[Searchpath::PersonalDirXdg];
1014 }
1015 } else
1016#endif
1017 {
1018 _personal_dir = config_dir;
1019 }
1020
1021 /* Make the necessary folders */
1022 FioCreateDirectory(config_dir);
1023#if defined(WITH_PERSONAL_DIR)
1025#endif
1026
1027 Debug(misc, 1, "{} found as personal directory", _personal_dir);
1028
1029 static const Subdirectory default_subdirs[] = {
1031 };
1032
1033 for (const auto &default_subdir : default_subdirs) {
1034 FioCreateDirectory(fmt::format("{}{}", _personal_dir, _subdirs[default_subdir]));
1035 }
1036
1037 /* If we have network we make a directory for the autodownloading of content */
1038 _searchpaths[Searchpath::AutodownloadDir] = _personal_dir + "content_download" PATHSEP;
1039 Debug(misc, 3, "{} added as search path", _searchpaths[Searchpath::AutodownloadDir]);
1041 FillValidSearchPaths(only_local_path);
1042
1043 /* Create the directory for each of the types of content */
1045 for (const auto &subdir : subdirs) {
1046 FioCreateDirectory(FioGetDirectory(Searchpath::AutodownloadDir, subdir));
1047 }
1048
1049 extern std::string _log_file;
1050 _log_file = _personal_dir + "openttd.log";
1051}
1052
1057void SanitizeFilename(std::string &filename)
1058{
1059 for (auto &c : filename) {
1060 switch (c) {
1061 /* The following characters are not allowed in filenames
1062 * on at least one of the supported operating systems: */
1063 case ':': case '\\': case '*': case '?': case '/':
1064 case '<': case '>': case '|': case '"':
1065 c = '_';
1066 break;
1067 }
1068 }
1069}
1070
1079std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1080{
1081 auto in = FileHandle::Open(filename, "rb");
1082 if (!in.has_value()) return nullptr;
1083
1084 fseek(*in, 0, SEEK_END);
1085 size_t len = ftell(*in);
1086 fseek(*in, 0, SEEK_SET);
1087 if (len > maxsize) return nullptr;
1088
1089 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1090
1091 mem.get()[len] = 0;
1092 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1093
1094 lenp = len;
1095 return mem;
1096}
1097
1104static bool MatchesExtension(std::string_view extension, const std::string &filename)
1105{
1106 if (extension.empty()) return true;
1107 if (filename.length() < extension.length()) return false;
1108
1109 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1110 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1111}
1112
1123static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1124{
1125 uint num = 0;
1126
1127 std::error_code error_code;
1128 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1129 if (dir_entry.is_directory()) {
1130 if (!recursive) continue;
1131 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1132 } else if (dir_entry.is_regular_file()) {
1133 std::string file = FS2OTTD(dir_entry.path().native());
1134 if (!MatchesExtension(extension, file)) continue;
1135 if (fs->AddFile(file, basepath_length, {})) num++;
1136 }
1137 }
1138 if (error_code) {
1139 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1140 }
1141
1142 return num;
1143}
1144
1152static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1153{
1154 uint num = 0;
1155
1156 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1157
1158 return num;
1159}
1160
1170uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1171{
1172 this->subdir = sd;
1173
1174 uint num = 0;
1175
1176 for (Searchpath sp : _valid_searchpaths) {
1177 /* Don't search in the working directory */
1178 if (sp == Searchpath::WorkingDir && !_do_scan_working_directory) continue;
1179
1180 std::string path = FioGetDirectory(sp, sd);
1181 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1182 }
1183
1184 if (tars && sd != Subdirectory::None) {
1185 for (const auto &tar : _tar_filelist[sd]) {
1186 num += ScanTar(this, extension, tar);
1187 }
1188 }
1189
1190 switch (sd) {
1192 num += this->Scan(extension, Subdirectory::OldGm, tars, recursive);
1193 [[fallthrough]];
1195 num += this->Scan(extension, Subdirectory::OldData, tars, recursive);
1196 break;
1197
1198 default: break;
1199 }
1200
1201 return num;
1202}
1203
1212uint FileScanner::Scan(std::string_view extension, const std::string &directory, bool recursive)
1213{
1214 std::string path(directory);
1215 AppendPathSeparator(path);
1216 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1217}
1218
1219std::optional<FileHandle> FileHandle::Open(const std::string &filename, std::string_view mode)
1220{
1221#if defined(_WIN32)
1222 /* Windows also requires mode to be wchar_t. */
1223 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1224#else
1225 auto f = fopen(filename.c_str(), std::string{mode}.c_str());
1226#endif /* _WIN32 */
1227
1228 if (f == nullptr) return std::nullopt;
1229 return FileHandle(f);
1230}
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
static std::optional< FileHandle > Open(const std::string &filename, std::string_view mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1219
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:1170
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:383
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:451
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
EnumClassIndexContainer< std::array< T, to_underlying(N)>, Index > EnumIndexArray
A typedef for EnumClassIndexContainer using std::array as the backing container type.
static bool ChangeWorkingDirectoryToExecutable(std::string_view exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:684
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:601
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:933
static bool IsValidSearchPath(Searchpath sp)
Checks whether the given search path is a valid search path.
Definition fileio.cpp:77
static const EnumIndexArray< std::string_view, Subdirectory, Subdirectory::End > _subdirs
Subdirectory names.
Definition fileio.cpp:39
bool FioRemove(const std::string &filename)
Remove a file.
Definition fileio.cpp:334
void SanitizeFilename(std::string &filename)
Sanitizes a filename, i.e.
Definition fileio.cpp:1057
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:1123
void DetermineBasePaths(std::string_view exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:762
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:354
EnumIndexArray< TarFileList, Subdirectory, Subdirectory::End > _tar_filelist
List of files within tar files found in each subdirectory.
Definition fileio.cpp:70
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:249
std::string _personal_dir
custom directory for personal settings, saves, newgrf, etc.
Definition fileio.cpp:924
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:742
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp:366
static bool MatchesExtension(std::string_view extension, const std::string &filename)
Helper to see whether a given filename matches the extension.
Definition fileio.cpp:1104
EnumIndexArray< TarList, Subdirectory, Subdirectory::End > _tar_list
List of tar files found in each subdirectory.
Definition fileio.cpp:68
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:322
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:1152
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:228
static std::string ExtractString(std::span< char > buffer)
Helper to extract a string for the tar header.
Definition fileio.cpp:446
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:720
std::string _config_file
Configuration file of OpenTTD.
Definition settings.cpp:64
EnumIndexArray< std::string, Searchpath, Searchpath::End > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:65
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
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1079
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:354
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.
@ TransportTycoonDeluxeDir
Search within the Transport Tycoon Deluxe data directory (if installed).
@ 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::wstring OTTD2FS(std::string_view name)
Convert from OpenTTD's encoding to a wide string.
Definition win32.cpp:387
std::string FS2OTTD(std::wstring_view name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:371
std::string _windows_file
Config file to store WindowDesc.
Definition window.cpp:105