/* Copyright 2023 flyinghead This file is part of Flycast. Flycast 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, either version 2 of the License, or (at your option) any later version. Flycast 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 Flycast. If not, see . */ #include "storage.h" #include "directory.h" #include "stdclass.h" // For macOS std::string os_PrecomposedString(std::string string); namespace hostfs { CustomStorage& customStorage(); #if !defined(__ANDROID__) || defined(LIBRETRO) CustomStorage& customStorage() { class NullStorage : public CustomStorage { public: bool isKnownPath(const std::string& path) override { return false; } std::vector listContent(const std::string& path) override { return std::vector(); } FILE *openFile(const std::string& path, const std::string& mode) override { die("Not implemented"); } std::string getParentPath(const std::string& path) override { die("Not implemented"); } std::string getSubPath(const std::string& reference, const std::string& relative) override { die("Not implemented"); } FileInfo getFileInfo(const std::string& path) override { die("Not implemented"); } void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) override { die("Not implemented"); } }; static NullStorage storage; return storage; } #endif #ifdef _WIN32 static const std::string separators = "/\\"; static const std::string native_separator = "\\"; #else static const std::string separators = "/"; static const std::string native_separator = "/"; #endif class StdStorage : public Storage { public: bool isKnownPath(const std::string& path) override { // assume it's known since standard storage should be asked last return true; } std::vector listContent(const std::string& path) override { if (path.empty()) return listRoots(); DIR *dir = flycast::opendir(path.c_str()); if (dir == nullptr) { WARN_LOG(COMMON, "Cannot read directory '%s' errno 0x%x", path.c_str(), errno); throw StorageException("Can't read directory " + path); } std::vector entries; while (true) { dirent *direntry = flycast::readdir(dir); if (direntry == nullptr) break; FileInfo entry; entry.name = direntry->d_name; #ifdef __APPLE__ entry.name = os_PrecomposedString(entry.name); #endif if (entry.name == "." || entry.name == "..") continue; if (path.find_last_of(separators) != path.size() - 1) entry.path = path + native_separator + entry.name; else entry.path = path + entry.name; // Silently skip unreadable entries if (flycast::access(entry.path.c_str(), R_OK) != 0) continue; bool isDir = false; #ifndef _WIN32 if (direntry->d_type == DT_DIR) isDir = true; else if (direntry->d_type == DT_UNKNOWN || direntry->d_type == DT_LNK) { struct stat st; if (flycast::stat(entry.path.c_str(), &st) != 0) { WARN_LOG(COMMON, "Cannot stat file '%s' errno 0x%x", entry.path.c_str(), errno); continue; } if (S_ISDIR(st.st_mode)) isDir = true; } #else // _WIN32 nowide::wstackstring wname; if (wname.convert(entry.path.c_str())) { DWORD attr = GetFileAttributesW(wname.get()); if (attr != INVALID_FILE_ATTRIBUTES) { if (attr & FILE_ATTRIBUTE_HIDDEN) continue; if (attr & FILE_ATTRIBUTE_DIRECTORY) isDir = true; } } #endif entry.isDirectory = isDir; entries.emplace_back(entry); } flycast::closedir(dir); return entries; } FILE *openFile(const std::string& path, const std::string& mode) override { return nowide::fopen(path.c_str(), mode.c_str()); } std::string getParentPath(const std::string& path) override { auto lastSlash = path.find_last_of(separators); #ifdef _WIN32 if (path.size() == 2 && std::isalpha(static_cast(path[0])) && path[1] == ':') return ""; if (lastSlash == 2 && path.size() == 3 && std::isalpha(static_cast(path[0])) && path[1] == ':') return ""; #else if (lastSlash == 0) return "/"; #endif if (lastSlash == std::string::npos) return "." + native_separator; std::string parentPath = path.substr(0, lastSlash); #ifdef _WIN32 if (parentPath.size() == 2 && std::isalpha(static_cast(parentPath[0])) && parentPath[1] == ':') parentPath += '\\'; #endif if (flycast::access(parentPath.c_str(), R_OK) != 0) return ""; else return parentPath; } std::string getSubPath(const std::string& reference, const std::string& subpath) override { // FIXME this is incorrect if reference isn't a directory return reference + native_separator + subpath; } FileInfo getFileInfo(const std::string& path) override { FileInfo info; info.path = path; size_t slash = get_last_slash_pos(path); if (slash != std::string::npos && slash < path.size() - 1) info.name = path.substr(slash + 1); else info.name = path; info.isWritable = flycast::access(path.c_str(), W_OK) == 0; #ifndef _WIN32 struct stat st; if (flycast::stat(path.c_str(), &st) != 0) { INFO_LOG(COMMON, "Cannot stat file '%s' errno %d", path.c_str(), errno); throw StorageException("Cannot stat " + path); } info.isDirectory = S_ISDIR(st.st_mode); info.size = st.st_size; #else // _WIN32 nowide::wstackstring wname; if (wname.convert(path.c_str())) { WIN32_FILE_ATTRIBUTE_DATA fileAttribs; if (GetFileAttributesExW(wname.get(), GetFileExInfoStandard, &fileAttribs)) { info.isDirectory = (fileAttribs.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; info.size = fileAttribs.nFileSizeLow + ((u64)fileAttribs.nFileSizeHigh << 32); } else { INFO_LOG(COMMON, "Cannot get attrbutes of '%s' error 0x%x", path.c_str(), GetLastError()); throw StorageException("Cannot get attributes of " + path); } } else { throw StorageException("Invalid file name " + path); } #endif return info; } private: std::vector listRoots() { #ifdef _WIN32 std::vector driveList; // List all the drives u32 drives = GetLogicalDrives(); for (int i = 0; i < 32; i++) if ((drives & (1 << i)) != 0) { std::string name = std::string(1, (char)('A' + i)) + ":"; driveList.emplace_back(name, name + "\\", true); } #ifdef TARGET_UWP // Add the home directory to the list of drives as it's not accessible from the root std::string home; const char *home_drive = nowide::getenv("HOMEDRIVE"); if (home_drive != nullptr) home = home_drive; home += nowide::getenv("HOMEPATH"); driveList.emplace_back(home, home, true); #endif return driveList; #elif defined(__ANDROID__) std::vector roots; const char *home = nowide::getenv("FLYCAST_HOME"); while (home != nullptr) { const char *pcolon = strchr(home, ':'); if (pcolon != nullptr) { roots.emplace_back(std::string(home, pcolon - home), std::string(home, pcolon - home), true); home = pcolon + 1; } else { roots.emplace_back(home, home, true); home = nullptr; } } return roots; #else return listContent("/"); #endif } }; static StdStorage stdStorage; std::vector AllStorage::listContent(const std::string& path) { if (path.empty()) { std::vector entries = stdStorage.listContent(path); std::vector customEntries = customStorage().listContent(path); entries.insert(entries.end(), customEntries.begin(), customEntries.end()); return entries; } if (customStorage().isKnownPath(path)) return customStorage().listContent(path); else return stdStorage.listContent(path); } FILE *AllStorage::openFile(const std::string& path, const std::string& mode) { if (customStorage().isKnownPath(path)) return customStorage().openFile(path, mode); else return stdStorage.openFile(path, mode); } std::string AllStorage::getParentPath(const std::string& path) { if (customStorage().isKnownPath(path)) return customStorage().getParentPath(path); else return stdStorage.getParentPath(path); } std::string AllStorage::getSubPath(const std::string& reference, const std::string& subpath) { if (customStorage().isKnownPath(reference)) return customStorage().getSubPath(reference, subpath); else return stdStorage.getSubPath(reference, subpath); } FileInfo AllStorage::getFileInfo(const std::string& path) { if (customStorage().isKnownPath(path)) return customStorage().getFileInfo(path); else return stdStorage.getFileInfo(path); } std::string AllStorage::getDefaultDirectory() { std::string directory; #if defined(__ANDROID__) const char *home = nowide::getenv("FLYCAST_HOME"); if (home != NULL) { const char *pcolon = strchr(home, ':'); if (pcolon != NULL) directory = std::string(home, pcolon - home); else directory = home; } #elif defined(__unix__) || defined(__APPLE__) const char *home = nowide::getenv("HOME"); if (home != NULL) directory = home; #elif defined(_WIN32) if (nowide::getenv("HOMEPATH") != NULL) { const char *home_drive = nowide::getenv("HOMEDRIVE"); if (home_drive != NULL) directory = home_drive; directory += nowide::getenv("HOMEPATH"); } #elif defined(__SWITCH__) directory = "/"; #endif if (directory.empty()) { directory = get_writable_config_path(""); if (directory.empty()) directory = "."; } return directory; } AllStorage& storage() { static AllStorage storage; return storage; } void addStorage(bool isDirectory, bool writeAccess, void (*callback)(bool cancelled, std::string selectedPath)) { customStorage().addStorage(isDirectory, writeAccess, callback); } }