From c052b7c720501f0b49527194baf625d8a4a50152 Mon Sep 17 00:00:00 2001 From: lorkydey <57268822+Lorkydey@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:53:00 +0200 Subject: [PATCH] feat(libc): query filesystem statistics by guest path --- .../prx/libSceLibcInternal/CMakeLists.txt | 12 +- .../prx/libSceLibcInternal/PathStatistics.cpp | 122 ++++++++++++++ core/libs/prx/libkernel/File/src/Stdio.cpp | 3 +- core/libs/tests/GuestStatvfs.cpp | 157 ++++++++++++++++++ 4 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceLibcInternal/PathStatistics.cpp create mode 100644 core/libs/tests/GuestStatvfs.cpp diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index adb9ecdf4..21e4cf2bf 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -2,9 +2,19 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp U target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) configure_windows_unwind(libSceLibcInternal) -target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp) +target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp PathStatistics.cpp) if(BUILD_TESTING) + add_test_executable(guest_statvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestStatvfs.cpp) + target_include_directories(guest_statvfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_statvfs_tests PRIVATE libSceLibcInternal libc) + configure_windows_unwind(guest_statvfs_tests) + add_test(NAME guest_statvfs COMMAND guest_statvfs_tests) + set_tests_properties(guest_statvfs PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_statvfs PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_fstatvfs_conversion_tests) diff --git a/core/libs/prx/libSceLibcInternal/PathStatistics.cpp b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp new file mode 100644 index 000000000..a621e90dd --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/PathStatistics.cpp @@ -0,0 +1,122 @@ +#include "prx/libc/include/FilesystemError.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +struct LibcStatvfs; +extern "C" int APS5_VABI fstatvfs_nid_postfix(int, LibcStatvfs*); + +namespace { +int Fail(int error) { + errno = error; + return -1; +} + +int HostError(int error) { + switch (error) { + case ENOENT: return 2; + case EINTR: return 4; + case EIO: return 5; + case ENOMEM: return 12; + case EACCES: return 13; + case ENOTDIR: return 20; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EAGAIN: return 35; + case ELOOP: return 62; + case ENAMETOOLONG: return 63; + default: throw std::system_error(error, std::generic_category(), "statvfs: unsupported host error"); + } +} + +struct Descriptor { + int value; + ~Descriptor() { + const int saved = errno; +#ifdef _WIN32 + ::_close(value); +#else + ::close(value); +#endif + errno = saved; + } +}; + +int OpenMetadata(const std::filesystem::path& path, bool directory) { +#ifdef _WIN32 + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + const DWORD error = GetLastError(); + if (error == ERROR_PATH_NOT_FOUND || error == ERROR_INVALID_NAME || error == ERROR_DIRECTORY) { + for (auto prefix = path.parent_path(); !prefix.empty();) { + const DWORD attributes = GetFileAttributesW(prefix.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY)) return Fail(20); + const auto parent = prefix.parent_path(); + if (parent == prefix) break; + prefix = parent; + } + } + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: return Fail(2); + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: return Fail(13); + case ERROR_DIRECTORY: return Fail(20); + case ERROR_INVALID_NAME: + case ERROR_INVALID_PARAMETER: return Fail(22); + case ERROR_CANT_RESOLVE_FILENAME: return Fail(62); + case ERROR_FILENAME_EXCED_RANGE: return Fail(63); + default: throw std::system_error(error, std::system_category(), "statvfs: CreateFileW"); + } + } + if (directory) { + BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(handle, &info)) { + const DWORD error = GetLastError(); + CloseHandle(handle); + throw std::system_error(error, std::system_category(), "statvfs: GetFileInformationByHandle"); + } + if (!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + CloseHandle(handle); + return Fail(20); + } + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), _O_RDONLY | _O_BINARY | _O_NOINHERIT); + if (descriptor < 0) { + const int error = errno; + CloseHandle(handle); + return Fail(HostError(error)); + } + return descriptor; +#else + const int descriptor = ::open(path.c_str(), O_PATH | O_CLOEXEC | (directory ? O_DIRECTORY : 0)); + return descriptor < 0 ? Fail(HostError(errno)) : descriptor; +#endif +} +} + +extern "C" int APS5_VABI statvfs_nid_postfix(const char* path, LibcStatvfs* output) { + if (path == nullptr) return Fail(14); + if (*path == '\0') return Fail(2); + const int saved = errno; + const auto last = std::string_view(path).back(); + const bool directory = last == '/' || last == '\\' || FinalPathComponentIsDot_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); + const int descriptor = OpenMetadata(native, directory); + if (descriptor < 0) return -1; + const Descriptor owner{descriptor}; + errno = saved; + return fstatvfs_nid_postfix(descriptor, output); +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 055624dbc..a51098acf 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -223,6 +223,7 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb } #else #include +#include #include #include #include @@ -387,7 +388,7 @@ static int NativeFstatfs(int descriptor, KernelStatfs* fs) { constexpr long PipeFilesystem = 0x50495045; struct stat file{}; if (::fstat(descriptor, &file) != 0) return SceErrorFromErrno(errno) & 0xffff; - if (S_ISSOCK(file.st_mode)) return GUEST_EINVAL; + if (S_ISSOCK(file.st_mode) && (::fcntl(descriptor, F_GETFL) & O_PATH) == 0) return GUEST_EINVAL; if (S_ISFIFO(file.st_mode)) { struct statfs kind{}; if (::fstatfs(descriptor, &kind) == 0 && static_cast(kind.f_type) == PipeFilesystem) return GUEST_EINVAL; diff --git a/core/libs/tests/GuestStatvfs.cpp b/core/libs/tests/GuestStatvfs.cpp new file mode 100644 index 000000000..a71484b6f --- /dev/null +++ b/core/libs/tests/GuestStatvfs.cpp @@ -0,0 +1,157 @@ +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#endif + +extern "C" int APS5_VABI statvfs_nid_postfix(const char*, void*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "statvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireVolume(const char* path, const std::filesystem::path& native) { + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(statvfs_nid_postfix(path, output.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + const auto* fields = output.data() + 1; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] == std::filesystem::space(native).capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +static void RequireError(const char* path, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + const int result = statvfs_nid_postfix(path, output.data()); + if (result != -1) + std::fprintf(stderr, "path %s: expected failure, got %d\n", path ? path : "(null)", result); + Require(result == -1); + if (*__error_nid_postfix() != error) + std::fprintf(stderr, "path %s: expected errno %d, got %d\n", path ? path : "(null)", error, *__error_nid_postfix()); + Require(*__error_nid_postfix() == error && output == saved); +} + +static std::size_t OpenHandles() { +#ifdef _WIN32 + DWORD count = 0; + Require(GetProcessHandleCount(GetCurrentProcess(), &count) != 0); + return count; +#else + return static_cast(std::distance(std::filesystem::directory_iterator("/proc/self/fd"), + std::filesystem::directory_iterator{})); +#endif +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-statvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto file = root / "file.txt"; + { + std::ofstream stream(file, std::ios::binary); + stream << "unchanged"; + Require(static_cast(stream)); + } + AddPathAlias_nid_no_patch("/anyps5-statvfs-test", root.string().c_str()); + RequireVolume("/anyps5-statvfs-test", root); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + RequireVolume("/anyps5-statvfs-test/", root); + RequireError(nullptr, 14); + RequireError("", 2); + RequireError("/anyps5-statvfs-test/missing", 2); + RequireError("/anyps5-statvfs-test/file.txt/child", 20); + RequireError("/anyps5-statvfs-test/file.txt/", 20); + RequireError("/anyps5-statvfs-test/file.txt/.", 20); + RequireError("/anyps5-statvfs-test/file.txt/../file.txt", 20); + RequireError("/anyps5-statvfs-test/missing/../file.txt", 2); + BlockPathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + RequireError("/anyps5-statvfs-unavailable", 2); + AddPathAlias_nid_no_patch("/anyps5-statvfs-unavailable", root.string().c_str()); + RemovePathAlias_nid_no_patch("/anyps5-statvfs-unavailable"); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + + const auto unicodeFile = root / std::filesystem::path(u8"\u65e5.txt"); + { + std::ofstream stream(unicodeFile); + stream << "unicode path"; + Require(static_cast(stream)); + } + RequireVolume("/anyps5-statvfs-test/\xE6\x97\xA5.txt", unicodeFile); + Require(std::filesystem::remove(unicodeFile)); +#ifdef _WIN32 + const HANDLE exclusive = CreateFileW(file.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + Require(exclusive != INVALID_HANDLE_VALUE); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(CloseHandle(exclusive) != 0); +#else + Require(::chmod(file.c_str(), 0) == 0); + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(::chmod(file.c_str(), 0600) == 0); + const auto fifo = root / "fifo"; + Require(::mkfifo(fifo.c_str(), 0600) == 0); + RequireVolume("/anyps5-statvfs-test/fifo", fifo); + const auto socketPath = root / "socket"; + const int socket = ::socket(AF_UNIX, SOCK_STREAM, 0); + Require(socket >= 0); + sockaddr_un address{}; + address.sun_family = AF_UNIX; + const auto name = socketPath.string(); + Require(name.size() < sizeof(address.sun_path)); + std::memcpy(address.sun_path, name.c_str(), name.size() + 1); + Require(::bind(socket, reinterpret_cast(&address), sizeof(address)) == 0); + RequireVolume("/anyps5-statvfs-test/socket", socketPath); + Require(::close(socket) == 0); + Require(std::filesystem::remove(socketPath)); + Require(std::filesystem::remove(fifo)); + std::filesystem::create_symlink(file.filename(), root / "link"); + RequireVolume("/anyps5-statvfs-test/link", file); + Require(std::filesystem::remove(root / "link")); + std::filesystem::create_symlink("missing", root / "dangling"); + RequireError("/anyps5-statvfs-test/dangling", 2); + Require(std::filesystem::remove(root / "dangling")); +#endif + const auto before = OpenHandles(); + for (int i = 0; i != 32; ++i) { + RequireVolume("/anyps5-statvfs-test/file.txt", file); + Require(statvfs_nid_postfix("/anyps5-statvfs-test/file.txt", nullptr) == -1); + Require(*__error_nid_postfix() == 14); + } + Require(OpenHandles() == before); + { + std::ifstream stream(file, std::ios::binary); + std::string text((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + Require(text == "unchanged"); + } + RemovePathAlias_nid_no_patch("/anyps5-statvfs-test"); + Require(std::filesystem::remove(file)); + Require(std::filesystem::remove(root)); +}