| |
| |
|
|
| #include <algorithm> |
| #include <sstream> |
| #include <unordered_map> |
|
|
| #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 <shlobj.h> |
| #else |
| #include <cstdlib> |
| #include <pwd.h> |
| #include <sys/types.h> |
| #include <unistd.h> |
| #endif |
|
|
| #ifdef __APPLE__ |
| #include <sys/param.h> |
|
|
| |
| |
| |
| |
| #ifndef __clang__ |
| #define availability(...) |
| #endif |
| #include <CoreFoundation/CFBundle.h> |
| #include <CoreFoundation/CFString.h> |
| #include <CoreFoundation/CFURL.h> |
| #ifdef availability |
| #undef availability |
| #endif |
| #endif |
|
|
| #ifndef MAX_PATH |
| #ifdef _WIN32 |
| |
| #define MAX_PATH 260 |
| #else |
| |
| #define MAX_PATH 1024 |
| #endif |
| #endif |
|
|
| namespace Common::FS { |
|
|
| namespace fs = std::filesystem; |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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& GetYuzuPathImpl(YuzuPath yuzu_path) { |
| return yuzu_paths.at(yuzu_path); |
| } |
|
|
| void SetYuzuPathImpl(YuzuPath yuzu_path, const fs::path& new_path) { |
| yuzu_paths.insert_or_assign(yuzu_path, new_path); |
| } |
|
|
| void Reinitialize(fs::path yuzu_path = {}) { |
| fs::path yuzu_path_cache; |
| fs::path yuzu_path_config; |
|
|
| #ifdef _WIN32 |
| #ifdef YUZU_ENABLE_PORTABLE |
| yuzu_path = GetExeDirectory() / PORTABLE_DIR; |
| #endif |
| if (!IsDir(yuzu_path)) { |
| yuzu_path = GetAppDataRoamingDirectory() / YUZU_DIR; |
| } |
|
|
| yuzu_path_cache = yuzu_path / CACHE_DIR; |
| yuzu_path_config = yuzu_path / CONFIG_DIR; |
| #elif ANDROID |
| ASSERT(!yuzu_path.empty()); |
| yuzu_path_cache = yuzu_path / CACHE_DIR; |
| yuzu_path_config = yuzu_path / CONFIG_DIR; |
| #else |
| #ifdef YUZU_ENABLE_PORTABLE |
| yuzu_path = GetCurrentDir() / PORTABLE_DIR; |
| #endif |
| if (Exists(yuzu_path) && IsDir(yuzu_path)) { |
| yuzu_path_cache = yuzu_path / CACHE_DIR; |
| yuzu_path_config = yuzu_path / CONFIG_DIR; |
| } else { |
| yuzu_path = GetDataDirectory("XDG_DATA_HOME") / YUZU_DIR; |
| yuzu_path_cache = GetDataDirectory("XDG_CACHE_HOME") / YUZU_DIR; |
| yuzu_path_config = GetDataDirectory("XDG_CONFIG_HOME") / YUZU_DIR; |
| } |
| #endif |
|
|
| GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path); |
| GenerateYuzuPath(YuzuPath::AmiiboDir, yuzu_path / AMIIBO_DIR); |
| GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path_cache); |
| GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path_config); |
| GenerateYuzuPath(YuzuPath::CrashDumpsDir, yuzu_path / CRASH_DUMPS_DIR); |
| GenerateYuzuPath(YuzuPath::DumpDir, yuzu_path / DUMP_DIR); |
| GenerateYuzuPath(YuzuPath::KeysDir, yuzu_path / KEYS_DIR); |
| GenerateYuzuPath(YuzuPath::LoadDir, yuzu_path / LOAD_DIR); |
| GenerateYuzuPath(YuzuPath::LogDir, yuzu_path / LOG_DIR); |
| GenerateYuzuPath(YuzuPath::NANDDir, yuzu_path / NAND_DIR); |
| GenerateYuzuPath(YuzuPath::PlayTimeDir, yuzu_path / PLAY_TIME_DIR); |
| GenerateYuzuPath(YuzuPath::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); |
| GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); |
| GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); |
| GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); |
| GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); |
| } |
|
|
| private: |
| PathManagerImpl() { |
| Reinitialize(); |
| } |
|
|
| ~PathManagerImpl() = default; |
|
|
| void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) { |
| void(FS::CreateDir(new_path)); |
|
|
| SetYuzuPathImpl(yuzu_path, new_path); |
| } |
|
|
| std::unordered_map<YuzuPath, fs::path> yuzu_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& GetYuzuPath(YuzuPath yuzu_path) { |
| return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path); |
| } |
|
|
| std::string GetYuzuPathString(YuzuPath yuzu_path) { |
| return PathToUTF8String(GetYuzuPath(yuzu_path)); |
| } |
|
|
| void SetYuzuPath(YuzuPath yuzu_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().SetYuzuPathImpl(yuzu_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]; |
|
|
| |
| 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 |
|
|
| |
|
|
| 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 <typename F> |
| 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<std::string_view> SplitPathComponents(std::string_view filename) { |
| std::vector<std::string_view> components; |
| ForEachPathComponent(filename, [&](auto component) { components.emplace_back(component); }); |
|
|
| return components; |
| } |
|
|
| std::vector<std::string> SplitPathComponentsCopy(std::string_view filename) { |
| std::vector<std::string> 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 |
|
|
| 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 |
| |
| 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); |
| } |
|
|
| } |
|
|