From 065867e2c24e9856c360fc2d6b9a86c92aedc43e Mon Sep 17 00:00:00 2001 From: Morph <39850852+Morph1984@users.noreply.github.com> Date: Tue, 25 May 2021 19:32:56 -0400 Subject: common: fs: Rework the Common Filesystem interface to make use of std::filesystem (#6270) * common: fs: fs_types: Create filesystem types Contains various filesystem types used by the Common::FS library * common: fs: fs_util: Add std::string to std::u8string conversion utility * common: fs: path_util: Add utlity functions for paths Contains various utility functions for getting or manipulating filesystem paths used by the Common::FS library * common: fs: file: Rewrite the IOFile implementation * common: fs: Reimplement Common::FS library using std::filesystem * common: fs: fs_paths: Add fs_paths to replace common_paths * common: fs: path_util: Add the rest of the path functions * common: Remove the previous Common::FS implementation * general: Remove unused fs includes * string_util: Remove unused function and include * nvidia_flags: Migrate to the new Common::FS library * settings: Migrate to the new Common::FS library * logging: backend: Migrate to the new Common::FS library * core: Migrate to the new Common::FS library * perf_stats: Migrate to the new Common::FS library * reporter: Migrate to the new Common::FS library * telemetry_session: Migrate to the new Common::FS library * key_manager: Migrate to the new Common::FS library * bis_factory: Migrate to the new Common::FS library * registered_cache: Migrate to the new Common::FS library * xts_archive: Migrate to the new Common::FS library * service: acc: Migrate to the new Common::FS library * applets/profile: Migrate to the new Common::FS library * applets/web: Migrate to the new Common::FS library * service: filesystem: Migrate to the new Common::FS library * loader: Migrate to the new Common::FS library * gl_shader_disk_cache: Migrate to the new Common::FS library * nsight_aftermath_tracker: Migrate to the new Common::FS library * vulkan_library: Migrate to the new Common::FS library * configure_debug: Migrate to the new Common::FS library * game_list_worker: Migrate to the new Common::FS library * config: Migrate to the new Common::FS library * configure_filesystem: Migrate to the new Common::FS library * configure_per_game_addons: Migrate to the new Common::FS library * configure_profile_manager: Migrate to the new Common::FS library * configure_ui: Migrate to the new Common::FS library * input_profiles: Migrate to the new Common::FS library * yuzu_cmd: config: Migrate to the new Common::FS library * yuzu_cmd: Migrate to the new Common::FS library * vfs_real: Migrate to the new Common::FS library * vfs: Migrate to the new Common::FS library * vfs_libzip: Migrate to the new Common::FS library * service: bcat: Migrate to the new Common::FS library * yuzu: main: Migrate to the new Common::FS library * vfs_real: Delete the contents of an existing file in CreateFile Current usages of CreateFile expect to delete the contents of an existing file, retain this behavior for now. * input_profiles: Don't iterate the input profile dir if it does not exist Silences an error produced in the log if the directory does not exist. * game_list_worker: Skip parsing file if the returned VfsFile is nullptr Prevents crashes in GetLoader when the virtual file is nullptr * common: fs: Validate paths for path length * service: filesystem: Open the mod load directory as read only --- src/common/CMakeLists.txt | 13 +- src/common/common_paths.h | 52 -- src/common/file_util.cpp | 1032 ---------------------------------------- src/common/file_util.h | 298 ------------ src/common/fs/file.cpp | 392 +++++++++++++++ src/common/fs/file.h | 450 ++++++++++++++++++ src/common/fs/fs.cpp | 610 ++++++++++++++++++++++++ src/common/fs/fs.h | 582 ++++++++++++++++++++++ src/common/fs/fs_paths.h | 27 ++ src/common/fs/fs_types.h | 73 +++ src/common/fs/fs_util.cpp | 13 + src/common/fs/fs_util.h | 25 + src/common/fs/path_util.cpp | 432 +++++++++++++++++ src/common/fs/path_util.h | 309 ++++++++++++ src/common/logging/backend.cpp | 27 +- src/common/logging/backend.h | 5 +- src/common/nvidia_flags.cpp | 24 +- src/common/settings.cpp | 16 +- src/common/string_util.cpp | 13 - src/common/string_util.h | 2 - 20 files changed, 2963 insertions(+), 1432 deletions(-) delete mode 100644 src/common/common_paths.h delete mode 100644 src/common/file_util.cpp delete mode 100644 src/common/file_util.h create mode 100644 src/common/fs/file.cpp create mode 100644 src/common/fs/file.h create mode 100644 src/common/fs/fs.cpp create mode 100644 src/common/fs/fs.h create mode 100644 src/common/fs/fs_paths.h create mode 100644 src/common/fs/fs_types.h create mode 100644 src/common/fs/fs_util.cpp create mode 100644 src/common/fs/fs_util.h create mode 100644 src/common/fs/path_util.cpp create mode 100644 src/common/fs/path_util.h (limited to 'src/common') diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 88644eeb6..eafb96b0b 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -109,7 +109,6 @@ add_library(common STATIC cityhash.cpp cityhash.h common_funcs.h - common_paths.h common_sizes.h common_types.h concepts.h @@ -118,8 +117,16 @@ add_library(common STATIC dynamic_library.h fiber.cpp fiber.h - file_util.cpp - file_util.h + fs/file.cpp + fs/file.h + fs/fs.cpp + fs/fs.h + fs/fs_paths.h + fs/fs_types.h + fs/fs_util.cpp + fs/fs_util.h + fs/path_util.cpp + fs/path_util.h hash.h hex_util.cpp hex_util.h diff --git a/src/common/common_paths.h b/src/common/common_paths.h deleted file mode 100644 index 3c593d5f6..000000000 --- a/src/common/common_paths.h +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -// Directory separators, do we need this? -#define DIR_SEP "/" -#define DIR_SEP_CHR '/' - -#ifndef MAX_PATH -#define MAX_PATH 260 -#endif - -// The user data dir -#define ROOT_DIR "." -#define USERDATA_DIR "user" -#ifdef USER_DIR -#define EMU_DATA_DIR USER_DIR -#else -#define EMU_DATA_DIR "yuzu" -#endif - -// Dirs in both User and Sys -#define EUR_DIR "EUR" -#define USA_DIR "USA" -#define JAP_DIR "JAP" - -// Subdirs in the User dir returned by GetUserPath(UserPath::UserDir) -#define CONFIG_DIR "config" -#define CACHE_DIR "cache" -#define SDMC_DIR "sdmc" -#define NAND_DIR "nand" -#define SYSDATA_DIR "sysdata" -#define KEYS_DIR "keys" -#define LOAD_DIR "load" -#define DUMP_DIR "dump" -#define SCREENSHOTS_DIR "screenshots" -#define SHADER_DIR "shader" -#define LOG_DIR "log" - -// Filenames -// Files in the directory returned by GetUserPath(UserPath::ConfigDir) -#define EMU_CONFIG "emu.ini" -#define DEBUGGER_CONFIG "debugger.ini" -#define LOGGER_CONFIG "logger.ini" -// Files in the directory returned by GetUserPath(UserPath::LogDir) -#define LOG_FILE "yuzu_log.txt" - -// Sys files -#define SHARED_FONT "shared_font.bin" -#define AES_KEYS "aes_keys.txt" diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp deleted file mode 100644 index 18fbfa25b..000000000 --- a/src/common/file_util.cpp +++ /dev/null @@ -1,1032 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include "common/assert.h" -#include "common/common_funcs.h" -#include "common/common_paths.h" -#include "common/file_util.h" -#include "common/logging/log.h" - -#ifdef _WIN32 -#include -// windows.h needs to be included before other windows headers -#include // getcwd -#include -#include -#include // for SHGetFolderPath -#include -#include "common/string_util.h" - -#ifdef _MSC_VER -// 64 bit offsets for MSVC -#define fseeko _fseeki64 -#define ftello _ftelli64 -#define fileno _fileno -#endif - -// 64 bit offsets for MSVC and MinGW. MinGW also needs this for using _wstat64 -#define stat _stat64 -#define fstat _fstat64 - -#else -#ifdef __APPLE__ -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#endif - -#if defined(__APPLE__) -// 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 -#include -#include -#ifdef availability -#undef availability -#endif - -#endif - -#include -#include - -#ifndef S_ISDIR -#define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR) -#endif - -// This namespace has various generic functions related to files and paths. -// The code still needs a ton of cleanup. -// REMEMBER: strdup considered harmful! -namespace Common::FS { - -// Remove any ending forward slashes from directory paths -// Modifies argument. -static void StripTailDirSlashes(std::string& fname) { - if (fname.length() <= 1) { - return; - } - - std::size_t i = fname.length(); - while (i > 0 && fname[i - 1] == DIR_SEP_CHR) { - --i; - } - fname.resize(i); -} - -bool Exists(const std::string& filename) { - struct stat file_info; - - std::string copy(filename); - StripTailDirSlashes(copy); - -#ifdef _WIN32 - // Windows needs a slash to identify a driver root - if (copy.size() != 0 && copy.back() == ':') - copy += DIR_SEP_CHR; - - int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info); -#else - int result = stat(copy.c_str(), &file_info); -#endif - - return (result == 0); -} - -bool IsDirectory(const std::string& filename) { - struct stat file_info; - - std::string copy(filename); - StripTailDirSlashes(copy); - -#ifdef _WIN32 - // Windows needs a slash to identify a driver root - if (copy.size() != 0 && copy.back() == ':') - copy += DIR_SEP_CHR; - - int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info); -#else - int result = stat(copy.c_str(), &file_info); -#endif - - if (result < 0) { - LOG_DEBUG(Common_Filesystem, "stat failed on {}: {}", filename, GetLastErrorMsg()); - return false; - } - - return S_ISDIR(file_info.st_mode); -} - -bool Delete(const std::string& filename) { - LOG_TRACE(Common_Filesystem, "file {}", filename); - - // Return true because we care about the file no - // being there, not the actual delete. - if (!Exists(filename)) { - LOG_DEBUG(Common_Filesystem, "{} does not exist", filename); - return true; - } - - // We can't delete a directory - if (IsDirectory(filename)) { - LOG_ERROR(Common_Filesystem, "Failed: {} is a directory", filename); - return false; - } - -#ifdef _WIN32 - if (!DeleteFileW(Common::UTF8ToUTF16W(filename).c_str())) { - LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg()); - return false; - } -#else - if (unlink(filename.c_str()) == -1) { - LOG_ERROR(Common_Filesystem, "unlink failed on {}: {}", filename, GetLastErrorMsg()); - return false; - } -#endif - - return true; -} - -bool CreateDir(const std::string& path) { - LOG_TRACE(Common_Filesystem, "directory {}", path); -#ifdef _WIN32 - if (::CreateDirectoryW(Common::UTF8ToUTF16W(path).c_str(), nullptr)) - return true; - DWORD error = GetLastError(); - if (error == ERROR_ALREADY_EXISTS) { - LOG_DEBUG(Common_Filesystem, "CreateDirectory failed on {}: already exists", path); - return true; - } - LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error); - return false; -#else - if (mkdir(path.c_str(), 0755) == 0) - return true; - - int err = errno; - - if (err == EEXIST) { - LOG_DEBUG(Common_Filesystem, "mkdir failed on {}: already exists", path); - return true; - } - - LOG_ERROR(Common_Filesystem, "mkdir failed on {}: {}", path, strerror(err)); - return false; -#endif -} - -bool CreateFullPath(const std::string& fullPath) { - int panicCounter = 100; - LOG_TRACE(Common_Filesystem, "path {}", fullPath); - - if (Exists(fullPath)) { - LOG_DEBUG(Common_Filesystem, "path exists {}", fullPath); - return true; - } - - std::size_t position = 0; - while (true) { - // Find next sub path - position = fullPath.find(DIR_SEP_CHR, position); - - // we're done, yay! - if (position == fullPath.npos) - return true; - - // Include the '/' so the first call is CreateDir("/") rather than CreateDir("") - std::string const subPath(fullPath.substr(0, position + 1)); - if (!IsDirectory(subPath) && !CreateDir(subPath)) { - LOG_ERROR(Common, "CreateFullPath: directory creation failed"); - return false; - } - - // A safety check - panicCounter--; - if (panicCounter <= 0) { - LOG_ERROR(Common, "CreateFullPath: directory structure is too deep"); - return false; - } - position++; - } -} - -bool DeleteDir(const std::string& filename) { - LOG_TRACE(Common_Filesystem, "directory {}", filename); - - // check if a directory - if (!IsDirectory(filename)) { - LOG_ERROR(Common_Filesystem, "Not a directory {}", filename); - return false; - } - -#ifdef _WIN32 - if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str())) - return true; -#else - if (rmdir(filename.c_str()) == 0) - return true; -#endif - LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg()); - - return false; -} - -bool Rename(const std::string& srcFilename, const std::string& destFilename) { - LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename); -#ifdef _WIN32 - if (_wrename(Common::UTF8ToUTF16W(srcFilename).c_str(), - Common::UTF8ToUTF16W(destFilename).c_str()) == 0) - return true; -#else - if (rename(srcFilename.c_str(), destFilename.c_str()) == 0) - return true; -#endif - LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, - GetLastErrorMsg()); - return false; -} - -bool Copy(const std::string& srcFilename, const std::string& destFilename) { - LOG_TRACE(Common_Filesystem, "{} --> {}", srcFilename, destFilename); -#ifdef _WIN32 - if (CopyFileW(Common::UTF8ToUTF16W(srcFilename).c_str(), - Common::UTF8ToUTF16W(destFilename).c_str(), FALSE)) - return true; - - LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, - GetLastErrorMsg()); - return false; -#else - using CFilePointer = std::unique_ptr; - - // Open input file - CFilePointer input{fopen(srcFilename.c_str(), "rb"), std::fclose}; - if (!input) { - LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename, - destFilename, GetLastErrorMsg()); - return false; - } - - // open output file - CFilePointer output{fopen(destFilename.c_str(), "wb"), std::fclose}; - if (!output) { - LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename, - destFilename, GetLastErrorMsg()); - return false; - } - - // copy loop - std::array buffer; - while (!feof(input.get())) { - // read input - std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input.get()); - if (rnum != buffer.size()) { - if (ferror(input.get()) != 0) { - LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}", - srcFilename, destFilename, GetLastErrorMsg()); - return false; - } - } - - // write output - std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output.get()); - if (wnum != rnum) { - LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename, - destFilename, GetLastErrorMsg()); - return false; - } - } - - return true; -#endif -} - -u64 GetSize(const std::string& filename) { - if (!Exists(filename)) { - LOG_ERROR(Common_Filesystem, "failed {}: No such file", filename); - return 0; - } - - if (IsDirectory(filename)) { - LOG_ERROR(Common_Filesystem, "failed {}: is a directory", filename); - return 0; - } - - struct stat buf; -#ifdef _WIN32 - if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0) -#else - if (stat(filename.c_str(), &buf) == 0) -#endif - { - LOG_TRACE(Common_Filesystem, "{}: {}", filename, buf.st_size); - return buf.st_size; - } - - LOG_ERROR(Common_Filesystem, "Stat failed {}: {}", filename, GetLastErrorMsg()); - return 0; -} - -u64 GetSize(const int fd) { - struct stat buf; - if (fstat(fd, &buf) != 0) { - LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg()); - return 0; - } - return buf.st_size; -} - -u64 GetSize(FILE* f) { - // can't use off_t here because it can be 32-bit - u64 pos = ftello(f); - if (fseeko(f, 0, SEEK_END) != 0) { - LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); - return 0; - } - u64 size = ftello(f); - if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) { - LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); - return 0; - } - return size; -} - -bool CreateEmptyFile(const std::string& filename) { - LOG_TRACE(Common_Filesystem, "{}", filename); - - if (!IOFile(filename, "wb").IsOpen()) { - LOG_ERROR(Common_Filesystem, "failed {}: {}", filename, GetLastErrorMsg()); - return false; - } - - return true; -} - -bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, - DirectoryEntryCallable callback) { - LOG_TRACE(Common_Filesystem, "directory {}", directory); - - // How many files + directories we found - u64 found_entries = 0; - - // Save the status of callback function - bool callback_error = false; - -#ifdef _WIN32 - // Find the first file in the directory. - WIN32_FIND_DATAW ffd; - - HANDLE handle_find = FindFirstFileW(Common::UTF8ToUTF16W(directory + "\\*").c_str(), &ffd); - if (handle_find == INVALID_HANDLE_VALUE) { - FindClose(handle_find); - return false; - } - // windows loop - do { - const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName)); -#else - DIR* dirp = opendir(directory.c_str()); - if (!dirp) - return false; - - // non windows loop - while (struct dirent* result = readdir(dirp)) { - const std::string virtual_name(result->d_name); -#endif - - if (virtual_name == "." || virtual_name == "..") - continue; - - u64 ret_entries = 0; - if (!callback(&ret_entries, directory, virtual_name)) { - callback_error = true; - break; - } - found_entries += ret_entries; - -#ifdef _WIN32 - } while (FindNextFileW(handle_find, &ffd) != 0); - FindClose(handle_find); -#else - } - closedir(dirp); -#endif - - if (callback_error) - return false; - - // num_entries_out is allowed to be specified nullptr, in which case we shouldn't try to set it - if (num_entries_out != nullptr) - *num_entries_out = found_entries; - return true; -} - -u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry, - unsigned int recursion) { - const auto callback = [recursion, &parent_entry](u64* num_entries_out, - const std::string& directory, - const std::string& virtual_name) -> bool { - FSTEntry entry; - entry.virtualName = virtual_name; - entry.physicalName = directory + DIR_SEP + virtual_name; - - if (IsDirectory(entry.physicalName)) { - entry.isDirectory = true; - // is a directory, lets go inside if we didn't recurse to often - if (recursion > 0) { - entry.size = ScanDirectoryTree(entry.physicalName, entry, recursion - 1); - *num_entries_out += entry.size; - } else { - entry.size = 0; - } - } else { // is a file - entry.isDirectory = false; - entry.size = GetSize(entry.physicalName); - } - (*num_entries_out)++; - - // Push into the tree - parent_entry.children.push_back(std::move(entry)); - return true; - }; - - u64 num_entries; - return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0; -} - -bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) { - const auto callback = [recursion](u64*, const std::string& directory, - const std::string& virtual_name) { - const std::string new_path = directory + DIR_SEP_CHR + virtual_name; - - if (IsDirectory(new_path)) { - if (recursion == 0) { - return false; - } - return DeleteDirRecursively(new_path, recursion - 1); - } - return Delete(new_path); - }; - - if (!ForeachDirectoryEntry(nullptr, directory, callback)) - return false; - - // Delete the outermost directory - DeleteDir(directory); - return true; -} - -void CopyDir([[maybe_unused]] const std::string& source_path, - [[maybe_unused]] const std::string& dest_path) { -#ifndef _WIN32 - if (source_path == dest_path) { - return; - } - if (!Exists(source_path)) { - return; - } - if (!Exists(dest_path)) { - CreateFullPath(dest_path); - } - - DIR* dirp = opendir(source_path.c_str()); - if (!dirp) { - return; - } - - while (struct dirent* result = readdir(dirp)) { - const std::string virtualName(result->d_name); - // check for "." and ".." - if (((virtualName[0] == '.') && (virtualName[1] == '\0')) || - ((virtualName[0] == '.') && (virtualName[1] == '.') && (virtualName[2] == '\0'))) { - continue; - } - - std::string source, dest; - source = source_path + virtualName; - dest = dest_path + virtualName; - if (IsDirectory(source)) { - source += '/'; - dest += '/'; - if (!Exists(dest)) { - CreateFullPath(dest); - } - CopyDir(source, dest); - } else if (!Exists(dest)) { - Copy(source, dest); - } - } - closedir(dirp); -#endif -} - -std::optional GetCurrentDir() { -// Get the current working directory (getcwd uses malloc) -#ifdef _WIN32 - wchar_t* dir = _wgetcwd(nullptr, 0); - if (!dir) { -#else - char* dir = getcwd(nullptr, 0); - if (!dir) { -#endif - LOG_ERROR(Common_Filesystem, "GetCurrentDirectory failed: {}", GetLastErrorMsg()); - return std::nullopt; - } -#ifdef _WIN32 - std::string strDir = Common::UTF16ToUTF8(dir); -#else - std::string strDir = dir; -#endif - free(dir); - return strDir; -} - -bool SetCurrentDir(const std::string& directory) { -#ifdef _WIN32 - return _wchdir(Common::UTF8ToUTF16W(directory).c_str()) == 0; -#else - return chdir(directory.c_str()) == 0; -#endif -} - -#if defined(__APPLE__) -std::string GetBundleDirectory() { - CFURLRef BundleRef; - char AppBundlePath[MAXPATHLEN]; - // Get the main bundle for the app - BundleRef = CFBundleCopyBundleURL(CFBundleGetMainBundle()); - CFStringRef BundlePath = CFURLCopyFileSystemPath(BundleRef, kCFURLPOSIXPathStyle); - CFStringGetFileSystemRepresentation(BundlePath, AppBundlePath, sizeof(AppBundlePath)); - CFRelease(BundleRef); - CFRelease(BundlePath); - - return AppBundlePath; -} -#endif - -#ifdef _WIN32 -const std::string& GetExeDirectory() { - static std::string exe_path; - if (exe_path.empty()) { - wchar_t wchar_exe_path[2048]; - GetModuleFileNameW(nullptr, wchar_exe_path, 2048); - exe_path = Common::UTF16ToUTF8(wchar_exe_path); - exe_path = exe_path.substr(0, exe_path.find_last_of('\\')); - } - return exe_path; -} - -std::string AppDataRoamingDirectory() { - PWSTR pw_local_path = nullptr; - // Only supported by Windows Vista or later - SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pw_local_path); - std::string local_path = Common::UTF16ToUTF8(pw_local_path); - CoTaskMemFree(pw_local_path); - return local_path; -} -#else -/** - * @return The user’s home directory on POSIX systems - */ -static const std::string& GetHomeDirectory() { - static std::string home_path; - if (home_path.empty()) { - const char* envvar = getenv("HOME"); - if (envvar) { - home_path = envvar; - } else { - auto pw = getpwuid(getuid()); - ASSERT_MSG(pw, - "$HOME isn’t defined, and the current user can’t be found in /etc/passwd."); - home_path = pw->pw_dir; - } - } - return home_path; -} - -/** - * Follows the XDG Base Directory Specification to get a directory path - * @param envvar The XDG environment variable to get the value from - * @return The directory path - * @sa http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html - */ -static const std::string GetUserDirectory(const std::string& envvar) { - const char* directory = getenv(envvar.c_str()); - - std::string user_dir; - if (directory) { - user_dir = directory; - } else { - std::string subdirectory; - if (envvar == "XDG_DATA_HOME") - subdirectory = DIR_SEP ".local" DIR_SEP "share"; - else if (envvar == "XDG_CONFIG_HOME") - subdirectory = DIR_SEP ".config"; - else if (envvar == "XDG_CACHE_HOME") - subdirectory = DIR_SEP ".cache"; - else - ASSERT_MSG(false, "Unknown XDG variable {}.", envvar); - user_dir = GetHomeDirectory() + subdirectory; - } - - ASSERT_MSG(!user_dir.empty(), "User directory {} mustn’t be empty.", envvar); - ASSERT_MSG(user_dir[0] == '/', "User directory {} must be absolute.", envvar); - - return user_dir; -} -#endif - -std::string GetSysDirectory() { - std::string sysDir; - -#if defined(__APPLE__) - sysDir = GetBundleDirectory(); - sysDir += DIR_SEP; - sysDir += SYSDATA_DIR; -#else - sysDir = SYSDATA_DIR; -#endif - sysDir += DIR_SEP; - - LOG_DEBUG(Common_Filesystem, "Setting to {}:", sysDir); - return sysDir; -} - -const std::string& GetUserPath(UserPath path, const std::string& new_path) { - static std::unordered_map paths; - auto& user_path = paths[UserPath::UserDir]; - - // Set up all paths and files on the first run - if (user_path.empty()) { -#ifdef _WIN32 - user_path = GetExeDirectory() + DIR_SEP USERDATA_DIR DIR_SEP; - if (!IsDirectory(user_path)) { - user_path = AppDataRoamingDirectory() + DIR_SEP EMU_DATA_DIR DIR_SEP; - } else { - LOG_INFO(Common_Filesystem, "Using the local user directory"); - } - - paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); - paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); -#else - if (Exists(ROOT_DIR DIR_SEP USERDATA_DIR)) { - user_path = ROOT_DIR DIR_SEP USERDATA_DIR DIR_SEP; - paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); - paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); - } else { - std::string data_dir = GetUserDirectory("XDG_DATA_HOME"); - std::string config_dir = GetUserDirectory("XDG_CONFIG_HOME"); - std::string cache_dir = GetUserDirectory("XDG_CACHE_HOME"); - - user_path = data_dir + DIR_SEP EMU_DATA_DIR DIR_SEP; - paths.emplace(UserPath::ConfigDir, config_dir + DIR_SEP EMU_DATA_DIR DIR_SEP); - paths.emplace(UserPath::CacheDir, cache_dir + DIR_SEP EMU_DATA_DIR DIR_SEP); - } -#endif - paths.emplace(UserPath::SDMCDir, user_path + SDMC_DIR DIR_SEP); - paths.emplace(UserPath::NANDDir, user_path + NAND_DIR DIR_SEP); - paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); - paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP); - paths.emplace(UserPath::ScreenshotsDir, user_path + SCREENSHOTS_DIR DIR_SEP); - paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP); - paths.emplace(UserPath::SysDataDir, user_path + SYSDATA_DIR DIR_SEP); - paths.emplace(UserPath::KeysDir, user_path + KEYS_DIR DIR_SEP); - // TODO: Put the logs in a better location for each OS - paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP); - } - - if (!new_path.empty()) { - if (!IsDirectory(new_path)) { - LOG_ERROR(Common_Filesystem, "Invalid path specified {}", new_path); - return paths[path]; - } else { - paths[path] = new_path; - } - - switch (path) { - case UserPath::RootDir: - user_path = paths[UserPath::RootDir] + DIR_SEP; - break; - case UserPath::UserDir: - user_path = paths[UserPath::RootDir] + DIR_SEP; - paths[UserPath::ConfigDir] = user_path + CONFIG_DIR DIR_SEP; - paths[UserPath::CacheDir] = user_path + CACHE_DIR DIR_SEP; - paths[UserPath::SDMCDir] = user_path + SDMC_DIR DIR_SEP; - paths[UserPath::NANDDir] = user_path + NAND_DIR DIR_SEP; - break; - default: - break; - } - } - - return paths[path]; -} - -std::string GetHactoolConfigurationPath() { -#ifdef _WIN32 - PWSTR pw_local_path = nullptr; - if (SHGetKnownFolderPath(FOLDERID_Profile, 0, nullptr, &pw_local_path) != S_OK) - return ""; - std::string local_path = Common::UTF16ToUTF8(pw_local_path); - CoTaskMemFree(pw_local_path); - return local_path + "\\.switch"; -#else - return GetHomeDirectory() + "/.switch"; -#endif -} - -std::string GetNANDRegistrationDir(bool system) { - if (system) - return GetUserPath(UserPath::NANDDir) + "system/Contents/registered/"; - return GetUserPath(UserPath::NANDDir) + "user/Contents/registered/"; -} - -std::size_t WriteStringToFile(bool text_file, const std::string& filename, std::string_view str) { - return IOFile(filename, text_file ? "w" : "wb").WriteString(str); -} - -std::size_t ReadFileToString(bool text_file, const std::string& filename, std::string& str) { - IOFile file(filename, text_file ? "r" : "rb"); - - if (!file.IsOpen()) - return 0; - - str.resize(static_cast(file.GetSize())); - return file.ReadArray(&str[0], str.size()); -} - -void SplitFilename83(const std::string& filename, std::array& short_name, - std::array& extension) { - static constexpr std::string_view forbidden_characters = ".\"/\\[]:;=, "; - - // On a FAT32 partition, 8.3 names are stored as a 11 bytes array, filled with spaces. - short_name = {{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\0'}}; - extension = {{' ', ' ', ' ', '\0'}}; - - auto point = filename.rfind('.'); - if (point == filename.size() - 1) { - point = filename.rfind('.', point); - } - - // Get short name. - int j = 0; - for (char letter : filename.substr(0, point)) { - if (forbidden_characters.find(letter, 0) != std::string::npos) { - continue; - } - if (j == 8) { - // TODO(Link Mauve): also do that for filenames containing a space. - // TODO(Link Mauve): handle multiple files having the same short name. - short_name[6] = '~'; - short_name[7] = '1'; - break; - } - short_name[j++] = static_cast(std::toupper(letter)); - } - - // Get extension. - if (point != std::string::npos) { - j = 0; - for (char letter : filename.substr(point + 1, 3)) { - extension[j++] = static_cast(std::toupper(letter)); - } - } -} - -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_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); -} - -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::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)); -} - -IOFile::IOFile() = default; - -IOFile::IOFile(const std::string& filename, const char openmode[], int flags) { - void(Open(filename, openmode, flags)); -} - -IOFile::~IOFile() { - Close(); -} - -IOFile::IOFile(IOFile&& other) noexcept { - Swap(other); -} - -IOFile& IOFile::operator=(IOFile&& other) noexcept { - Swap(other); - return *this; -} - -void IOFile::Swap(IOFile& other) noexcept { - std::swap(m_file, other.m_file); -} - -bool IOFile::Open(const std::string& filename, const char openmode[], int flags) { - Close(); - bool m_good; -#ifdef _WIN32 - if (flags != 0) { - m_file = _wfsopen(Common::UTF8ToUTF16W(filename).c_str(), - Common::UTF8ToUTF16W(openmode).c_str(), flags); - m_good = m_file != nullptr; - } else { - m_good = _wfopen_s(&m_file, Common::UTF8ToUTF16W(filename).c_str(), - Common::UTF8ToUTF16W(openmode).c_str()) == 0; - } -#else - m_file = std::fopen(filename.c_str(), openmode); - m_good = m_file != nullptr; -#endif - - return m_good; -} - -bool IOFile::Close() { - if (!IsOpen() || 0 != std::fclose(m_file)) { - return false; - } - - m_file = nullptr; - return true; -} - -u64 IOFile::GetSize() const { - if (IsOpen()) { - return FS::GetSize(m_file); - } - return 0; -} - -bool IOFile::Seek(s64 off, int origin) const { - return IsOpen() && 0 == fseeko(m_file, off, origin); -} - -u64 IOFile::Tell() const { - if (IsOpen()) { - return ftello(m_file); - } - return std::numeric_limits::max(); -} - -bool IOFile::Flush() { - return IsOpen() && 0 == std::fflush(m_file); -} - -std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) const { - if (!IsOpen()) { - return std::numeric_limits::max(); - } - - if (length == 0) { - return 0; - } - - DEBUG_ASSERT(data != nullptr); - - return std::fread(data, data_size, length, m_file); -} - -std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { - if (!IsOpen()) { - return std::numeric_limits::max(); - } - - if (length == 0) { - return 0; - } - - DEBUG_ASSERT(data != nullptr); - - return std::fwrite(data, data_size, length, m_file); -} - -bool IOFile::Resize(u64 size) { - return IsOpen() && 0 == -#ifdef _WIN32 - // ector: _chsize sucks, not 64-bit safe - // F|RES: changed to _chsize_s. i think it is 64-bit safe - _chsize_s(_fileno(m_file), size) -#else - // TODO: handle 64bit and growing - ftruncate(fileno(m_file), size) -#endif - ; -} - -} // namespace Common::FS diff --git a/src/common/file_util.h b/src/common/file_util.h deleted file mode 100644 index 840cde2a6..000000000 --- a/src/common/file_util.h +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "common/common_types.h" -#ifdef _MSC_VER -#include "common/string_util.h" -#endif - -namespace Common::FS { - -// User paths for GetUserPath -enum class UserPath { - CacheDir, - ConfigDir, - KeysDir, - LogDir, - NANDDir, - RootDir, - SDMCDir, - LoadDir, - DumpDir, - ScreenshotsDir, - ShaderDir, - SysDataDir, - UserDir, -}; - -// FileSystem tree node/ -struct FSTEntry { - bool isDirectory; - u64 size; // file length or number of entries from children - std::string physicalName; // name on disk - std::string virtualName; // name in FST names table - std::vector children; -}; - -// Returns true if file filename exists -[[nodiscard]] bool Exists(const std::string& filename); - -// Returns true if filename is a directory -[[nodiscard]] bool IsDirectory(const std::string& filename); - -// Returns the size of filename (64bit) -[[nodiscard]] u64 GetSize(const std::string& filename); - -// Overloaded GetSize, accepts file descriptor -[[nodiscard]] u64 GetSize(int fd); - -// Overloaded GetSize, accepts FILE* -[[nodiscard]] u64 GetSize(FILE* f); - -// Returns true if successful, or path already exists. -bool CreateDir(const std::string& filename); - -// Creates the full path of fullPath returns true on success -bool CreateFullPath(const std::string& fullPath); - -// Deletes a given filename, return true on success -// Doesn't supports deleting a directory -bool Delete(const std::string& filename); - -// Deletes a directory filename, returns true on success -bool DeleteDir(const std::string& filename); - -// renames file srcFilename to destFilename, returns true on success -bool Rename(const std::string& srcFilename, const std::string& destFilename); - -// copies file srcFilename to destFilename, returns true on success -bool Copy(const std::string& srcFilename, const std::string& destFilename); - -// creates an empty file filename, returns true on success -bool CreateEmptyFile(const std::string& filename); - -/** - * @param num_entries_out to be assigned by the callable with the number of iterated directory - * entries, never null - * @param directory the path to the enclosing directory - * @param virtual_name the entry name, without any preceding directory info - * @return whether handling the entry succeeded - */ -using DirectoryEntryCallable = std::function; - -/** - * Scans a directory, calling the callback for each file/directory contained within. - * If the callback returns failure, scanning halts and this function returns failure as well - * @param num_entries_out assigned by the function with the number of iterated directory entries, - * can be null - * @param directory the directory to scan - * @param callback The callback which will be called for each entry - * @return whether scanning the directory succeeded - */ -bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, - DirectoryEntryCallable callback); - -/** - * Scans the directory tree, storing the results. - * @param directory the parent directory to start scanning from - * @param parent_entry FSTEntry where the filesystem tree results will be stored. - * @param recursion Number of children directories to read before giving up. - * @return the total number of files/directories found - */ -u64 ScanDirectoryTree(const std::string& directory, FSTEntry& parent_entry, - unsigned int recursion = 0); - -// deletes the given directory and anything under it. Returns true on success. -bool DeleteDirRecursively(const std::string& directory, unsigned int recursion = 256); - -// Returns the current directory -[[nodiscard]] std::optional GetCurrentDir(); - -// Create directory and copy contents (does not overwrite existing files) -void CopyDir(const std::string& source_path, const std::string& dest_path); - -// Set the current directory to given directory -bool SetCurrentDir(const std::string& directory); - -// Returns a pointer to a string with a yuzu data dir in the user's home -// directory. To be used in "multi-user" mode (that is, installed). -const std::string& GetUserPath(UserPath path, const std::string& new_path = ""); - -[[nodiscard]] std::string GetHactoolConfigurationPath(); - -[[nodiscard]] std::string GetNANDRegistrationDir(bool system = false); - -// Returns the path to where the sys file are -[[nodiscard]] std::string GetSysDirectory(); - -#ifdef __APPLE__ -[[nodiscard]] std::string GetBundleDirectory(); -#endif - -#ifdef _WIN32 -[[nodiscard]] const std::string& GetExeDirectory(); -[[nodiscard]] std::string AppDataRoamingDirectory(); -#endif - -std::size_t WriteStringToFile(bool text_file, const std::string& filename, std::string_view str); - -std::size_t ReadFileToString(bool text_file, const std::string& filename, std::string& str); - -/** - * Splits the filename into 8.3 format - * Loosely implemented following https://en.wikipedia.org/wiki/8.3_filename - * @param filename The normal filename to use - * @param short_name A 9-char array in which the short name will be written - * @param extension A 4-char array in which the extension will be written - */ -void SplitFilename83(const std::string& filename, std::array& short_name, - std::array& extension); - -// 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); - -// 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); - -// Removes the final '/' or '\' if one exists -[[nodiscard]] std::string_view RemoveTrailingSlash(std::string_view path); - -// Creates a new vector containing indices [first, last) from the original. -template -[[nodiscard]] std::vector SliceVector(const std::vector& vector, std::size_t first, - std::size_t last) { - if (first >= last) { - return {}; - } - last = std::min(last, vector.size()); - return std::vector(vector.begin() + first, vector.begin() + first + last); -} - -enum class DirectorySeparator { - ForwardSlash, - BackwardSlash, - PlatformDefault, -}; - -// 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); - -// To deal with Windows being dumb at Unicode -template -void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode) { -#ifdef _MSC_VER - fstream.open(Common::UTF8ToUTF16W(filename), openmode); -#else - fstream.open(filename, openmode); -#endif -} - -// simple wrapper for cstdlib file functions to -// hopefully will make error checking easier -// and make forgetting an fclose() harder -class IOFile : public NonCopyable { -public: - IOFile(); - // flags is used for windows specific file open mode flags, which - // allows yuzu to open the logs in shared write mode, so that the file - // isn't considered "locked" while yuzu is open and people can open the log file and view it - IOFile(const std::string& filename, const char openmode[], int flags = 0); - - ~IOFile(); - - IOFile(IOFile&& other) noexcept; - IOFile& operator=(IOFile&& other) noexcept; - - void Swap(IOFile& other) noexcept; - - bool Open(const std::string& filename, const char openmode[], int flags = 0); - bool Close(); - - template - std::size_t ReadArray(T* data, std::size_t length) const { - static_assert(std::is_trivially_copyable_v, - "Given array does not consist of trivially copyable objects"); - - return ReadImpl(data, length, sizeof(T)); - } - - template - std::size_t WriteArray(const T* data, std::size_t length) { - static_assert(std::is_trivially_copyable_v, - "Given array does not consist of trivially copyable objects"); - - return WriteImpl(data, length, sizeof(T)); - } - - template - std::size_t ReadBytes(T* data, std::size_t length) const { - static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); - return ReadArray(reinterpret_cast(data), length); - } - - template - std::size_t WriteBytes(const T* data, std::size_t length) { - static_assert(std::is_trivially_copyable_v, "T must be trivially copyable"); - return WriteArray(reinterpret_cast(data), length); - } - - template - std::size_t WriteObject(const T& object) { - static_assert(!std::is_pointer_v, "WriteObject arguments must not be a pointer"); - return WriteArray(&object, 1); - } - - std::size_t WriteString(std::string_view str) { - return WriteArray(str.data(), str.length()); - } - - [[nodiscard]] bool IsOpen() const { - return nullptr != m_file; - } - - bool Seek(s64 off, int origin) const; - [[nodiscard]] u64 Tell() const; - [[nodiscard]] u64 GetSize() const; - bool Resize(u64 size); - bool Flush(); - - // clear error state - void Clear() { - std::clearerr(m_file); - } - -private: - std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) const; - std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size); - - std::FILE* m_file = nullptr; -}; - -} // namespace Common::FS diff --git a/src/common/fs/file.cpp b/src/common/fs/file.cpp new file mode 100644 index 000000000..9f3de1cb0 --- /dev/null +++ b/src/common/fs/file.cpp @@ -0,0 +1,392 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" + +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +#ifdef _MSC_VER +#define fileno _fileno +#define fseeko _fseeki64 +#define ftello _ftelli64 +#endif + +namespace Common::FS { + +namespace fs = std::filesystem; + +namespace { + +#ifdef _WIN32 + +/** + * Converts the file access mode and file type enums to a file access mode wide string. + * + * @param mode File access mode + * @param type File type + * + * @returns A pointer to a wide string representing the file access mode. + */ +[[nodiscard]] constexpr const wchar_t* AccessModeToWStr(FileAccessMode mode, FileType type) { + switch (type) { + case FileType::BinaryFile: + switch (mode) { + case FileAccessMode::Read: + return L"rb"; + case FileAccessMode::Write: + return L"wb"; + case FileAccessMode::Append: + return L"ab"; + case FileAccessMode::ReadWrite: + return L"r+b"; + case FileAccessMode::ReadAppend: + return L"a+b"; + } + break; + case FileType::TextFile: + switch (mode) { + case FileAccessMode::Read: + return L"r"; + case FileAccessMode::Write: + return L"w"; + case FileAccessMode::Append: + return L"a"; + case FileAccessMode::ReadWrite: + return L"r+"; + case FileAccessMode::ReadAppend: + return L"a+"; + } + break; + } + + return L""; +} + +/** + * Converts the file-share access flag enum to a Windows defined file-share access flag. + * + * @param flag File-share access flag + * + * @returns Windows defined file-share access flag. + */ +[[nodiscard]] constexpr int ToWindowsFileShareFlag(FileShareFlag flag) { + switch (flag) { + case FileShareFlag::ShareNone: + default: + return _SH_DENYRW; + case FileShareFlag::ShareReadOnly: + return _SH_DENYWR; + case FileShareFlag::ShareWriteOnly: + return _SH_DENYRD; + case FileShareFlag::ShareReadWrite: + return _SH_DENYNO; + } +} + +#else + +/** + * Converts the file access mode and file type enums to a file access mode string. + * + * @param mode File access mode + * @param type File type + * + * @returns A pointer to a string representing the file access mode. + */ +[[nodiscard]] constexpr const char* AccessModeToStr(FileAccessMode mode, FileType type) { + switch (type) { + case FileType::BinaryFile: + switch (mode) { + case FileAccessMode::Read: + return "rb"; + case FileAccessMode::Write: + return "wb"; + case FileAccessMode::Append: + return "ab"; + case FileAccessMode::ReadWrite: + return "r+b"; + case FileAccessMode::ReadAppend: + return "a+b"; + } + break; + case FileType::TextFile: + switch (mode) { + case FileAccessMode::Read: + return "r"; + case FileAccessMode::Write: + return "w"; + case FileAccessMode::Append: + return "a"; + case FileAccessMode::ReadWrite: + return "r+"; + case FileAccessMode::ReadAppend: + return "a+"; + } + break; + } + + return ""; +} + +#endif + +/** + * Converts the seek origin enum to a seek origin integer. + * + * @param origin Seek origin + * + * @returns Seek origin integer. + */ +[[nodiscard]] constexpr int ToSeekOrigin(SeekOrigin origin) { + switch (origin) { + case SeekOrigin::SetOrigin: + default: + return SEEK_SET; + case SeekOrigin::CurrentPosition: + return SEEK_CUR; + case SeekOrigin::End: + return SEEK_END; + } +} + +} // Anonymous namespace + +std::string ReadStringFromFile(const std::filesystem::path& path, FileType type) { + if (!IsFile(path)) { + return ""; + } + + IOFile io_file{path, FileAccessMode::Read, type}; + + return io_file.ReadString(io_file.GetSize()); +} + +size_t WriteStringToFile(const std::filesystem::path& path, FileType type, + std::string_view string) { + if (!IsFile(path)) { + return 0; + } + + IOFile io_file{path, FileAccessMode::Write, type}; + + return io_file.WriteString(string); +} + +size_t AppendStringToFile(const std::filesystem::path& path, FileType type, + std::string_view string) { + if (!Exists(path)) { + return WriteStringToFile(path, type, string); + } + + if (!IsFile(path)) { + return 0; + } + + IOFile io_file{path, FileAccessMode::Append, type}; + + return io_file.WriteString(string); +} + +IOFile::IOFile() = default; + +IOFile::IOFile(const std::string& path, FileAccessMode mode, FileType type, FileShareFlag flag) { + Open(path, mode, type, flag); +} + +IOFile::IOFile(std::string_view path, FileAccessMode mode, FileType type, FileShareFlag flag) { + Open(path, mode, type, flag); +} + +IOFile::IOFile(const fs::path& path, FileAccessMode mode, FileType type, FileShareFlag flag) { + Open(path, mode, type, flag); +} + +IOFile::~IOFile() { + Close(); +} + +IOFile::IOFile(IOFile&& other) noexcept { + std::swap(file_path, other.file_path); + std::swap(file_access_mode, other.file_access_mode); + std::swap(file_type, other.file_type); + std::swap(file, other.file); +} + +IOFile& IOFile::operator=(IOFile&& other) noexcept { + std::swap(file_path, other.file_path); + std::swap(file_access_mode, other.file_access_mode); + std::swap(file_type, other.file_type); + std::swap(file, other.file); + return *this; +} + +fs::path IOFile::GetPath() const { + return file_path; +} + +FileAccessMode IOFile::GetAccessMode() const { + return file_access_mode; +} + +FileType IOFile::GetType() const { + return file_type; +} + +void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, FileShareFlag flag) { + Close(); + + file_path = path; + file_access_mode = mode; + file_type = type; + + errno = 0; + +#ifdef _WIN32 + if (flag != FileShareFlag::ShareNone) { + file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag)); + } else { + _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type)); + } +#else + file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); +#endif + + if (!IsOpen()) { + const auto ec = std::error_code{errno, std::generic_category()}; + LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}, ec_message={}", + PathToUTF8String(file_path), ec.message()); + } +} + +void IOFile::Close() { + if (!IsOpen()) { + return; + } + + errno = 0; + + const auto close_result = std::fclose(file) == 0; + + if (!close_result) { + const auto ec = std::error_code{errno, std::generic_category()}; + LOG_ERROR(Common_Filesystem, "Failed to close the file at path={}, ec_message={}", + PathToUTF8String(file_path), ec.message()); + } + + file = nullptr; +} + +bool IOFile::IsOpen() const { + return file != nullptr; +} + +std::string IOFile::ReadString(size_t length) const { + std::vector string_buffer(length); + + const auto chars_read = ReadSpan(string_buffer); + const auto string_size = chars_read != length ? chars_read : length; + + return std::string{string_buffer.data(), string_size}; +} + +size_t IOFile::WriteString(std::span string) const { + return WriteSpan(string); +} + +bool IOFile::Flush() const { + if (!IsOpen()) { + return false; + } + + errno = 0; + + const auto flush_result = std::fflush(file) == 0; + + if (!flush_result) { + const auto ec = std::error_code{errno, std::generic_category()}; + LOG_ERROR(Common_Filesystem, "Failed to flush the file at path={}, ec_message={}", + PathToUTF8String(file_path), ec.message()); + } + + return flush_result; +} + +bool IOFile::SetSize(u64 size) const { + if (!IsOpen()) { + return false; + } + + errno = 0; + +#ifdef _WIN32 + const auto set_size_result = _chsize_s(fileno(file), static_cast(size)) == 0; +#else + const auto set_size_result = ftruncate(fileno(file), static_cast(size)) == 0; +#endif + + if (!set_size_result) { + const auto ec = std::error_code{errno, std::generic_category()}; + LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={}, size={}, ec_message={}", + PathToUTF8String(file_path), size, ec.message()); + } + + return set_size_result; +} + +u64 IOFile::GetSize() const { + if (!IsOpen()) { + return 0; + } + + std::error_code ec; + + const auto file_size = fs::file_size(file_path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to retrieve the file size of path={}, ec_message={}", + PathToUTF8String(file_path), ec.message()); + return 0; + } + + return file_size; +} + +bool IOFile::Seek(s64 offset, SeekOrigin origin) const { + if (!IsOpen()) { + return false; + } + + errno = 0; + + const auto seek_result = fseeko(file, offset, ToSeekOrigin(origin)) == 0; + + if (!seek_result) { + const auto ec = std::error_code{errno, std::generic_category()}; + LOG_ERROR(Common_Filesystem, + "Failed to seek the file at path={}, offset={}, origin={}, ec_message={}", + PathToUTF8String(file_path), offset, origin, ec.message()); + } + + return seek_result; +} + +s64 IOFile::Tell() const { + if (!IsOpen()) { + return 0; + } + + errno = 0; + + return ftello(file); +} + +} // namespace Common::FS diff --git a/src/common/fs/file.h b/src/common/fs/file.h new file mode 100644 index 000000000..209f9664b --- /dev/null +++ b/src/common/fs/file.h @@ -0,0 +1,450 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common/concepts.h" +#include "common/fs/fs_types.h" +#include "common/fs/fs_util.h" + +namespace Common::FS { + +enum class SeekOrigin { + SetOrigin, // Seeks from the start of the file. + CurrentPosition, // Seeks from the current file pointer position. + End, // Seeks from the end of the file. +}; + +/** + * Opens a file stream at path with the specified open mode. + * + * @param file_stream Reference to file stream + * @param path Filesystem path + * @param open_mode File stream open mode + */ +template +void OpenFileStream(FileStream& file_stream, const std::filesystem::path& path, + std::ios_base::openmode open_mode) { + file_stream.open(path, open_mode); +} + +#ifdef _WIN32 +template +void OpenFileStream(FileStream& file_stream, const Path& path, std::ios_base::openmode open_mode) { + if constexpr (IsChar) { + file_stream.open(ToU8String(path), open_mode); + } else { + file_stream.open(std::filesystem::path{path}, open_mode); + } +} +#endif + +/** + * Reads an entire file at path and returns a string of the contents read from the file. + * If the filesystem object at path is not a file, this function returns an empty string. + * + * @param path Filesystem path + * @param type File type + * + * @returns A string of the contents read from the file. + */ +[[nodiscard]] std::string ReadStringFromFile(const std::filesystem::path& path, FileType type); + +#ifdef _WIN32 +template +[[nodiscard]] std::string ReadStringFromFile(const Path& path, FileType type) { + if constexpr (IsChar) { + return ReadStringFromFile(ToU8String(path), type); + } else { + return ReadStringFromFile(std::filesystem::path{path}, type); + } +} +#endif + +/** + * Writes a string to a file at path and returns the number of characters successfully written. + * If an file already exists at path, its contents will be erased. + * If the filesystem object at path is not a file, this function returns 0. + * + * @param path Filesystem path + * @param type File type + * + * @returns Number of characters successfully written. + */ +[[nodiscard]] size_t WriteStringToFile(const std::filesystem::path& path, FileType type, + std::string_view string); + +#ifdef _WIN32 +template +[[nodiscard]] size_t WriteStringToFile(const Path& path, FileType type, std::string_view string) { + if constexpr (IsChar) { + return WriteStringToFile(ToU8String(path), type, string); + } else { + return WriteStringToFile(std::filesystem::path{path}, type, string); + } +} +#endif + +/** + * Appends a string to a file at path and returns the number of characters successfully written. + * If a file does not exist at path, WriteStringToFile is called instead. + * If the filesystem object at path is not a file, this function returns 0. + * + * @param path Filesystem path + * @param type File type + * + * @returns Number of characters successfully written. + */ +[[nodiscard]] size_t AppendStringToFile(const std::filesystem::path& path, FileType type, + std::string_view string); + +#ifdef _WIN32 +template +[[nodiscard]] size_t AppendStringToFile(const Path& path, FileType type, std::string_view string) { + if constexpr (IsChar) { + return AppendStringToFile(ToU8String(path), type, string); + } else { + return AppendStringToFile(std::filesystem::path{path}, type, string); + } +} +#endif + +class IOFile final : NonCopyable { +public: + IOFile(); + + explicit IOFile(const std::string& path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly); + + explicit IOFile(std::string_view path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly); + + /** + * An IOFile is a lightweight wrapper on C Library file operations. + * Automatically closes an open file on the destruction of an IOFile object. + * + * @param path Filesystem path + * @param mode File access mode + * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file + * @param flag (Windows only) File-share access flag, default is ShareReadOnly + */ + explicit IOFile(const std::filesystem::path& path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly); + + virtual ~IOFile(); + + IOFile(IOFile&& other) noexcept; + IOFile& operator=(IOFile&& other) noexcept; + + /** + * Gets the path of the file. + * + * @returns The path of the file. + */ + [[nodiscard]] std::filesystem::path GetPath() const; + + /** + * Gets the access mode of the file. + * + * @returns The access mode of the file. + */ + [[nodiscard]] FileAccessMode GetAccessMode() const; + + /** + * Gets the type of the file. + * + * @returns The type of the file. + */ + [[nodiscard]] FileType GetType() const; + + /** + * Opens a file at path with the specified file access mode. + * This function behaves differently depending on the FileAccessMode. + * These behaviors are documented in each enum value of FileAccessMode. + * + * @param path Filesystem path + * @param mode File access mode + * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file + * @param flag (Windows only) File-share access flag, default is ShareReadOnly + */ + void Open(const std::filesystem::path& path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly); + +#ifdef _WIN32 + template + [[nodiscard]] void Open(const Path& path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly) { + using ValueType = typename Path::value_type; + if constexpr (IsChar) { + Open(ToU8String(path), mode, type, flag); + } else { + Open(std::filesystem::path{path}, mode, type, flag); + } + } +#endif + + /// Closes the file if it is opened. + void Close(); + + /** + * Checks whether the file is open. + * Use this to check whether the calls to Open() or Close() succeeded. + * + * @returns True if the file is open, false otherwise. + */ + [[nodiscard]] bool IsOpen() const; + + /** + * Helper function which deduces the value type of a contiguous STL container used in ReadSpan. + * If T is not a contiguous STL container as defined by the concept IsSTLContainer, this calls + * ReadObject and T must be a trivially copyable object. + * + * See ReadSpan for more details if T is a contiguous container. + * See ReadObject for more details if T is a trivially copyable object. + * + * @tparam T Contiguous container or trivially copyable object + * + * @param data Container of T::value_type data or reference to object + * + * @returns Count of T::value_type data or objects successfully read. + */ + template + [[nodiscard]] size_t Read(T& data) const { + if constexpr (IsSTLContainer) { + using ContiguousType = typename T::value_type; + static_assert(std::is_trivially_copyable_v, + "Data type must be trivially copyable."); + return ReadSpan(data); + } else { + return ReadObject(data) ? 1 : 0; + } + } + + /** + * Helper function which deduces the value type of a contiguous STL container used in WriteSpan. + * If T is not a contiguous STL container as defined by the concept IsSTLContainer, this calls + * WriteObject and T must be a trivially copyable object. + * + * See WriteSpan for more details if T is a contiguous container. + * See WriteObject for more details if T is a trivially copyable object. + * + * @tparam T Contiguous container or trivially copyable object + * + * @param data Container of T::value_type data or const reference to object + * + * @returns Count of T::value_type data or objects successfully written. + */ + template + [[nodiscard]] size_t Write(const T& data) const { + if constexpr (IsSTLContainer) { + using ContiguousType = typename T::value_type; + static_assert(std::is_trivially_copyable_v, + "Data type must be trivially copyable."); + return WriteSpan(data); + } else { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + return WriteObject(data) ? 1 : 0; + } + } + + /** + * Reads a span of T data from a file sequentially. + * This function reads from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully read. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks read permissions + * - Attempting to read beyond the end-of-file + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully read. + */ + template + [[nodiscard]] size_t ReadSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fread(data.data(), sizeof(T), data.size(), file); + } + + /** + * Writes a span of T data to a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the (count of T * sizeof(T)) bytes successfully written. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks write permissions + * + * @tparam T Data type + * + * @param data Span of T data + * + * @returns Count of T data successfully written. + */ + template + [[nodiscard]] size_t WriteSpan(std::span data) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + + if (!IsOpen()) { + return 0; + } + + return std::fwrite(data.data(), sizeof(T), data.size(), file); + } + + /** + * Reads a T object from a file sequentially. + * This function reads from the current position of the file pointer and + * advances it by the sizeof(T) bytes successfully read. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks read permissions + * - Attempting to read beyond the end-of-file + * + * @tparam T Data type + * + * @param object Reference to object + * + * @returns True if the object is successfully read from the file, false otherwise. + */ + template + [[nodiscard]] bool ReadObject(T& object) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + static_assert(!std::is_pointer_v, "T must not be a pointer to an object."); + + if (!IsOpen()) { + return false; + } + + return std::fread(&object, sizeof(T), 1, file) == 1; + } + + /** + * Writes a T object to a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the sizeof(T) bytes successfully written. + * + * Failures occur when: + * - The file is not open + * - The opened file lacks write permissions + * + * @tparam T Data type + * + * @param object Const reference to object + * + * @returns True if the object is successfully written to the file, false otherwise. + */ + template + [[nodiscard]] bool WriteObject(const T& object) const { + static_assert(std::is_trivially_copyable_v, "Data type must be trivially copyable."); + static_assert(!std::is_pointer_v, "T must not be a pointer to an object."); + + if (!IsOpen()) { + return false; + } + + return std::fwrite(&object, sizeof(T), 1, file) == 1; + } + + /** + * Specialized function to read a string of a given length from a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the number of characters successfully read. + * The size of the returned string may not match length if not all bytes are successfully read. + * + * @param length Length of the string + * + * @returns A string read from the file. + */ + [[nodiscard]] std::string ReadString(size_t length) const; + + /** + * Specialized function to write a string to a file sequentially. + * This function writes from the current position of the file pointer and + * advances it by the number of characters successfully written. + * + * @param string Span of const char backed std::string or std::string_view + * + * @returns Number of characters successfully written. + */ + [[nodiscard]] size_t WriteString(std::span string) const; + + /** + * Flushes any unwritten buffered data into the file. + * + * @returns True if the flush was successful, false otherwise. + */ + [[nodiscard]] bool Flush() const; + + /** + * Resizes the file to a given size. + * If the file is resized to a smaller size, the remainder of the file is discarded. + * If the file is resized to a larger size, the new area appears as if zero-filled. + * + * Failures occur when: + * - The file is not open + * + * @param size File size in bytes + * + * @returns True if the file resize succeeded, false otherwise. + */ + [[nodiscard]] bool SetSize(u64 size) const; + + /** + * Gets the size of the file. + * + * Failures occur when: + * - The file is not open + * + * @returns The file size in bytes of the file. Returns 0 on failure. + */ + [[nodiscard]] u64 GetSize() const; + + /** + * Moves the current position of the file pointer with the specified offset and seek origin. + * + * @param offset Offset from seek origin + * @param origin Seek origin + * + * @returns True if the file pointer has moved to the specified offset, false otherwise. + */ + [[nodiscard]] bool Seek(s64 offset, SeekOrigin origin = SeekOrigin::SetOrigin) const; + + /** + * Gets the current position of the file pointer. + * + * @returns The current position of the file pointer. + */ + [[nodiscard]] s64 Tell() const; + +private: + std::filesystem::path file_path; + FileAccessMode file_access_mode; + FileType file_type; + + std::FILE* file = nullptr; +}; + +} // namespace Common::FS diff --git a/src/common/fs/fs.cpp b/src/common/fs/fs.cpp new file mode 100644 index 000000000..d492480d9 --- /dev/null +++ b/src/common/fs/fs.cpp @@ -0,0 +1,610 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/logging/log.h" + +namespace Common::FS { + +namespace fs = std::filesystem; + +// File Operations + +bool NewFile(const fs::path& path, u64 size) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path.parent_path())) { + LOG_ERROR(Common_Filesystem, "Parent directory of path={} does not exist", + PathToUTF8String(path)); + return false; + } + + if (Exists(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} exists", PathToUTF8String(path)); + return false; + } + + IOFile io_file{path, FileAccessMode::Write}; + + if (!io_file.IsOpen()) { + LOG_ERROR(Common_Filesystem, "Failed to create a file at path={}", PathToUTF8String(path)); + return false; + } + + if (!io_file.SetSize(size)) { + LOG_ERROR(Common_Filesystem, "Failed to resize the file at path={} to size={}", + PathToUTF8String(path), size); + return false; + } + + io_file.Close(); + + LOG_DEBUG(Common_Filesystem, "Successfully created a file at path={} with size={}", + PathToUTF8String(path), size); + + return true; +} + +bool RemoveFile(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return true; + } + + if (!IsFile(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a file", + PathToUTF8String(path)); + return false; + } + + std::error_code ec; + + fs::remove(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to remove the file at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully removed the file at path={}", + PathToUTF8String(path)); + + return true; +} + +bool RenameFile(const fs::path& old_path, const fs::path& new_path) { + if (!ValidatePath(old_path) || !ValidatePath(new_path)) { + LOG_ERROR(Common_Filesystem, + "One or both input path(s) is not valid, old_path={}, new_path={}", + PathToUTF8String(old_path), PathToUTF8String(new_path)); + return false; + } + + if (!Exists(old_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} does not exist", + PathToUTF8String(old_path)); + return false; + } + + if (!IsFile(old_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} is not a file", + PathToUTF8String(old_path)); + return false; + } + + if (Exists(new_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} exists", + PathToUTF8String(new_path)); + return false; + } + + std::error_code ec; + + fs::rename(old_path, new_path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to rename the file from old_path={} to new_path={}, ec_message={}", + PathToUTF8String(old_path), PathToUTF8String(new_path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully renamed the file from old_path={} to new_path={}", + PathToUTF8String(old_path), PathToUTF8String(new_path)); + + return true; +} + +std::shared_ptr FileOpen(const fs::path& path, FileAccessMode mode, FileType type, + FileShareFlag flag) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return nullptr; + } + + if (!IsFile(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a file", + PathToUTF8String(path)); + return nullptr; + } + + auto io_file = std::make_shared(path, mode, type, flag); + + if (!io_file->IsOpen()) { + io_file.reset(); + + LOG_ERROR(Common_Filesystem, + "Failed to open the file at path={} with mode={}, type={}, flag={}", + PathToUTF8String(path), mode, type, flag); + + return nullptr; + } + + LOG_DEBUG(Common_Filesystem, + "Successfully opened the file at path={} with mode={}, type={}, flag={}", + PathToUTF8String(path), mode, type, flag); + + return io_file; +} + +// Directory Operations + +bool CreateDir(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path.parent_path())) { + LOG_ERROR(Common_Filesystem, "Parent directory of path={} does not exist", + PathToUTF8String(path)); + return false; + } + + if (IsDir(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} exists and is a directory", + PathToUTF8String(path)); + return true; + } + + std::error_code ec; + + fs::create_directory(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to create the directory at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully created the directory at path={}", + PathToUTF8String(path)); + + return true; +} + +bool CreateDirs(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (IsDir(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} exists and is a directory", + PathToUTF8String(path)); + return true; + } + + std::error_code ec; + + fs::create_directories(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to create the directories at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully created the directories at path={}", + PathToUTF8String(path)); + + return true; +} + +bool CreateParentDir(const fs::path& path) { + return CreateDir(path.parent_path()); +} + +bool CreateParentDirs(const fs::path& path) { + return CreateDirs(path.parent_path()); +} + +bool RemoveDir(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return true; + } + + if (!IsDir(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", + PathToUTF8String(path)); + return false; + } + + std::error_code ec; + + fs::remove(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to remove the directory at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully removed the directory at path={}", + PathToUTF8String(path)); + + return true; +} + +bool RemoveDirRecursively(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return true; + } + + if (!IsDir(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", + PathToUTF8String(path)); + return false; + } + + std::error_code ec; + + fs::remove_all(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to remove the directory and its contents at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully removed the directory and its contents at path={}", + PathToUTF8String(path)); + + return true; +} + +bool RemoveDirContentsRecursively(const fs::path& path) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return false; + } + + if (!Exists(path)) { + LOG_DEBUG(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return true; + } + + if (!IsDir(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", + PathToUTF8String(path)); + return false; + } + + std::error_code ec; + + for (const auto& entry : fs::recursive_directory_iterator(path, ec)) { + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to completely enumerate the directory at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + break; + } + + fs::remove(entry.path(), ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to remove the filesystem object at path={}, ec_message={}", + PathToUTF8String(entry.path()), ec.message()); + break; + } + } + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to remove all the contents of the directory at path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, + "Successfully removed all the contents of the directory at path={}", + PathToUTF8String(path)); + + return true; +} + +bool RenameDir(const fs::path& old_path, const fs::path& new_path) { + if (!ValidatePath(old_path) || !ValidatePath(new_path)) { + LOG_ERROR(Common_Filesystem, + "One or both input path(s) is not valid, old_path={}, new_path={}", + PathToUTF8String(old_path), PathToUTF8String(new_path)); + return false; + } + + if (!Exists(old_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} does not exist", + PathToUTF8String(old_path)); + return false; + } + + if (!IsDir(old_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at old_path={} is not a directory", + PathToUTF8String(old_path)); + return false; + } + + if (Exists(new_path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at new_path={} exists", + PathToUTF8String(new_path)); + return false; + } + + std::error_code ec; + + fs::rename(old_path, new_path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to rename the file from old_path={} to new_path={}, ec_message={}", + PathToUTF8String(old_path), PathToUTF8String(new_path), ec.message()); + return false; + } + + LOG_DEBUG(Common_Filesystem, "Successfully renamed the file from old_path={} to new_path={}", + PathToUTF8String(old_path), PathToUTF8String(new_path)); + + return true; +} + +void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable& callback, + DirEntryFilter filter) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return; + } + + if (!Exists(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return; + } + + if (!IsDir(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", + PathToUTF8String(path)); + return; + } + + bool callback_error = false; + + std::error_code ec; + + for (const auto& entry : fs::directory_iterator(path, ec)) { + if (ec) { + break; + } + + if (True(filter & DirEntryFilter::File) && + entry.status().type() == fs::file_type::regular) { + if (!callback(entry.path())) { + callback_error = true; + break; + } + } + + if (True(filter & DirEntryFilter::Directory) && + entry.status().type() == fs::file_type::directory) { + if (!callback(entry.path())) { + callback_error = true; + break; + } + } + } + + if (callback_error || ec) { + LOG_ERROR(Common_Filesystem, + "Failed to visit all the directory entries of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return; + } + + LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}", + PathToUTF8String(path)); +} + +void IterateDirEntriesRecursively(const std::filesystem::path& path, + const DirEntryCallable& callback, DirEntryFilter filter) { + if (!ValidatePath(path)) { + LOG_ERROR(Common_Filesystem, "Input path is not valid, path={}", PathToUTF8String(path)); + return; + } + + if (!Exists(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} does not exist", + PathToUTF8String(path)); + return; + } + + if (!IsDir(path)) { + LOG_ERROR(Common_Filesystem, "Filesystem object at path={} is not a directory", + PathToUTF8String(path)); + return; + } + + bool callback_error = false; + + std::error_code ec; + + for (const auto& entry : fs::recursive_directory_iterator(path, ec)) { + if (ec) { + break; + } + + if (True(filter & DirEntryFilter::File) && + entry.status().type() == fs::file_type::regular) { + if (!callback(entry.path())) { + callback_error = true; + break; + } + } + + if (True(filter & DirEntryFilter::Directory) && + entry.status().type() == fs::file_type::directory) { + if (!callback(entry.path())) { + callback_error = true; + break; + } + } + } + + if (callback_error || ec) { + LOG_ERROR(Common_Filesystem, + "Failed to visit all the directory entries of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return; + } + + LOG_DEBUG(Common_Filesystem, "Successfully visited all the directory entries of path={}", + PathToUTF8String(path)); +} + +// Generic Filesystem Operations + +bool Exists(const fs::path& path) { + return fs::exists(path); +} + +bool IsFile(const fs::path& path) { + return fs::is_regular_file(path); +} + +bool IsDir(const fs::path& path) { + return fs::is_directory(path); +} + +fs::path GetCurrentDir() { + std::error_code ec; + + const auto current_path = fs::current_path(ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to get the current path, ec_message={}", ec.message()); + return {}; + } + + return current_path; +} + +bool SetCurrentDir(const fs::path& path) { + std::error_code ec; + + fs::current_path(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to set the current path to path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return false; + } + + return true; +} + +fs::file_type GetEntryType(const fs::path& path) { + std::error_code ec; + + const auto file_status = fs::status(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to retrieve the entry type of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return fs::file_type::not_found; + } + + return file_status.type(); +} + +u64 GetSize(const fs::path& path) { + std::error_code ec; + + const auto file_size = fs::file_size(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, "Failed to retrieve the file size of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return 0; + } + + return file_size; +} + +u64 GetFreeSpaceSize(const fs::path& path) { + std::error_code ec; + + const auto space_info = fs::space(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to retrieve the available free space of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return 0; + } + + return space_info.free; +} + +u64 GetTotalSpaceSize(const fs::path& path) { + std::error_code ec; + + const auto space_info = fs::space(path, ec); + + if (ec) { + LOG_ERROR(Common_Filesystem, + "Failed to retrieve the total capacity of path={}, ec_message={}", + PathToUTF8String(path), ec.message()); + return 0; + } + + return space_info.capacity; +} + +} // namespace Common::FS diff --git a/src/common/fs/fs.h b/src/common/fs/fs.h new file mode 100644 index 000000000..f6f256349 --- /dev/null +++ b/src/common/fs/fs.h @@ -0,0 +1,582 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include "common/fs/fs_types.h" +#include "common/fs/fs_util.h" + +namespace Common::FS { + +class IOFile; + +// File Operations + +/** + * Creates a new file at path with the specified size. + * + * Failures occur when: + * - Input path is not valid + * - The input path's parent directory does not exist + * - Filesystem object at path exists + * - Filesystem at path is read only + * + * @param path Filesystem path + * @param size File size + * + * @returns True if the file creation succeeds, false otherwise. + */ +[[nodiscard]] bool NewFile(const std::filesystem::path& path, u64 size = 0); + +#ifdef _WIN32 +template +[[nodiscard]] bool NewFile(const Path& path, u64 size = 0) { + if constexpr (IsChar) { + return NewFile(ToU8String(path), size); + } else { + return NewFile(std::filesystem::path{path}, size); + } +} +#endif + +/** + * Removes a file at path. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a file + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if file removal succeeds or file does not exist, false otherwise. + */ +[[nodiscard]] bool RemoveFile(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RemoveFile(const Path& path) { + if constexpr (IsChar) { + return RemoveFile(ToU8String(path)); + } else { + return RemoveFile(std::filesystem::path{path}); + } +} +#endif + +/** + * Renames a file from old_path to new_path. + * + * Failures occur when: + * - One or both input path(s) is not valid + * - Filesystem object at old_path does not exist + * - Filesystem object at old_path is not a file + * - Filesystem object at new_path exists + * - Filesystem at either path is read only + * + * @param old_path Old filesystem path + * @param new_path New filesystem path + * + * @returns True if file rename succeeds, false otherwise. + */ +[[nodiscard]] bool RenameFile(const std::filesystem::path& old_path, + const std::filesystem::path& new_path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RenameFile(const Path1& old_path, const Path2& new_path) { + using ValueType1 = typename Path1::value_type; + using ValueType2 = typename Path2::value_type; + if constexpr (IsChar && IsChar) { + return RenameFile(ToU8String(old_path), ToU8String(new_path)); + } else if constexpr (IsChar && !IsChar) { + return RenameFile(ToU8String(old_path), new_path); + } else if constexpr (!IsChar && IsChar) { + return RenameFile(old_path, ToU8String(new_path)); + } else { + return RenameFile(std::filesystem::path{old_path}, std::filesystem::path{new_path}); + } +} +#endif + +/** + * Opens a file at path with the specified file access mode. + * This function behaves differently depending on the FileAccessMode. + * These behaviors are documented in each enum value of FileAccessMode. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a file + * - The file is not opened + * + * @param path Filesystem path + * @param mode File access mode + * @param type File type, default is BinaryFile. Use TextFile to open the file as a text file + * @param flag (Windows only) File-share access flag, default is ShareReadOnly + * + * @returns A shared pointer to the opened file. Returns nullptr on failure. + */ +[[nodiscard]] std::shared_ptr FileOpen(const std::filesystem::path& path, + FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly); + +#ifdef _WIN32 +template +[[nodiscard]] std::shared_ptr FileOpen(const Path& path, FileAccessMode mode, + FileType type = FileType::BinaryFile, + FileShareFlag flag = FileShareFlag::ShareReadOnly) { + if constexpr (IsChar) { + return FileOpen(ToU8String(path), mode, type, flag); + } else { + return FileOpen(std::filesystem::path{path}, mode, type, flag); + } +} +#endif + +// Directory Operations + +/** + * Creates a directory at path. + * Note that this function will *always* assume that the input path is a directory. For example, + * if the input path is /path/to/directory/file.txt, it will create a directory called "file.txt". + * If you intend to create the parent directory of a file, use CreateParentDir instead. + * + * Failures occur when: + * - Input path is not valid + * - The input path's parent directory does not exist + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if directory creation succeeds or directory already exists, false otherwise. + */ +[[nodiscard]] bool CreateDir(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool CreateDir(const Path& path) { + if constexpr (IsChar) { + return CreateDir(ToU8String(path)); + } else { + return CreateDir(std::filesystem::path{path}); + } +} +#endif + +/** + * Recursively creates a directory at path. + * Note that this function will *always* assume that the input path is a directory. For example, + * if the input path is /path/to/directory/file.txt, it will create a directory called "file.txt". + * If you intend to create the parent directory of a file, use CreateParentDirs instead. + * Unlike CreateDir, this creates all of input path's parent directories if they do not exist. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if directory creation succeeds or directory already exists, false otherwise. + */ +[[nodiscard]] bool CreateDirs(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool CreateDirs(const Path& path) { + if constexpr (IsChar) { + return CreateDirs(ToU8String(path)); + } else { + return CreateDirs(std::filesystem::path{path}); + } +} +#endif + +/** + * Creates the parent directory of a given path. + * This function calls CreateDir(path.parent_path()), see CreateDir for more details. + * + * @param path Filesystem path + * + * @returns True if directory creation succeeds or directory already exists, false otherwise. + */ +[[nodiscard]] bool CreateParentDir(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool CreateParentDir(const Path& path) { + if constexpr (IsChar) { + return CreateParentDir(ToU8String(path)); + } else { + return CreateParentDir(std::filesystem::path{path}); + } +} +#endif + +/** + * Recursively creates the parent directory of a given path. + * This function calls CreateDirs(path.parent_path()), see CreateDirs for more details. + * + * @param path Filesystem path + * + * @returns True if directory creation succeeds or directory already exists, false otherwise. + */ +[[nodiscard]] bool CreateParentDirs(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool CreateParentDirs(const Path& path) { + if constexpr (IsChar) { + return CreateParentDirs(ToU8String(path)); + } else { + return CreateParentDirs(std::filesystem::path{path}); + } +} +#endif + +/** + * Removes a directory at path. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a directory + * - The given directory is not empty + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if directory removal succeeds or directory does not exist, false otherwise. + */ +[[nodiscard]] bool RemoveDir(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RemoveDir(const Path& path) { + if constexpr (IsChar) { + return RemoveDir(ToU8String(path)); + } else { + return RemoveDir(std::filesystem::path{path}); + } +} +#endif + +/** + * Removes all the contents within the given directory and removes the directory itself. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a directory + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if the directory and all of its contents are removed successfully, false otherwise. + */ +[[nodiscard]] bool RemoveDirRecursively(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RemoveDirRecursively(const Path& path) { + if constexpr (IsChar) { + return RemoveDirRecursively(ToU8String(path)); + } else { + return RemoveDirRecursively(std::filesystem::path{path}); + } +} +#endif + +/** + * Removes all the contents within the given directory without removing the directory itself. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a directory + * - Filesystem at path is read only + * + * @param path Filesystem path + * + * @returns True if all of the directory's contents are removed successfully, false otherwise. + */ +[[nodiscard]] bool RemoveDirContentsRecursively(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RemoveDirContentsRecursively(const Path& path) { + if constexpr (IsChar) { + return RemoveDirContentsRecursively(ToU8String(path)); + } else { + return RemoveDirContentsRecursively(std::filesystem::path{path}); + } +} +#endif + +/** + * Renames a directory from old_path to new_path. + * + * Failures occur when: + * - One or both input path(s) is not valid + * - Filesystem object at old_path does not exist + * - Filesystem object at old_path is not a directory + * - Filesystem object at new_path exists + * - Filesystem at either path is read only + * + * @param old_path Old filesystem path + * @param new_path New filesystem path + * + * @returns True if directory rename succeeds, false otherwise. + */ +[[nodiscard]] bool RenameDir(const std::filesystem::path& old_path, + const std::filesystem::path& new_path); + +#ifdef _WIN32 +template +[[nodiscard]] bool RenameDir(const Path1& old_path, const Path2& new_path) { + using ValueType1 = typename Path1::value_type; + using ValueType2 = typename Path2::value_type; + if constexpr (IsChar && IsChar) { + return RenameDir(ToU8String(old_path), ToU8String(new_path)); + } else if constexpr (IsChar && !IsChar) { + return RenameDir(ToU8String(old_path), new_path); + } else if constexpr (!IsChar && IsChar) { + return RenameDir(old_path, ToU8String(new_path)); + } else { + return RenameDir(std::filesystem::path{old_path}, std::filesystem::path{new_path}); + } +} +#endif + +/** + * Iterates over the directory entries of a given directory. + * This does not iterate over the sub-directories of the given directory. + * The DirEntryCallable callback is called for each visited directory entry. + * A filter can be set to control which directory entries are visited based on their type. + * By default, both files and directories are visited. + * If the callback returns false or there is an error, the iteration is immediately halted. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path is not a directory + * + * @param path Filesystem path + * @param callback Callback to be called for each visited directory entry + * @param filter Directory entry type filter + */ +void IterateDirEntries(const std::filesystem::path& path, const DirEntryCallable& callback, + DirEntryFilter filter = DirEntryFilter::All); + +#ifdef _WIN32 +template +void IterateDirEntries(const Path& path, const DirEntryCallable& callback, + DirEntryFilter filter = DirEntryFilter::All) { + if constexpr (IsChar) { + IterateDirEntries(ToU8String(path), callback, filter); + } else { + IterateDirEntries(std::filesystem::path{path}, callback, filter); + } +} +#endif + +/** + * Iterates over the directory entries of a given directory and its sub-directories. + * The DirEntryCallable callback is called for each visited directory entry. + * A filter can be set to control which directory entries are visited based on their type. + * By default, both files and directories are visited. + * If the callback returns false or there is an error, the iteration is immediately halted. + * + * Failures occur when: + * - Input path is not valid + * - Filesystem object at path does not exist + * - Filesystem object at path is not a directory + * + * @param path Filesystem path + * @param callback Callback to be called for each visited directory entry + * @param filter Directory entry type filter + */ +void IterateDirEntriesRecursively(const std::filesystem::path& path, + const DirEntryCallable& callback, + DirEntryFilter filter = DirEntryFilter::All); + +#ifdef _WIN32 +template +void IterateDirEntriesRecursively(const Path& path, const DirEntryCallable& callback, + DirEntryFilter filter = DirEntryFilter::All) { + if constexpr (IsChar) { + IterateDirEntriesRecursively(ToU8String(path), callback, filter); + } else { + IterateDirEntriesRecursively(std::filesystem::path{path}, callback, filter); + } +} +#endif + +// Generic Filesystem Operations + +/** + * Returns whether a filesystem object at path exists. + * + * @param path Filesystem path + * + * @returns True if a filesystem object at path exists, false otherwise. + */ +[[nodiscard]] bool Exists(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool Exists(const Path& path) { + if constexpr (IsChar) { + return Exists(ToU8String(path)); + } else { + return Exists(std::filesystem::path{path}); + } +} +#endif + +/** + * Returns whether a filesystem object at path is a file. + * + * @param path Filesystem path + * + * @returns True if a filesystem object at path is a file, false otherwise. + */ +[[nodiscard]] bool IsFile(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool IsFile(const Path& path) { + if constexpr (IsChar) { + return IsFile(ToU8String(path)); + } else { + return IsFile(std::filesystem::path{path}); + } +} +#endif + +/** + * Returns whether a filesystem object at path is a directory. + * + * @param path Filesystem path + * + * @returns True if a filesystem object at path is a directory, false otherwise. + */ +[[nodiscard]] bool IsDir(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool IsDir(const Path& path) { + if constexpr (IsChar) { + return IsDir(ToU8String(path)); + } else { + return IsDir(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the current working directory. + * + * @returns The current working directory. Returns an empty path on failure. + */ +[[nodiscard]] std::filesystem::path GetCurrentDir(); + +/** + * Sets the current working directory to path. + * + * @returns True if the current working directory is successfully set, false otherwise. + */ +[[nodiscard]] bool SetCurrentDir(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool SetCurrentDir(const Path& path) { + if constexpr (IsChar) { + return SetCurrentDir(ToU8String(path)); + } else { + return SetCurrentDir(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the entry type of the filesystem object at path. + * + * @param path Filesystem path + * + * @returns The entry type of the filesystem object. Returns file_type::not_found on failure. + */ +[[nodiscard]] std::filesystem::file_type GetEntryType(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] std::filesystem::file_type GetEntryType(const Path& path) { + if constexpr (IsChar) { + return GetEntryType(ToU8String(path)); + } else { + return GetEntryType(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the size of the filesystem object at path. + * + * @param path Filesystem path + * + * @returns The size in bytes of the filesystem object. Returns 0 on failure. + */ +[[nodiscard]] u64 GetSize(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] u64 GetSize(const Path& path) { + if constexpr (IsChar) { + return GetSize(ToU8String(path)); + } else { + return GetSize(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the free space size of the filesystem at path. + * + * @param path Filesystem path + * + * @returns The free space size in bytes of the filesystem at path. Returns 0 on failure. + */ +[[nodiscard]] u64 GetFreeSpaceSize(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] u64 GetFreeSpaceSize(const Path& path) { + if constexpr (IsChar) { + return GetFreeSpaceSize(ToU8String(path)); + } else { + return GetFreeSpaceSize(std::filesystem::path{path}); + } +} +#endif + +/** + * Gets the total capacity of the filesystem at path. + * + * @param path Filesystem path + * + * @returns The total capacity in bytes of the filesystem at path. Returns 0 on failure. + */ +[[nodiscard]] u64 GetTotalSpaceSize(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] u64 GetTotalSpaceSize(const Path& path) { + if constexpr (IsChar) { + return GetTotalSpaceSize(ToU8String(path)); + } else { + return GetTotalSpaceSize(std::filesystem::path{path}); + } +} +#endif + +} // namespace Common::FS diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h new file mode 100644 index 000000000..b32614797 --- /dev/null +++ b/src/common/fs/fs_paths.h @@ -0,0 +1,27 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +// yuzu data directories + +#define YUZU_DIR "yuzu" +#define PORTABLE_DIR "user" + +// Sub-directories contained within a yuzu data directory + +#define CACHE_DIR "cache" +#define CONFIG_DIR "config" +#define DUMP_DIR "dump" +#define KEYS_DIR "keys" +#define LOAD_DIR "load" +#define LOG_DIR "log" +#define NAND_DIR "nand" +#define SCREENSHOTS_DIR "screenshots" +#define SDMC_DIR "sdmc" +#define SHADER_DIR "shader" + +// yuzu-specific files + +#define LOG_FILE "yuzu_log.txt" diff --git a/src/common/fs/fs_types.h b/src/common/fs/fs_types.h new file mode 100644 index 000000000..089980aee --- /dev/null +++ b/src/common/fs/fs_types.h @@ -0,0 +1,73 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +#include "common/common_funcs.h" +#include "common/common_types.h" + +namespace Common::FS { + +enum class FileAccessMode { + /** + * If the file at path exists, it opens the file for reading. + * If the file at path does not exist, it fails to open the file. + */ + Read = 1 << 0, + /** + * If the file at path exists, the existing contents of the file are erased. + * The empty file is then opened for writing. + * If the file at path does not exist, it creates and opens a new empty file for writing. + */ + Write = 1 << 1, + /** + * If the file at path exists, it opens the file for reading and writing. + * If the file at path does not exist, it fails to open the file. + */ + ReadWrite = Read | Write, + /** + * If the file at path exists, it opens the file for appending. + * If the file at path does not exist, it creates and opens a new empty file for appending. + */ + Append = 1 << 2, + /** + * If the file at path exists, it opens the file for both reading and appending. + * If the file at path does not exist, it creates and opens a new empty file for both + * reading and appending. + */ + ReadAppend = Read | Append, +}; + +enum class FileType { + BinaryFile, + TextFile, +}; + +enum class FileShareFlag { + ShareNone, // Provides exclusive access to the file. + ShareReadOnly, // Provides read only shared access to the file. + ShareWriteOnly, // Provides write only shared access to the file. + ShareReadWrite, // Provides read and write shared access to the file. +}; + +enum class DirEntryFilter { + File = 1 << 0, + Directory = 1 << 1, + All = File | Directory, +}; +DECLARE_ENUM_FLAG_OPERATORS(DirEntryFilter); + +/** + * A callback function which takes in the path of a directory entry. + * + * @param path The path of a directory entry + * + * @returns A boolean value. + * Return true to indicate whether the callback is successful, false otherwise. + */ +using DirEntryCallable = std::function; + +} // namespace Common::FS diff --git a/src/common/fs/fs_util.cpp b/src/common/fs/fs_util.cpp new file mode 100644 index 000000000..0ddfc3131 --- /dev/null +++ b/src/common/fs/fs_util.cpp @@ -0,0 +1,13 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/fs/fs_util.h" + +namespace Common::FS { + +std::u8string ToU8String(std::string_view utf8_string) { + return std::u8string{utf8_string.begin(), utf8_string.end()}; +} + +} // namespace Common::FS diff --git a/src/common/fs/fs_util.h b/src/common/fs/fs_util.h new file mode 100644 index 000000000..951df53b6 --- /dev/null +++ b/src/common/fs/fs_util.h @@ -0,0 +1,25 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +namespace Common::FS { + +template +concept IsChar = std::same_as; + +/** + * Converts a UTF-8 encoded std::string or std::string_view to a std::u8string. + * + * @param utf8_string UTF-8 encoded string + * + * @returns UTF-8 encoded std::u8string. + */ +[[nodiscard]] std::u8string ToU8String(std::string_view utf8_string); + +} // namespace Common::FS diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp new file mode 100644 index 000000000..8b732a21c --- /dev/null +++ b/src/common/fs/path_util.cpp @@ -0,0 +1,432 @@ +// Copyright 2021 yuzu Emulator Project +// 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 + +#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 + * 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()}; +} + +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}; +} + +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 new file mode 100644 index 000000000..a9fadbceb --- /dev/null +++ b/src/common/fs/path_util.h @@ -0,0 +1,309 @@ +// Copyright 2021 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#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. + * + * @param path Filesystem path + * + * @returns UTF-8 encoded std::string. + */ +[[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path); + +/** + * Validates a given path. + * + * A given path is valid if it meets these conditions: + * - The path is not empty + * - The path is not too long + * + * @param path Filesystem path + * + * @returns True if the path is valid, false otherwise. + */ +[[nodiscard]] bool ValidatePath(const std::filesystem::path& path); + +#ifdef _WIN32 +template +[[nodiscard]] bool ValidatePath(const Path& path) { + if constexpr (IsChar) { + return ValidatePath(ToU8String(path)); + } else { + return ValidatePath(std::filesystem::path{path}); + } +} +#endif + +/** + * 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 diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 96efa977d..6aa8ac960 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -11,13 +11,13 @@ #include #include #include + #ifdef _WIN32 -#include // For _SH_DENYWR #include // For OutputDebugStringW -#else -#define _SH_DENYWR 0 #endif + #include "common/assert.h" +#include "common/fs/fs.h" #include "common/logging/backend.h" #include "common/logging/log.h" #include "common/logging/text_formatter.h" @@ -148,19 +148,16 @@ void ColorConsoleBackend::Write(const Entry& entry) { PrintColoredMessage(entry); } -FileBackend::FileBackend(const std::string& filename) { - const auto old_filename = filename + ".old.txt"; +FileBackend::FileBackend(const std::filesystem::path& filename) { + auto old_filename = filename; + old_filename += ".old.txt"; - if (FS::Exists(old_filename)) { - FS::Delete(old_filename); - } - if (FS::Exists(filename)) { - FS::Rename(filename, old_filename); - } + // Existence checks are done within the functions themselves. + // We don't particularly care if these succeed or not. + void(FS::RemoveFile(old_filename)); + void(FS::RenameFile(filename, old_filename)); - // _SH_DENYWR allows read only access to the file for other programs. - // It is #defined to 0 on other platforms - file = FS::IOFile(filename, "w", _SH_DENYWR); + file = FS::IOFile(filename, FS::FileAccessMode::Write, FS::FileType::TextFile); } void FileBackend::Write(const Entry& entry) { @@ -181,7 +178,7 @@ void FileBackend::Write(const Entry& entry) { bytes_written += file.WriteString(FormatLogMessage(entry).append(1, '\n')); if (entry.log_level >= Level::Error) { - file.Flush(); + void(file.Flush()); } } diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index 9dd2589c3..eb629a33f 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -4,10 +4,11 @@ #pragma once #include +#include #include #include #include -#include "common/file_util.h" +#include "common/fs/file.h" #include "common/logging/filter.h" #include "common/logging/log.h" @@ -81,7 +82,7 @@ public: */ class FileBackend : public Backend { public: - explicit FileBackend(const std::string& filename); + explicit FileBackend(const std::filesystem::path& filename); static const char* Name() { return "file"; diff --git a/src/common/nvidia_flags.cpp b/src/common/nvidia_flags.cpp index d537517db..d1afd1f1d 100644 --- a/src/common/nvidia_flags.cpp +++ b/src/common/nvidia_flags.cpp @@ -2,24 +2,30 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include -#include +#include #include -#include "common/file_util.h" +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" #include "common/nvidia_flags.h" namespace Common { void ConfigureNvidiaEnvironmentFlags() { #ifdef _WIN32 - const std::string shader_path = Common::FS::SanitizePath( - fmt::format("{}/nvidia/", Common::FS::GetUserPath(Common::FS::UserPath::ShaderDir))); - const std::string windows_path = - Common::FS::SanitizePath(shader_path, Common::FS::DirectorySeparator::BackwardSlash); - void(Common::FS::CreateFullPath(shader_path + '/')); - void(_putenv(fmt::format("__GL_SHADER_DISK_CACHE_PATH={}", windows_path).c_str())); + const auto nvidia_shader_dir = + Common::FS::GetYuzuPath(Common::FS::YuzuPath::ShaderDir) / "nvidia"; + + if (!Common::FS::CreateDirs(nvidia_shader_dir)) { + return; + } + + const auto windows_path_string = + Common::FS::PathToUTF8String(nvidia_shader_dir.lexically_normal()); + + void(_putenv(fmt::format("__GL_SHADER_DISK_CACHE_PATH={}", windows_path_string).c_str())); void(_putenv("__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1")); #endif } diff --git a/src/common/settings.cpp b/src/common/settings.cpp index e29cbf506..bcb4e4be1 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -5,7 +5,7 @@ #include #include "common/assert.h" -#include "common/file_util.h" +#include "common/fs/path_util.h" #include "common/logging/log.h" #include "common/settings.h" @@ -34,6 +34,10 @@ void LogSettings() { LOG_INFO(Config, "{}: {}", name, value); }; + const auto log_path = [](std::string_view name, const std::filesystem::path& path) { + LOG_INFO(Config, "{}: {}", name, Common::FS::PathToUTF8String(path)); + }; + LOG_INFO(Config, "yuzu Configuration:"); log_setting("Controls_UseDockedMode", values.use_docked_mode.GetValue()); log_setting("System_RngSeed", values.rng_seed.GetValue().value_or(0)); @@ -59,11 +63,11 @@ void LogSettings() { log_setting("Audio_EnableAudioStretching", values.enable_audio_stretching.GetValue()); log_setting("Audio_OutputDevice", values.audio_device_id); log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd); - log_setting("DataStorage_CacheDir", Common::FS::GetUserPath(Common::FS::UserPath::CacheDir)); - log_setting("DataStorage_ConfigDir", Common::FS::GetUserPath(Common::FS::UserPath::ConfigDir)); - log_setting("DataStorage_LoadDir", Common::FS::GetUserPath(Common::FS::UserPath::LoadDir)); - log_setting("DataStorage_NandDir", Common::FS::GetUserPath(Common::FS::UserPath::NANDDir)); - log_setting("DataStorage_SdmcDir", Common::FS::GetUserPath(Common::FS::UserPath::SDMCDir)); + log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir)); + log_path("DataStorage_ConfigDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir)); + log_path("DataStorage_LoadDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::LoadDir)); + log_path("DataStorage_NANDDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir)); + log_path("DataStorage_SDMCDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir)); log_setting("Debugging_ProgramArgs", values.program_args); log_setting("Services_BCATBackend", values.bcat_backend); log_setting("Services_BCATBoxcatLocal", values.bcat_boxcat_local); diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index 7b614ad89..e6344fd41 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -9,7 +9,6 @@ #include #include -#include "common/common_paths.h" #include "common/logging/log.h" #include "common/string_util.h" @@ -93,18 +92,6 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _ return true; } -void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _Path, - const std::string& _Filename) { - _CompleteFilename = _Path; - - // check for seperator - if (DIR_SEP_CHR != *_CompleteFilename.rbegin()) - _CompleteFilename += DIR_SEP_CHR; - - // add the filename - _CompleteFilename += _Filename; -} - void SplitString(const std::string& str, const char delim, std::vector& output) { std::istringstream iss(str); output.resize(1); diff --git a/src/common/string_util.h b/src/common/string_util.h index a32c07c06..7e90a9ca5 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -32,8 +32,6 @@ void SplitString(const std::string& str, char delim, std::vector& o bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _pFilename, std::string* _pExtension); -void BuildCompleteFilename(std::string& _CompleteFilename, const std::string& _Path, - const std::string& _Filename); [[nodiscard]] std::string ReplaceAll(std::string result, const std::string& src, const std::string& dest); -- cgit v1.2.3