// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include "common/assert.h" #include "common/fs/fs.h" #ifdef ANDROID #include "common/fs/fs_android.h" #endif #include "common/fs/fs_paths.h" #include "common/fs/path_util.h" #include "common/logging/log.h" #ifdef _WIN32 #include // Used in GetExeDirectory() #else #include // Used in Get(Home/Data)Directory() #include // Used in GetHomeDirectory() #include // Used in GetHomeDirectory() #include // Used in GetDataDirectory() #endif #ifdef __APPLE__ #include // Used in GetBundleDirectory() // CFURL contains __attribute__ directives that gcc does not know how to parse, so we need to just // ignore them if we're not using clang. The macro is only used to prevent linking against // functions that don't exist on older versions of macOS, and the worst case scenario is a linker // error, so this is perfectly safe, just inconvenient. #ifndef __clang__ #define availability(...) #endif #include // Used in GetBundleDirectory() #include // Used in GetBundleDirectory() #include // Used in GetBundleDirectory() #ifdef availability #undef availability #endif #endif #ifndef MAX_PATH #ifdef _WIN32 // This is the maximum number of UTF-16 code units permissible in Windows file paths #define MAX_PATH 260 #else // This is the maximum number of UTF-8 code units permissible in all other OSes' file paths #define MAX_PATH 1024 #endif #endif namespace Common::FS { namespace fs = std::filesystem; /** * The PathManagerImpl is a singleton allowing to manage the mapping of * EdenPath enums to real filesystem paths. * This class provides 2 functions: GetEdenPathImpl and SetEdenPathImpl. * These are used by GetEdenPath and SetEdenPath respectively to get or modify * the path mapped by the EdenPath enum. */ class PathManagerImpl { public: static PathManagerImpl& GetInstance() { static PathManagerImpl path_manager_impl; return path_manager_impl; } PathManagerImpl(const PathManagerImpl&) = delete; PathManagerImpl& operator=(const PathManagerImpl&) = delete; PathManagerImpl(PathManagerImpl&&) = delete; PathManagerImpl& operator=(PathManagerImpl&&) = delete; [[nodiscard]] const fs::path& GetEdenPathImpl(EdenPath eden_path) { return eden_paths.at(eden_path); } [[nodiscard]] const fs::path& GetLegacyPathImpl(LegacyPath legacy_path) { return legacy_paths.at(legacy_path); } void CreateEdenPaths() { std::for_each(eden_paths.begin(), eden_paths.end(), [](auto &path) { void(FS::CreateDir(path.second)); }); } void SetEdenPathImpl(EdenPath eden_path, const fs::path& new_path) { eden_paths.insert_or_assign(eden_path, new_path); } void SetLegacyPathImpl(LegacyPath legacy_path, const fs::path& new_path) { legacy_paths.insert_or_assign(legacy_path, new_path); } /// In non-android devices, the current directory will first search for "user" /// if such directory (and it must be a directory) is found, that takes priority /// over the global configuration directory (in other words, portable directories /// take priority over the global ones, always) /// On Android, the behaviour is to look for the current directory only. void Reinitialize(fs::path eden_path = {}) { fs::path eden_path_cache; fs::path eden_path_config; #ifdef _WIN32 // User directory takes priority over global %AppData% directory eden_path = GetExeDirectory() / PORTABLE_DIR; if (!Exists(eden_path) || !IsDir(eden_path)) { eden_path = GetAppDataRoamingDirectory() / EDEN_DIR; } eden_path_cache = eden_path / CACHE_DIR; eden_path_config = eden_path / CONFIG_DIR; #define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(LegacyPath::titleName##Dir, GetAppDataRoamingDirectory() / upperName##_DIR); \ GenerateLegacyPath(LegacyPath::titleName##ConfigDir, GetAppDataRoamingDirectory() / upperName##_DIR / CONFIG_DIR); \ GenerateLegacyPath(LegacyPath::titleName##CacheDir, GetAppDataRoamingDirectory() / upperName##_DIR / CACHE_DIR); LEGACY_PATH(Citron, CITRON) LEGACY_PATH(Sudachi, SUDACHI) LEGACY_PATH(Yuzu, YUZU) LEGACY_PATH(Suyu, SUYU) #undef LEGACY_PATH #elif ANDROID ASSERT(!eden_path.empty()); eden_path_cache = eden_path / CACHE_DIR; eden_path_config = eden_path / CONFIG_DIR; #else eden_path = GetCurrentDir() / PORTABLE_DIR; if (!Exists(eden_path) || !IsDir(eden_path)) { eden_path = GetDataDirectory("XDG_DATA_HOME") / EDEN_DIR; eden_path_cache = GetDataDirectory("XDG_CACHE_HOME") / EDEN_DIR; eden_path_config = GetDataDirectory("XDG_CONFIG_HOME") / EDEN_DIR; } else { eden_path_cache = eden_path / CACHE_DIR; eden_path_config = eden_path / CONFIG_DIR; } #define LEGACY_PATH(titleName, upperName) GenerateLegacyPath(LegacyPath::titleName##Dir, GetDataDirectory("XDG_DATA_HOME") / upperName##_DIR); \ GenerateLegacyPath(LegacyPath::titleName##ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / upperName##_DIR); \ GenerateLegacyPath(LegacyPath::titleName##CacheDir, GetDataDirectory("XDG_CACHE_HOME") / upperName##_DIR); LEGACY_PATH(Citron, CITRON) LEGACY_PATH(Sudachi, SUDACHI) LEGACY_PATH(Yuzu, YUZU) LEGACY_PATH(Suyu, SUYU) #undef LEGACY_PATH #endif GenerateEdenPath(EdenPath::EdenDir, eden_path); GenerateEdenPath(EdenPath::AmiiboDir, eden_path / AMIIBO_DIR); GenerateEdenPath(EdenPath::CacheDir, eden_path_cache); GenerateEdenPath(EdenPath::ConfigDir, eden_path_config); GenerateEdenPath(EdenPath::CrashDumpsDir, eden_path / CRASH_DUMPS_DIR); GenerateEdenPath(EdenPath::DumpDir, eden_path / DUMP_DIR); GenerateEdenPath(EdenPath::KeysDir, eden_path / KEYS_DIR); GenerateEdenPath(EdenPath::LoadDir, eden_path / LOAD_DIR); GenerateEdenPath(EdenPath::LogDir, eden_path / LOG_DIR); GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR); GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR); GenerateEdenPath(EdenPath::ScreenshotsDir, eden_path / SCREENSHOTS_DIR); GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR); GenerateEdenPath(EdenPath::ShaderDir, eden_path / SHADER_DIR); GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_DIR); GenerateEdenPath(EdenPath::IconsDir, eden_path / ICONS_DIR); } private: PathManagerImpl() { Reinitialize(); } ~PathManagerImpl() = default; void GenerateEdenPath(EdenPath eden_path, const fs::path& new_path) { // Defer path creation SetEdenPathImpl(eden_path, new_path); } void GenerateLegacyPath(LegacyPath legacy_path, const fs::path& new_path) { SetLegacyPathImpl(legacy_path, new_path); } std::unordered_map eden_paths; std::unordered_map legacy_paths; }; bool ValidatePath(const fs::path& path) { if (path.empty()) { LOG_ERROR(Common_Filesystem, "Input path is empty, path={}", PathToUTF8String(path)); return false; } #ifdef _WIN32 if (path.u16string().size() >= MAX_PATH) { LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path)); return false; } #else if (path.u8string().size() >= MAX_PATH) { LOG_ERROR(Common_Filesystem, "Input path is too long, path={}", PathToUTF8String(path)); return false; } #endif return true; } fs::path ConcatPath(const fs::path& first, const fs::path& second) { const bool second_has_dir_sep = IsDirSeparator(second.u8string().front()); if (!second_has_dir_sep) { return (first / second).lexically_normal(); } fs::path concat_path = first; concat_path += second; return concat_path.lexically_normal(); } fs::path ConcatPathSafe(const fs::path& base, const fs::path& offset) { const auto concatenated_path = ConcatPath(base, offset); if (!IsPathSandboxed(base, concatenated_path)) { return base; } return concatenated_path; } bool IsPathSandboxed(const fs::path& base, const fs::path& path) { const auto base_string = RemoveTrailingSeparators(base.lexically_normal()).u8string(); const auto path_string = RemoveTrailingSeparators(path.lexically_normal()).u8string(); if (path_string.size() < base_string.size()) { return false; } return base_string.compare(0, base_string.size(), path_string, 0, base_string.size()) == 0; } bool IsDirSeparator(char character) { return character == '/' || character == '\\'; } bool IsDirSeparator(char8_t character) { return character == u8'/' || character == u8'\\'; } fs::path RemoveTrailingSeparators(const fs::path& path) { if (path.empty()) { return path; } auto string_path = path.u8string(); while (IsDirSeparator(string_path.back())) { string_path.pop_back(); } return fs::path{string_path}; } void SetAppDirectory(const std::string& app_directory) { PathManagerImpl::GetInstance().Reinitialize(app_directory); } const fs::path& GetEdenPath(EdenPath eden_path) { return PathManagerImpl::GetInstance().GetEdenPathImpl(eden_path); } const std::filesystem::path& GetLegacyPath(LegacyPath legacy_path) { return PathManagerImpl::GetInstance().GetLegacyPathImpl(legacy_path); } std::string GetEdenPathString(EdenPath eden_path) { return PathToUTF8String(GetEdenPath(eden_path)); } std::string GetLegacyPathString(LegacyPath legacy_path) { return PathToUTF8String(GetLegacyPath(legacy_path)); } void SetEdenPath(EdenPath eden_path, const fs::path& new_path) { if (!FS::IsDir(new_path)) { LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} is not a directory", PathToUTF8String(new_path)); return; } PathManagerImpl::GetInstance().SetEdenPathImpl(eden_path, new_path); } void CreateEdenPaths() { PathManagerImpl::GetInstance().CreateEdenPaths(); } #ifdef _WIN32 fs::path GetExeDirectory() { wchar_t exe_path[MAX_PATH]; if (GetModuleFileNameW(nullptr, exe_path, MAX_PATH) == 0) { LOG_ERROR(Common_Filesystem, "Failed to get the path to the executable of the current process"); } return fs::path{exe_path}.parent_path(); } fs::path GetAppDataRoamingDirectory() { PWSTR appdata_roaming_path = nullptr; SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &appdata_roaming_path); auto fs_appdata_roaming_path = fs::path{appdata_roaming_path}; CoTaskMemFree(appdata_roaming_path); if (fs_appdata_roaming_path.empty()) { LOG_ERROR(Common_Filesystem, "Failed to get the path to the %APPDATA% directory"); } return fs_appdata_roaming_path; } #else fs::path GetHomeDirectory() { const char* home_env_var = getenv("HOME"); if (home_env_var) { return fs::path{home_env_var}; } LOG_INFO(Common_Filesystem, "$HOME is not defined in the environment variables, " "attempting to query passwd to get the home path of the current user"); const auto* pw = getpwuid(getuid()); if (!pw) { LOG_ERROR(Common_Filesystem, "Failed to get the home path of the current user"); return {}; } return fs::path{pw->pw_dir}; } fs::path GetDataDirectory(const std::string& env_name) { const char* data_env_var = getenv(env_name.c_str()); if (data_env_var) { return fs::path{data_env_var}; } if (env_name == "XDG_DATA_HOME") { return GetHomeDirectory() / ".local/share"; } else if (env_name == "XDG_CACHE_HOME") { return GetHomeDirectory() / ".cache"; } else if (env_name == "XDG_CONFIG_HOME") { return GetHomeDirectory() / ".config"; } return {}; } #endif #ifdef __APPLE__ fs::path GetBundleDirectory() { char app_bundle_path[MAXPATHLEN]; // Get the main bundle for the app CFURLRef bundle_ref = CFBundleCopyBundleURL(CFBundleGetMainBundle()); CFStringRef bundle_path = CFURLCopyFileSystemPath(bundle_ref, kCFURLPOSIXPathStyle); CFStringGetFileSystemRepresentation(bundle_path, app_bundle_path, sizeof(app_bundle_path)); CFRelease(bundle_ref); CFRelease(bundle_path); return fs::path{app_bundle_path}; } #endif // vvvvvvvvvv Deprecated vvvvvvvvvv // std::string_view RemoveTrailingSlash(std::string_view path) { if (path.empty()) { return path; } if (path.back() == '\\' || path.back() == '/') { path.remove_suffix(1); return path; } return path; } template static void ForEachPathComponent(std::string_view filename, F&& cb) { const char* component_begin = filename.data(); const char* const end = component_begin + filename.size(); for (const char* it = component_begin; it != end; ++it) { const char c = *it; if (c == '\\' || c == '/') { if (component_begin != it) { cb(std::string_view{component_begin, it}); } component_begin = it + 1; } } if (component_begin != end) { cb(std::string_view{component_begin, end}); } } std::vector SplitPathComponents(std::string_view filename) { std::vector components; ForEachPathComponent(filename, [&](auto component) { components.emplace_back(component); }); return components; } std::vector SplitPathComponentsCopy(std::string_view filename) { std::vector components; ForEachPathComponent(filename, [&](auto component) { components.emplace_back(component); }); return components; } std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { std::string path(path_); #ifdef ANDROID if (Android::IsContentUri(path)) { return path; } #endif // ANDROID char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/'; if (directory_separator == DirectorySeparator::PlatformDefault) { #ifdef _WIN32 type1 = '/'; type2 = '\\'; #endif } std::replace(path.begin(), path.end(), type1, type2); auto start = path.begin(); #ifdef _WIN32 // allow network paths which start with a double backslash (e.g. \\server\share) if (start != path.end()) ++start; #endif path.erase(std::unique(start, path.end(), [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }), path.end()); return std::string(RemoveTrailingSlash(path)); } std::string GetParentPath(std::string_view path) { if (path.empty()) { return std::string(path); } #ifdef ANDROID if (path[0] != '/') { std::string path_string{path}; return FS::Android::GetParentDirectory(path_string); } #endif const auto name_bck_index = path.rfind('\\'); const auto name_fwd_index = path.rfind('/'); std::size_t name_index; if (name_bck_index == std::string_view::npos || name_fwd_index == std::string_view::npos) { name_index = (std::min)(name_bck_index, name_fwd_index); } else { name_index = (std::max)(name_bck_index, name_fwd_index); } return std::string(path.substr(0, name_index)); } std::string_view GetPathWithoutTop(std::string_view path) { if (path.empty()) { return path; } while (path[0] == '\\' || path[0] == '/') { path.remove_prefix(1); if (path.empty()) { return path; } } const auto name_bck_index = path.find('\\'); const auto name_fwd_index = path.find('/'); return path.substr((std::min)(name_bck_index, name_fwd_index) + 1); } } // namespace Common::FS