common: fs: path_util: Add the rest of the path functions

This commit is contained in:
Morph
2021-04-13 01:23:30 -04:00
parent cdf26b22c6
commit 0b22742a20
2 changed files with 649 additions and 0 deletions

View File

@@ -2,16 +2,400 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <unordered_map>
#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 <shlobj.h> // Used in GetExeDirectory()
#else
#include <cstdlib> // Used in Get(Home/Data)Directory()
#include <pwd.h> // Used in GetHomeDirectory()
#include <sys/types.h> // Used in GetHomeDirectory()
#include <unistd.h> // Used in GetDataDirectory()
#endif
#ifdef __APPLE__
#include <sys/param.h> // 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 <CoreFoundation/CFBundle.h> // Used in GetBundleDirectory()
#include <CoreFoundation/CFString.h> // Used in GetBundleDirectory()
#include <CoreFoundation/CFURL.h> // 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<YuzuPath, fs::path> 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<std::string> SplitPathComponents(std::string_view filename) {
std::string copy(filename);
std::replace(copy.begin(), copy.end(), '\\', '/');
std::vector<std::string> 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

View File

@@ -5,9 +5,26 @@
#pragma once
#include <filesystem>
#include <vector>
#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 <typename Path1, typename Path2>
[[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<ValueType1> && IsChar<ValueType2>) {
return ConcatPath(ToU8String(first), ToU8String(second));
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
return ConcatPath(ToU8String(first), second);
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
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 <typename Path1, typename Path2>
[[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<ValueType1> && IsChar<ValueType2>) {
return ConcatPathSafe(ToU8String(base), ToU8String(offset));
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
return ConcatPathSafe(ToU8String(base), offset);
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
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 <typename Path1, typename Path2>
[[nodiscard]] bool IsPathSandboxed(const Path1& base, const Path2& path) {
using ValueType1 = typename Path1::value_type;
using ValueType2 = typename Path2::value_type;
if constexpr (IsChar<ValueType1> && IsChar<ValueType2>) {
return IsPathSandboxed(ToU8String(base), ToU8String(path));
} else if constexpr (IsChar<ValueType1> && !IsChar<ValueType2>) {
return IsPathSandboxed(ToU8String(base), path);
} else if constexpr (!IsChar<ValueType1> && IsChar<ValueType2>) {
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 <typename Path>
[[nodiscard]] std::filesystem::path RemoveTrailingSeparators(const Path& path) {
if constexpr (IsChar<typename Path::value_type>) {
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 <typename Path>
[[nodiscard]] void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) {
if constexpr (IsChar<typename Path::value_type>) {
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<std::string> 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