This adds an action to the Game List context menu that lets users link save data from Eden to Ryujinx, or vice versa. Unfortunately, this isn't so simple to deal with due to the way Ryujinx's saves work. Ryujinx stores its saves in the... config directory... in `bis/user/save`. Unlike Yuzu, however, it doesn't store things by TitleID, instead it's just a bunch of directories from 000...01 to 000...0f and so on. The way it *maps* TitleID to SaveID is via `imkvdb.arc` in `bis/system/save/8000000000000000/0/` and also an identical copy in the `1` directory for... some reason. `imkvdb.arc` is handled by `FlatMapKeyValueStore` in LibHac, which, as the name implies, is a key-value storage system that `imkvdb.arc`, and seemingly `imkvdb.arc` alone, uses. The way this class is written is really weird, almost as if it's designed to accommodate more types of kvdbs... but for now we can safely assume that there aren't gonna be any other `kvdb` implementations added to HorizonNX. Regardless, the file format is ridiculously simple so I didn't actually need to do a deep dive into C# code... of which I can basically only read Avalonia. A simple `xxd` on the `imkvdb.arc` is all that's needed, and here's everything that matters: - The `IMKV` magic header (4 bytes) - 8 bytes that don't really have anything useful to us, except for a size byte (presumably a `u32`) strewn at offset `0x08` from the start of the file, which is useless to us - Then we start the `IMEN` list. I don't know what the `IM` stands for, but `IMEN` is just, well, an ENtry. Offsets shown are relative to the start of the `IMEN` header. * 4-byte `IMEN` magic header at 0x0 * 8 bytes of filler data. It contains two `0x40` bytes, but I'm not really sure what they do * TitleID (u64) at `0xC`, for example `00a0 df10 501f 0001` for Legends: Arceus (the byte order is swapped) * 0x38 bytes of filler starting at offset 0x14 * SaveID (u64) at `0x4C`, for example `0a00 0000 0000 0000` for my Legends: Arceus save * 0x38 bytes of filler starting at offset 0x54 Full example for Legends: Arceus: ``` 000001b0: 494d 454e 4000 0000 4000 0000 00a0 df10 IMEN@...@....... 000001c0: 501f 0001 0100 0000 0000 0000 0000 0000 P............... 000001d0: 0000 0000 0000 0000 0000 0000 0100 0000 ................ 000001e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 000001f0: 0000 0000 0000 0000 0000 0000 0a00 0000 ................ 00000200: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000210: 0000 0000 0100 0000 0000 0000 0000 0000 ................ 00000220: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000230: 0000 0000 0000 0000 0000 0000 494d 454e ............IMEN ``` Ultimately, the size of the `IMEN` sits at 0x8C or 140 bytes. With this knowledge reading all the TitleID -> SaveID pairs is basically free, and outside of validation and stuff is like 15 lines of relevant code. Some interesting caveats, though: - There are two entries for some TitleIDs for... some reason? Ignoring the second one seems to work though. - Within each save directory, there are directories `0` and `1`... and only `0` ever seems used??? It's where Ryujinx points you to for save, so I just chose to use that. Once everything is parsed, the rest of the implementation is extremely trivial: - When the user requests a Ryujinx link, match the current program_id to the corresponding SaveID in `imkvdb` - If it doesn't exist, just error out (save data is probably nonexistent) - If it does though, give the user the option to use Eden's current save data OR Ryujinx's current save data. Old save data is deleted depending on which one you chose. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2815 Reviewed-by: Lizzie <lizzie@eden-emu.dev> Reviewed-by: MaranBr <maranbr@eden-emu.dev>
523 lines
16 KiB
C++
523 lines
16 KiB
C++
// 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 <algorithm>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <unordered_map>
|
|
|
|
#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 <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
|
|
|
|
#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(EmuPath 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(EmuPath 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(EmuPath::titleName##Dir, GetAppDataRoamingDirectory() / upperName##_DIR); \
|
|
GenerateLegacyPath(EmuPath::titleName##ConfigDir, GetAppDataRoamingDirectory() / upperName##_DIR / CONFIG_DIR); \
|
|
GenerateLegacyPath(EmuPath::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(EmuPath::titleName##Dir, GetDataDirectory("XDG_DATA_HOME") / upperName##_DIR); \
|
|
GenerateLegacyPath(EmuPath::titleName##ConfigDir, GetDataDirectory("XDG_CONFIG_HOME") / upperName##_DIR); \
|
|
GenerateLegacyPath(EmuPath::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);
|
|
|
|
#ifdef _WIN32
|
|
GenerateLegacyPath(EmuPath::RyujinxDir, GetAppDataRoamingDirectory() / RYUJINX_DIR);
|
|
#else
|
|
// In Ryujinx's infinite wisdom, it places EVERYTHING in the config directory on UNIX
|
|
// This is incredibly stupid and violates a million XDG standards, but whatever
|
|
GenerateLegacyPath(EmuPath::RyujinxDir, GetDataDirectory("XDG_CONFIG_HOME") / RYUJINX_DIR);
|
|
#endif
|
|
|
|
}
|
|
|
|
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(EmuPath legacy_path, const fs::path& new_path) {
|
|
SetLegacyPathImpl(legacy_path, new_path);
|
|
}
|
|
|
|
std::unordered_map<EdenPath, fs::path> eden_paths;
|
|
std::unordered_map<EmuPath, fs::path> 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(EmuPath legacy_path) {
|
|
return PathManagerImpl::GetInstance().GetLegacyPathImpl(legacy_path);
|
|
}
|
|
|
|
std::string GetEdenPathString(EdenPath eden_path) {
|
|
return PathToUTF8String(GetEdenPath(eden_path));
|
|
}
|
|
|
|
std::string GetLegacyPathString(EmuPath 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 <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 // 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
|