diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 8e061ff6c..18fbfa25b 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -3,7 +3,6 @@ // Refer to the license.txt file included. #include -#include #include #include #include @@ -68,122 +67,290 @@ #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 { -namespace fs = std::filesystem; -bool Exists(const fs::path& path) { - std::error_code ec; - return fs::exists(path, ec); +// 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 IsDirectory(const fs::path& path) { - std::error_code ec; - return fs::is_directory(path, ec); +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 Delete(const fs::path& path) { - LOG_TRACE(Common_Filesystem, "file {}", path.string()); +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(path)) { - LOG_DEBUG(Common_Filesystem, "{} does not exist", path.string()); + if (!Exists(filename)) { + LOG_DEBUG(Common_Filesystem, "{} does not exist", filename); return true; } - std::error_code ec; - return fs::remove(path, ec); + // 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 fs::path& path) { - LOG_TRACE(Common_Filesystem, "directory {}", path.string()); +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; - if (Exists(path)) { - LOG_DEBUG(Common_Filesystem, "path exists {}", path.string()); + int err = errno; + + if (err == EEXIST) { + LOG_DEBUG(Common_Filesystem, "mkdir failed on {}: already exists", path); return true; } - std::error_code ec; - const bool success = fs::create_directory(path, ec); - - if (!success) { - LOG_ERROR(Common_Filesystem, "Unable to create directory: {}", ec.message()); - return false; - } - - return true; + LOG_ERROR(Common_Filesystem, "mkdir failed on {}: {}", path, strerror(err)); + return false; +#endif } -bool CreateDirs(const fs::path& path) { - LOG_TRACE(Common_Filesystem, "path {}", path.string()); +bool CreateFullPath(const std::string& fullPath) { + int panicCounter = 100; + LOG_TRACE(Common_Filesystem, "path {}", fullPath); - if (Exists(path)) { - LOG_DEBUG(Common_Filesystem, "path exists {}", path.string()); + if (Exists(fullPath)) { + LOG_DEBUG(Common_Filesystem, "path exists {}", fullPath); return true; } - std::error_code ec; - const bool success = fs::create_directories(path, ec); + std::size_t position = 0; + while (true) { + // Find next sub path + position = fullPath.find(DIR_SEP_CHR, position); - if (!success) { - LOG_ERROR(Common_Filesystem, "Unable to create directories: {}", ec.message()); + // 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; } - return true; +#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 CreateFullPath(const fs::path& path) { - LOG_TRACE(Common_Filesystem, "path {}", path); - - if (path.has_extension()) { - return CreateDirs(path.parent_path()); - } else { - return CreateDirs(path); - } +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 Rename(const fs::path& src, const fs::path& dst) { - LOG_TRACE(Common_Filesystem, "{} --> {}", src.string(), dst.string()); +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; - std::error_code ec; - fs::rename(src, dst, ec); + LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, + GetLastErrorMsg()); + return false; +#else + using CFilePointer = std::unique_ptr; - if (ec) { - LOG_ERROR(Common_Filesystem, "Unable to rename file from {} to {}: {}", src.string(), - dst.string(), ec.message()); + // 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; } - return true; -} - -bool Copy(const fs::path& src, const fs::path& dst) { - LOG_TRACE(Common_Filesystem, "{} --> {}", src.string(), dst.string()); - - std::error_code ec; - const bool success = fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec); - - if (!success) { - LOG_ERROR(Common_Filesystem, "Unable to copy file {} to {}: {}", src.string(), dst.string(), - ec.message()); + // 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 fs::path& path) { - std::error_code ec; - const auto size = fs::file_size(path, ec); - - if (ec) { - LOG_ERROR(Common_Filesystem, "Unable to retrieve file size ({}): {}", path.string(), - ec.message()); +u64 GetSize(const std::string& filename) { + if (!Exists(filename)) { + LOG_ERROR(Common_Filesystem, "failed {}: No such file", filename); return 0; } - return size; + 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) { @@ -233,7 +400,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, } // windows loop do { - const std::string virtual_name = std::filesystem::path(ffd.cFileName).string(); + const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName)); #else DIR* dirp = opendir(directory.c_str()); if (!dirp) @@ -271,58 +438,132 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, return true; } -bool DeleteDirRecursively(const fs::path& path) { - std::error_code ec; - fs::remove_all(path, ec); +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 (ec) { - LOG_ERROR(Common_Filesystem, "Unable to completely delete directory {}: {}", path.string(), - ec.message()); + 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(const fs::path& src, const fs::path& dst) { - constexpr auto copy_flags = fs::copy_options::skip_existing | fs::copy_options::recursive; +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); + } - std::error_code ec; - fs::copy(src, dst, copy_flags, ec); - - if (ec) { - LOG_ERROR(Common_Filesystem, "Error copying directory {} to {}: {}", src.string(), - dst.string(), ec.message()); + DIR* dirp = opendir(source_path.c_str()); + if (!dirp) { return; } - LOG_TRACE(Common_Filesystem, "Successfully copied directory."); + 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() { - std::error_code ec; - auto path = fs::current_path(ec); - - if (ec) { - LOG_ERROR(Common_Filesystem, "Unable to retrieve current working directory: {}", - ec.message()); +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; } - - return {std::move(path)}; +#ifdef _WIN32 + std::string strDir = Common::UTF16ToUTF8(dir); +#else + std::string strDir = dir; +#endif + free(dir); + return strDir; } -bool SetCurrentDir(const fs::path& path) { - std::error_code ec; - fs::current_path(path, ec); - - if (ec) { - LOG_ERROR(Common_Filesystem, "Unable to set {} as working directory: {}", path.string(), - ec.message()); - return false; - } - - return true; +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__) diff --git a/src/common/file_util.h b/src/common/file_util.h index cf006cc9d..840cde2a6 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -39,40 +38,48 @@ enum class UserPath { UserDir, }; -// Returns true if the path exists -[[nodiscard]] bool Exists(const std::filesystem::path& path); +// 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 path is a directory -[[nodiscard]] bool IsDirectory(const std::filesystem::path& path); +// 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::filesystem::path& path); +[[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::filesystem::path& path); +bool CreateDir(const std::string& filename); -// Create all directories in path -// Returns true if successful, or path already exists. -[[nodiscard("Directory creation can fail and must be tested")]] bool CreateDirs( - const std::filesystem::path& path); +// Creates the full path of fullPath returns true on success +bool CreateFullPath(const std::string& fullPath); -// Creates directories in path. Returns true on success. -[[deprecated("This function is deprecated, use CreateDirs")]] bool CreateFullPath( - const std::filesystem::path& path); +// Deletes a given filename, return true on success +// Doesn't supports deleting a directory +bool Delete(const std::string& filename); -// Deletes a given file at the path. -// This will also delete empty directories. -// Return true on success -bool Delete(const std::filesystem::path& path); +// Deletes a directory filename, returns true on success +bool DeleteDir(const std::string& filename); -// Renames file src to dst, returns true on success -bool Rename(const std::filesystem::path& src, const std::filesystem::path& dst); +// renames file srcFilename to destFilename, returns true on success +bool Rename(const std::string& srcFilename, const std::string& destFilename); -// copies file src to dst, returns true on success -bool Copy(const std::filesystem::path& src, const std::filesystem::path& dst); +// 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); @@ -99,17 +106,27 @@ using DirectoryEntryCallable = std::function GetCurrentDir(); +[[nodiscard]] std::optional GetCurrentDir(); // Create directory and copy contents (does not overwrite existing files) -void CopyDir(const std::filesystem::path& src, const std::filesystem::path& dst); +void CopyDir(const std::string& source_path, const std::string& dest_path); -// Set the current directory to given path -bool SetCurrentDir(const std::filesystem::path& 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). diff --git a/src/core/file_sys/vfs_real.cpp b/src/core/file_sys/vfs_real.cpp index 3b70f7755..488687ba9 100644 --- a/src/core/file_sys/vfs_real.cpp +++ b/src/core/file_sys/vfs_real.cpp @@ -94,13 +94,9 @@ VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, Mode perms) { VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, Mode perms) { const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault); - const auto parent_path = FS::GetParentPath(path); - + const auto path_fwd = FS::SanitizePath(path, FS::DirectorySeparator::ForwardSlash); if (!FS::Exists(path)) { - if (!FS::CreateDirs(parent_path)) { - return nullptr; - } - + FS::CreateFullPath(path_fwd); if (!FS::CreateEmptyFile(path)) { return nullptr; }