/* * This file is part of OpenTTD. * 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. * 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. * 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 . */ /** @file fileio.cpp Standard In/Out file operations */ #include "stdafx.h" #include "fileio_func.h" #include "debug.h" #include "fios.h" #include "string_func.h" #include "tar_type.h" #ifdef _WIN32 #include # define access _taccess #elif defined(__HAIKU__) #include #include #else #include #include #endif #include #include #include #include #ifdef WITH_XDG_BASEDIR #include #endif #include "safeguards.h" /** Size of the #Fio data buffer. */ #define FIO_BUFFER_SIZE 512 /** Structure for keeping several open files with just one data buffer. */ struct Fio { byte *buffer, *buffer_end; ///< position pointer in local buffer and last valid byte of buffer byte buffer_start[FIO_BUFFER_SIZE]; ///< local buffer when read from file size_t pos; ///< current (system) position in file FILE *cur_fh; ///< current file handle std::string filename; ///< current filename std::array handles; ///< array of file handles we can have open std::array filenames; ///< array of filenames we (should) have open std::array shortnames;///< array of short names for spriteloader's use }; static Fio _fio; ///< #Fio instance. /** Whether the working directory should be scanned. */ static bool _do_scan_working_directory = true; extern std::string _config_file; extern std::string _highscore_file; /** * Get position in the current file. * @return Position in the file. */ size_t FioGetPos() { return _fio.pos + (_fio.buffer - _fio.buffer_end); } /** * Get the filename associated with a slot. * @param slot Index of queried file. * @return Name of the file. */ const char *FioGetFilename(uint8 slot) { return _fio.shortnames[slot].c_str(); } /** * Seek in the current file. * @param pos New position. * @param mode Type of seek (\c SEEK_CUR means \a pos is relative to current position, \c SEEK_SET means \a pos is absolute). */ void FioSeekTo(size_t pos, int mode) { if (mode == SEEK_CUR) pos += FioGetPos(); _fio.buffer = _fio.buffer_end = _fio.buffer_start + FIO_BUFFER_SIZE; _fio.pos = pos; if (fseek(_fio.cur_fh, _fio.pos, SEEK_SET) < 0) { DEBUG(misc, 0, "Seeking in %s failed", _fio.filename.c_str()); } } /** * Switch to a different file and seek to a position. * @param slot Slot number of the new file. * @param pos New absolute position in the new file. */ void FioSeekToFile(uint8 slot, size_t pos) { FILE *f = _fio.handles[slot]; assert(f != nullptr); _fio.cur_fh = f; _fio.filename = _fio.filenames[slot]; FioSeekTo(pos, SEEK_SET); } /** * Read a byte from the file. * @return Read byte. */ byte FioReadByte() { if (_fio.buffer == _fio.buffer_end) { _fio.buffer = _fio.buffer_start; size_t size = fread(_fio.buffer, 1, FIO_BUFFER_SIZE, _fio.cur_fh); _fio.pos += size; _fio.buffer_end = _fio.buffer_start + size; if (size == 0) return 0; } return *_fio.buffer++; } /** * Skip \a n bytes ahead in the file. * @param n Number of bytes to skip reading. */ void FioSkipBytes(int n) { for (;;) { int m = min(_fio.buffer_end - _fio.buffer, n); _fio.buffer += m; n -= m; if (n == 0) break; FioReadByte(); n--; } } /** * Read a word (16 bits) from the file (in low endian format). * @return Read word. */ uint16 FioReadWord() { byte b = FioReadByte(); return (FioReadByte() << 8) | b; } /** * Read a double word (32 bits) from the file (in low endian format). * @return Read word. */ uint32 FioReadDword() { uint b = FioReadWord(); return (FioReadWord() << 16) | b; } /** * Read a block. * @param ptr Destination buffer. * @param size Number of bytes to read. */ void FioReadBlock(void *ptr, size_t size) { FioSeekTo(FioGetPos(), SEEK_SET); _fio.pos += fread(ptr, 1, size, _fio.cur_fh); } /** * Close the file at the given slot number. * @param slot File index to close. */ static inline void FioCloseFile(int slot) { if (_fio.handles[slot] != nullptr) { fclose(_fio.handles[slot]); _fio.shortnames[slot].clear(); _fio.handles[slot] = nullptr; } } /** Close all slotted open files. */ void FioCloseAll() { for (int i = 0; i != lengthof(_fio.handles); i++) { FioCloseFile(i); } } /** * Open a slotted file. * @param slot Index to assign. * @param filename Name of the file at the disk. * @param subdir The sub directory to search this file in. */ void FioOpenFile(int slot, const std::string &filename, Subdirectory subdir) { FILE *f; f = FioFOpenFile(filename, "rb", subdir); if (f == nullptr) usererror("Cannot open file '%s'", filename.c_str()); long pos = ftell(f); if (pos < 0) usererror("Cannot read file '%s'", filename.c_str()); FioCloseFile(slot); // if file was opened before, close it _fio.handles[slot] = f; _fio.filenames[slot] = filename; /* Store the filename without path and extension */ auto t = filename.rfind(PATHSEPCHAR); std::string sn = filename.substr(t != std::string::npos ? t + 1 : 0); _fio.shortnames[slot] = sn.substr(0, sn.rfind('.')); strtolower(_fio.shortnames[slot]); FioSeekToFile(slot, (size_t)pos); } static const char * const _subdirs[] = { "", "save" PATHSEP, "save" PATHSEP "autosave" PATHSEP, "scenario" PATHSEP, "scenario" PATHSEP "heightmap" PATHSEP, "gm" PATHSEP, "data" PATHSEP, "baseset" PATHSEP, "newgrf" PATHSEP, "lang" PATHSEP, "ai" PATHSEP, "ai" PATHSEP "library" PATHSEP, "game" PATHSEP, "game" PATHSEP "library" PATHSEP, "screenshot" PATHSEP, }; static_assert(lengthof(_subdirs) == NUM_SUBDIRS); /** * The search paths OpenTTD could search through. * At least one of the slots has to be filled with a path. * An empty string tells that there is no such path for the * current operating system. */ std::array _searchpaths; TarList _tar_list[NUM_SUBDIRS]; TarFileList _tar_filelist[NUM_SUBDIRS]; typedef std::map TarLinkList; static TarLinkList _tar_linklist[NUM_SUBDIRS]; ///< List of directory links /** * Checks whether the given search path is a valid search path * @param sp the search path to check * @return true if the search path is valid */ bool IsValidSearchPath(Searchpath sp) { return sp < _searchpaths.size() && !_searchpaths[sp].empty(); } /** * Check whether the given file exists * @param filename the file to try for existence. * @param subdir the subdirectory to look in * @return true if and only if the file can be opened */ bool FioCheckFileExists(const std::string &filename, Subdirectory subdir) { FILE *f = FioFOpenFile(filename, "rb", subdir); if (f == nullptr) return false; FioFCloseFile(f); return true; } /** * Test whether the given filename exists. * @param filename the file to test. * @return true if and only if the file exists. */ bool FileExists(const std::string &filename) { return access(OTTD2FS(filename.c_str()), 0) == 0; } /** * Close a file in a safe way. */ void FioFCloseFile(FILE *f) { fclose(f); } /** * Find a path to the filename in one of the search directories. * @param subdir Subdirectory to try. * @param filename Filename to look for. * @return String containing the path if the path was found, else an empty string. */ std::string FioFindFullPath(Subdirectory subdir, const char *filename) { Searchpath sp; assert(subdir < NUM_SUBDIRS); FOR_ALL_SEARCHPATHS(sp) { std::string buf = FioGetDirectory(sp, subdir); buf += filename; if (FileExists(buf)) return buf; #if !defined(_WIN32) /* Be, as opening files, aware that sometimes the filename * might be in uppercase when it is in lowercase on the * disk. Of course Windows doesn't care about casing. */ if (strtolower(buf, _searchpaths[sp].size() - 1) && FileExists(buf)) return buf; #endif } return {}; } std::string FioGetDirectory(Searchpath sp, Subdirectory subdir) { assert(subdir < NUM_SUBDIRS); assert(sp < NUM_SEARCHPATHS); return _searchpaths[sp] + _subdirs[subdir]; } std::string FioFindDirectory(Subdirectory subdir) { Searchpath sp; /* Find and return the first valid directory */ FOR_ALL_SEARCHPATHS(sp) { std::string ret = FioGetDirectory(sp, subdir); if (FileExists(ret)) return ret; } /* Could not find the directory, fall back to a base path */ return _personal_dir; } static FILE *FioFOpenFileSp(const std::string &filename, const char *mode, Searchpath sp, Subdirectory subdir, size_t *filesize) { #if defined(_WIN32) && defined(UNICODE) /* fopen is implemented as a define with ellipses for * Unicode support (prepend an L). As we are not sending * a string, but a variable, it 'renames' the variable, * so make that variable to makes it compile happily */ wchar_t Lmode[5]; MultiByteToWideChar(CP_ACP, 0, mode, -1, Lmode, lengthof(Lmode)); #endif FILE *f = nullptr; std::string buf; if (subdir == NO_DIRECTORY) { buf = filename; } else { buf = _searchpaths[sp] + _subdirs[subdir] + filename; } #if defined(_WIN32) if (mode[0] == 'r' && GetFileAttributes(OTTD2FS(buf.c_str())) == INVALID_FILE_ATTRIBUTES) return nullptr; #endif f = fopen(buf.c_str(), mode); #if !defined(_WIN32) if (f == nullptr && strtolower(buf, subdir == NO_DIRECTORY ? 0 : _searchpaths[sp].size() - 1) ) { f = fopen(buf.c_str(), mode); } #endif if (f != nullptr && filesize != nullptr) { /* Find the size of the file */ fseek(f, 0, SEEK_END); *filesize = ftell(f); fseek(f, 0, SEEK_SET); } return f; } /** * Opens a file from inside a tar archive. * @param entry The entry to open. * @param[out] filesize If not \c nullptr, size of the opened file. * @return File handle of the opened file, or \c nullptr if the file is not available. * @note The file is read from within the tar file, and may not return \c EOF after reading the whole file. */ FILE *FioFOpenFileTar(const TarFileListEntry &entry, size_t *filesize) { FILE *f = fopen(entry.tar_filename, "rb"); if (f == nullptr) return f; if (fseek(f, entry.position, SEEK_SET) < 0) { fclose(f); return nullptr; } if (filesize != nullptr) *filesize = entry.size; return f; } /** * Opens a OpenTTD file somewhere in a personal or global directory. * @param filename Name of the file to open. * @param subdir Subdirectory to open. * @return File handle of the opened file, or \c nullptr if the file is not available. */ FILE *FioFOpenFile(const std::string &filename, const char *mode, Subdirectory subdir, size_t *filesize) { FILE *f = nullptr; Searchpath sp; assert(subdir < NUM_SUBDIRS || subdir == NO_DIRECTORY); FOR_ALL_SEARCHPATHS(sp) { f = FioFOpenFileSp(filename, mode, sp, subdir, filesize); if (f != nullptr || subdir == NO_DIRECTORY) break; } /* We can only use .tar in case of data-dir, and read-mode */ if (f == nullptr && mode[0] == 'r' && subdir != NO_DIRECTORY) { /* Filenames in tars are always forced to be lowercase */ std::string resolved_name = filename; strtolower(resolved_name); /* Resolve ".." */ std::istringstream ss(resolved_name); std::vector tokens; std::string token; while (std::getline(ss, token, PATHSEPCHAR)) { if (token == "..") { if (tokens.size() < 2) return nullptr; tokens.pop_back(); } else { tokens.push_back(token); } } resolved_name.clear(); bool first = true; for (const std::string &token : tokens) { if (!first) { resolved_name += PATHSEP; } resolved_name += token; first = false; } /* Resolve ONE directory link */ for (TarLinkList::iterator link = _tar_linklist[subdir].begin(); link != _tar_linklist[subdir].end(); link++) { const std::string &src = link->first; size_t len = src.length(); if (resolved_name.length() >= len && resolved_name[len - 1] == PATHSEPCHAR && src.compare(0, len, resolved_name, 0, len) == 0) { /* Apply link */ resolved_name.replace(0, len, link->second); break; // Only resolve one level } } TarFileList::iterator it = _tar_filelist[subdir].find(resolved_name); if (it != _tar_filelist[subdir].end()) { f = FioFOpenFileTar(it->second, filesize); } } /* Sometimes a full path is given. To support * the 'subdirectory' must be 'removed'. */ if (f == nullptr && subdir != NO_DIRECTORY) { switch (subdir) { case BASESET_DIR: f = FioFOpenFile(filename, mode, OLD_GM_DIR, filesize); if (f != nullptr) break; FALLTHROUGH; case NEWGRF_DIR: f = FioFOpenFile(filename, mode, OLD_DATA_DIR, filesize); break; default: f = FioFOpenFile(filename, mode, NO_DIRECTORY, filesize); break; } } return f; } /** * Create a directory with the given name * If the parent directory does not exist, it will try to create that as well. * @param name the new name of the directory */ void FioCreateDirectory(const std::string &name) { auto p = name.find_last_of(PATHSEPCHAR); if (p != std::string::npos) { std::string dirname = name.substr(0, p); DIR *dir = ttd_opendir(dirname.c_str()); if (dir == nullptr) { FioCreateDirectory(dirname); // Try creating the parent directory, if we couldn't open it } else { closedir(dir); } } /* Ignore directory creation errors; they'll surface later on, and most * of the time they are 'directory already exists' errors anyhow. */ #if defined(_WIN32) CreateDirectory(OTTD2FS(name.c_str()), nullptr); #elif defined(OS2) && !defined(__INNOTEK_LIBC__) mkdir(OTTD2FS(name.c_str())); #else mkdir(OTTD2FS(name.c_str()), 0755); #endif } /** * Appends, if necessary, the path separator character to the end of the string. * It does not add the path separator to zero-sized strings. * @param buf string to append the separator to * @return true iff the operation succeeded */ void AppendPathSeparator(std::string &buf) { if (buf.empty()) return; if (buf.back() != PATHSEPCHAR) buf.push_back(PATHSEPCHAR); } static void TarAddLink(const std::string &srcParam, const std::string &destParam, Subdirectory subdir) { std::string src = srcParam; std::string dest = destParam; /* Tar internals assume lowercase */ std::transform(src.begin(), src.end(), src.begin(), tolower); std::transform(dest.begin(), dest.end(), dest.begin(), tolower); TarFileList::iterator dest_file = _tar_filelist[subdir].find(dest); if (dest_file != _tar_filelist[subdir].end()) { /* Link to file. Process the link like the destination file. */ _tar_filelist[subdir].insert(TarFileList::value_type(src, dest_file->second)); } else { /* Destination file not found. Assume 'link to directory' * Append PATHSEPCHAR to 'src' and 'dest' if needed */ const std::string src_path = ((*src.rbegin() == PATHSEPCHAR) ? src : src + PATHSEPCHAR); const std::string dst_path = (dest.length() == 0 ? "" : ((*dest.rbegin() == PATHSEPCHAR) ? dest : dest + PATHSEPCHAR)); _tar_linklist[subdir].insert(TarLinkList::value_type(src_path, dst_path)); } } /** * Simplify filenames from tars. * Replace '/' by #PATHSEPCHAR, and force 'name' to lowercase. * @param name Filename to process. */ static void SimplifyFileName(char *name) { /* Force lowercase */ strtolower(name); /* Tar-files always have '/' path-separator, but we want our PATHSEPCHAR */ #if (PATHSEPCHAR != '/') for (char *n = name; *n != '\0'; n++) if (*n == '/') *n = PATHSEPCHAR; #endif } /** * Perform the scanning of a particular subdirectory. * @param sd The subdirectory to scan. * @return The number of found tar files. */ uint TarScanner::DoScan(Subdirectory sd) { _tar_filelist[sd].clear(); _tar_list[sd].clear(); uint num = this->Scan(".tar", sd, false); if (sd == BASESET_DIR || sd == NEWGRF_DIR) num += this->Scan(".tar", OLD_DATA_DIR, false); return num; } /* static */ uint TarScanner::DoScan(TarScanner::Mode mode) { DEBUG(misc, 1, "Scanning for tars"); TarScanner fs; uint num = 0; if (mode & TarScanner::BASESET) { num += fs.DoScan(BASESET_DIR); } if (mode & TarScanner::NEWGRF) { num += fs.DoScan(NEWGRF_DIR); } if (mode & TarScanner::AI) { num += fs.DoScan(AI_DIR); num += fs.DoScan(AI_LIBRARY_DIR); } if (mode & TarScanner::GAME) { num += fs.DoScan(GAME_DIR); num += fs.DoScan(GAME_LIBRARY_DIR); } if (mode & TarScanner::SCENARIO) { num += fs.DoScan(SCENARIO_DIR); num += fs.DoScan(HEIGHTMAP_DIR); } DEBUG(misc, 1, "Scan complete, found %d files", num); return num; } /** * Add a single file to the scanned files of a tar, circumventing the scanning code. * @param sd The sub directory the file is in. * @param filename The name of the file to add. * @return True if the additions went correctly. */ bool TarScanner::AddFile(Subdirectory sd, const char *filename) { this->subdir = sd; return this->AddFile(filename, 0); } bool TarScanner::AddFile(const char *filename, size_t basepath_length, const char *tar_filename) { /* No tar within tar. */ assert(tar_filename == nullptr); /* The TAR-header, repeated for every file */ struct TarHeader { char name[100]; ///< Name of the file char mode[8]; char uid[8]; char gid[8]; char size[12]; ///< Size of the file, in ASCII char mtime[12]; char chksum[8]; char typeflag; char linkname[100]; char magic[6]; char version[2]; char uname[32]; char gname[32]; char devmajor[8]; char devminor[8]; char prefix[155]; ///< Path of the file char unused[12]; }; /* Check if we already seen this file */ TarList::iterator it = _tar_list[this->subdir].find(filename); if (it != _tar_list[this->subdir].end()) return false; FILE *f = fopen(filename, "rb"); /* Although the file has been found there can be * a number of reasons we cannot open the file. * Most common case is when we simply have not * been given read access. */ if (f == nullptr) return false; const char *dupped_filename = stredup(filename); _tar_list[this->subdir][filename].filename = dupped_filename; _tar_list[this->subdir][filename].dirname = nullptr; TarLinkList links; ///< Temporary list to collect links TarHeader th; char buf[sizeof(th.name) + 1], *end; char name[sizeof(th.prefix) + 1 + sizeof(th.name) + 1]; char link[sizeof(th.linkname) + 1]; char dest[sizeof(th.prefix) + 1 + sizeof(th.name) + 1 + 1 + sizeof(th.linkname) + 1]; size_t num = 0, pos = 0; /* Make a char of 512 empty bytes */ char empty[512]; memset(&empty[0], 0, sizeof(empty)); for (;;) { // Note: feof() always returns 'false' after 'fseek()'. Cool, isn't it? size_t num_bytes_read = fread(&th, 1, 512, f); if (num_bytes_read != 512) break; pos += num_bytes_read; /* Check if we have the new tar-format (ustar) or the old one (a lot of zeros after 'link' field) */ if (strncmp(th.magic, "ustar", 5) != 0 && memcmp(&th.magic, &empty[0], 512 - offsetof(TarHeader, magic)) != 0) { /* If we have only zeros in the block, it can be an end-of-file indicator */ if (memcmp(&th, &empty[0], 512) == 0) continue; DEBUG(misc, 0, "The file '%s' isn't a valid tar-file", filename); fclose(f); return false; } name[0] = '\0'; /* The prefix contains the directory-name */ if (th.prefix[0] != '\0') { strecpy(name, th.prefix, lastof(name)); strecat(name, PATHSEP, lastof(name)); } /* Copy the name of the file in a safe way at the end of 'name' */ strecat(name, th.name, lastof(name)); /* Calculate the size of the file.. for some strange reason this is stored as a string */ strecpy(buf, th.size, lastof(buf)); size_t skip = strtoul(buf, &end, 8); switch (th.typeflag) { case '\0': case '0': { // regular file /* Ignore empty files */ if (skip == 0) break; if (strlen(name) == 0) break; /* Store this entry in the list */ TarFileListEntry entry; entry.tar_filename = dupped_filename; entry.size = skip; entry.position = pos; /* Convert to lowercase and our PATHSEPCHAR */ SimplifyFileName(name); DEBUG(misc, 6, "Found file in tar: %s (" PRINTF_SIZE " bytes, " PRINTF_SIZE " offset)", name, skip, pos); if (_tar_filelist[this->subdir].insert(TarFileList::value_type(name, entry)).second) num++; break; } case '1': // hard links case '2': { // symbolic links /* Copy the destination of the link in a safe way at the end of 'linkname' */ strecpy(link, th.linkname, lastof(link)); if (strlen(name) == 0 || strlen(link) == 0) break; /* Convert to lowercase and our PATHSEPCHAR */ SimplifyFileName(name); SimplifyFileName(link); /* Only allow relative links */ if (link[0] == PATHSEPCHAR) { DEBUG(misc, 1, "Ignoring absolute link in tar: %s -> %s", name, link); break; } /* Process relative path. * Note: The destination of links must not contain any directory-links. */ strecpy(dest, name, lastof(dest)); char *destpos = strrchr(dest, PATHSEPCHAR); if (destpos == nullptr) destpos = dest; *destpos = '\0'; char *pos = link; while (*pos != '\0') { char *next = strchr(pos, PATHSEPCHAR); if (next == nullptr) { next = pos + strlen(pos); } else { /* Terminate the substring up to the path separator character. */ *next++= '\0'; } if (strcmp(pos, ".") == 0) { /* Skip '.' (current dir) */ } else if (strcmp(pos, "..") == 0) { /* level up */ if (dest[0] == '\0') { DEBUG(misc, 1, "Ignoring link pointing outside of data directory: %s -> %s", name, link); break; } /* Truncate 'dest' after last PATHSEPCHAR. * This assumes that the truncated part is a real directory and not a link. */ destpos = strrchr(dest, PATHSEPCHAR); if (destpos == nullptr) destpos = dest; *destpos = '\0'; } else { /* Append at end of 'dest' */ if (destpos != dest) destpos = strecpy(destpos, PATHSEP, lastof(dest)); destpos = strecpy(destpos, pos, lastof(dest)); } if (destpos >= lastof(dest)) { DEBUG(misc, 0, "The length of a link in tar-file '%s' is too large (malformed?)", filename); fclose(f); return false; } pos = next; } /* Store links in temporary list */ DEBUG(misc, 6, "Found link in tar: %s -> %s", name, dest); links.insert(TarLinkList::value_type(name, dest)); break; } case '5': // directory /* Convert to lowercase and our PATHSEPCHAR */ SimplifyFileName(name); /* Store the first directory name we detect */ DEBUG(misc, 6, "Found dir in tar: %s", name); if (_tar_list[this->subdir][filename].dirname == nullptr) _tar_list[this->subdir][filename].dirname = stredup(name); break; default: /* Ignore other types */ break; } /* Skip to the next block.. */ skip = Align(skip, 512); if (fseek(f, skip, SEEK_CUR) < 0) { DEBUG(misc, 0, "The file '%s' can't be read as a valid tar-file", filename); fclose(f); return false; } pos += skip; } DEBUG(misc, 1, "Found tar '%s' with " PRINTF_SIZE " new files", filename, num); fclose(f); /* Resolve file links and store directory links. * We restrict usage of links to two cases: * 1) Links to directories: * Both the source path and the destination path must NOT contain any further links. * When resolving files at most one directory link is resolved. * 2) Links to files: * The destination path must NOT contain any links. * The source path may contain one directory link. */ for (TarLinkList::iterator link = links.begin(); link != links.end(); link++) { const std::string &src = link->first; const std::string &dest = link->second; TarAddLink(src, dest, this->subdir); } return true; } /** * Extract the tar with the given filename in the directory * where the tar resides. * @param tar_filename the name of the tar to extract. * @param subdir The sub directory the tar is in. * @return false on failure. */ bool ExtractTar(const std::string &tar_filename, Subdirectory subdir) { TarList::iterator it = _tar_list[subdir].find(tar_filename); /* We don't know the file. */ if (it == _tar_list[subdir].end()) return false; const char *dirname = (*it).second.dirname; /* The file doesn't have a sub directory! */ if (dirname == nullptr) { DEBUG(misc, 1, "Extracting %s failed; archive rejected, the contents must be in a sub directory", tar_filename.c_str()); return false; } std::string filename = tar_filename; auto p = filename.find_last_of(PATHSEPCHAR); /* The file's path does not have a separator? */ if (p == std::string::npos) return false; filename.replace(p + 1, std::string::npos, dirname); DEBUG(misc, 8, "Extracting %s to directory %s", tar_filename.c_str(), filename.c_str()); FioCreateDirectory(filename); for (TarFileList::iterator it2 = _tar_filelist[subdir].begin(); it2 != _tar_filelist[subdir].end(); it2++) { if (tar_filename != it2->second.tar_filename) continue; filename.replace(p + 1, std::string::npos, it2->first); DEBUG(misc, 9, " extracting %s", filename.c_str()); /* First open the file in the .tar. */ size_t to_copy = 0; std::unique_ptr in(FioFOpenFileTar(it2->second, &to_copy)); if (!in) { DEBUG(misc, 6, "Extracting %s failed; could not open %s", filename.c_str(), tar_filename.c_str()); return false; } /* Now open the 'output' file. */ std::unique_ptr out(fopen(filename.c_str(), "wb")); if (!out) { DEBUG(misc, 6, "Extracting %s failed; could not open %s", filename.c_str(), filename.c_str()); return false; } /* Now read from the tar and write it into the file. */ char buffer[4096]; size_t read; for (; to_copy != 0; to_copy -= read) { read = fread(buffer, 1, min(to_copy, lengthof(buffer)), in.get()); if (read <= 0 || fwrite(buffer, 1, read, out.get()) != read) break; } if (to_copy != 0) { DEBUG(misc, 6, "Extracting %s failed; still %i bytes to copy", filename.c_str(), (int)to_copy); return false; } } DEBUG(misc, 9, " extraction successful"); return true; } #if defined(_WIN32) /** * Determine the base (personal dir and game data dir) paths * @param exe the path from the current path to the executable * @note defined in the OS related files (os2.cpp, win32.cpp, unix.cpp etc) */ extern void DetermineBasePaths(const char *exe); #else /* defined(_WIN32) */ /** * Changes the working directory to the path of the give executable. * For OSX application bundles '.app' is the required extension of the bundle, * so when we crop the path to there, when can remove the name of the bundle * in the same way we remove the name from the executable name. * @param exe the path to the executable */ static bool ChangeWorkingDirectoryToExecutable(const char *exe) { char tmp[MAX_PATH]; strecpy(tmp, exe, lastof(tmp)); bool success = false; #ifdef WITH_COCOA char *app_bundle = strchr(tmp, '.'); while (app_bundle != nullptr && strncasecmp(app_bundle, ".app", 4) != 0) app_bundle = strchr(&app_bundle[1], '.'); if (app_bundle != nullptr) *app_bundle = '\0'; #endif /* WITH_COCOA */ char *s = strrchr(tmp, PATHSEPCHAR); if (s != nullptr) { *s = '\0'; if (chdir(tmp) != 0) { DEBUG(misc, 0, "Directory with the binary does not exist?"); } else { success = true; } } return success; } /** * Whether we should scan the working directory. * It should not be scanned if it's the root or * the home directory as in both cases a big data * directory can cause huge amounts of unrelated * files scanned. Furthermore there are nearly no * use cases for the home/root directory to have * OpenTTD directories. * @return true if it should be scanned. */ bool DoScanWorkingDirectory() { /* No working directory, so nothing to do. */ if (_searchpaths[SP_WORKING_DIR].empty()) return false; /* Working directory is root, so do nothing. */ if (_searchpaths[SP_WORKING_DIR] == PATHSEP) return false; /* No personal/home directory, so the working directory won't be that. */ if (_searchpaths[SP_PERSONAL_DIR].empty()) return true; std::string tmp = _searchpaths[SP_WORKING_DIR] + PERSONAL_DIR; AppendPathSeparator(tmp); return _searchpaths[SP_PERSONAL_DIR] != tmp; } /** * Determine the base (personal dir and game data dir) paths * @param exe the path to the executable */ void DetermineBasePaths(const char *exe) { std::string tmp; #if defined(WITH_XDG_BASEDIR) && defined(WITH_PERSONAL_DIR) const char *xdg_data_home = xdgDataHome(nullptr); tmp = xdg_data_home; tmp += PATHSEP; tmp += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR; free(xdg_data_home); AppendPathSeparator(tmp); _searchpaths[SP_PERSONAL_DIR_XDG] = tmp; #endif #if defined(OS2) || !defined(WITH_PERSONAL_DIR) _searchpaths[SP_PERSONAL_DIR].clear(); #else #ifdef __HAIKU__ BPath path; find_directory(B_USER_SETTINGS_DIRECTORY, &path); const char *homedir = stredup(path.Path()); #else /* getenv is highly unsafe; duplicate it as soon as possible, * or at least before something else touches the environment * variables in any way. It can also contain all kinds of * unvalidated data we rather not want internally. */ const char *homedir = getenv("HOME"); if (homedir != nullptr) { homedir = stredup(homedir); } if (homedir == nullptr) { const struct passwd *pw = getpwuid(getuid()); homedir = (pw == nullptr) ? nullptr : stredup(pw->pw_dir); } #endif if (homedir != nullptr) { ValidateString(homedir); tmp = homedir; tmp += PATHSEP; tmp += PERSONAL_DIR; AppendPathSeparator(tmp); _searchpaths[SP_PERSONAL_DIR] = tmp; free(homedir); } else { _searchpaths[SP_PERSONAL_DIR].clear(); } #endif #if defined(WITH_SHARED_DIR) tmp = SHARED_DIR; AppendPathSeparator(tmp); _searchpaths[SP_SHARED_DIR] = tmp; #else _searchpaths[SP_SHARED_DIR].clear(); #endif char cwd[MAX_PATH]; if (getcwd(cwd, MAX_PATH) == nullptr) *cwd = '\0'; if (_config_file.empty()) { /* Get the path to working directory of OpenTTD. */ tmp = cwd; AppendPathSeparator(tmp); _searchpaths[SP_WORKING_DIR] = tmp; _do_scan_working_directory = DoScanWorkingDirectory(); } else { /* Use the folder of the config file as working directory. */ size_t end = _config_file.find_last_of(PATHSEPCHAR); if (end == std::string::npos) { /* _config_file is not in a folder, so use current directory. */ tmp = cwd; AppendPathSeparator(tmp); _searchpaths[SP_WORKING_DIR] = tmp; } else { _searchpaths[SP_WORKING_DIR] = _config_file.substr(0, end + 1); } } /* Change the working directory to that one of the executable */ if (ChangeWorkingDirectoryToExecutable(exe)) { char buf[MAX_PATH]; if (getcwd(buf, lengthof(buf)) == nullptr) { tmp.clear(); } else { tmp = buf; } AppendPathSeparator(tmp); _searchpaths[SP_BINARY_DIR] = tmp; } else { _searchpaths[SP_BINARY_DIR].clear(); } if (cwd[0] != '\0') { /* Go back to the current working directory. */ if (chdir(cwd) != 0) { DEBUG(misc, 0, "Failed to return to working directory!"); } } #if !defined(GLOBAL_DATA_DIR) _searchpaths[SP_INSTALLATION_DIR].clear(); #else tmp = GLOBAL_DATA_DIR; AppendPathSeparator(tmp); _searchpaths[SP_INSTALLATION_DIR] = tmp; #endif #ifdef WITH_COCOA extern void cocoaSetApplicationBundleDir(); cocoaSetApplicationBundleDir(); #else _searchpaths[SP_APPLICATION_BUNDLE_DIR].clear(); #endif } #endif /* defined(_WIN32) */ std::string _personal_dir; /** * Acquire the base paths (personal dir and game data dir), * fill all other paths (save dir, autosave dir etc) and * make the save and scenario directories. * @param exe the path from the current path to the executable */ void DeterminePaths(const char *exe) { DetermineBasePaths(exe); #if defined(WITH_XDG_BASEDIR) && defined(WITH_PERSONAL_DIR) const char *xdg_config_home = xdgConfigHome(nullptr); std::string config_home(xdg_config_home); config_home += PATHSEP; config_home += PERSONAL_DIR[0] == '.' ? &PERSONAL_DIR[1] : PERSONAL_DIR; free(xdg_config_home); AppendPathSeparator(config_home); #endif Searchpath sp; FOR_ALL_SEARCHPATHS(sp) { if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue; DEBUG(misc, 4, "%s added as search path", _searchpaths[sp].c_str()); } std::string config_dir; if (!_config_file.empty()) { config_dir = _searchpaths[SP_WORKING_DIR]; } else { std::string personal_dir = FioFindFullPath(BASE_DIR, "openttd.cfg"); if (!personal_dir.empty()) { auto end = personal_dir.find_last_of(PATHSEPCHAR); if (end != std::string::npos) personal_dir.erase(end + 1); config_dir = personal_dir; } else { #if defined(WITH_XDG_BASEDIR) && defined(WITH_PERSONAL_DIR) /* No previous configuration file found. Use the configuration folder from XDG. */ config_dir = config_home; #else static const Searchpath new_openttd_cfg_order[] = { SP_PERSONAL_DIR, SP_BINARY_DIR, SP_WORKING_DIR, SP_SHARED_DIR, SP_INSTALLATION_DIR }; config_dir.clear(); for (uint i = 0; i < lengthof(new_openttd_cfg_order); i++) { if (IsValidSearchPath(new_openttd_cfg_order[i])) { config_dir = _searchpaths[new_openttd_cfg_order[i]]; break; } } #endif } _config_file = config_dir + "openttd.cfg"; } DEBUG(misc, 3, "%s found as config directory", config_dir.c_str()); _highscore_file = config_dir + "hs.dat"; extern std::string _hotkeys_file; _hotkeys_file = config_dir + "hotkeys.cfg"; extern std::string _windows_file; _windows_file = config_dir + "windows.cfg"; #if defined(WITH_XDG_BASEDIR) && defined(WITH_PERSONAL_DIR) if (config_dir == config_home) { /* We are using the XDG configuration home for the config file, * then store the rest in the XDG data home folder. */ _personal_dir = _searchpaths[SP_PERSONAL_DIR_XDG]; } else #endif { _personal_dir = config_dir; } /* Make the necessary folders */ FioCreateDirectory(config_dir); #if defined(WITH_PERSONAL_DIR) FioCreateDirectory(_personal_dir); #endif DEBUG(misc, 3, "%s found as personal directory", _personal_dir.c_str()); static const Subdirectory default_subdirs[] = { SAVE_DIR, AUTOSAVE_DIR, SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR, SCREENSHOT_DIR }; for (uint i = 0; i < lengthof(default_subdirs); i++) { FioCreateDirectory(_personal_dir + _subdirs[default_subdirs[i]]); } /* If we have network we make a directory for the autodownloading of content */ _searchpaths[SP_AUTODOWNLOAD_DIR] = _personal_dir + "content_download" PATHSEP; FioCreateDirectory(_searchpaths[SP_AUTODOWNLOAD_DIR]); /* Create the directory for each of the types of content */ const Subdirectory dirs[] = { SCENARIO_DIR, HEIGHTMAP_DIR, BASESET_DIR, NEWGRF_DIR, AI_DIR, AI_LIBRARY_DIR, GAME_DIR, GAME_LIBRARY_DIR }; for (uint i = 0; i < lengthof(dirs); i++) { FioCreateDirectory(FioGetDirectory(SP_AUTODOWNLOAD_DIR, dirs[i])); } extern std::string _log_file; _log_file = _personal_dir + "openttd.log"; } /** * Sanitizes a filename, i.e. removes all illegal characters from it. * @param filename the "\0" terminated filename */ void SanitizeFilename(char *filename) { for (; *filename != '\0'; filename++) { switch (*filename) { /* The following characters are not allowed in filenames * on at least one of the supported operating systems: */ case ':': case '\\': case '*': case '?': case '/': case '<': case '>': case '|': case '"': *filename = '_'; break; } } } /** * Load a file into memory. * @param filename Name of the file to load. * @param[out] lenp Length of loaded data. * @param maxsize Maximum size to load. * @return Pointer to new memory containing the loaded data, or \c nullptr if loading failed. * @note If \a maxsize less than the length of the file, loading fails. */ std::unique_ptr ReadFileToMem(const std::string &filename, size_t &lenp, size_t maxsize) { FILE *in = fopen(filename.c_str(), "rb"); if (in == nullptr) return nullptr; FileCloser fc(in); fseek(in, 0, SEEK_END); size_t len = ftell(in); fseek(in, 0, SEEK_SET); if (len > maxsize) return nullptr; /* std::unique_ptr assumes new/delete unless a custom deleter is supplied. * As we don't want to have to carry that deleter all over the place, use * new directly to allocate the memory instead of malloc. */ std::unique_ptr mem(static_cast(::operator new(len + 1))); mem.get()[len] = 0; if (fread(mem.get(), len, 1, in) != 1) return nullptr; lenp = len; return mem; } /** * Helper to see whether a given filename matches the extension. * @param extension The extension to look for. * @param filename The filename to look in for the extension. * @return True iff the extension is nullptr, or the filename ends with it. */ static bool MatchesExtension(const char *extension, const char *filename) { if (extension == nullptr) return true; const char *ext = strrchr(filename, extension[0]); return ext != nullptr && strcasecmp(ext, extension) == 0; } /** * Scan a single directory (and recursively its children) and add * any graphics sets that are found. * @param fs the file scanner to add the files to * @param extension the extension of files to search for. * @param path full path we're currently at * @param basepath_length from where in the path are we 'based' on the search path * @param recursive whether to recursively search the sub directories */ static uint ScanPath(FileScanner *fs, const char *extension, const char *path, size_t basepath_length, bool recursive) { extern bool FiosIsValidFile(const char *path, const struct dirent *ent, struct stat *sb); uint num = 0; struct stat sb; struct dirent *dirent; DIR *dir; if (path == nullptr || (dir = ttd_opendir(path)) == nullptr) return 0; while ((dirent = readdir(dir)) != nullptr) { const char *d_name = FS2OTTD(dirent->d_name); if (!FiosIsValidFile(path, dirent, &sb)) continue; std::string filename(path); filename += d_name; if (S_ISDIR(sb.st_mode)) { /* Directory */ if (!recursive) continue; if (strcmp(d_name, ".") == 0 || strcmp(d_name, "..") == 0) continue; AppendPathSeparator(filename); num += ScanPath(fs, extension, filename.c_str(), basepath_length, recursive); } else if (S_ISREG(sb.st_mode)) { /* File */ if (MatchesExtension(extension, filename.c_str()) && fs->AddFile(filename.c_str(), basepath_length, nullptr)) num++; } } closedir(dir); return num; } /** * Scan the given tar and add graphics sets when it finds one. * @param fs the file scanner to scan for * @param extension the extension of files to search for. * @param tar the tar to search in. */ static uint ScanTar(FileScanner *fs, const char *extension, TarFileList::iterator tar) { uint num = 0; const char *filename = (*tar).first.c_str(); if (MatchesExtension(extension, filename) && fs->AddFile(filename, 0, (*tar).second.tar_filename)) num++; return num; } /** * Scan for files with the given extension in the given search path. * @param extension the extension of files to search for. * @param sd the sub directory to search in. * @param tars whether to search in the tars too. * @param recursive whether to search recursively * @return the number of found files, i.e. the number of times that * AddFile returned true. */ uint FileScanner::Scan(const char *extension, Subdirectory sd, bool tars, bool recursive) { this->subdir = sd; Searchpath sp; TarFileList::iterator tar; uint num = 0; FOR_ALL_SEARCHPATHS(sp) { /* Don't search in the working directory */ if (sp == SP_WORKING_DIR && !_do_scan_working_directory) continue; std::string path = FioGetDirectory(sp, sd); num += ScanPath(this, extension, path.c_str(), path.size(), recursive); } if (tars && sd != NO_DIRECTORY) { FOR_ALL_TARS(tar, sd) { num += ScanTar(this, extension, tar); } } switch (sd) { case BASESET_DIR: num += this->Scan(extension, OLD_GM_DIR, tars, recursive); FALLTHROUGH; case NEWGRF_DIR: num += this->Scan(extension, OLD_DATA_DIR, tars, recursive); break; default: break; } return num; } /** * Scan for files with the given extension in the given search path. * @param extension the extension of files to search for. * @param directory the sub directory to search in. * @param recursive whether to search recursively * @return the number of found files, i.e. the number of times that * AddFile returned true. */ uint FileScanner::Scan(const char *extension, const char *directory, bool recursive) { std::string path(directory); AppendPathSeparator(path); return ScanPath(this, extension, path.c_str(), path.size(), recursive); }