OpenTTD Source 20250529-master-g10c159a79f
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"
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
38static const std::string_view _subdirs[] = {
39 "",
40 "save" PATHSEP,
41 "save" PATHSEP "autosave" PATHSEP,
42 "scenario" PATHSEP,
43 "scenario" PATHSEP "heightmap" PATHSEP,
44 "gm" PATHSEP,
45 "data" PATHSEP,
46 "baseset" PATHSEP,
47 "newgrf" PATHSEP,
48 "lang" PATHSEP,
49 "ai" PATHSEP,
50 "ai" PATHSEP "library" PATHSEP,
51 "game" PATHSEP,
52 "game" PATHSEP "library" PATHSEP,
53 "screenshot" PATHSEP,
54 "social_integration" PATHSEP,
55 "docs" 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(std::string_view filename, Subdirectory subdir)
122{
123 auto f = FioFOpenFile(filename, "rb", subdir);
124 return f.has_value();
125}
126
132bool FileExists(std::string_view filename)
133{
134 std::error_code ec;
135 return std::filesystem::exists(OTTD2FS(filename), ec);
136}
137
144std::string FioFindFullPath(Subdirectory subdir, std::string_view 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 fmt::format("{}{}", _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(std::string_view filename, std::string_view 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.data(), static_cast<int>(std::size(mode)), 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 = fmt::format("{}{}{}", _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(std::string_view filename, std::string_view 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 for (std::string token; std::getline(ss, token, PATHSEPCHAR); /* nothing */) {
262 if (token == "..") {
263 if (tokens.size() < 2) return std::nullopt;
264 tokens.pop_back();
265 } else if (token == ".") {
266 /* Do nothing. "." means current folder, but you can create tar files with "." in the path.
267 * This confuses our file resolver. So, act like this folder doesn't exist. */
268 } else {
269 tokens.push_back(token);
270 }
271 }
272
273 resolved_name.clear();
274 bool first = true;
275 for (const std::string &token : tokens) {
276 if (!first) {
277 resolved_name += PATHSEP;
278 }
279 resolved_name += token;
280 first = false;
281 }
282
283 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
284 if (it != _tar_filelist[subdir].end()) {
285 f = FioFOpenFileTar(it->second, filesize);
286 }
287 }
288
289 /* Sometimes a full path is given. To support
290 * the 'subdirectory' must be 'removed'. */
291 if (!f.has_value() && subdir != NO_DIRECTORY) {
292 switch (subdir) {
293 case BASESET_DIR:
294 f = FioFOpenFile(filename, mode, OLD_GM_DIR, filesize);
295 if (f.has_value()) break;
296 [[fallthrough]];
297 case NEWGRF_DIR:
298 f = FioFOpenFile(filename, mode, OLD_DATA_DIR, filesize);
299 break;
300
301 default:
302 f = FioFOpenFile(filename, mode, NO_DIRECTORY, filesize);
303 break;
304 }
305 }
306
307 return f;
308}
309
315void FioCreateDirectory(const std::string &name)
316{
317 /* Ignore directory creation errors; they'll surface later on. */
318 std::error_code error_code;
319 std::filesystem::create_directories(OTTD2FS(name), error_code);
320}
321
327bool FioRemove(const std::string &filename)
328{
329 std::filesystem::path path = OTTD2FS(filename);
330 std::error_code error_code;
331 std::filesystem::remove(path, error_code);
332 if (error_code) {
333 Debug(misc, 0, "Removing {} failed: {}", filename, error_code.message());
334 return false;
335 }
336 return true;
337}
338
345void AppendPathSeparator(std::string &buf)
346{
347 if (buf.empty()) return;
348
349 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
350}
351
357static void SimplifyFileName(std::string &name)
358{
359 for (char &c : name) {
360 /* Force lowercase */
361 c = std::tolower(c);
362#if (PATHSEPCHAR != '/')
363 /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */
364 if (c == '/') c = PATHSEPCHAR;
365#endif
366 }
367}
368
375{
376 _tar_filelist[sd].clear();
377 _tar_list[sd].clear();
378 uint num = this->Scan(".tar", sd, false);
379 if (sd == BASESET_DIR || sd == NEWGRF_DIR) num += this->Scan(".tar", OLD_DATA_DIR, false);
380 return num;
381}
382
383/* static */ uint TarScanner::DoScan(TarScanner::Modes modes)
384{
385 Debug(misc, 2, "Scanning for tars");
386 TarScanner fs;
387 uint num = 0;
388 if (modes.Test(TarScanner::Mode::Baseset)) {
389 num += fs.DoScan(BASESET_DIR);
390 }
391 if (modes.Test(TarScanner::Mode::NewGRF)) {
392 num += fs.DoScan(NEWGRF_DIR);
393 }
394 if (modes.Test(TarScanner::Mode::AI)) {
395 num += fs.DoScan(AI_DIR);
396 num += fs.DoScan(AI_LIBRARY_DIR);
397 }
398 if (modes.Test(TarScanner::Mode::Game)) {
399 num += fs.DoScan(GAME_DIR);
400 num += fs.DoScan(GAME_LIBRARY_DIR);
401 }
402 if (modes.Test(TarScanner::Mode::Scenario)) {
403 num += fs.DoScan(SCENARIO_DIR);
404 num += fs.DoScan(HEIGHTMAP_DIR);
405 }
406 Debug(misc, 2, "Scan complete, found {} files", num);
407 return num;
408}
409
416bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
417{
418 this->subdir = sd;
419 return this->AddFile(filename, 0);
420}
421
432static std::string ExtractString(std::span<char> buffer)
433{
434 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
435}
436
437bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
438{
439 /* No tar within tar. */
440 assert(tar_filename.empty());
441
442 /* The TAR-header, repeated for every file */
443 struct TarHeader {
444 char name[100];
445 char mode[8];
446 char uid[8];
447 char gid[8];
448 char size[12];
449 char mtime[12];
450 char chksum[8];
451 char typeflag;
452 char linkname[100];
453 char magic[6];
454 char version[2];
455 char uname[32];
456 char gname[32];
457 char devmajor[8];
458 char devminor[8];
459 char prefix[155];
460
461 char unused[12];
462 };
463 static_assert(sizeof(TarHeader) == 512);
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().native());
480 SimplifyFileName(filename_base);
481
482 TarHeader th;
483 size_t num = 0, pos = 0;
484
485 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
486 size_t num_bytes_read = fread(&th, 1, sizeof(TarHeader), f);
487 if (num_bytes_read != sizeof(TarHeader)) break;
488 pos += num_bytes_read;
489
490 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
491 auto last_of_th = &th.unused[std::size(th.unused)];
492 if (std::string_view{th.magic, 5} != "ustar" && std::any_of(th.magic, last_of_th, [](auto c) { return c != 0; })) {
493 /* If we have only zeros in the block, it can be an end-of-file indicator */
494 if (std::all_of(th.name, last_of_th, [](auto c) { return c == 0; })) continue;
495
496 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
497 return false;
498 }
499
500 std::string name;
501
502 /* The prefix contains the directory-name */
503 if (th.prefix[0] != '\0') {
504 name = ExtractString(th.prefix);
505 name += PATHSEP;
506 }
507
508 /* Copy the name of the file in a safe way at the end of 'name' */
509 name += ExtractString(th.name);
510
511 /* The size of the file, for some strange reason, this is stored as a string in octals. */
512 std::string size = ExtractString(th.size);
513 size_t skip = 0;
514 if (!size.empty()) {
515 auto value = ParseInteger<size_t>(size, 8);
516 if (!value.has_value()) {
517 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
518 fclose(f);
519 return false;
520 }
521 skip = *value;
522 }
523
524 switch (th.typeflag) {
525 case '\0':
526 case '0': { // regular file
527 if (name.empty()) break;
528
529 /* Store this entry in the list */
530 TarFileListEntry entry;
531 entry.tar_filename = filename;
532 entry.size = skip;
533 entry.position = pos;
534
535 /* Convert to lowercase and our PATHSEPCHAR */
536 SimplifyFileName(name);
537
538 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
539 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
540
541 break;
542 }
543
544 case '1': // hard links
545 case '2': { // symbolic links
546 std::string link = ExtractString(th.linkname);
547
548 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
549 break;
550 }
551
552 case '5': // directory
553 /* Convert to lowercase and our PATHSEPCHAR */
554 SimplifyFileName(name);
555
556 /* Store the first directory name we detect */
557 Debug(misc, 6, "Found dir in tar: {}", name);
558 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = std::move(name);
559 break;
560
561 default:
562 /* Ignore other types */
563 break;
564 }
565
566 /* Skip to the next block.. */
567 skip = Align(skip, 512);
568 if (fseek(f, skip, SEEK_CUR) < 0) {
569 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
570 return false;
571 }
572 pos += skip;
573 }
574
575 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
576
577 return true;
578}
579
587bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
588{
589 TarList::iterator it = _tar_list[subdir].find(tar_filename);
590 /* We don't know the file. */
591 if (it == _tar_list[subdir].end()) return false;
592
593 const auto &dirname = (*it).second;
594
595 /* The file doesn't have a sub directory! */
596 if (dirname.empty()) {
597 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
598 return false;
599 }
600
601 std::string filename = tar_filename;
602 auto p = filename.find_last_of(PATHSEPCHAR);
603 /* The file's path does not have a separator? */
604 if (p == std::string::npos) return false;
605
606 filename.replace(p + 1, std::string::npos, dirname);
607 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, filename);
608 FioCreateDirectory(filename);
609
610 for (auto &it2 : _tar_filelist[subdir]) {
611 if (tar_filename != it2.second.tar_filename) continue;
612
613 /* it2.first is tarball + PATHSEPCHAR + name. */
614 std::string_view name = it2.first;
615 name.remove_prefix(name.find_first_of(PATHSEPCHAR) + 1);
616 filename.replace(p + 1, std::string::npos, name);
617
618 Debug(misc, 9, " extracting {}", filename);
619
620 /* First open the file in the .tar. */
621 size_t to_copy = 0;
622 auto in = FioFOpenFileTar(it2.second, &to_copy);
623 if (!in.has_value()) {
624 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
625 return false;
626 }
627
628 /* Now open the 'output' file. */
629 auto out = FileHandle::Open(filename, "wb");
630 if (!out.has_value()) {
631 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
632 return false;
633 }
634
635 /* Now read from the tar and write it into the file. */
636 char buffer[4096];
637 size_t read;
638 for (; to_copy != 0; to_copy -= read) {
639 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
640 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
641 }
642
643 if (to_copy != 0) {
644 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
645 return false;
646 }
647 }
648
649 Debug(misc, 9, " extraction successful");
650 return true;
651}
652
653#if defined(_WIN32)
659extern void DetermineBasePaths(std::string_view exe);
660
662char *getcwd(char *buf, size_t size);
663#else /* defined(_WIN32) */
664
672static bool ChangeWorkingDirectoryToExecutable(std::string_view 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 auto home_env = GetEnv("HOME"); // Stack var, shouldn't be freed
738 if (home_env.has_value()) 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(std::string_view exe)
751{
752 std::string tmp;
753 const std::string homedir = GetHomeDir();
754#ifdef USE_XDG
755 if (auto xdg_data_home = GetEnv("XDG_DATA_HOME"); xdg_data_home.has_value()) {
756 tmp = *xdg_data_home;
757 tmp += PATHSEP;
758 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
760 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
761
762 tmp += "content_download";
765 } else if (!homedir.empty()) {
766 tmp = homedir;
767 tmp += PATHSEP ".local" PATHSEP "share" PATHSEP;
768 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
770 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
771
772 tmp += "content_download";
775 } else {
776 _searchpaths[SP_PERSONAL_DIR_XDG].clear();
778 }
779#endif
780
781#if !defined(WITH_PERSONAL_DIR)
783#else
784 if (!homedir.empty()) {
785 tmp = std::move(homedir);
786 tmp += PATHSEP;
787 tmp += PERSONAL_DIR;
790
791 tmp += "content_download";
794 } else {
797 }
798#endif
799
800#if defined(WITH_SHARED_DIR)
801 tmp = SHARED_DIR;
804#else
806#endif
807
808 char cwd[MAX_PATH];
809 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
810
811 if (_config_file.empty()) {
812 /* Get the path to working directory of OpenTTD. */
813 tmp = cwd;
816
818 } else {
819 /* Use the folder of the config file as working directory. */
820 size_t end = _config_file.find_last_of(PATHSEPCHAR);
821 if (end == std::string::npos) {
822 /* _config_file is not in a folder, so use current directory. */
823 tmp = cwd;
824 } else {
825 tmp = FS2OTTD(std::filesystem::weakly_canonical(std::filesystem::path(OTTD2FS(_config_file))).parent_path().native());
826 }
829 }
830
831 /* Change the working directory to that one of the executable */
833 char buf[MAX_PATH];
834 if (getcwd(buf, lengthof(buf)) == nullptr) {
835 tmp.clear();
836 } else {
837 tmp = buf;
838 }
841 } else {
843 }
844
845 if (cwd[0] != '\0') {
846 /* Go back to the current working directory. */
847 if (chdir(cwd) != 0) {
848 Debug(misc, 0, "Failed to return to working directory!");
849 }
850 }
851
852#if !defined(GLOBAL_DATA_DIR)
854#else
855 tmp = GLOBAL_DATA_DIR;
857 _searchpaths[SP_INSTALLATION_DIR] = std::move(tmp);
858#endif
859#ifdef WITH_COCOA
860extern void CocoaSetApplicationBundleDir();
861 CocoaSetApplicationBundleDir();
862#else
864#endif
865}
866#endif /* defined(_WIN32) */
867
868std::string _personal_dir;
869
877void DeterminePaths(std::string_view exe, bool only_local_path)
878{
880 FillValidSearchPaths(only_local_path);
881
882#ifdef USE_XDG
883 std::string config_home;
884 std::string homedir = GetHomeDir();
885 if (auto xdg_config_home = GetEnv("XDG_CONFIG_HOME"); xdg_config_home.has_value()) {
886 config_home = *xdg_config_home;
887 config_home += PATHSEP;
888 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
889 } else if (!homedir.empty()) {
890 /* Defaults to ~/.config */
891 config_home = std::move(homedir);
892 config_home += PATHSEP ".config" PATHSEP;
893 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
894 }
895 AppendPathSeparator(config_home);
896#endif
897
898 for (Searchpath sp : _valid_searchpaths) {
899 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
900 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
901 }
902
903 std::string config_dir;
904 if (!_config_file.empty()) {
905 config_dir = _searchpaths[SP_WORKING_DIR];
906 } else {
907 std::string personal_dir = FioFindFullPath(BASE_DIR, "openttd.cfg");
908 if (!personal_dir.empty()) {
909 auto end = personal_dir.find_last_of(PATHSEPCHAR);
910 if (end != std::string::npos) personal_dir.erase(end + 1);
911 config_dir = std::move(personal_dir);
912 } else {
913#ifdef USE_XDG
914 /* No previous configuration file found. Use the configuration folder from XDG. */
915 config_dir = config_home;
916#else
917 static const Searchpath new_openttd_cfg_order[] = {
919 };
920
921 config_dir.clear();
922 for (const auto &searchpath : new_openttd_cfg_order) {
923 if (IsValidSearchPath(searchpath)) {
924 config_dir = _searchpaths[searchpath];
925 break;
926 }
927 }
928#endif
929 }
930 _config_file = config_dir + "openttd.cfg";
931 }
932
933 Debug(misc, 1, "{} found as config directory", config_dir);
934
935 _highscore_file = config_dir + "hs.dat";
936 extern std::string _hotkeys_file;
937 _hotkeys_file = config_dir + "hotkeys.cfg";
938 extern std::string _windows_file;
939 _windows_file = config_dir + "windows.cfg";
940 extern std::string _private_file;
941 _private_file = config_dir + "private.cfg";
942 extern std::string _secrets_file;
943 _secrets_file = config_dir + "secrets.cfg";
944 extern std::string _favs_file;
945 _favs_file = config_dir + "favs.cfg";
946
947#ifdef USE_XDG
948 if (config_dir == config_home) {
949 /* We are using the XDG configuration home for the config file,
950 * then store the rest in the XDG data home folder. */
951 _personal_dir = _searchpaths[SP_PERSONAL_DIR_XDG];
952 if (only_local_path) {
953 /* In case of XDG and we only want local paths and we detected that
954 * the user either manually indicated the XDG path or didn't use
955 * "-c" option, we change the working-dir to the XDG personal-dir,
956 * as this is most likely what the user is expecting. */
957 _searchpaths[SP_WORKING_DIR] = _searchpaths[SP_PERSONAL_DIR_XDG];
958 }
959 } else
960#endif
961 {
962 _personal_dir = config_dir;
963 }
964
965 /* Make the necessary folders */
966 FioCreateDirectory(config_dir);
967#if defined(WITH_PERSONAL_DIR)
969#endif
970
971 Debug(misc, 1, "{} found as personal directory", _personal_dir);
972
973 static const Subdirectory default_subdirs[] = {
975 };
976
977 for (const auto &default_subdir : default_subdirs) {
978 FioCreateDirectory(fmt::format("{}{}", _personal_dir, _subdirs[default_subdir]));
979 }
980
981 /* If we have network we make a directory for the autodownloading of content */
982 _searchpaths[SP_AUTODOWNLOAD_DIR] = _personal_dir + "content_download" PATHSEP;
983 Debug(misc, 3, "{} added as search path", _searchpaths[SP_AUTODOWNLOAD_DIR]);
985 FillValidSearchPaths(only_local_path);
986
987 /* Create the directory for each of the types of content */
989 for (const auto &subdir : subdirs) {
990 FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, subdir));
991 }
992
993 extern std::string _log_file;
994 _log_file = _personal_dir + "openttd.log";
995}
996
1001void SanitizeFilename(std::string &filename)
1002{
1003 for (auto &c : filename) {
1004 switch (c) {
1005 /* The following characters are not allowed in filenames
1006 * on at least one of the supported operating systems: */
1007 case ':': case '\\': case '*': case '?': case '/':
1008 case '<': case '>': case '|': case '"':
1009 c = '_';
1010 break;
1011 }
1012 }
1013}
1014
1023std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1024{
1025 auto in = FileHandle::Open(filename, "rb");
1026 if (!in.has_value()) return nullptr;
1027
1028 fseek(*in, 0, SEEK_END);
1029 size_t len = ftell(*in);
1030 fseek(*in, 0, SEEK_SET);
1031 if (len > maxsize) return nullptr;
1032
1033 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1034
1035 mem.get()[len] = 0;
1036 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1037
1038 lenp = len;
1039 return mem;
1040}
1041
1048static bool MatchesExtension(std::string_view extension, const std::string &filename)
1049{
1050 if (extension.empty()) return true;
1051 if (filename.length() < extension.length()) return false;
1052
1053 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1054 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1055}
1056
1066static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1067{
1068 uint num = 0;
1069
1070 std::error_code error_code;
1071 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1072 if (dir_entry.is_directory()) {
1073 if (!recursive) continue;
1074 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1075 } else if (dir_entry.is_regular_file()) {
1076 std::string file = FS2OTTD(dir_entry.path().native());
1077 if (!MatchesExtension(extension, file)) continue;
1078 if (fs->AddFile(file, basepath_length, {})) num++;
1079 }
1080 }
1081 if (error_code) {
1082 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1083 }
1084
1085 return num;
1086}
1087
1094static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1095{
1096 uint num = 0;
1097
1098 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1099
1100 return num;
1101}
1102
1112uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1113{
1114 this->subdir = sd;
1115
1116 uint num = 0;
1117
1118 for (Searchpath sp : _valid_searchpaths) {
1119 /* Don't search in the working directory */
1120 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
1121
1122 std::string path = FioGetDirectory(sp, sd);
1123 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1124 }
1125
1126 if (tars && sd != NO_DIRECTORY) {
1127 for (const auto &tar : _tar_filelist[sd]) {
1128 num += ScanTar(this, extension, tar);
1129 }
1130 }
1131
1132 switch (sd) {
1133 case BASESET_DIR:
1134 num += this->Scan(extension, OLD_GM_DIR, tars, recursive);
1135 [[fallthrough]];
1136 case NEWGRF_DIR:
1137 num += this->Scan(extension, OLD_DATA_DIR, tars, recursive);
1138 break;
1139
1140 default: break;
1141 }
1142
1143 return num;
1144}
1145
1154uint FileScanner::Scan(std::string_view extension, const std::string &directory, bool recursive)
1155{
1156 std::string path(directory);
1157 AppendPathSeparator(path);
1158 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1159}
1160
1168std::optional<FileHandle> FileHandle::Open(const std::string &filename, std::string_view mode)
1169{
1170#if defined(_WIN32)
1171 /* Windows also requires mode to be wchar_t. */
1172 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1173#else
1174 auto f = fopen(filename.c_str(), std::string{mode}.c_str());
1175#endif /* _WIN32 */
1176
1177 if (f == nullptr) return std::nullopt;
1178 return FileHandle(f);
1179}
constexpr bool Test(Tvalue_type value) const
Test if the value-th bit is set.
Enum-as-bit-set wrapper.
static std::optional< FileHandle > Open(const std::string &filename, std::string_view mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1168
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:1112
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
@ AI
Scan for AIs and its libraries.
@ Scenario
Scan for scenarios and heightmaps.
@ Game
Scan for game scripts.
@ Baseset
Scan for base sets.
@ NewGRF
Scan for non-base sets.
uint DoScan(Subdirectory sd)
Perform the scanning of a particular subdirectory.
Definition fileio.cpp:374
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:437
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
static bool ChangeWorkingDirectoryToExecutable(std::string_view exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:672
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:587
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:877
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:327
void SanitizeFilename(std::string &filename)
Sanitizes a filename, i.e.
Definition fileio.cpp:1001
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:1066
void DetermineBasePaths(std::string_view exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:750
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:345
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:242
std::string _personal_dir
custom directory for personal settings, saves, newgrf, etc.
Definition fileio.cpp:868
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:730
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp:357
static bool MatchesExtension(std::string_view extension, const std::string &filename)
Helper to see whether a given filename matches the extension.
Definition fileio.cpp:1048
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:315
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:132
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:1094
std::array< std::string, NUM_SEARCHPATHS > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:65
bool FioCheckFileExists(std::string_view filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:121
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:432
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:708
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:144
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1023
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:345
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.
Definition fileio_type.h:87
@ OLD_DATA_DIR
Old subdirectory for the data.
Definition fileio_type.h:94
@ NO_DIRECTORY
A path without any base directory.
@ AI_LIBRARY_DIR
Subdirectory for all AI libraries.
Definition fileio_type.h:99
@ 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.
Definition fileio_type.h:98
@ OLD_GM_DIR
Old subdirectory for the music.
Definition fileio_type.h:93
@ SCENARIO_DIR
Base directory for all scenarios.
Definition fileio_type.h:91
@ BASE_DIR
Base directory for all subdirectories.
Definition fileio_type.h:88
@ SAVE_DIR
Base directory for all savegames.
Definition fileio_type.h:89
@ NUM_SUBDIRS
Number of subdirectories.
@ HEIGHTMAP_DIR
Subdirectory of scenario for heightmaps.
Definition fileio_type.h:92
@ NEWGRF_DIR
Subdirectory for all NewGRFs.
Definition fileio_type.h:96
@ AUTOSAVE_DIR
Subdirectory of save for autosaves.
Definition fileio_type.h:90
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game)
Definition fileio_type.h:95
@ 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: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:860
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:321
static void StrMakeValid(Builder &builder, StringConsumer &consumer, StringValidationSettings settings)
Copies the valid (UTF-8) characters from consumer to the builder.
Definition string.cpp:117
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:308
Parse strings.
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:354
std::string FS2OTTD(std::wstring_view 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:104