OpenTTD Source 15.0-beta2
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 <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 char * const _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(const std::string &filename, Subdirectory subdir)
122{
123 auto f = FioFOpenFile(filename, "rb", subdir);
124 return f.has_value();
125}
126
132bool FileExists(const std::string &filename)
133{
134 std::error_code ec;
135 return std::filesystem::exists(OTTD2FS(filename), ec);
136}
137
144std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
145{
146 assert(subdir < NUM_SUBDIRS);
147
148 for (Searchpath sp : _valid_searchpaths) {
149 std::string buf = FioGetDirectory(sp, subdir);
150 buf += filename;
151 if (FileExists(buf)) return buf;
152#if !defined(_WIN32)
153 /* Be, as opening files, aware that sometimes the filename
154 * might be in uppercase when it is in lowercase on the
155 * disk. Of course Windows doesn't care about casing. */
156 if (strtolower(buf, _searchpaths[sp].size() - 1) && FileExists(buf)) return buf;
157#endif
158 }
159
160 return {};
161}
162
163std::string FioGetDirectory(Searchpath sp, Subdirectory subdir)
164{
165 assert(subdir < NUM_SUBDIRS);
166 assert(sp < NUM_SEARCHPATHS);
167
168 return _searchpaths[sp] + _subdirs[subdir];
169}
170
171std::string FioFindDirectory(Subdirectory subdir)
172{
173 /* Find and return the first valid directory */
174 for (Searchpath sp : _valid_searchpaths) {
175 std::string ret = FioGetDirectory(sp, subdir);
176 if (FileExists(ret)) return ret;
177 }
178
179 /* Could not find the directory, fall back to a base path */
180 return _personal_dir;
181}
182
183static std::optional<FileHandle> FioFOpenFileSp(const std::string &filename, const char *mode, Searchpath sp, Subdirectory subdir, size_t *filesize)
184{
185#if defined(_WIN32)
186 /* fopen is implemented as a define with ellipses for
187 * Unicode support (prepend an L). As we are not sending
188 * a string, but a variable, it 'renames' the variable,
189 * so make that variable to makes it compile happily */
190 wchar_t Lmode[5];
191 MultiByteToWideChar(CP_ACP, 0, mode, -1, Lmode, static_cast<int>(std::size(Lmode)));
192#endif
193 std::string buf;
194
195 if (subdir == NO_DIRECTORY) {
196 buf = filename;
197 } else {
198 buf = _searchpaths[sp] + _subdirs[subdir] + filename;
199 }
200
201 auto f = FileHandle::Open(buf, mode);
202#if !defined(_WIN32)
203 if (!f.has_value() && strtolower(buf, subdir == NO_DIRECTORY ? 0 : _searchpaths[sp].size() - 1) ) {
204 f = FileHandle::Open(buf, mode);
205 }
206#endif
207 if (f.has_value() && filesize != nullptr) {
208 /* Find the size of the file */
209 fseek(*f, 0, SEEK_END);
210 *filesize = ftell(*f);
211 fseek(*f, 0, SEEK_SET);
212 }
213 return f;
214}
215
223static std::optional<FileHandle> FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
224{
225 auto f = FileHandle::Open(entry.tar_filename, "rb");
226 if (!f.has_value()) return std::nullopt;
227
228 if (fseek(*f, entry.position, SEEK_SET) < 0) {
229 return std::nullopt;
230 }
231
232 if (filesize != nullptr) *filesize = entry.size;
233 return f;
234}
235
242std::optional<FileHandle> FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
243{
244 std::optional<FileHandle> f = std::nullopt;
245 assert(subdir < NUM_SUBDIRS || subdir == NO_DIRECTORY);
246
247 for (Searchpath sp : _valid_searchpaths) {
248 f = FioFOpenFileSp(filename, mode, sp, subdir, filesize);
249 if (f.has_value() || subdir == NO_DIRECTORY) break;
250 }
251
252 /* We can only use .tar in case of data-dir, and read-mode */
253 if (!f.has_value() && mode[0] == 'r' && subdir != NO_DIRECTORY) {
254 /* Filenames in tars are always forced to be lowercase */
255 std::string resolved_name = filename;
256 strtolower(resolved_name);
257
258 /* Resolve ".." */
259 std::istringstream ss(resolved_name);
260 std::vector<std::string> tokens;
261 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
464 /* Check if we already seen this file */
465 TarList::iterator it = _tar_list[this->subdir].find(filename);
466 if (it != _tar_list[this->subdir].end()) return false;
467
468 auto of = FileHandle::Open(filename, "rb");
469 /* Although the file has been found there can be
470 * a number of reasons we cannot open the file.
471 * Most common case is when we simply have not
472 * been given read access. */
473 if (!of.has_value()) return false;
474 auto &f = *of;
475
476 _tar_list[this->subdir][filename] = std::string{};
477
478 std::string filename_base = FS2OTTD(std::filesystem::path(OTTD2FS(filename)).filename());
479 SimplifyFileName(filename_base);
480
481 TarHeader th;
482 size_t num = 0, pos = 0;
483
484 /* Make a char of 512 empty bytes */
485 char empty[512];
486 memset(&empty[0], 0, sizeof(empty));
487
488 for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it?
489 size_t num_bytes_read = fread(&th, 1, 512, f);
490 if (num_bytes_read != 512) break;
491 pos += num_bytes_read;
492
493 /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */
494 if (strncmp(th.magic, "ustar", 5) != 0 && memcmp(&th.magic, &empty[0], 512 - offsetof(TarHeader, magic)) != 0) {
495 /* If we have only zeros in the block, it can be an end-of-file indicator */
496 if (memcmp(&th, &empty[0], 512) == 0) continue;
497
498 Debug(misc, 0, "The file '{}' isn't a valid tar-file", filename);
499 return false;
500 }
501
502 std::string name;
503
504 /* The prefix contains the directory-name */
505 if (th.prefix[0] != '\0') {
506 name = ExtractString(th.prefix);
507 name += PATHSEP;
508 }
509
510 /* Copy the name of the file in a safe way at the end of 'name' */
511 name += ExtractString(th.name);
512
513 /* The size of the file, for some strange reason, this is stored as a string in octals. */
514 std::string size = ExtractString(th.size);
515 size_t skip = 0;
516 if (!size.empty()) {
517 StrTrimInPlace(size);
518 auto [_, err] = std::from_chars(size.data(), size.data() + size.size(), skip, 8);
519 if (err != std::errc()) {
520 Debug(misc, 0, "The file '{}' has an invalid size for '{}'", filename, name);
521 fclose(f);
522 return false;
523 }
524 }
525
526 switch (th.typeflag) {
527 case '\0':
528 case '0': { // regular file
529 if (name.empty()) break;
530
531 /* Store this entry in the list */
532 TarFileListEntry entry;
533 entry.tar_filename = filename;
534 entry.size = skip;
535 entry.position = pos;
536
537 /* Convert to lowercase and our PATHSEPCHAR */
538 SimplifyFileName(name);
539
540 Debug(misc, 6, "Found file in tar: {} ({} bytes, {} offset)", name, skip, pos);
541 if (_tar_filelist[this->subdir].insert(TarFileList::value_type(filename_base + PATHSEPCHAR + name, entry)).second) num++;
542
543 break;
544 }
545
546 case '1': // hard links
547 case '2': { // symbolic links
548 std::string link = ExtractString(th.linkname);
549
550 Debug(misc, 5, "Ignoring link in tar: {} -> {}", name, link);
551 break;
552 }
553
554 case '5': // directory
555 /* Convert to lowercase and our PATHSEPCHAR */
556 SimplifyFileName(name);
557
558 /* Store the first directory name we detect */
559 Debug(misc, 6, "Found dir in tar: {}", name);
560 if (_tar_list[this->subdir][filename].empty()) _tar_list[this->subdir][filename] = std::move(name);
561 break;
562
563 default:
564 /* Ignore other types */
565 break;
566 }
567
568 /* Skip to the next block.. */
569 skip = Align(skip, 512);
570 if (fseek(f, skip, SEEK_CUR) < 0) {
571 Debug(misc, 0, "The file '{}' can't be read as a valid tar-file", filename);
572 return false;
573 }
574 pos += skip;
575 }
576
577 Debug(misc, 4, "Found tar '{}' with {} new files", filename, num);
578
579 return true;
580}
581
589bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
590{
591 TarList::iterator it = _tar_list[subdir].find(tar_filename);
592 /* We don't know the file. */
593 if (it == _tar_list[subdir].end()) return false;
594
595 const auto &dirname = (*it).second;
596
597 /* The file doesn't have a sub directory! */
598 if (dirname.empty()) {
599 Debug(misc, 3, "Extracting {} failed; archive rejected, the contents must be in a sub directory", tar_filename);
600 return false;
601 }
602
603 std::string filename = tar_filename;
604 auto p = filename.find_last_of(PATHSEPCHAR);
605 /* The file's path does not have a separator? */
606 if (p == std::string::npos) return false;
607
608 filename.replace(p + 1, std::string::npos, dirname);
609 Debug(misc, 8, "Extracting {} to directory {}", tar_filename, filename);
610 FioCreateDirectory(filename);
611
612 for (auto &it2 : _tar_filelist[subdir]) {
613 if (tar_filename != it2.second.tar_filename) continue;
614
615 /* it2.first is tarball + PATHSEPCHAR + name. */
616 std::string_view name = it2.first;
617 name.remove_prefix(name.find_first_of(PATHSEPCHAR) + 1);
618 filename.replace(p + 1, std::string::npos, name);
619
620 Debug(misc, 9, " extracting {}", filename);
621
622 /* First open the file in the .tar. */
623 size_t to_copy = 0;
624 auto in = FioFOpenFileTar(it2.second, &to_copy);
625 if (!in.has_value()) {
626 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, tar_filename);
627 return false;
628 }
629
630 /* Now open the 'output' file. */
631 auto out = FileHandle::Open(filename, "wb");
632 if (!out.has_value()) {
633 Debug(misc, 6, "Extracting {} failed; could not open {}", filename, filename);
634 return false;
635 }
636
637 /* Now read from the tar and write it into the file. */
638 char buffer[4096];
639 size_t read;
640 for (; to_copy != 0; to_copy -= read) {
641 read = fread(buffer, 1, std::min(to_copy, lengthof(buffer)), *in);
642 if (read <= 0 || fwrite(buffer, 1, read, *out) != read) break;
643 }
644
645 if (to_copy != 0) {
646 Debug(misc, 6, "Extracting {} failed; still {} bytes to copy", filename, to_copy);
647 return false;
648 }
649 }
650
651 Debug(misc, 9, " extraction successful");
652 return true;
653}
654
655#if defined(_WIN32)
661extern void DetermineBasePaths(const char *exe);
662#else /* defined(_WIN32) */
663
671static bool ChangeWorkingDirectoryToExecutable(const char *exe)
672{
673 std::string path = exe;
674
675#ifdef WITH_COCOA
676 for (size_t pos = path.find_first_of('.'); pos != std::string::npos; pos = path.find_first_of('.', pos + 1)) {
677 if (StrEqualsIgnoreCase(path.substr(pos, 4), ".app")) {
678 path.erase(pos);
679 break;
680 }
681 }
682#endif /* WITH_COCOA */
683
684 size_t pos = path.find_last_of(PATHSEPCHAR);
685 if (pos == std::string::npos) return false;
686
687 path.erase(pos);
688
689 if (chdir(path.c_str()) != 0) {
690 Debug(misc, 0, "Directory with the binary does not exist?");
691 return false;
692 }
693
694 return true;
695}
696
708{
709 /* No working directory, so nothing to do. */
710 if (_searchpaths[SP_WORKING_DIR].empty()) return false;
711
712 /* Working directory is root, so do nothing. */
713 if (_searchpaths[SP_WORKING_DIR] == PATHSEP) return false;
714
715 /* No personal/home directory, so the working directory won't be that. */
716 if (_searchpaths[SP_PERSONAL_DIR].empty()) return true;
717
718 std::string tmp = _searchpaths[SP_WORKING_DIR] + PERSONAL_DIR;
720
721 return _searchpaths[SP_PERSONAL_DIR] != tmp;
722}
723
729static std::string GetHomeDir()
730{
731#ifdef __HAIKU__
732 BPath path;
733 find_directory(B_USER_SETTINGS_DIRECTORY, &path);
734 return std::string(path.Path());
735#else
736 const char *home_env = std::getenv("HOME"); // Stack var, shouldn't be freed
737 if (home_env != nullptr) return std::string(home_env);
738
739 const struct passwd *pw = getpwuid(getuid());
740 if (pw != nullptr) return std::string(pw->pw_dir);
741#endif
742 return {};
743}
744
749void DetermineBasePaths(const char *exe)
750{
751 std::string tmp;
752 const std::string homedir = GetHomeDir();
753#ifdef USE_XDG
754 const char *xdg_data_home = std::getenv("XDG_DATA_HOME");
755 if (xdg_data_home != nullptr) {
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());
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(const char *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 const char *xdg_config_home = std::getenv("XDG_CONFIG_HOME");
886 if (xdg_config_home != nullptr) {
887 config_home = xdg_config_home;
888 config_home += PATHSEP;
889 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
890 } else if (!homedir.empty()) {
891 /* Defaults to ~/.config */
892 config_home = std::move(homedir);
893 config_home += PATHSEP ".config" PATHSEP;
894 config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR;
895 }
896 AppendPathSeparator(config_home);
897#endif
898
899 for (Searchpath sp : _valid_searchpaths) {
900 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
901 Debug(misc, 3, "{} added as search path", _searchpaths[sp]);
902 }
903
904 std::string config_dir;
905 if (!_config_file.empty()) {
906 config_dir = _searchpaths[SP_WORKING_DIR];
907 } else {
908 std::string personal_dir = FioFindFullPath(BASE_DIR, "openttd.cfg");
909 if (!personal_dir.empty()) {
910 auto end = personal_dir.find_last_of(PATHSEPCHAR);
911 if (end != std::string::npos) personal_dir.erase(end + 1);
912 config_dir = std::move(personal_dir);
913 } else {
914#ifdef USE_XDG
915 /* No previous configuration file found. Use the configuration folder from XDG. */
916 config_dir = config_home;
917#else
918 static const Searchpath new_openttd_cfg_order[] = {
920 };
921
922 config_dir.clear();
923 for (const auto &searchpath : new_openttd_cfg_order) {
924 if (IsValidSearchPath(searchpath)) {
925 config_dir = _searchpaths[searchpath];
926 break;
927 }
928 }
929#endif
930 }
931 _config_file = config_dir + "openttd.cfg";
932 }
933
934 Debug(misc, 1, "{} found as config directory", config_dir);
935
936 _highscore_file = config_dir + "hs.dat";
937 extern std::string _hotkeys_file;
938 _hotkeys_file = config_dir + "hotkeys.cfg";
939 extern std::string _windows_file;
940 _windows_file = config_dir + "windows.cfg";
941 extern std::string _private_file;
942 _private_file = config_dir + "private.cfg";
943 extern std::string _secrets_file;
944 _secrets_file = config_dir + "secrets.cfg";
945 extern std::string _favs_file;
946 _favs_file = config_dir + "favs.cfg";
947
948#ifdef USE_XDG
949 if (config_dir == config_home) {
950 /* We are using the XDG configuration home for the config file,
951 * then store the rest in the XDG data home folder. */
952 _personal_dir = _searchpaths[SP_PERSONAL_DIR_XDG];
953 if (only_local_path) {
954 /* In case of XDG and we only want local paths and we detected that
955 * the user either manually indicated the XDG path or didn't use
956 * "-c" option, we change the working-dir to the XDG personal-dir,
957 * as this is most likely what the user is expecting. */
958 _searchpaths[SP_WORKING_DIR] = _searchpaths[SP_PERSONAL_DIR_XDG];
959 }
960 } else
961#endif
962 {
963 _personal_dir = config_dir;
964 }
965
966 /* Make the necessary folders */
967 FioCreateDirectory(config_dir);
968#if defined(WITH_PERSONAL_DIR)
970#endif
971
972 Debug(misc, 1, "{} found as personal directory", _personal_dir);
973
974 static const Subdirectory default_subdirs[] = {
976 };
977
978 for (const auto &default_subdir : default_subdirs) {
979 FioCreateDirectory(_personal_dir + _subdirs[default_subdir]);
980 }
981
982 /* If we have network we make a directory for the autodownloading of content */
983 _searchpaths[SP_AUTODOWNLOAD_DIR] = _personal_dir + "content_download" PATHSEP;
984 Debug(misc, 3, "{} added as search path", _searchpaths[SP_AUTODOWNLOAD_DIR]);
986 FillValidSearchPaths(only_local_path);
987
988 /* Create the directory for each of the types of content */
990 for (const auto &subdir : subdirs) {
991 FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, subdir));
992 }
993
994 extern std::string _log_file;
995 _log_file = _personal_dir + "openttd.log";
996}
997
1002void SanitizeFilename(std::string &filename)
1003{
1004 for (auto &c : filename) {
1005 switch (c) {
1006 /* The following characters are not allowed in filenames
1007 * on at least one of the supported operating systems: */
1008 case ':': case '\\': case '*': case '?': case '/':
1009 case '<': case '>': case '|': case '"':
1010 c = '_';
1011 break;
1012 }
1013 }
1014}
1015
1024std::unique_ptr<char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
1025{
1026 auto in = FileHandle::Open(filename, "rb");
1027 if (!in.has_value()) return nullptr;
1028
1029 fseek(*in, 0, SEEK_END);
1030 size_t len = ftell(*in);
1031 fseek(*in, 0, SEEK_SET);
1032 if (len > maxsize) return nullptr;
1033
1034 std::unique_ptr<char[]> mem = std::make_unique<char[]>(len + 1);
1035
1036 mem.get()[len] = 0;
1037 if (fread(mem.get(), len, 1, *in) != 1) return nullptr;
1038
1039 lenp = len;
1040 return mem;
1041}
1042
1049static bool MatchesExtension(std::string_view extension, const std::string &filename)
1050{
1051 if (extension.empty()) return true;
1052 if (filename.length() < extension.length()) return false;
1053
1054 std::string_view filename_sv = filename; // String view to avoid making another copy of the substring.
1055 return StrCompareIgnoreCase(extension, filename_sv.substr(filename_sv.length() - extension.length())) == 0;
1056}
1057
1067static uint ScanPath(FileScanner *fs, std::string_view extension, const std::filesystem::path &path, size_t basepath_length, bool recursive)
1068{
1069 uint num = 0;
1070
1071 std::error_code error_code;
1072 for (const auto &dir_entry : std::filesystem::directory_iterator(path, error_code)) {
1073 if (dir_entry.is_directory()) {
1074 if (!recursive) continue;
1075 num += ScanPath(fs, extension, dir_entry.path(), basepath_length, recursive);
1076 } else if (dir_entry.is_regular_file()) {
1077 std::string file = FS2OTTD(dir_entry.path());
1078 if (!MatchesExtension(extension, file)) continue;
1079 if (fs->AddFile(file, basepath_length, {})) num++;
1080 }
1081 }
1082 if (error_code) {
1083 Debug(misc, 9, "Unable to read directory {}: {}", path.string(), error_code.message());
1084 }
1085
1086 return num;
1087}
1088
1095static uint ScanTar(FileScanner *fs, std::string_view extension, const TarFileList::value_type &tar)
1096{
1097 uint num = 0;
1098
1099 if (MatchesExtension(extension, tar.first) && fs->AddFile(tar.first, 0, tar.second.tar_filename)) num++;
1100
1101 return num;
1102}
1103
1113uint FileScanner::Scan(std::string_view extension, Subdirectory sd, bool tars, bool recursive)
1114{
1115 this->subdir = sd;
1116
1117 uint num = 0;
1118
1119 for (Searchpath sp : _valid_searchpaths) {
1120 /* Don't search in the working directory */
1121 if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue;
1122
1123 std::string path = FioGetDirectory(sp, sd);
1124 num += ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1125 }
1126
1127 if (tars && sd != NO_DIRECTORY) {
1128 for (const auto &tar : _tar_filelist[sd]) {
1129 num += ScanTar(this, extension, tar);
1130 }
1131 }
1132
1133 switch (sd) {
1134 case BASESET_DIR:
1135 num += this->Scan(extension, OLD_GM_DIR, tars, recursive);
1136 [[fallthrough]];
1137 case NEWGRF_DIR:
1138 num += this->Scan(extension, OLD_DATA_DIR, tars, recursive);
1139 break;
1140
1141 default: break;
1142 }
1143
1144 return num;
1145}
1146
1155uint FileScanner::Scan(const std::string_view extension, const std::string &directory, bool recursive)
1156{
1157 std::string path(directory);
1158 AppendPathSeparator(path);
1159 return ScanPath(this, extension, OTTD2FS(path), path.size(), recursive);
1160}
1161
1169std::optional<FileHandle> FileHandle::Open(const std::string &filename, const std::string &mode)
1170{
1171#if defined(_WIN32)
1172 /* Windows also requires mode to be wchar_t. */
1173 auto f = _wfopen(OTTD2FS(filename).c_str(), OTTD2FS(mode).c_str());
1174#else
1175 auto f = fopen(filename.c_str(), mode.c_str());
1176#endif /* _WIN32 */
1177
1178 if (f == nullptr) return std::nullopt;
1179 return FileHandle(f);
1180}
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, const std::string &mode)
Open an RAII file handle if possible.
Definition fileio.cpp:1169
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:1113
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
std::string FioFindFullPath(Subdirectory subdir, const std::string &filename)
Find a path to the filename in one of the search directories.
Definition fileio.cpp:144
bool ExtractTar(const std::string &tar_filename, Subdirectory subdir)
Extract the tar with the given filename in the directory where the tar resides.
Definition fileio.cpp:589
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: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:1002
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:1067
void AppendPathSeparator(std::string &buf)
Appends, if necessary, the path separator character to the end of the string.
Definition fileio.cpp:345
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:729
bool FioCheckFileExists(const std::string &filename, Subdirectory subdir)
Check whether the given file exists.
Definition fileio.cpp:121
static void SimplifyFileName(std::string &name)
Simplify filenames from tars.
Definition fileio.cpp: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:1049
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
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:1095
static bool ChangeWorkingDirectoryToExecutable(const char *exe)
Changes the working directory to the path of the give executable.
Definition fileio.cpp:671
bool FileExists(const std::string &filename)
Test whether the given filename exists.
Definition fileio.cpp:132
std::array< std::string, NUM_SEARCHPATHS > _searchpaths
The search paths OpenTTD could search through.
Definition fileio.cpp:65
static std::optional< FileHandle > FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize)
Opens a file from inside a tar archive.
Definition fileio.cpp:223
static std::string ExtractString(std::span< char > buffer)
Helper to extract a string for the tar header.
Definition fileio.cpp: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:707
std::string _config_file
Configuration file of OpenTTD.
Definition settings.cpp:62
void DetermineBasePaths(const char *exe)
Determine the base (personal dir and game data dir) paths.
Definition fileio.cpp:749
std::optional< FileHandle > FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize)
Opens a OpenTTD file somewhere in a personal or global directory.
Definition fileio.cpp:242
std::unique_ptr< char[]> ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize)
Load a file into memory.
Definition fileio.cpp:1024
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.
@ 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:64
std::string _favs_file
Picker favourites configuration file of OpenTTD.
Definition settings.cpp:65
std::string _private_file
Private configuration file of OpenTTD.
Definition settings.cpp:63
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
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:123
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:368
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:355
void StrTrimInPlace(std::string &str)
Trim the spaces from given string in place, i.e.
Definition string.cpp:281
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:104