diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 0538b0d52b..d9c5ee3fb7 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -2,16 +2,400 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include + +#include "common/fs/fs.h" +#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 namespace Common::FS { namespace fs = std::filesystem; +/** + * The PathManagerImpl is a singleton allowing to manage the mapping of + * YuzuPath enums to real filesystem paths. + * This class provides 2 functions: GetYuzuPathImpl and SetYuzuPathImpl. + * These are used by GetYuzuPath and SetYuzuPath respectively to get or modify + * the path mapped by the YuzuPath 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& 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); + } + +private: + PathManagerImpl() { +#ifdef _WIN32 + auto yuzu_path = GetExeDirectory() / PORTABLE_DIR; + + if (!IsDir(yuzu_path)) { + yuzu_path = GetAppDataRoamingDirectory() / YUZU_DIR; + } + + GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path); + GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR); + GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR); +#else + auto yuzu_path = GetCurrentDir() / PORTABLE_DIR; + + if (Exists(yuzu_path) && IsDir(yuzu_path)) { + GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path); + GenerateYuzuPath(YuzuPath::CacheDir, yuzu_path / CACHE_DIR); + GenerateYuzuPath(YuzuPath::ConfigDir, yuzu_path / CONFIG_DIR); + } else { + yuzu_path = GetDataDirectory("XDG_DATA_HOME") / YUZU_DIR; + + GenerateYuzuPath(YuzuPath::YuzuDir, yuzu_path); + GenerateYuzuPath(YuzuPath::CacheDir, GetDataDirectory("XDG_CACHE_HOME") / YUZU_DIR); + GenerateYuzuPath(YuzuPath::ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / YUZU_DIR); + } +#endif + + 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::ScreenshotsDir, yuzu_path / SCREENSHOTS_DIR); + GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); + GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); + } + + ~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 yuzu_paths; +}; + std::string PathToUTF8String(const fs::path& path) { const auto utf8_string = path.u8string(); return std::string{utf8_string.begin(), utf8_string.end()}; } +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}; +} + +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]; + + GetModuleFileNameW(nullptr, exe_path, MAX_PATH); + + if (!exe_path) { + 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; +} + +std::vector SplitPathComponents(std::string_view filename) { + std::string copy(filename); + std::replace(copy.begin(), copy.end(), '\\', '/'); + std::vector out; + + std::stringstream stream(copy); + std::string item; + while (std::getline(stream, item, '/')) { + out.push_back(std::move(item)); + } + + return out; +} + +std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { + std::string path(path_); + 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_view GetParentPath(std::string_view path) { + 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 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 diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index 119debaf05..7fa848c1ae 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -5,9 +5,26 @@ #pragma once #include +#include + +#include "common/fs/fs_util.h" namespace Common::FS { +enum class YuzuPath { + YuzuDir, // Where yuzu stores its data. + CacheDir, // Where cached filesystem data is stored. + ConfigDir, // Where config files are stored. + DumpDir, // Where dumped data is stored. + KeysDir, // Where key files are stored. + LoadDir, // Where cheat/mod files are stored. + LogDir, // Where log files are stored. + NANDDir, // Where the emulated NAND is stored. + ScreenshotsDir, // Where yuzu screenshots are stored. + SDMCDir, // Where the emulated SDMC is stored. + ShaderDir, // Where shaders are stored. +}; + /** * Converts a filesystem path to a UTF-8 encoded std::string. * @@ -17,4 +34,252 @@ namespace Common::FS { */ [[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path); +/** + * Concatenates two filesystem paths together. + * + * This is needed since the following occurs when using std::filesystem::path's operator/: + * first: "/first/path" + * second: "/second/path" (Note that the second path has a directory separator in the front) + * first / second yields "/second/path" when the desired result is first/path/second/path + * + * @param first First filesystem path + * @param second Second filesystem path + * + * @returns A concatenated filesystem path. + */ +[[nodiscard]] std::filesystem::path ConcatPath(const std::filesystem::path& first, + const std::filesystem::path& second); + +#ifdef _WIN32 +template +[[nodiscard]] std::filesystem::path ConcatPath(const Path1& first, const Path2& second) { + using ValueType1 = typename Path1::value_type; + using ValueType2 = typename Path2::value_type; + if constexpr (IsChar && IsChar) { + return ConcatPath(ToU8String(first), ToU8String(second)); + } else if constexpr (IsChar && !IsChar) { + return ConcatPath(ToU8String(first), second); + } else if constexpr (!IsChar && IsChar) { + return ConcatPath(first, ToU8String(second)); + } else { + return ConcatPath(std::filesystem::path{first}, std::filesystem::path{second}); + } +} +#endif + +/** + * Safe variant of ConcatPath that takes in a base path and an offset path from the given base path. + * + * If ConcatPath(base, offset) resolves to a path that is sandboxed within the base path, + * this will return the concatenated path. Otherwise this will return the base path. + * + * @param base Base filesystem path + * @param offset Offset filesystem path + * + * @returns A concatenated filesystem path if it is within the base path, + * returns the base path otherwise. + */ +[[nodiscard]] std::filesystem::path ConcatPathSafe(const std::filesystem::path& base, + const std::filesystem::path& offset); + +#ifdef _WIN32 +template +[[nodiscard]] std::filesystem::path ConcatPathSafe(const Path1& base, const Path2& offset) { + using ValueType1 = typename Path1::value_type; + using ValueType2 = typename Path2::value_type; + if constexpr (IsChar && IsChar) { + return ConcatPathSafe(ToU8String(base), ToU8String(offset)); + } else if constexpr (IsChar && !IsChar) { + return ConcatPathSafe(ToU8String(base), offset); + } else if constexpr (!IsChar && IsChar) { + return ConcatPathSafe(base, ToU8String(offset)); + } else { + return ConcatPathSafe(std::filesystem::path{base}, std::filesystem::path{offset}); + } +} +#endif + +/** + * Checks whether a given path is sandboxed within a given base path. + * + * @param base Base filesystem path + * @param path Filesystem path + * + * @returns True if the given path is sandboxed within the given base path, false otherwise. + */ +[[nodiscard]] bool IsPathSandboxed(const std::filesystem::path& base, + const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool IsPathSandboxed(const Path1& base, const Path2& path) { + using ValueType1 = typename Path1::value_type; + using ValueType2 = typename Path2::value_type; + if constexpr (IsChar && IsChar) { + return IsPathSandboxed(ToU8String(base), ToU8String(path)); + } else if constexpr (IsChar && !IsChar) { + return IsPathSandboxed(ToU8String(base), path); + } else if constexpr (!IsChar && IsChar) { + return IsPathSandboxed(base, ToU8String(path)); + } else { + return IsPathSandboxed(std::filesystem::path{base}, std::filesystem::path{path}); + } +} +#endif + +/** + * Checks if a character is a directory separator (either a forward slash or backslash). + * + * @param character Character + * + * @returns True if the character is a directory separator, false otherwise. + */ +[[nodiscard]] bool IsDirSeparator(char character); + +/** + * Checks if a character is a directory separator (either a forward slash or backslash). + * + * @param character Character + * + * @returns True if the character is a directory separator, false otherwise. + */ +[[nodiscard]] bool IsDirSeparator(char8_t character); + +/** + * Removes any trailing directory separators if they exist in the given path. + * + * @param path Filesystem path + * + * @returns The filesystem path without any trailing directory separators. + */ +[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const Path& path) { + if constexpr (IsChar) { + return RemoveTrailingSeparators(ToU8String(path)); + } else { + return RemoveTrailingSeparators(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the filesystem path associated with the YuzuPath enum. + * + * @param yuzu_path YuzuPath enum + * + * @returns The filesystem path associated with the YuzuPath enum. + */ +[[nodiscard]] const std::filesystem::path& GetYuzuPath(YuzuPath yuzu_path); + +/** + * Gets the filesystem path associated with the YuzuPath enum as a UTF-8 encoded std::string. + * + * @param yuzu_path YuzuPath enum + * + * @returns The filesystem path associated with the YuzuPath enum as a UTF-8 encoded std::string. + */ +[[nodiscard]] std::string GetYuzuPathString(YuzuPath yuzu_path); + +/** + * Sets a new filesystem path associated with the YuzuPath enum. + * If the filesystem object at new_path is not a directory, this function will not do anything. + * + * @param yuzu_path YuzuPath enum + * @param new_path New filesystem path + */ +void SetYuzuPath(YuzuPath yuzu_path, const std::filesystem::path& new_path); + +#ifdef _WIN32 +template +[[nodiscard]] void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) { + if constexpr (IsChar) { + SetYuzuPath(yuzu_path, ToU8String(new_path)); + } else { + SetYuzuPath(yuzu_path, std::filesystem::path{new_path}); + } +} +#endif + +#ifdef _WIN32 + +/** + * Gets the path of the directory containing the executable of the current process. + * + * @returns The path of the directory containing the executable of the current process. + */ +[[nodiscard]] std::filesystem::path GetExeDirectory(); + +/** + * Gets the path of the current user's %APPDATA% directory (%USERPROFILE%/AppData/Roaming). + * + * @returns The path of the current user's %APPDATA% directory. + */ +[[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory(); + +#else + +/** + * Gets the path of the directory specified by the #HOME environment variable. + * If $HOME is not defined, it will attempt to query the user database in passwd instead. + * + * @returns The path of the current user's home directory. + */ +[[nodiscard]] std::filesystem::path GetHomeDirectory(); + +/** + * Gets the relevant paths for yuzu to store its data based on the given XDG environment variable. + * See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + * Defaults to $HOME/.local/share for main application data, + * $HOME/.cache for cached data, and $HOME/.config for configuration files. + * + * @param env_name XDG environment variable name + * + * @returns The path where yuzu should store its data. + */ +[[nodiscard]] std::filesystem::path GetDataDirectory(const std::string& env_name); + +#endif + +#ifdef __APPLE__ + +[[nodiscard]] std::filesystem::path GetBundleDirectory(); + +#endif + +// vvvvvvvvvv Deprecated vvvvvvvvvv // + +// Removes the final '/' or '\' if one exists +[[nodiscard]] std::string_view RemoveTrailingSlash(std::string_view path); + +enum class DirectorySeparator { + ForwardSlash, + BackwardSlash, + PlatformDefault, +}; + +// Splits the path on '/' or '\' and put the components into a vector +// i.e. "C:\Users\Yuzu\Documents\save.bin" becomes {"C:", "Users", "Yuzu", "Documents", "save.bin" } +[[nodiscard]] std::vector SplitPathComponents(std::string_view filename); + +// Removes trailing slash, makes all '\\' into '/', and removes duplicate '/'. Makes '/' into '\\' +// depending if directory_separator is BackwardSlash or PlatformDefault and running on windows +[[nodiscard]] std::string SanitizePath( + std::string_view path, + DirectorySeparator directory_separator = DirectorySeparator::ForwardSlash); + +// Gets all of the text up to the last '/' or '\' in the path. +[[nodiscard]] std::string_view GetParentPath(std::string_view path); + +// Gets all of the text after the first '/' or '\' in the path. +[[nodiscard]] std::string_view GetPathWithoutTop(std::string_view path); + +// Gets the filename of the path +[[nodiscard]] std::string_view GetFilename(std::string_view path); + +// Gets the extension of the filename +[[nodiscard]] std::string_view GetExtensionFromFilename(std::string_view name); + } // namespace Common::FS