| |
| |
| |
|
|
| #include <array> |
| #include <fstream> |
| #include <limits> |
| #include <memory> |
| #include <sstream> |
| #include <unordered_map> |
| #include <boost/iostreams/device/file_descriptor.hpp> |
| #include <boost/iostreams/stream.hpp> |
| #include <fmt/format.h> |
| #include "common/assert.h" |
| #include "common/common_funcs.h" |
| #include "common/common_paths.h" |
| #include "common/error.h" |
| #include "common/file_util.h" |
| #include "common/logging/log.h" |
| #include "common/scope_exit.h" |
| #include "common/string_util.h" |
|
|
| #ifdef _WIN32 |
| #include <windows.h> |
| |
| #include <direct.h> |
| #include <io.h> |
| #include <share.h> |
| #include <shellapi.h> |
| #include <shlobj.h> |
| #include <tchar.h> |
| #include "common/string_util.h" |
|
|
| #ifdef _MSC_VER |
| |
| #define fseeko _fseeki64 |
| #define ftello _ftelli64 |
| #define fileno _fileno |
| #endif |
|
|
| |
| #ifndef __MINGW64__ |
| #define stat _stat64 |
| #define fstat _fstat64 |
| #endif |
|
|
| #else |
| #ifdef __APPLE__ |
| #include <sys/param.h> |
| #endif |
| #include <cctype> |
| #include <cerrno> |
| #include <cstdlib> |
| #include <cstring> |
| #include <dirent.h> |
| #include <pwd.h> |
| #include <unistd.h> |
| #endif |
|
|
| #if defined(__APPLE__) |
| |
| |
| |
| |
| #ifndef __clang__ |
| #define availability(...) |
| #endif |
| #include <CoreFoundation/CFBundle.h> |
| #include <CoreFoundation/CFString.h> |
| #include <CoreFoundation/CFURL.h> |
| #ifdef availability |
| #undef availability |
| #endif |
|
|
| #endif |
|
|
| #ifdef ANDROID |
| #include "common/android_storage.h" |
| #include "common/string_util.h" |
| #endif |
|
|
| #include <algorithm> |
| #include <sys/stat.h> |
|
|
| #ifndef S_ISDIR |
| #define S_ISDIR(m) (((m)&S_IFMT) == S_IFDIR) |
| #endif |
|
|
| |
| |
| |
| namespace FileUtil { |
|
|
| using Common::GetLastErrorMsg; |
|
|
| |
| |
| 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) { |
| std::string copy(filename); |
| StripTailDirSlashes(copy); |
|
|
| #ifdef _WIN32 |
| struct stat file_info; |
| |
| if (copy.size() != 0 && copy.back() == ':') |
| copy += DIR_SEP_CHR; |
|
|
| int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info); |
| #elif ANDROID |
| int result = AndroidStorage::FileExists(filename) ? 0 : -1; |
| #else |
| struct stat file_info; |
| int result = stat(copy.c_str(), &file_info); |
| #endif |
|
|
| return (result == 0); |
| } |
|
|
| bool IsDirectory(const std::string& filename) { |
| #ifdef ANDROID |
| return AndroidStorage::IsDirectory(filename); |
| #endif |
|
|
| struct stat file_info; |
|
|
| std::string copy(filename); |
| StripTailDirSlashes(copy); |
|
|
| #ifdef _WIN32 |
| |
| 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); |
|
|
| |
| |
| if (!Exists(filename)) { |
| LOG_DEBUG(Common_Filesystem, "{} does not exist", filename); |
| return true; |
| } |
|
|
| |
| 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; |
| } |
| #elif ANDROID |
| if (!AndroidStorage::DeleteDocument(filename)) { |
| LOG_ERROR(Common_Filesystem, "unlink failed on {}", filename); |
| 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; |
| #elif ANDROID |
| std::string directory = path; |
| std::string filename = path; |
| if (Common::EndsWith(path, "/")) { |
| directory = GetParentPath(path); |
| filename = GetParentPath(path); |
| } |
| directory = GetParentPath(directory); |
| filename = GetFilename(filename); |
| |
| if (directory.empty()) { |
| directory = "/"; |
| } |
| if (!AndroidStorage::CreateDir(directory, filename)) { |
| LOG_ERROR(Common_Filesystem, "mkdir failed on {}", path); |
| return false; |
| }; |
| return true; |
| #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 (FileUtil::Exists(fullPath)) { |
| LOG_DEBUG(Common_Filesystem, "path exists {}", fullPath); |
| return true; |
| } |
|
|
| std::size_t position = 0; |
| while (true) { |
| |
| position = fullPath.find(DIR_SEP_CHR, position); |
|
|
| |
| if (position == fullPath.npos) |
| return true; |
|
|
| |
| std::string const subPath(fullPath.substr(0, position + 1)); |
| if (!FileUtil::IsDirectory(subPath) && !FileUtil::CreateDir(subPath)) { |
| LOG_ERROR(Common, "CreateFullPath: directory creation failed"); |
| return false; |
| } |
|
|
| |
| 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); |
|
|
| |
| if (!FileUtil::IsDirectory(filename)) { |
| LOG_ERROR(Common_Filesystem, "Not a directory {}", filename); |
| return false; |
| } |
|
|
| #ifdef _WIN32 |
| if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str())) |
| return true; |
| #elif ANDROID |
| if (AndroidStorage::DeleteDocument(filename)) |
| 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; |
| #elif ANDROID |
| if (AndroidStorage::RenameFile(srcFilename, std::string(GetFilename(destFilename)))) |
| 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; |
| #elif ANDROID |
| return AndroidStorage::CopyFile(srcFilename, std::string(GetParentPath(destFilename)), |
| std::string(GetFilename(destFilename))); |
| #else |
|
|
| |
| FILE* input = fopen(srcFilename.c_str(), "rb"); |
| if (!input) { |
| LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename, |
| destFilename, GetLastErrorMsg()); |
| return false; |
| } |
| SCOPE_EXIT({ fclose(input); }); |
|
|
| |
| FILE* output = fopen(destFilename.c_str(), "wb"); |
| if (!output) { |
| LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename, |
| destFilename, GetLastErrorMsg()); |
| return false; |
| } |
| SCOPE_EXIT({ fclose(output); }); |
|
|
| |
| std::array<char, 1024> buffer; |
| while (!feof(input)) { |
| |
| std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input); |
| if (rnum != buffer.size()) { |
| if (ferror(input) != 0) { |
| LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}", |
| srcFilename, destFilename, GetLastErrorMsg()); |
| return false; |
| } |
| } |
|
|
| |
| std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output); |
| 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) |
| #elif ANDROID |
| u64 result = AndroidStorage::GetSize(filename); |
| LOG_TRACE(Common_Filesystem, "{}: {}", filename, result); |
| return result; |
| #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) { |
| |
| 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 (!FileUtil::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); |
|
|
| |
| u64 found_entries = 0; |
|
|
| |
| bool callback_error = false; |
|
|
| #ifdef _WIN32 |
| |
| 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; |
| } |
| |
| do { |
| const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName)); |
| #elif ANDROID |
| |
| auto result = AndroidStorage::GetFilesName(directory); |
| for (auto virtual_name : result) { |
| #else |
| DIR* dirp = opendir(directory.c_str()); |
| if (!dirp) |
| return false; |
|
|
| |
| 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); |
| #elif ANDROID |
| } |
| #else |
| } |
| closedir(dirp); |
| #endif |
|
|
| if (callback_error) |
| return false; |
|
|
| |
| 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; |
| |
| if (recursion > 0) { |
| entry.size = ScanDirectoryTree(entry.physicalName, entry, recursion - 1); |
| *num_entries_out += entry.size; |
| } else { |
| entry.size = 0; |
| } |
| } else { |
| entry.isDirectory = false; |
| entry.size = GetSize(entry.physicalName); |
| } |
| (*num_entries_out)++; |
|
|
| |
| parent_entry.children.push_back(std::move(entry)); |
| return true; |
| }; |
|
|
| u64 num_entries; |
| return ForeachDirectoryEntry(&num_entries, directory, callback) ? num_entries : 0; |
| } |
|
|
| void GetAllFilesFromNestedEntries(FSTEntry& directory, std::vector<FSTEntry>& output) { |
| std::vector<FSTEntry> files; |
| for (auto& entry : directory.children) { |
| if (entry.isDirectory) { |
| GetAllFilesFromNestedEntries(entry, output); |
| } else { |
| output.push_back(entry); |
| } |
| } |
| } |
|
|
| bool DeleteDirRecursively(const std::string& directory, unsigned int recursion) { |
| const auto callback = [recursion]([[maybe_unused]] u64* num_entries_out, |
| const std::string& directory, |
| const std::string& virtual_name) -> bool { |
| 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; |
|
|
| |
| FileUtil::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 (!FileUtil::Exists(source_path)) |
| return; |
| if (!FileUtil::Exists(dest_path)) |
| FileUtil::CreateFullPath(dest_path); |
|
|
| #ifdef ANDROID |
| auto result = AndroidStorage::GetFilesName(source_path); |
| for (auto virtualName : result) { |
| #else |
| DIR* dirp = opendir(source_path.c_str()); |
| if (!dirp) |
| return; |
|
|
| while (struct dirent* result = readdir(dirp)) { |
| const std::string virtualName(result->d_name); |
| #endif |
|
|
| |
| 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 (!FileUtil::Exists(dest)) |
| FileUtil::CreateFullPath(dest); |
| CopyDir(source, dest); |
| } else if (!FileUtil::Exists(dest)) |
| FileUtil::Copy(source, dest); |
| } |
|
|
| #ifndef ANDROID |
| closedir(dirp); |
| #endif |
| #endif |
| } |
|
|
| std::optional<std::string> GetCurrentDir() { |
| |
| #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 {}; |
| } |
| #ifdef _WIN32 |
| std::string strDir = Common::UTF16ToUTF8(dir); |
| #else |
| std::string strDir = dir; |
| #endif |
| free(dir); |
|
|
| if (!strDir.ends_with(DIR_SEP)) { |
| strDir += DIR_SEP; |
| } |
| 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::optional<std::string> GetBundleDirectory() { |
| |
| CFBundleRef bundle_ref = CFBundleGetMainBundle(); |
| if (!bundle_ref) { |
| return {}; |
| } |
|
|
| CFURLRef bundle_url_ref = CFBundleCopyBundleURL(bundle_ref); |
| if (!bundle_url_ref) { |
| return {}; |
| } |
| SCOPE_EXIT({ CFRelease(bundle_url_ref); }); |
|
|
| CFStringRef bundle_path_ref = CFURLCopyFileSystemPath(bundle_url_ref, kCFURLPOSIXPathStyle); |
| if (!bundle_path_ref) { |
| return {}; |
| } |
| SCOPE_EXIT({ CFRelease(bundle_path_ref); }); |
|
|
| char app_bundle_path[MAXPATHLEN]; |
| if (!CFStringGetFileSystemRepresentation(bundle_path_ref, app_bundle_path, |
| sizeof(app_bundle_path))) { |
| return {}; |
| } |
|
|
| std::string path_str(app_bundle_path); |
| if (!path_str.ends_with(DIR_SEP)) { |
| path_str += DIR_SEP; |
| } |
| return path_str; |
| } |
| #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; |
| |
| 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 |
| |
| |
| |
| 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; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| [[maybe_unused]] 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 {} musn’t be empty.", envvar); |
| ASSERT_MSG(user_dir[0] == '/', "User directory {} must be absolute.", envvar); |
|
|
| return user_dir; |
| } |
| #endif |
|
|
| namespace { |
| std::unordered_map<UserPath, std::string> g_paths; |
| std::unordered_map<UserPath, std::string> g_default_paths; |
| } |
|
|
| void SetUserPath(const std::string& path) { |
| std::string& user_path = g_paths[UserPath::UserDir]; |
|
|
| if (!path.empty() && CreateFullPath(path)) { |
| LOG_INFO(Common_Filesystem, "Using {} as the user directory", path); |
| user_path = path; |
| g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); |
| g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); |
| } else { |
| #ifdef _WIN32 |
| user_path = GetExeDirectory() + DIR_SEP USERDATA_DIR DIR_SEP; |
| if (!FileUtil::IsDirectory(user_path)) { |
| user_path = AppDataRoamingDirectory() + DIR_SEP EMU_DATA_DIR DIR_SEP; |
| } else { |
| LOG_INFO(Common_Filesystem, "Using the local user directory"); |
| } |
|
|
| g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); |
| g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); |
| #elif ANDROID |
| user_path = "/"; |
| g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); |
| g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); |
| #else |
| auto current_dir = FileUtil::GetCurrentDir(); |
| if (current_dir.has_value() && |
| FileUtil::Exists(current_dir.value() + USERDATA_DIR DIR_SEP)) { |
| user_path = current_dir.value() + USERDATA_DIR DIR_SEP; |
| g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); |
| g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); |
| } else { |
| std::string data_dir = GetUserDirectory("XDG_DATA_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; |
| std::string config_dir = |
| GetUserDirectory("XDG_CONFIG_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; |
| std::string cache_dir = |
| GetUserDirectory("XDG_CACHE_HOME") + DIR_SEP EMU_DATA_DIR DIR_SEP; |
|
|
| #if defined(__APPLE__) |
| |
| |
| if (!FileUtil::Exists(data_dir) && !FileUtil::Exists(config_dir) && |
| !FileUtil::Exists(cache_dir)) { |
| data_dir = GetHomeDirectory() + DIR_SEP APPLE_EMU_DATA_DIR DIR_SEP; |
| config_dir = data_dir + CONFIG_DIR DIR_SEP; |
| cache_dir = data_dir + CACHE_DIR DIR_SEP; |
| } |
| #endif |
|
|
| user_path = data_dir; |
| g_paths.emplace(UserPath::ConfigDir, config_dir); |
| g_paths.emplace(UserPath::CacheDir, cache_dir); |
| } |
| #endif |
| } |
|
|
| g_paths.emplace(UserPath::SDMCDir, user_path + SDMC_DIR DIR_SEP); |
| g_paths.emplace(UserPath::NANDDir, user_path + NAND_DIR DIR_SEP); |
| g_paths.emplace(UserPath::SysDataDir, user_path + SYSDATA_DIR DIR_SEP); |
| |
| g_paths.emplace(UserPath::LogDir, user_path + LOG_DIR DIR_SEP); |
| g_paths.emplace(UserPath::CheatsDir, user_path + CHEATS_DIR DIR_SEP); |
| g_paths.emplace(UserPath::DLLDir, user_path + DLL_DIR DIR_SEP); |
| g_paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP); |
| g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP); |
| g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); |
| g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP); |
| g_default_paths = g_paths; |
| } |
|
|
| std::string g_currentRomPath{}; |
|
|
| void SetCurrentRomPath(const std::string& path) { |
| g_currentRomPath = path; |
| } |
|
|
| bool StringReplace(std::string& haystack, const std::string& a, const std::string& b, bool swap) { |
| const auto& needle = swap ? b : a; |
| const auto& replacement = swap ? a : b; |
| if (needle.empty()) { |
| return false; |
| } |
| auto index = haystack.find(needle, 0); |
| if (index == std::string::npos) { |
| return false; |
| } |
| haystack.replace(index, needle.size(), replacement); |
| return true; |
| } |
|
|
| std::string SerializePath(const std::string& input, bool is_saving) { |
| auto result = input; |
| StringReplace(result, "%CITRA_ROM_FILE%", g_currentRomPath, is_saving); |
| StringReplace(result, "%CITRA_USER_DIR%", GetUserPath(UserPath::UserDir), is_saving); |
| return result; |
| } |
|
|
| const std::string& GetUserPath(UserPath path) { |
| |
| if (g_paths.empty()) |
| SetUserPath(); |
| return g_paths[path]; |
| } |
|
|
| const std::string& GetDefaultUserPath(UserPath path) { |
| |
| if (g_default_paths.empty()) |
| SetUserPath(); |
| return g_default_paths[path]; |
| } |
|
|
| void UpdateUserPath(UserPath path, const std::string& filename) { |
| if (filename.empty()) { |
| return; |
| } |
| if (!FileUtil::IsDirectory(filename)) { |
| LOG_ERROR(Common_Filesystem, "Path is not a directory. UserPath: {} filename: {}", path, |
| filename); |
| return; |
| } |
| g_paths[path] = SanitizePath(filename) + DIR_SEP; |
| } |
|
|
| 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<u32>(file.GetSize())); |
| return file.ReadArray(str.data(), str.size()); |
| } |
|
|
| void SplitFilename83(const std::string& filename, std::array<char, 9>& short_name, |
| std::array<char, 4>& extension) { |
| const std::string forbidden_characters = ".\"/\\[]:;=, "; |
|
|
| |
| short_name = {{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\0'}}; |
| extension = {{' ', ' ', ' ', '\0'}}; |
|
|
| std::string::size_type point = filename.rfind('.'); |
| if (point == filename.size() - 1) |
| point = filename.rfind('.', point); |
|
|
| |
| int j = 0; |
| for (char letter : filename.substr(0, point)) { |
| if (forbidden_characters.find(letter, 0) != std::string::npos) |
| continue; |
| if (j == 8) { |
| |
| |
| short_name[6] = '~'; |
| short_name[7] = '1'; |
| break; |
| } |
| short_name[j++] = Common::ToUpper(letter); |
| } |
|
|
| |
| if (point != std::string::npos) { |
| j = 0; |
| for (char letter : filename.substr(point + 1, 3)) |
| extension[j++] = Common::ToUpper(letter); |
| } |
| } |
|
|
| std::vector<std::string> SplitPathComponents(std::string_view filename) { |
| std::string copy(filename); |
| std::replace(copy.begin(), copy.end(), '\\', '/'); |
| std::vector<std::string> out; |
|
|
| std::stringstream stream(copy); |
| std::string item; |
| while (std::getline(stream, item, '/')) { |
| out.push_back(std::move(item)); |
| } |
|
|
| return out; |
| } |
|
|
| std::string_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 path; |
| } |
|
|
| 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_); |
| #ifdef ANDROID |
| return std::string(RemoveTrailingSlash(path)); |
| #endif |
| char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; |
| char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/'; |
|
|
| if (directory_separator == DirectorySeparator::PlatformDefault) { |
| #ifdef _WIN32 |
| type1 = '/'; |
| type2 = '\\'; |
| #endif |
| } |
|
|
| std::replace(path.begin(), path.end(), type1, type2); |
|
|
| auto start = path.begin(); |
| #ifdef _WIN32 |
| |
| if (start != path.end()) |
| ++start; |
| #endif |
| path.erase(std::unique(start, path.end(), |
| [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }), |
| path.end()); |
| return std::string(RemoveTrailingSlash(path)); |
| } |
|
|
| IOFile::IOFile() = default; |
|
|
| IOFile::IOFile(const std::string& filename, const char openmode[], int flags) |
| : filename(filename), openmode(openmode), flags(flags) { |
| Open(); |
| } |
|
|
| 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); |
| std::swap(m_fd, other.m_fd); |
| std::swap(m_good, other.m_good); |
| std::swap(filename, other.filename); |
| std::swap(openmode, other.openmode); |
| std::swap(flags, other.flags); |
| } |
|
|
| bool IOFile::Open() { |
| Close(); |
|
|
| #ifdef _WIN32 |
| if (flags == 0) { |
| flags = _SH_DENYNO; |
| } |
| m_file = _wfsopen(Common::UTF8ToUTF16W(filename).c_str(), |
| Common::UTF8ToUTF16W(openmode).c_str(), flags); |
| m_good = m_file != nullptr; |
|
|
| #elif ANDROID |
| |
| AndroidStorage::AndroidOpenMode android_open_mode = AndroidStorage::ParseOpenmode(openmode); |
| if (android_open_mode == AndroidStorage::AndroidOpenMode::WRITE || |
| android_open_mode == AndroidStorage::AndroidOpenMode::READ_WRITE || |
| android_open_mode == AndroidStorage::AndroidOpenMode::WRITE_APPEND || |
| android_open_mode == AndroidStorage::AndroidOpenMode::WRITE_TRUNCATE || |
| android_open_mode == AndroidStorage::AndroidOpenMode::READ_WRITE_TRUNCATE || |
| android_open_mode == AndroidStorage::AndroidOpenMode::READ_WRITE_APPEND) { |
| if (!FileUtil::Exists(filename)) { |
| std::string directory(GetParentPath(filename)); |
| std::string display_name(GetFilename(filename)); |
| if (!AndroidStorage::CreateFile(directory, display_name)) { |
| m_good = m_file != nullptr; |
| return m_good; |
| } |
| } |
| } |
| m_fd = AndroidStorage::OpenContentUri(filename, android_open_mode); |
| if (m_fd != -1) { |
| int error_num = 0; |
| m_file = fdopen(m_fd, openmode.c_str()); |
| error_num = errno; |
| if (error_num != 0 && m_file == nullptr) { |
| LOG_ERROR(Common_Filesystem, "Error on file: {}, error: {}", filename, |
| strerror(error_num)); |
| } |
| } |
|
|
| m_good = m_file != nullptr; |
| #else |
| m_file = std::fopen(filename.c_str(), openmode.c_str()); |
| m_good = m_file != nullptr; |
| #endif |
|
|
| return m_good; |
| } |
|
|
| bool IOFile::Close() { |
| if (!IsOpen() || 0 != std::fclose(m_file)) |
| m_good = false; |
|
|
| m_file = nullptr; |
| return m_good; |
| } |
|
|
| u64 IOFile::GetSize() const { |
| if (IsOpen()) |
| return FileUtil::GetSize(m_file); |
|
|
| return 0; |
| } |
|
|
| bool IOFile::Seek(s64 off, int origin) { |
| if (!IsOpen() || 0 != fseeko(m_file, off, origin)) |
| m_good = false; |
|
|
| return m_good; |
| } |
|
|
| u64 IOFile::Tell() const { |
| if (IsOpen()) |
| return ftello(m_file); |
|
|
| return std::numeric_limits<u64>::max(); |
| } |
|
|
| bool IOFile::Flush() { |
| if (!IsOpen() || 0 != std::fflush(m_file)) |
| m_good = false; |
|
|
| return m_good; |
| } |
|
|
| std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) { |
| if (!IsOpen()) { |
| m_good = false; |
| return std::numeric_limits<std::size_t>::max(); |
| } |
|
|
| if (length == 0) { |
| return 0; |
| } |
|
|
| DEBUG_ASSERT(data != nullptr); |
|
|
| return std::fread(data, data_size, length, m_file); |
| } |
|
|
| #ifdef _WIN32 |
| static std::size_t pread(int fd, void* buf, std::size_t count, uint64_t offset) { |
| long unsigned int read_bytes = 0; |
| OVERLAPPED overlapped = {0}; |
| HANDLE file = reinterpret_cast<HANDLE>(_get_osfhandle(fd)); |
|
|
| overlapped.OffsetHigh = static_cast<uint32_t>(offset >> 32); |
| overlapped.Offset = static_cast<uint32_t>(offset & 0xFFFF'FFFFLL); |
| SetLastError(0); |
| bool ret = ReadFile(file, buf, static_cast<uint32_t>(count), &read_bytes, &overlapped); |
|
|
| if (!ret && GetLastError() != ERROR_HANDLE_EOF) { |
| errno = GetLastError(); |
| return std::numeric_limits<std::size_t>::max(); |
| } |
| return read_bytes; |
| } |
| #else |
| #define pread ::pread |
| #endif |
|
|
| std::size_t IOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, |
| std::size_t offset) { |
| if (!IsOpen()) { |
| m_good = false; |
| return std::numeric_limits<std::size_t>::max(); |
| } |
|
|
| if (length == 0) { |
| return 0; |
| } |
|
|
| DEBUG_ASSERT(data != nullptr); |
|
|
| return pread(fileno(m_file), data, data_size * length, offset); |
| } |
|
|
| std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { |
| if (!IsOpen()) { |
| m_good = false; |
| return std::numeric_limits<std::size_t>::max(); |
| } |
|
|
| if (length == 0) { |
| return 0; |
| } |
|
|
| DEBUG_ASSERT(data != nullptr); |
|
|
| return std::fwrite(data, data_size, length, m_file); |
| } |
|
|
| bool IOFile::Resize(u64 size) { |
| if (!IsOpen() || 0 != |
| #ifdef _WIN32 |
| |
| |
| _chsize_s(_fileno(m_file), size) |
| #else |
| |
| ftruncate(fileno(m_file), size) |
| #endif |
| ) |
| m_good = false; |
|
|
| return m_good; |
| } |
|
|
| template <typename T> |
| using boost_iostreams = boost::iostreams::stream<T>; |
|
|
| template <> |
| void OpenFStream<std::ios_base::in>( |
| boost_iostreams<boost::iostreams::file_descriptor_source>& fstream, |
| const std::string& filename) { |
| IOFile file(filename, "r"); |
| if (file.GetFd() == -1) |
| return; |
| int fd = dup(file.GetFd()); |
| if (fd == -1) |
| return; |
| boost::iostreams::file_descriptor_source file_descriptor_source(fd, |
| boost::iostreams::close_handle); |
| fstream.open(file_descriptor_source); |
| } |
|
|
| template <> |
| void OpenFStream<std::ios_base::out>( |
| boost_iostreams<boost::iostreams::file_descriptor_sink>& fstream, const std::string& filename) { |
| IOFile file(filename, "w"); |
| if (file.GetFd() == -1) |
| return; |
| int fd = dup(file.GetFd()); |
| if (fd == -1) |
| return; |
| boost::iostreams::file_descriptor_sink file_descriptor_sink(fd, boost::iostreams::close_handle); |
| fstream.open(file_descriptor_sink); |
| } |
| } |
|
|