// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include #include "common/assert.h" #include "common/fs/file.h" #include "common/fs/fs.h" #ifdef ANDROID #include "common/fs/fs_android.h" #endif #include "common/logging/log.h" #include "common/literals.h" #ifdef _WIN32 #include #include #else #include #include #include #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 (Exists(path) && !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) && !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)); } #elif ANDROID if (Android::IsContentUri(path)) { ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!"); auto const fd = Android::OpenContentUri(path, Android::OpenMode::Read); if (fd != -1) { file = fdopen(fd, "r"); if (errno != 0 && file == nullptr) LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(), strerror(errno)); } else { LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str()); } } else { file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); } #elif defined(__HAIKU__) || defined(__managarm__) || defined(__OPENORBIS__) file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); #else if (type == FileType::BinaryFile && mode == FileAccessMode::Read) { struct stat st; mmap_fd = open(path.c_str(), O_RDONLY); fstat(mmap_fd, &st); mmap_size = st.st_size; int map_flags = MAP_PRIVATE; #ifdef MAP_PREFAULT_READ // Prefaults reads so the final resulting pagetable from this big stupid mmap() // isn't comically lazily loaded, we just coalesce everything in-place for our // lovely mmap flags; if we didn't prefault the reads the page table will be // constructed in-place (i.e on a read-by-read basis) causing lovely soft-faults // which would nuke any performance gains. // // This of course incurs a cost in the initial mmap(2) call, but that is fine. map_flags |= MAP_PREFAULT_READ; #endif #ifdef MAP_NOSYNC // This causes physical media to not be synched to our file/memory // This means that if the read-only file is written to, we won't see changes // or we may see changes which are just funnily scattered, in any case // this presumes the files won't be changed during execution // // Do not ever use this on write files (if we ever support that); this will create // a fun amount of fragmentation on the disk. map_flags |= MAP_NOSYNC; #endif #ifdef MAP_ALIGNED_SUPER // File must be big enough that it's worth to super align. We can't just super-align every // file otherwise we will run out of alignments for actually important files :) // System doesn't guarantee a super alignment, but if it's available it will delete // about 3 layers(?) of the TLB tree for each read/write. // Again the cost of faults may make this negligible gains, but hey, we gotta work // what we gotta work with. using namespace Common::Literals; u64 big_file_threshold = 512_MiB; map_flags |= u64(st.st_size) >= big_file_threshold ? MAP_ALIGNED_SUPER : 0; #endif mmap_base = (u8*)mmap(nullptr, mmap_size, PROT_READ, map_flags, mmap_fd, 0); if (mmap_base == MAP_FAILED) { close(mmap_fd); mmap_fd = -1; } else { posix_madvise(mmap_base, mmap_size, POSIX_MADV_WILLNEED); } } // mmap(2) failed or simply we can't use it if (mmap_fd == -1) { 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() { #ifdef __unix__ if (mmap_fd != -1) { munmap(mmap_base, mmap_size); close(mmap_fd); mmap_fd = -1; } #endif if (file) { 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 { #ifdef __unix__ return file != nullptr || mmap_fd != -1; #else return file != nullptr; #endif } 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 { #ifdef __unix__ ASSERT(mmap_fd == -1); #endif if (file) { errno = 0; auto const 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; } return false; } bool IOFile::Commit() const { #ifdef __unix__ ASSERT(mmap_fd == -1); #endif if (file) { errno = 0; #ifdef _WIN32 const auto commit_result = std::fflush(file) == 0 && _commit(fileno(file)) == 0; #else const auto commit_result = std::fflush(file) == 0 && fsync(fileno(file)) == 0; #endif if (!commit_result) { const auto ec = std::error_code{errno, std::generic_category()}; LOG_ERROR(Common_Filesystem, "Failed to commit the file at path={}, ec_message={}", PathToUTF8String(file_path), ec.message()); } return commit_result; } return false; } bool IOFile::SetSize(u64 size) const { #ifdef __unix__ ASSERT(mmap_fd == -1); #endif if (file) { errno = 0; #ifdef _WIN32 const auto set_size_result = _chsize_s(fileno(file), s64(size)) == 0; #else const auto set_size_result = ftruncate(fileno(file), s64(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; } return false; } u64 IOFile::GetSize() const { #ifdef __unix__ if (mmap_fd != -1) return mmap_size; #endif if (file) { // Flush any unwritten buffered data into the file prior to retrieving the file mmap_size. std::fflush(file); #if ANDROID u64 file_size = 0; if (Android::IsContentUri(file_path)) { file_size = Android::GetSize(file_path); } else { std::error_code ec; file_size = fs::file_size(file_path, ec); if (ec) { LOG_ERROR(Common_Filesystem, "Failed to retrieve the file mmap_size of path={}, ec_message={}", PathToUTF8String(file_path), ec.message()); return 0; } } #else std::error_code ec; auto const file_size = fs::file_size(file_path, ec); if (ec) { LOG_ERROR(Common_Filesystem, "Failed to retrieve the file mmap_size of path={}, ec_message={}", PathToUTF8String(file_path), ec.message()); return 0; } #endif return file_size; } return 0; } bool IOFile::Seek(s64 offset, SeekOrigin origin) const { #ifdef __unix__ if (mmap_fd != -1) { // fuck you to whoever made this method const switch (origin) { case SeekOrigin::SetOrigin: mmap_offset = off_t(offset); break; case SeekOrigin::CurrentPosition: mmap_offset += off_t(offset); break; case SeekOrigin::End: mmap_offset = off_t(mmap_size) + off_t(offset); break; } return true; } #endif if (file) { 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; } return false; } s64 IOFile::Tell() const { #ifdef __unix__ if (mmap_fd != -1) { errno = 0; return s64(mmap_offset); } #endif if (file) { errno = 0; return ftello(file); } return 0; } } // namespace Common::FS