OpenTTD Source 20250205-master-gfd85ab1e2c
fileio.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
7
10#include "stdafx.h"
11#include "fileio_func.h"
13#include "debug.h"
14#include "fios.h"
15#include "string_func.h"
16#include "tar_type.h"
17#ifdef _WIN32
18#include <windows.h>
19#elif defined(__HAIKU__)
20#include <Path.h>
21#include <storage/FindDirectory.h>
22#else
23#include <unistd.h>
24#include <pwd.h>
25#endif
26#include <charconv>
27#include <sys/stat.h>
28#include <sstream>
29#include <filesystem>
30
31#include "safeguards.h"
32
34static bool _do_scan_working_directory = true;
35
36extern std::string _config_file;
37extern std::string _highscore_file;
38
39static const char * const _subdirs[] = {
40 "",
41 "save" PATHSEP,
42 "save" PATHSEP "autosave" PATHSEP,
43 "scenario" PATHSEP,
44 "scenario" PATHSEP "heightmap" PATHSEP,
45 "gm" PATHSEP,
46 "data" PATHSEP,
47 "baseset" PATHSEP,
48 "newgrf" PATHSEP,
49 "lang" PATHSEP,
50 "ai" PATHSEP,
51 "ai" PATHSEP "library" PATHSEP,
52 "game" PATHSEP,
53 "game" PATHSEP "library" PATHSEP,
54 "screenshot" PATHSEP,
55 "social_integration" PATHSEP,
56 "docs" PATHSEP,
57};
58static_assert(lengthof(_subdirs) == NUM_SUBDIRS);
59
66std::array<std::string, NUM_SEARCHPATHS> _searchpaths;
67std::vector<Searchpath> _valid_searchpaths;
68std::array<TarList, NUM_SUBDIRS> _tar_list;
69TarFileList _tar_filelist[NUM_SUBDIRS];
70
77{
78 return sp < _searchpaths.size() && !_searchpaths[sp].empty();
79}
80
81static void FillValidSearchPaths(bool only_local_path)
82{
83 _valid_searchpaths.clear();
84
85 std::set<std::string> seen{};
86 for (Searchpath sp = SP_FIRST_DIR; sp < NUM_SEARCHPATHS; sp++) {
87 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
88
89 if (only_local_path) {
90 switch (sp) {
91 case SP_WORKING_DIR: // Can be influence by "-c" option.
92 case SP_BINARY_DIR: // Most likely contains all the language files.
93 case SP_AUTODOWNLOAD_DIR: // Otherwise we cannot download in-game content.
94 break;
95
96 default:
97 continue;
98 }
99 }
100
101 if (IsValidSearchPath(sp)) {
102 if (seen.count(_searchpaths[sp]) != 0) continue;
103 seen.insert(_searchpaths[sp]);
104 _valid_searchpaths.emplace_back(sp);
105 }
106 }
107
108 /* The working-directory is special, as it is controlled by _do_scan_working_directory.
109 * Only add the search path if it isn't already in the set. To preserve the same order
110 * as the enum, insert it in the front. */
112 _valid_searchpaths.insert(_valid_searchpaths.begin(), SP_WORKING_DIR);
113 }
114}
115
122bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
123{
124 auto f = FioFOpenFile(filename, "rb", subdir);
125 return f.has_value();
126}
127
133bool FileExists(const std::string &filename)
134{
135 std::error_code ec;
136 return std::filesystem::exists(OTTD2FS(filename), ec);
137}
138
145std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
146{
147 assert(subdir < NUM_SUBDIRS);
148
149 for (Searchpath sp : _valid_searchpaths) {
150 std::string buf = FioGetDirectory(sp, subdir);
151 buf += filename;
152 if (FileExists(buf)) return buf;
153#if !defined(_WIN32)
154 /* Be, as opening files, aware that sometimes the filename
155 * might be in uppercase when it is in lowercase on the
156 * disk. Of course Windows doesn't care about casing. */
157 if (strtolower(buf, _searchpaths[sp].size() - 1) && FileExists(buf)) return buf;
158#endif
159 }
160
161 return {};
162}
163
164std::string FioGetDirectory(Searchpath sp, Subdirectory subdir)
165{
166 assert(subdir < NUM_SUBDIRS);
167 assert(sp < NUM_SEARCHPATHS);
168
169 return _searchpaths[sp] + _subdirs[subdir];
170}
171
172std::string FioFindDirectory(Subdirectory subdir)
173{
174 /* Find and return the first valid directory */
175 for (Searchpath sp : _valid_searchpaths) {
176 std::string ret = FioGetDirectory(sp, subdir);
177 if (FileExists(ret)) return ret;
178 }
179
180 /* Could not find the directory, fall back to a base path */
181 return _personal_dir;
182}
183
184static std::optional<FileHandle> FioFOpenFileSp(const std::string &filename, const char *mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
185{
186#if defined(_WIN32)
187 /* fopen is implemented as a define with ellipses for
188 * Unicode support (prepend an L). As we are not sending
189 * a string, but a variable, it 'renames' the variable,
190 * so make that variable to makes it compile happily */
191 wchar_t Lmode[5];
192 MultiByteToWideChar(CP_ACP, 0, mode, -1, Lmode, static_cast<int>(std::size(Lmode)));
193#endif
194 std::string buf;
195
196 if (subdir == NO_DIRECTORY) {
197 buf = filename;
198 } else {
199 buf = _searchpaths[sp] + _subdirs[subdir] + filename;
200 }
201
202 auto f = FileHandle::Open(buf, mode);
203#if !defined(_WIN32)
204 if (!f.has_value() && strtolower(buf, subdir == NO_DIRECTORY ? 0 : _searchpaths[sp].size() - 1) ) {
205 f = FileHandle::Open(buf, mode);
206 }
207#endif
208 if (f.has_value() && filesize != nullptr) {
209 /* Find the size of the file */
210 fseek(*f, 0, SEEK_END);
211 *filesize = ftell(*f);
212 fseek(*f, 0, SEEK_SET);
213 }
214 return f;
215}
216
224static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
225{
226 auto f = FileHandle::Open(entry.tar_filename, "rb");
227 if (!f.has_value()) return std::nullopt;
228
229 if (fseek(*f, entry.position, SEEK_SET) < 0) {
230 return std::nullopt;
231 }
232
233 if (filesize != nullptr) *filesize = entry.size;
234 return f;
235}
236
243std::optional<FileHandle> FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
244{
245 std::optional<FileHandle> f = std::nullopt;
246 assert(subdir < NUM_SUBDIRS || subdir == NO_DIRECTORY);
247
248 for (Searchpath sp : _valid_searchpaths) {
249 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
250 if (f.has_value() || subdir == NO_DIRECTORY) break;
251 }
252
253 /* We can only use .tar in case of data-dir, and read-mode */
254 if (!f.has_value() && mode[0] == 'r' && subdir != NO_DIRECTORY) {
255 /* Filenames in tars are always forced to be lowercase */
256 std::string resolved_name = filename;
257 strtolower(resolved_name);
258
259 /* Resolve ".." */
260 std::istringstream ss(resolved_name);
261 std::vector<std::string> tokens;
262 std::string token;
263 while (std::getline(ss, token, PATHSEPCHAR)) {
264 if (token == "..") {
265 if (tokens.size() < 2) return std::nullopt;
266 tokens.pop_back();
267 } else if (token == ".") {
268 /* Do nothing. "." means current folder, but you can create tar files with "." in the path.
269 * This confuses our file resolver. So, act like this folder doesn't exist. */
270 } else {
271 tokens.push_back(token);
272 }
273 }
274
275 resolved_name.clear();
276 bool first = true;
277 for (const std::string &token : tokens) {
278 if (!first) {
279 resolved_name += PATHSEP;
280 }
281 resolved_name += token;
282 first = false;
283 }
284
285 TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name);
286 if (it != _tar_filelist[subdir].end()) {
287 f = FioFOpenFileTar(it->second, filesize);
288 }
289 }
290
291 /* Sometimes a full path is given. To support
292 * the 'subdirectory' must be 'removed'. */
293 if (!f.has_value() && subdir != NO_DIRECTORY) {
294 switch (subdir) {
295 case BASESET_DIR:
296 f = FioFOpenFile(filename, mode, OLD_GM_DIR, filesize);
297 if (f.has_value()) break;
298 [[fallthrough]];
299 case NEWGRF_DIR:
300 f = FioFOpenFile(filename, mode, OLD_DATA_DIR, filesize);
301 break;
302
303 default:
304 f = FioFOpenFile(filename, mode, NO_DIRECTORY, filesize);
305 break;
306 }
307 }
308
309 return f;
310}
311
317void FioCreateDirectory(const std::string &name)
318{
319 /* Ignore directory creation errors; they'll surface later on. */
320 std::error_code error_code;
321 std::filesystem::create_directories(OTTD2FS(name), error_code);
322}
323
329bool FioRemove(const std::string &filename)
330{
331 std::filesystem::path path = OTTD2FS(filename);
332 std::error_code error_code;
333 std::filesystem::remove(path, error_code);
334 if (error_code) {
335 Debug(misc, 0, "Removing {} failed: {}", filename, error_code.message());
336 return false;
337 }
338 return true;
339}
340
347void AppendPathSeparator(std::string &buf)
348{
349 if (buf.empty()) return;
350
351 if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR);
352}
353
359static void SimplifyFileName(std::string &name)
360{
361 for (char &c : name) {
362 /* Force lowercase */
363 c = std::tolower(c);
364#if (PATHSEPCHAR != '/')
365 /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */
366 if (c == '/') c = PATHSEPCHAR;
367#endif
368 }
369}
370
377{
378 _tar_filelist[sd].clear();
379 _tar_list[sd].clear();
380 uint num = this->Scan(".tar", sd, false);
381 if (sd == BASESET_DIR || sd == NEWGRF_DIR) num += this->Scan(".tar", OLD_DATA_DIR, false);
382 return num;
383}
384
386{
387 Debug(misc, 2, "Scanning for tars");
388 TarScanner fs;
389 uint num = 0;
390 if (mode & TarScanner::BASESET) {
391 num += fs.DoScan(BASESET_DIR);
392 }
393 if (mode & TarScanner::NEWGRF) {
394 num += fs.DoScan(NEWGRF_DIR);
395 }
396 if (mode & TarScanner::AI) {
397 num += fs.DoScan(AI_DIR);
398 num += fs.DoScan(AI_LIBRARY_DIR);
399 }
400 if (mode & TarScanner::GAME) {
401 num += fs.DoScan(GAME_DIR);
402 num += fs.DoScan(GAME_LIBRARY_DIR);
403 }
404 if (mode & TarScanner::SCENARIO) {
405 num += fs.DoScan(SCENARIO_DIR);
406 num += fs.DoScan(HEIGHTMAP_DIR);
407 }
408 Debug(misc, 2, "Scan complete, found {} files", num);
409 return num;
410}
411
418bool TarScanner::AddFile(Subdirectory sd, const std::string &filename)
419{
420 this->subdir = sd;
421 return this->AddFile(filename, 0);
422}
423
434static std::string ExtractString(std::span<char> buffer)
435{
436 return StrMakeValid(std::string_view(buffer.begin(), buffer.end()));
437}
438
439bool TarScanner::AddFile(const std::string &filename, size_t, [[maybe_unused]] const std::string &tar_filename)
440{
441 /* No tar within tar. */
442 assert(tar_filename.empty());
443
444 /* The TAR-header, repeated for every file */
445 struct TarHeader {
446 char name[100];
447 char mode[8];
448 char uid[8];
449 char gid[8];
450 char size[12];
451 char mtime[12];
452 char chksum[8];
453 char typeflag;
454 char linkname[100];
455 char magic[6];
456 char version[2];
457 char uname[32];
458 char gname[32];
459 char devmajor[8];
460 char devminor[8];
461 char prefix[155];
462
463 char unused[12];
464 };
465
466 /* Check if we already seen this file */
467 TarList::iterator it = _tar_list[this->subdir].find(filename);
468 if (it != _tar_list[this->subdir].end()) return false;
469
470 auto of = FileHandle::Open(filename, "rb");
471 /* Although the file has been found there can be
472 * a number of reasons we cannot open the file.
473 * Most common case is when we simply have not
474 * been given read access. */
475 if (!of.has_value()) return false;
476 auto &f = *of;
477
478 _tar_list[this->subdir][filename] = std::string{};
479
480 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename());
481 SimplifyFileName(filename_base);
482
483 TarHeader th;
484 size_t num = 0, pos = 0;
485
486 /* Make a char of 512 empty bytes */
487 char empty[512];
488 memset(&empty[0], 0, sizeof(empty));
489
490 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
491 size_t num_bytes_read = fread(&th, 1, 512, f);
492 if (num_bytes_read != 512) break;
493 pos += num_bytes_read;
494
495 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
496 if (strncmp(th.magic, "ustar", 5) != 0 && memcmp(&th.magic, &empty[0], 512 - offsetof(TarHeader, magic)) != 0) {
497 /* If we have only zeros in the block, it can be an end-of-file indicator */
498 if (memcmp(&th, &empty[0], 512) == 0) continue;
499
500 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
501 return false;
502 }
503
504 std::string name;
505
506 /* The prefix contains the directory-name */
507 if (th.prefix[0] != '\0') {
508 name = ExtractString(th.prefix);
509 name += PATHSEP;
510 }
511
512 /* Copy the name of the file in a safe way at the end of 'name' */
513 name += ExtractString(th.name);
514
515 /* The size of the file, for some strange reason, this is stored as a string in octals. */
516 std::string size = ExtractString(th.size);
517 size_t skip = 0;
518 if (!size.empty()) {
519 StrTrimInPlace(size);
520 auto [_, err] = std::from_chars(size.data(), size.data() + size.size(), skip, 8);
521 if (err != std::errc()) {
522 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
523 fclose(f);
524 return false;
525 }
526 }
527
528 switch (th.typeflag) {
529 case '\0':
530 case '0': { // regular file
531 if (name.empty()) break;
532
533 /* Store this entry in the list */
534 TarFileListEntry entry;
535 entry.tar_filename = filename;
536 entry.size = skip;
537 entry.position = pos;
538
539 /* Convert to lowercase and our PATHSEPCHAR */
540 SimplifyFileName(name);
541
542 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
543 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
544
545 break;
546 }
547
548 case '1': // hard links
549 case '2': { // symbolic links
550 std::string link = ExtractString(th.linkname);
551
552 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
553 break;
554 }
555
556 case '5': // directory
557 /* Convert to lowercase and our PATHSEPCHAR */
558 SimplifyFileName(name);
559
560 /* Store the first directory name we detect */
561 Debug(misc, 6, "Found dir in tar: {}", name);
562 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = name;
563 break;
564
565 default:
566 /* Ignore other types */
567 break;
568 }
569
570 /* Skip to the next block.. */
571 skip = Align(skip, 512);
572 if (fseek(f, skip, SEEK_CUR) < 0) {
573 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
574 return false;
575 }
576 pos += skip;
577 }
578
579 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
580
581 return true;
582}
583
591bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
592{
593 TarList::iterator it = _tar_list[subdir].find(tar_filename);
594 /* We don't know the file. */
595 if (it == _tar_list[subdir].end()) return false;
596
597 const auto &dirname = (*it).second;
598
599 /* The file doesn't have a sub directory! */
600 if (dirname.empty()) {
601 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
602 return false;
603 }
604
605 std::string filename = tar_filename;
606 auto p = filename.find_last_of(PATHSEPCHAR);
607 /* The file's path does not have a separator? */
608 if (p == std::string::npos) return false;
609
610 filename.replace(p + 1, std::string::npos, dirname);
611 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, filename);
612 FioCreateDirectory(filename);
613
614 for (auto &it2 : _tar_filelist[subdir]) {
615 if (tar_filename != it2.second.tar_filename) continue;
616
617 /* it2.first is tarball + PATHSEPCHAR + name. */
618 std::string_view name = it2.first;
619 name.remove_prefix(name.find_first_of(PATHSEPCHAR) + 1);
620 filename.replace(p + 1, std::string::npos, name);
621
622 Debug(misc, 9, " extracting {}", filename);
623
624 /* First open the file in the .tar. */
625 size_t to_copy = 0;
626 auto in = FioFOpenFileTar(it2.second, &to_copy);
627 if (!in.has_value()) {
628 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
629 return false;
630 }
631
632 /* Now open the 'output' file. */
633 auto out = FileHandle::Open(filename, "wb");
634 if (!out.has_value()) {
635 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
636 return false;
637 }
638
639 /* Now read from the tar and write it into the file. */
640 char buffer[4096];
641 size_t read;
642 for (; to_copy != 0; to_copy -= read) {
643 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
644 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
645 }
646
647 if (to_copy != 0) {
648 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
649 return false;
650 }
651 }
652
653 Debug(misc, 9, " extraction successful");
654 return true;
655}
656
657#if defined(_WIN32)
663extern void DetermineBasePaths(const char *exe);
664#else /* defined(_WIN32) */
665
673static bool ChangeWorkingDirectoryToExecutable(const char *exe)
674{
675 std::string path = exe;
676
677#ifdef WITH_COCOA
678 for (size_t pos = path.find_first_of('.'); pos != std::string::npos; pos = path.find_first_of('.', pos + 1)) {
679 if (StrEqualsIgnoreCase(path.substr(pos, 4), ".app")) {
680 path.erase(pos);
681 break;
682 }
683 }
684#endif /* WITH_COCOA */
685
686 size_t pos = path.find_last_of(PATHSEPCHAR);
687 if (pos == std::string::npos) return false;
688
689 path.erase(pos);
690
691 if (chdir(path.c_str()) != 0) {
692 Debug(misc, 0, "Directory with the binary does not exist?");
693 return false;
694 }
695
696 return true;
697}
698
710{
711 /* No working directory, so nothing to do. */
712 if (_searchpaths[SP_WORKING_DIR].empty()) return false;
713
714 /* Working directory is root, so do nothing. */
715 if (_searchpaths[SP_WORKING_DIR] == PATHSEP) return false;
716
717 /* No personal/home directory, so the working directory won't be that. */
718 if (_searchpaths[SP_PERSONAL_DIR].empty()) return true;
719
720 std::string tmp = _searchpaths[SP_WORKING_DIR] + PERSONAL_DIR;
722
723 return _searchpaths[SP_PERSONAL_DIR] != tmp;
724}
725
731static std::string GetHomeDir()
732{
733#ifdef __HAIKU__
734 BPath path;
735 find_directory(B_USER_SETTINGS_DIRECTORY, &path);
736 return std::string(path.Path());
737#else
738 const char *home_env = std::getenv("HOME"); // Stack var, shouldn't be freed
739 if (home_env != nullptr) return std::string(home_env);
740
741 const struct passwd *pw = getpwuid(getuid());
742 if (pw != nullptr) return std::string(pw->pw_dir);
743#endif
744 return {};
745}
746
751void DetermineBasePaths(const char *exe)
752{
753 std::string tmp;
754 const std::string homedir = GetHomeDir();
755#ifdef USE_XDG
756 const char *xdg_data_home = std::getenv("XDG_DATA_HOME");
757 if (xdg_data_home != nullptr) {
758 tmp = xdg_data_home;
759 tmp += PATHSEP;
760 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
762 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
763
764 tmp += "content_download";
767 } else if (!homedir.empty()) {
768 tmp = homedir;
769 tmp += PATHSEP ".local" PATHSEP "share" PATHSEP;
770 tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
772 _searchpaths[SP_PERSONAL_DIR_XDG] = tmp;
773
774 tmp += "content_download";
777 } else {
778 _searchpaths[SP_PERSONAL_DIR_XDG].clear();
780 }
781#endif
782
783#if !defined(WITH_PERSONAL_DIR)
785#else
786 if (!homedir.empty()) {
787 tmp = homedir;
788 tmp += PATHSEP;
789 tmp += PERSONAL_DIR;
792
793 tmp += "content_download";
796 } else {
799 }
800#endif
801
802#if defined(WITH_SHARED_DIR)
803 tmp = SHARED_DIR;
806#else
808#endif
809
810 char cwd[MAX_PATH];
811 if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0';
812
813 if (_config_file.empty()) {
814 /* Get the path to working directory of OpenTTD. */
815 tmp = cwd;
818
820 } else {
821 /* Use the folder of the config file as working directory. */
822 size_t end = _config_file.find_last_of(PATHSEPCHAR);
823 if (end == std::string::npos) {
824 /* _config_file is not in a folder, so use current directory. */
825 tmp = cwd;
826 } else {
827 tmp = FS2OTTD(std::filesystem::weakly_canonical(std::filesystem::path(OTTD2FS(_config_file))).parent_path());
828 }
831 }
832
833 /* Change the working directory to that one of the executable */
835 char buf[MAX_PATH];
836 if (getcwd(buf, lengthof(buf)) == nullptr) {
837 tmp.clear();
838 } else {
839 tmp = buf;
840 }
843 } else {
845 }
846
847 if (cwd[0] != '\0') {
848 /* Go back to the current working directory. */
849 if (chdir(cwd) != 0) {
850 Debug(misc, 0, "Failed to return to working directory!");
851 }
852 }
853
854#if !defined(GLOBAL_DATA_DIR)
856#else
857 tmp = GLOBAL_DATA_DIR;
860#endif
861#ifdef WITH_COCOA
862extern void CocoaSetApplicationBundleDir();
863 CocoaSetApplicationBundleDir();
864#else
866#endif
867}
868#endif /* defined(_WIN32) */
869
870std::string _personal_dir;
871
879void DeterminePaths(const char *exe, bool only_local_path)
880{
882 FillValidSearchPaths(only_local_path);
883
884#ifdef USE_XDG
885 std::string config_home;
886 const std::string homedir = GetHomeDir();
887 const char *xdg_config_home = std::getenv("XDG_CONFIG_HOME");
888 if (xdg_config_home != nullptr) {
889 config_home = xdg_config_home;
890 config_home += PATHSEP;
891 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
892 } else if (!homedir.empty()) {
893 /* Defaults to ~/.config */
894 config_home = homedir;
895 config_home += PATHSEP ".config" PATHSEP;
896 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
897 }
898 AppendPathSeparator(config_home);
899#endif
900
901 for (Searchpath sp : _valid_searchpaths) {
902 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
903 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
904 }
905
906 std::string config_dir;
907 if (!_config_file.empty()) {
908 config_dir = _searchpaths[SP_WORKING_DIR];
909 } else {
910 std::string personal_dir = FioFindFullPath(BASE_DIR, "openttd.cfg");
911 if (!personal_dir.empty()) {
912 auto end = personal_dir.find_last_of(PATHSEPCHAR);
913 if (end != std::string::npos) personal_dir.erase(end + 1);
914 config_dir = personal_dir;
915 } else {
916#ifdef USE_XDG
917 /* No previous configuration file found. Use the configuration folder from XDG. */
918 config_dir = config_home;
919#else
920 static const Searchpath new_openttd_cfg_order[] = {
922 };
923
924 config_dir.clear();
925 for (const auto &searchpath : new_openttd_cfg_order) {
926 if (IsValidSearchPath(searchpath)) {
927 config_dir = _searchpaths[searchpath];
928 break;
929 }
930 }
931#endif
932 }
933 _config_file = config_dir + "openttd.cfg";
934 }
935
936 Debug(misc, 1, "{} found as config directory", config_dir);
937
938 _highscore_file = config_dir + "hs.dat";
939 extern std::string _hotkeys_file;
940 _hotkeys_file = config_dir + "hotkeys.cfg";
941 extern std::string _windows_file;
942 _windows_file = config_dir + "windows.cfg";
943 extern std::string _private_file;
944 _private_file = config_dir + "private.cfg";
945 extern std::string _secrets_file;
946 _secrets_file = config_dir + "secrets.cfg";
947 extern std::string _favs_file;
948 _favs_file = config_dir + "favs.cfg";
949
950#ifdef USE_XDG
951 if (config_dir == config_home) {
952 /* We are using the XDG configuration home for the config file,
953 * then store the rest in the XDG data home folder. */
954 _personal_dir = _searchpaths[SP_PERSONAL_DIR_XDG];
955 if (only_local_path) {
956 /* In case of XDG and we only want local paths and we detected that
957 * the user either manually indicated the XDG path or didn't use
958 * "-c" option, we change the working-dir to the XDG personal-dir,
959 * as this is most likely what the user is expecting. */
960 _searchpaths[SP_WORKING_DIR] = _searchpaths[SP_PERSONAL_DIR_XDG];
961 }
962 } else
963#endif
964 {
965 _personal_dir = config_dir;
966 }
967
968 /* Make the necessary folders */
969 FioCreateDirectory(config_dir);
970#if defined(WITH_PERSONAL_DIR)
972#endif
973
974 Debug(misc, 1, "{} found as personal directory", _personal_dir);
975
976 static const Subdirectory default_subdirs[] = {
978 };
979
980 for (const auto &default_subdir : default_subdirs) {
981 FioCreateDirectory(_personal_dir + _subdirs[default_subdir]);
982 }
983
984 /* If we have network we make a directory for the autodownloading of content */
985 _searchpaths[SP_AUTODOWNLOAD_DIR] = _personal_dir + "content_download" PATHSEP;
986 Debug(misc, 3, "{} added as search path", _searchpaths[SP_AUTODOWNLOAD_DIR]);
988 FillValidSearchPaths(only_local_path);
989
990 /* Create the directory for each of the types of content */
992 for (const auto &subdir : subdirs) {
993 FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, subdir));
994 }
995
996 extern std::string _log_file;
997 _log_file = _personal_dir + "openttd.log";
998}
999
1004void SanitizeFilename(std::string &filename)
1005{
1006 for (auto &c : filename) {
1007 switch (c) {
1008 /* The following characters are not allowed in filenames
1009 * on at least one of the supported operating systems: */
1010 case ':': case '\\': case '*': case '?': case '/':
1011 case '<': case '>': case '|': case '"':
1012 c = '_';
1013 break;
1014 }
1015 }
1016}
1017
1026std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1027{
1028 auto in = FileHandle::Open(filename, "rb");
1029 if (!in.has_value()) return nullptr;
1030
1031 fseek(*in, 0, SEEK_END);
1032 size_t len = ftell(*in);
1033 fseek(*in, 0, SEEK_SET);
1034 if (len > maxsize) return nullptr;
1035
1036 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1037
1038 mem.get()[len] = 0;
1039 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1040
1041 lenp = len;
1042 return mem;
1043}
1044
1051static bool MatchesExtension(std::string_view extension, const std::string &filename)
1052{
1053 if (extension.empty()) return true;
1054 if (filename.length() < extension.length()) return false;
1055
1056 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1057 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1058}
1059
1069static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1070{
1071 uint num = 0;
1072
1073 std::error_code error_code;
1074 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1075 if (dir_entry.is_directory()) {
1076 if (!recursive) continue;
1077 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1078 } else if (dir_entry.is_regular_file()) {
1079 std::string file = FS2OTTD(dir_entry.path());
1080 if (!MatchesExtension(extension, file)) continue;
1081 if (fs->AddFile(file, basepath_length, {})) num++;
1082 }
1083 }
1084 if (error_code) {
1085 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1086 }
1087
1088 return num;
1089}
1090
1097static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1098{
1099 uint num = 0;
1100
1101 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1102
1103 return num;
1104}
1105
1115uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1116{
1117 this->subdir = sd;
1118
1119 uint num = 0;
1120
1121 for (Searchpath sp : _valid_searchpaths) {
1122 /* Don't search in the working directory */
1123 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
1124
1125 std::string path = FioGetDirectory(sp, sd);
1126 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1127 }
1128
1129 if (tars && sd != NO_DIRECTORY) {
1130 for (const auto &tar : _tar_filelist[sd]) {
1131 num += ScanTar(this, extension, tar);
1132 }
1133 }
1134
1135 switch (sd) {
1136 case BASESET_DIR:
1137 num += this->Scan(extension, OLD_GM_DIR, tars, recursive);
1138 [[fallthrough]];
1139 case NEWGRF_DIR:
1140 num += this->Scan(extension, OLD_DATA_DIR, tars, recursive);
1141 break;
1142
1143 default: break;
1144 }
1145
1146 return num;
1147}
1148
1157uint FileScanner::Scan(const std::string_view extension, const std::string &directory, bool recursive)
1158{
1159 std::string path(directory);
1160 AppendPathSeparator(path);
1161 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1162}
1163
1171std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
1172{
1173#if defined(_WIN32)
1174 /* Windows also requires mode to be wchar_t. */
1175 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1176#else
1177 auto f = fopen(filename.c_str(), mode.c_str());
1178#endif /* _WIN32 */
1179
1180 if (f == nullptr) return std::nullopt;
1181 return FileHandle(f);
1182}
static std::optional< FileHandle > Open(const std::string &filename, const std::string &mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1171
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:1115
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
Mode
The mode of tar scanning.
Definition fileio_func.h:63
@ NEWGRF
Scan for non-base sets.
Definition fileio_func.h:66
@ BASESET
Scan for base sets.
Definition fileio_func.h:65
@ GAME
Scan for game scripts.
Definition fileio_func.h:69
@ SCENARIO
Scan for scenarios and heightmaps.
Definition fileio_func.h:68
@ AI
Scan for AIs and its libraries.
Definition fileio_func.h:67
uint DoScan(Subdirectory sd)
Perform the scanning of a particular subdirectory.
Definition fileio.cpp:376
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:439
Functions related to debugging.
#define Debug(category, level, format_string,...)
Ouptut a line of debugging information.
Definition debug.h:37
std::string _log_file
Filename to reroute output of a forked OpenTTD to.
Definition dedicated.cpp:14
std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:145
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:591
void DeterminePaths(const char *exe, bool only_local_path)
Acquire the base paths (personal dir and game data dir), fill all other paths (save dir,...
Definition fileio.cpp:879
static bool IsValidSearchPath(Searchpath sp)
Checks whether the given search path is a valid search path.
Definition fileio.cpp:76
bool FioRemove(const std::string &filename)
Remove a file.
Definition fileio.cpp:329
void SanitizeFilename(std::string &filename)
Sanitizes a filename, i.e.
Definition fileio.cpp:1004
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:1069
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:347
std::string _personal_dir
custom directory for personal settings, saves, newgrf, etc.
Definition fileio.cpp:870
static bool _do_scan_working_directory
Whether the working directory should be scanned.
Definition fileio.cpp:34
static std::string GetHomeDir()
Gets the home directory of the user.
Definition fileio.cpp:731
bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:122
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp:359
static bool MatchesExtension(std::string_view extension, const std::string &filename)
Helper to see whether a given filename matches the extension.
Definition fileio.cpp:1051
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:317
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:1097
static bool ChangeWorkingDirectoryToExecutable(const char *exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:673
bool FileExists(const std::string &filename)
Test whether the given filename exists.
Definition fileio.cpp:133
std::array< std::string, NUM_SEARCHPATHS > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:66
static std::optional< FileHandle > FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
Opens a file from inside a tar archive.
Definition fileio.cpp:224
static std::string ExtractString(std::span< char > buffer)
Helper to extract a string for the tar header.
Definition fileio.cpp:434
std::string _highscore_file
The file to store the highscore data in.
Definition highscore.cpp:24
bool DoScanWorkingDirectory()
Whether we should scan the working directory.
Definition fileio.cpp:709
std::string _config_file
Configuration file of OpenTTD.
Definition settings.cpp:60
void DetermineBasePaths(const char *exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:751
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:243
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1026
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:347
Searchpath
Types of searchpaths OpenTTD might use.
@ SP_SHARED_DIR
Search in the shared directory, like 'Shared Files' under Windows.
@ SP_INSTALLATION_DIR
Search in the installation directory.
@ SP_AUTODOWNLOAD_PERSONAL_DIR_XDG
Search within the autodownload directory located in the personal directory (XDG variant)
@ SP_BINARY_DIR
Search in the directory where the binary resides.
@ SP_AUTODOWNLOAD_PERSONAL_DIR
Search within the autodownload directory located in the personal directory.
@ SP_PERSONAL_DIR
Search in the personal directory.
@ SP_WORKING_DIR
Search in the working directory.
@ SP_APPLICATION_BUNDLE_DIR
Search within the application bundle.
@ SP_AUTODOWNLOAD_DIR
Search within the autodownload directory.
Subdirectory
The different kinds of subdirectories OpenTTD uses.
@ OLD_DATA_DIR
Old subdirectory for the data.
@ NO_DIRECTORY
A path without any base directory.
@ AI_LIBRARY_DIR
Subdirectory for all AI libraries.
@ SCREENSHOT_DIR
Subdirectory for all screenshots.
@ SOCIAL_INTEGRATION_DIR
Subdirectory for all social integration plugins.
@ GAME_LIBRARY_DIR
Subdirectory for all GS libraries.
@ AI_DIR
Subdirectory for all AI files.
@ OLD_GM_DIR
Old subdirectory for the music.
@ SCENARIO_DIR
Base directory for all scenarios.
@ BASE_DIR
Base directory for all subdirectories.
@ SAVE_DIR
Base directory for all savegames.
@ NUM_SUBDIRS
Number of subdirectories.
@ HEIGHTMAP_DIR
Subdirectory of scenario for heightmaps.
@ NEWGRF_DIR
Subdirectory for all NewGRFs.
@ AUTOSAVE_DIR
Subdirectory of save for autosaves.
@ BASESET_DIR
Subdirectory for all base data (base sets, intro game)
@ GAME_DIR
Subdirectory for all game scripts.
Declarations for savegames operations.
constexpr T Align(const T x, uint n)
Return the smallest multiple of n equal or greater than x.
Definition math_func.hpp:37
A number of safeguards to prevent using unsafe methods.
std::string _secrets_file
Secrets configuration file of OpenTTD.
Definition settings.cpp:62
std::string _favs_file
Picker favourites configuration file of OpenTTD.
Definition settings.cpp:63
std::string _private_file
Private configuration file of OpenTTD.
Definition settings.cpp:61
Base for loading sprites.
Definition of base types and functions in a cross-platform compatible way.
#define lengthof(array)
Return the length of an fixed size array.
Definition stdafx.h:277
static void StrMakeValid(T &dst, const char *str, const char *last, StringValidationSettings settings)
Copies the valid (UTF-8) characters from str up to last to the dst.
Definition string.cpp:107
bool StrEqualsIgnoreCase(const std::string_view str1, const std::string_view str2)
Compares two string( view)s for equality, while ignoring the case of the characters.
Definition string.cpp:347
int StrCompareIgnoreCase(const std::string_view str1, const std::string_view str2)
Compares two string( view)s, while ignoring the case of the characters.
Definition string.cpp:334
void StrTrimInPlace(std::string &str)
Trim the spaces from given string in place, i.e.
Definition string.cpp:260
Functions related to low-level strings.
Structs, typedefs and macros used for TAR file handling.
std::wstring OTTD2FS(const std::string &name)
Convert from OpenTTD's encoding to a wide string.
Definition win32.cpp:354
std::string FS2OTTD(const std::wstring &name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:337
std::string _windows_file
Config file to store WindowDesc.
Definition window.cpp:102