// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #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 * SuyuPath enums to real filesystem paths. * This class provides 2 functions: GetSuyuPathImpl and SetSuyuPathImpl. * These are used by GetSuyuPath and SetSuyuPath respectively to get or modify * the path mapped by the SuyuPath 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& GetSuyuPathImpl(SuyuPath suyu_path) { return suyu_paths.at(suyu_path); } void SetSuyuPathImpl(SuyuPath suyu_path, const fs::path& new_path) { suyu_paths.insert_or_assign(suyu_path, new_path); } void Reinitialize(fs::path suyu_path = {}) { fs::path suyu_path_cache; fs::path suyu_path_config; #ifdef _WIN32 #ifdef SUYU_ENABLE_PORTABLE suyu_path = GetExeDirectory() / PORTABLE_DIR; #endif if (!IsDir(suyu_path)) { suyu_path = GetAppDataRoamingDirectory() / SUYU_DIR; } suyu_path_cache = suyu_path / CACHE_DIR; suyu_path_config = suyu_path / CONFIG_DIR; #elif ANDROID ASSERT(!suyu_path.empty()); suyu_path_cache = suyu_path / CACHE_DIR; suyu_path_config = suyu_path / CONFIG_DIR; #else #ifdef SUYU_ENABLE_PORTABLE suyu_path = GetCurrentDir() / PORTABLE_DIR; #endif if (Exists(suyu_path) && IsDir(suyu_path)) { suyu_path_cache = suyu_path / CACHE_DIR; suyu_path_config = suyu_path / CONFIG_DIR; } else { suyu_path = GetDataDirectory("XDG_DATA_HOME") / SUYU_DIR; suyu_path_cache = GetDataDirectory("XDG_CACHE_HOME") / SUYU_DIR; suyu_path_config = GetDataDirectory("XDG_CONFIG_HOME") / SUYU_DIR; } #endif GenerateSuyuPath(SuyuPath::SuyuDir, suyu_path); GenerateSuyuPath(SuyuPath::AmiiboDir, suyu_path / AMIIBO_DIR); GenerateSuyuPath(SuyuPath::CacheDir, suyu_path_cache); GenerateSuyuPath(SuyuPath::ConfigDir, suyu_path_config); GenerateSuyuPath(SuyuPath::CrashDumpsDir, suyu_path / CRASH_DUMPS_DIR); GenerateSuyuPath(SuyuPath::DumpDir, suyu_path / DUMP_DIR); GenerateSuyuPath(SuyuPath::KeysDir, suyu_path / KEYS_DIR); GenerateSuyuPath(SuyuPath::LoadDir, suyu_path / LOAD_DIR); GenerateSuyuPath(SuyuPath::LogDir, suyu_path / LOG_DIR); GenerateSuyuPath(SuyuPath::NANDDir, suyu_path / NAND_DIR); GenerateSuyuPath(SuyuPath::PlayTimeDir, suyu_path / PLAY_TIME_DIR); GenerateSuyuPath(SuyuPath::ScreenshotsDir, suyu_path / SCREENSHOTS_DIR); GenerateSuyuPath(SuyuPath::SDMCDir, suyu_path / SDMC_DIR); GenerateSuyuPath(SuyuPath::ShaderDir, suyu_path / SHADER_DIR); GenerateSuyuPath(SuyuPath::TASDir, suyu_path / TAS_DIR); GenerateSuyuPath(SuyuPath::IconsDir, suyu_path / ICONS_DIR); } private: PathManagerImpl() { Reinitialize(); } ~PathManagerImpl() = default; void GenerateSuyuPath(SuyuPath suyu_path, const fs::path& new_path) { void(FS::CreateDir(new_path)); SetSuyuPathImpl(suyu_path, new_path); } std::unordered_map suyu_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& GetSuyuPath(SuyuPath suyu_path) { return PathManagerImpl::GetInstance().GetSuyuPathImpl(suyu_path); } std::string GetSuyuPathString(SuyuPath suyu_path) { return PathToUTF8String(GetSuyuPath(suyu_path)); } void SetSuyuPath(SuyuPath suyu_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().SetSuyuPathImpl(suyu_path, new_path); } #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); } std::string_view GetFilename(std::string_view path) { const auto name_index = path.find_last_of("\\/"); if (name_index == std::string_view::npos) { return {}; } return path.substr(name_index + 1); } std::string_view GetExtensionFromFilename(std::string_view name) { const std::size_t index = name.rfind('.'); if (index == std::string_view::npos) { return {}; } return name.substr(index + 1); } } // namespace Common::FS