shadPS4/src/common/io_file.cpp
Stephen Miller d8204641fa
libkernel: Filesystem code cleanup (#2554)
* sceKernelOpen: Clean up flag handling

* sceKernelOpen: fix params

Based on decompilation, the second parameter of _open should be flags.
Additionally swaps the return and parameter types to align with our current standards.

* sceKernelOpen: Fix errors

Based on POSIX spec, if part of the path is missing, ENOENT is the correct return.
Additionally, decompilation suggests that open sets errno too.

* Fix exports

Fixes function exports to align with what they should be, based on what I've seen from decompilation and our module generator.

* Proper errno behavior on open

Since sceKernelOpen calls posix_open, errno should be set during this process.
Simplest way to handle that is to move the actual open code to posix_open and adjust error cases accordingly.

* Reorganize open calls, add error log

* Improve close

Removes the EPERM return, as it makes no sense, and swaps sceKernelClose with posix_close to properly emulate errno behavior.

* Fix log on close

* posix_open fixups

* Readd hack in posix_close

It's either this, or removing LLE DiscMap.
Or shadow implements posix sockets.

* Missing exports

Commented out while I gradually work through them all

* Remaining placeholder exports

* Swap some stuff around

I see nothing that suggests "open" only takes two parameters, so this should be completely safe.
It's also more accurate to how these are handled in libkernel, and means I won't need to reorganize anything for readv and writev.

* Update file_system.cpp

* Implement write and posix_write

* Oops

* Implement posix_readv and sceKernelReadv

Also fixes error behavior on readv, as that function shouldn't be returning any kernel error codes.

* Move sceKernelUnlink

Will deal with this one later, was just annoyed by how it's location doesn't align with the export order.

* Cleanup readv

* Implement posix_writev and sceKernelWritev

Also fixes error behavior on writev, since it shouldn't ever return kernel errors (since our device files return those)

* More cleanup on older functions

* Swap around sceKernelLseek and posix_lseek

This ensures that these have the correct error behavior, and makes their behavior align with the updated implementations for earlier functions.

* Update file_system.cpp

* Implement read

Also fixes error behavior

* Swap sceKernelMkdir and posix_mkdir

Fixes errno behavior on kernel calls, also fixed some incorrect error returns.

* Fix errno behavior on sceKernelRmdir

Also reduces function logging to bring it closer to the level of logging seen in other filesystem functions.

* Slight clean up of sceKernelStat

Fixes error behavior and changes some of the old data types.

* Refactor fstat

Fixes errno behavior, implements fstat, and shifts exports around based on file position.
Might reorganize function locations later though.

* Implement posix_ftruncate

Implements posix_ftruncate and fixes errno behavior for sceKernelFtruncate

* Add missing error conversions for more device functions

* Implement posix_rename, fix sceKernelRename errno behavior

* Add posix_preadv and posix_pread

Also fixes some incorrect error returns, fixes errno behavior, and removes an unnecessary hack.

* Fix compile

* Implement posix_getdents, getdirentries, and posix_getdirentries

Also fixes errno behavior for the kernel variants of these functions.

* Fix errno behavior of sceKernelFsync

* Implement posix_pwrite and posix_unlink

Also fixes errno behavior in related functions.

* Update file_system.cpp

* Remove SetPosixErrno

Ideally, we've handled all possible error conditions before calling these functions, so treat errors in platform-specific code as IO errors and return POSIX_EIO instead.

* Update header exports

Not sure where these get used, but might as well keep them consistent with the rest of this.

* Check if file exists before calling platform-specific code

Bloodborne checks if a file doesn't exist using open, checking if it specifically failed with error code ENOENT. To avoid working with platform-specific errnos, add a proper error return for if the file doesn't exist.
Fixes a regression in Bloodborne.

* Clang

Out of all the changes, this is apparently the only thing Clang-Format doesn't like.
I'm honestly surprised.

* Improve error checks on posix_unlink

Just because a file isn't opened doesn't mean the file doesn't exist.
Fixes the error returned if host_path.empty(), and removes the error return for when GetFile fails.

* Fix the Bloodborne fix

* Limit exports to tested functions

* More confirmed working exports

* Remaining stuff my games can test

* FS exports from firmware tests

* Bring back missing exports from main

I don't have any bootable things that call these, but since they were working well enough on main, they should be fine to readd.

* Add export for posix_pread

Spotted in Dreams a while back, might as well add it.

* Revert "Remove SetPosixErrno"

This reverts commit bdfc0c246c.

* Revert SetPosixErrno changes

shadow's using it for posix sockets, so significant modifications would introduce unnecessary merge conflicts.

* Update comment

* Add EACCES errno to SetPosixErrno

Seen in Gravity Rush.
Also reorganizes the switch case based on the posix errno value, since ordering by errno makes no sense on some OSes.

* More export fixups

Missed these during my initial pass through FS stuff because they were in kernel.cpp for some reason.

* Symbols from FS tests

Tested by messing around with firmware elfs, these atleast don't cause any crashes.

* Remove inaccurate error behavior

Seek can have offsets past the end of a file.
Also add logging for two valid whence values that are unsupported on Windows.
I'll need to verify that SEEK_HOLE and SEEK_DATA correspond to 3 and 4 respectively, I've yet to check source to verify.

* Fix error log

Oops

* Clang

Clang

* Remove close hack

Since LLE libSceDiscMap is no longer a concern, this hack shouldn't be needed.
Since sockets are still stubbed, and close can be used on sockets, I've added a warning log just in case this still occurs in some titles.

* Change SetPosixErrno unreachable to warning

I changed it to an unreachable in an earlier commit to make testing easier.
At this point, having an unreachable for this seems unnecessary, so change it to a warning instead.

* Remove Bloodborne hack

Games should be able to unlink files that aren't opened file descriptors. As far as I've tested, this doesn't break Bloodborne.
2025-03-26 18:03:35 +02:00

421 lines
10 KiB
C++

// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <vector>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/error.h"
#include "common/io_file.h"
#include "common/logging/log.h"
#include "common/path_util.h"
#ifdef _WIN32
#include "common/ntapi.h"
#include <io.h>
#include <share.h>
#include <windows.h>
#else
#include <unistd.h>
#endif
#ifdef _MSC_VER
#define fileno _fileno
#define fseeko _fseeki64
#define ftello _ftelli64
#endif
namespace Common::FS {
namespace fs = std::filesystem;
namespace {
#ifdef _WIN32
[[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"";
}
[[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
[[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
[[nodiscard]] constexpr int ToSeekOrigin(SeekOrigin origin) {
switch (origin) {
case SeekOrigin::SetOrigin:
return SEEK_SET;
case SeekOrigin::CurrentPosition:
return SEEK_CUR;
case SeekOrigin::End:
return SEEK_END;
default:
LOG_ERROR(Common_Filesystem, "Unsupported origin {}, defaulting to SEEK_SET",
static_cast<u32>(origin));
return SEEK_SET;
}
}
} // Anonymous namespace
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;
}
int 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;
int result = 0;
#ifdef _WIN32
if (flag != FileShareFlag::ShareNone) {
file = _wfsopen(path.c_str(), AccessModeToWStr(mode, type), ToWindowsFileShareFlag(flag));
result = errno;
} else {
result = _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
}
#else
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
result = errno;
#endif
if (!IsOpen()) {
const auto ec = std::error_code{result, std::generic_category()};
LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}, error_message={}",
PathToUTF8String(file_path), ec.message());
}
return result;
}
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;
#ifdef _WIN64
if (file_mapping && file_access_mode == FileAccessMode::ReadWrite) {
CloseHandle(std::bit_cast<HANDLE>(file_mapping));
}
#endif
}
void IOFile::Unlink() {
if (!IsOpen()) {
return;
}
// Mark the file for deletion
// TODO: Also remove the file path?
#ifdef _WIN64
FILE_DISPOSITION_INFORMATION disposition;
IO_STATUS_BLOCK iosb;
const int fd = fileno(file);
HANDLE hfile = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
disposition.DeleteFile = TRUE;
NtSetInformationFile(hfile, &iosb, &disposition, sizeof(disposition),
FileDispositionInformation);
#else
if (unlink(file_path.c_str()) != 0) {
const auto ec = std::error_code{errno, std::generic_category()};
LOG_ERROR(Common_Filesystem, "Failed to unlink the file at path={}, ec_message={}",
PathToUTF8String(file_path), ec.message());
}
#endif
}
uintptr_t IOFile::GetFileMapping() {
if (file_mapping) {
return file_mapping;
}
#ifdef _WIN64
const int fd = fileno(file);
HANDLE hfile = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
HANDLE mapping = nullptr;
if (file_access_mode == FileAccessMode::ReadWrite) {
mapping = CreateFileMapping2(hfile, NULL, FILE_MAP_WRITE, PAGE_READWRITE, SEC_COMMIT, 0,
NULL, NULL, 0);
} else {
mapping = hfile;
}
file_mapping = std::bit_cast<uintptr_t>(mapping);
ASSERT_MSG(file_mapping, "{}", Common::GetLastErrorMsg());
return file_mapping;
#else
file_mapping = fileno(file);
return file_mapping;
#endif
}
std::string IOFile::ReadString(size_t length) const {
std::vector<char> string_buffer(length);
const auto chars_read = ReadSpan<char>(string_buffer);
const auto string_size = chars_read != length ? chars_read : length;
return std::string{string_buffer.data(), string_size};
}
bool IOFile::Flush() const {
if (!IsOpen()) {
return false;
}
errno = 0;
#ifdef _WIN32
const auto flush_result = std::fflush(file) == 0;
#else
const auto flush_result = std::fflush(file) == 0;
#endif
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::Commit() const {
if (!IsOpen()) {
return false;
}
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;
}
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<s64>(size)) == 0;
#else
const auto set_size_result = ftruncate(fileno(file), static_cast<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;
}
u64 IOFile::GetSize() const {
if (!IsOpen()) {
return 0;
}
// Flush any unwritten buffered data into the file prior to retrieving the file size.
std::fflush(file);
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, static_cast<u32>(origin), ec.message());
}
return seek_result;
}
s64 IOFile::Tell() const {
if (!IsOpen()) {
return 0;
}
errno = 0;
return ftello(file);
}
u64 GetDirectorySize(const std::filesystem::path& path) {
if (!fs::exists(path)) {
return 0;
}
u64 total = 0;
for (const auto& entry : fs::recursive_directory_iterator(path)) {
if (fs::is_regular_file(entry.path())) {
total += fs::file_size(entry.path());
}
}
return total;
}
} // namespace Common::FS