From 0f12fc9b7ef02af58412f84c73923befc37be536 Mon Sep 17 00:00:00 2001 From: Danigamer05 <96824289+Danigamer05@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:31:58 +0100 Subject: [PATCH] feat(libc): export the std::__sce_v2 filesystem calls Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 12 + core/libs/prx/libc/include/Api.hpp | 16 ++ .../prx/libc/include/FilesystemForwards.hpp | 27 +++ .../prx/libc/src/VersionedFilesystemCalls.cpp | 212 ++++++++++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libkernel/File/src/FilesystemForwards.cpp | 35 +++ core/libs/tests/GuestSceV2FilesystemCalls.cpp | 193 ++++++++++++++++ docs/dev/debt/libc.md | 1 + 8 files changed, 497 insertions(+) create mode 100644 core/libs/prx/libc/include/FilesystemForwards.hpp create mode 100644 core/libs/prx/libc/src/VersionedFilesystemCalls.cpp create mode 100644 core/libs/prx/libkernel/File/src/FilesystemForwards.cpp create mode 100644 core/libs/tests/GuestSceV2FilesystemCalls.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 486a88e552..c2dbaa7ea1 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -144,6 +144,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Directory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Filesystem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VersionedFilesystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/VersionedFilesystemCalls.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Syslog.cpp @@ -300,6 +301,17 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_v2_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_sce_v2_filesystem_calls_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceV2FilesystemCalls.cpp) + target_include_directories(guest_sce_v2_filesystem_calls_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sce_v2_filesystem_calls_tests PRIVATE libc libkernel zarchive) + configure_windows_unwind(guest_sce_v2_filesystem_calls_tests) + add_test(NAME guest_sce_v2_filesystem_calls COMMAND guest_sce_v2_filesystem_calls_tests) + set_tests_properties(guest_sce_v2_filesystem_calls PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sce_v2_filesystem_calls PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 876692d6e4..11b8cd036d 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -237,6 +237,22 @@ int APS5_VABI _ZNSt8__sce_v210filesystem10_Close_dirEPv_nid_postfix(void* handle bool APS5_VABI _ZNSt8__sce_v210filesystem12_Current_getERA260_c_nid_postfix(char* target); void APS5_VABI _ZNSt8__sce_v210filesystem18_Xfilesystem_errorEPKcNS_10error_codeE_nid_postfix(); void APS5_VABI _ZNSt8__sce_v210filesystem18_Xfilesystem_errorEPKcRKNS0_4pathENS_10error_codeE_nid_postfix(); +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(const char* path, int* perms, int* error); +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(const char* path, int* perms); +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix(const char* path, int* error); +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix(const char* path); +std::int64_t APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix(const char* path, std::int64_t* seconds, std::int64_t* nanoseconds); +bool APS5_VABI _ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix(const char* path, std::int64_t microseconds); +int APS5_VABI _ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(const char* path, const char* existing); +bool APS5_VABI _ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix(const char* path); +int APS5_VABI _ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix(const char* from, const char* to); +int APS5_VABI _ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix(const char* path, std::uint64_t size); +int APS5_VABI _ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix(const char* path, int perms); +int APS5_VABI _ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix(const char* from, const char* to); +int APS5_VABI _ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix(const char* path, std::uint64_t* space); +int APS5_VABI _ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(const char* path); char* APS5_VABI getenv_nid_postfix(const char* name); int APS5_VABI setenv_nid_postfix(const char* name, const char* value, int overwrite); int APS5_VABI unsetenv_nid_postfix(const char* name); diff --git a/core/libs/prx/libc/include/FilesystemForwards.hpp b/core/libs/prx/libc/include/FilesystemForwards.hpp new file mode 100644 index 0000000000..97e5c2e761 --- /dev/null +++ b/core/libs/prx/libc/include/FilesystemForwards.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "SceTypes.hpp" +#include "general/VabiMacros.hpp" + +#include +#include + +struct FilesystemForwards_nid_no_patch { + int (APS5_VABI* stat)(const char*, FileStat*); + int (APS5_VABI* fstat)(int, FileStat*); + int (APS5_VABI* open)(const char*, int, std::uint16_t); + std::int64_t (APS5_VABI* read)(int, void*, std::size_t); + std::int64_t (APS5_VABI* write)(int, const void*, std::size_t); + int (APS5_VABI* close)(int); + int (APS5_VABI* mkdir)(const char*, std::uint16_t); + int (APS5_VABI* unlink)(const char*); + int (APS5_VABI* rename)(const char*, const char*); + int (APS5_VABI* truncate)(const char*, std::int64_t); + int (APS5_VABI* chmod)(const char*, std::uint16_t); + int (APS5_VABI* fchmod)(int, std::uint16_t); + int (APS5_VABI* utimes)(const char*, const KernelTimeval*); + int (APS5_VABI* posixStat)(const char*, FileStat*); + int (APS5_VABI* posixRmdir)(const char*); +}; + +extern "C" void RegisterFilesystemForwards_nid_no_patch(const FilesystemForwards_nid_no_patch* forwards); diff --git a/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp b/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp new file mode 100644 index 0000000000..ea3eb73e13 --- /dev/null +++ b/core/libs/prx/libc/src/VersionedFilesystemCalls.cpp @@ -0,0 +1,212 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/FilesystemForwards.hpp" +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint32_t KernelErrnoBase = 0x80020000u; +constexpr std::uint32_t KernelExists = 0x80020011u; +constexpr std::uint32_t KernelNoSuchFile = 0x80020002u; +constexpr std::uint64_t NotFoundCodes = 0x2000000000140001ull; +constexpr int OutOfMemory = 12; +constexpr int NotSupported = 45; +constexpr int NoSystemCall = 78; +constexpr int TypeNotFound = -1; +constexpr int TypeNone = 0; +constexpr int TypeUnknown = 8; +constexpr int TypesByMode[] = {6, 5, 8, 2, 8, 4, 8, 1, 8, 3, 8, 7}; +constexpr std::uint16_t DefaultDirectoryMode = 0777; +constexpr std::uint16_t CopiedPermissions = 06777; +constexpr int CopyTargetFlags = 0x601; +constexpr std::uint16_t CopyTargetMode = 0777; +constexpr std::size_t CopyBlock = 0x10000; +constexpr std::int64_t MicrosecondsPerSecond = 1000000; +constexpr std::int64_t NanosecondsPerMicrosecond = 1000; + +std::atomic registeredForwards{nullptr}; + +const FilesystemForwards_nid_no_patch& Kernel(const char* function) { + const auto* forwards = registeredForwards.load(std::memory_order_acquire); + if (!forwards) throw std::runtime_error(std::string(function) + ": libkernel filesystem calls are not registered"); + return *forwards; +} + +int ErrorOf(int result) { + const auto code = static_cast(result); + return (code & 0xffff0000u) == KernelErrnoBase ? static_cast(code & 0xffffu) : 1; +} + +int StatusOf(int result) { + return result == 0 ? 0 : ErrorOf(result); +} + +bool NotFound(int result) { + const auto index = static_cast(result) - KernelNoSuchFile; + return index <= 0x3du && ((NotFoundCodes >> index) & 1u) != 0; +} + +int TypeOf(std::uint16_t mode) { + const auto index = static_cast(mode) - 0x1000u; + return index >= 0xc000u ? TypeUnknown : TypesByMode[index >> 12]; +} + +int StatusType(const char* function, const char* path, int* perms, int* error) { + FileStat status{}; + const int result = Kernel(function).stat(path, &status); + int type = TypeNone; + if (NotFound(result)) { + type = TypeNotFound; + } else if (result == 0) { + if (perms) *perms = status.st_mode & 0xfff; + type = TypeOf(status.st_mode); + } + if (error) *error = StatusOf(result); + return type; +} + +int CopyContents(const FilesystemForwards_nid_no_patch& kernel, int source, int target, std::byte* buffer) { + for (;;) { + const auto count = kernel.read(source, buffer, CopyBlock); + if (count == 0) { + FileStat status{}; + int result = kernel.fstat(source, &status); + if (result == 0) result = kernel.fchmod(target, static_cast(status.st_mode & CopiedPermissions)); + return StatusOf(result); + } + if (count < 0) return StatusOf(static_cast(count)); + for (std::int64_t done = 0; done < count;) { + const auto written = kernel.write(target, buffer + done, static_cast(count - done)); + if (written < 0) return StatusOf(static_cast(written)); + done += written; + } + } +} + +} + +extern "C" { + +void RegisterFilesystemForwards_nid_no_patch(const FilesystemForwards_nid_no_patch* forwards) { + registeredForwards.store(forwards, std::memory_order_release); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(const char* path, int* perms, int* error) { + return StatusType(__func__, path, perms, error); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(const char* path, int* perms) { + return StatusType(__func__, path, perms, nullptr); +} + +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix(const char* path, int* error) { + FileStat status{}; + const int result = Kernel(__func__).stat(path, &status); + if (result != 0 && error) *error = result; + return result != 0 ? UINT64_MAX : static_cast(status.st_size); +} + +std::uint64_t APS5_VABI _ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix(const char* path) { + FileStat status{}; + return Kernel(__func__).stat(path, &status) != 0 ? UINT64_MAX : static_cast(status.st_size); +} + +std::int64_t APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix(const char* path) { + FileStat status{}; + return Kernel(__func__).posixStat(path, &status) != 0 ? -1 : status.st_mtim.tv_sec; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix(const char* path, std::int64_t* seconds, std::int64_t* nanoseconds) { + FileStat status{}; + const int result = Kernel(__func__).stat(path, &status); + if (result != 0) return ErrorOf(result); + *seconds = status.st_mtim.tv_sec; + *nanoseconds = status.st_mtim.tv_nsec; + return 0; +} + +bool APS5_VABI _ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix(const char* path, std::int64_t microseconds) { + const auto& kernel = Kernel(__func__); + FileStat status{}; + if (kernel.stat(path, &status) != 0) return false; + std::int64_t seconds = microseconds / MicrosecondsPerSecond; + std::int64_t remainder = microseconds - seconds * MicrosecondsPerSecond; + if (remainder < 0) { + --seconds; + remainder += MicrosecondsPerSecond; + } + const KernelTimeval times[2] = {{status.st_atim.tv_sec, status.st_atim.tv_nsec / NanosecondsPerMicrosecond}, {seconds, remainder}}; + return kernel.utimes(path, times) == 0; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(const char* path, const char* existing) { + const auto& kernel = Kernel(__func__); + std::uint16_t mode = DefaultDirectoryMode; + if (existing) { + FileStat status{}; + const int result = kernel.stat(existing, &status); + if (result != 0) return -ErrorOf(result); + mode = status.st_mode; + } + const int result = kernel.mkdir(path, mode); + if (result == 0) return 1; + if (static_cast(result) == KernelExists) return 0; + return -ErrorOf(result); +} + +bool APS5_VABI _ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix(const char* path) { + return Kernel(__func__).posixRmdir(path) == 0; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix(const char* path) { + return StatusOf(Kernel(__func__).unlink(path)); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix(const char* from, const char* to) { + return StatusOf(Kernel(__func__).rename(from, to)); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix(const char* path, std::uint64_t size) { + return StatusOf(Kernel(__func__).truncate(path, static_cast(size))); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix(const char* path, int perms) { + return StatusOf(Kernel(__func__).chmod(path, static_cast(perms))); +} + +int APS5_VABI _ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix(const char* from, const char* to) { + const auto& kernel = Kernel(__func__); + const int source = kernel.open(from, 0, 0); + if (source < 0) return ErrorOf(source); + const int target = kernel.open(to, CopyTargetFlags, CopyTargetMode); + if (target < 0) { + kernel.close(source); + return ErrorOf(target); + } + const std::unique_ptr buffer(new (std::nothrow) std::byte[CopyBlock]); + const int result = buffer ? CopyContents(kernel, source, target, buffer.get()) : OutOfMemory; + kernel.close(target); + kernel.close(source); + return result; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix(const char*, std::uint64_t* space) { + space[0] = UINT64_MAX; + space[1] = UINT64_MAX; + space[2] = UINT64_MAX; + return NoSystemCall; +} + +int APS5_VABI _ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(const char* path) { + return std::strncmp(path, "/", 2) == 0 ? 0 : NotSupported; +} + +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 00a515cccf..bfd7cc129c 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/Kqueue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FilesystemForwards.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Terminal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp diff --git a/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp b/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp new file mode 100644 index 0000000000..83eb7605a6 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/FilesystemForwards.cpp @@ -0,0 +1,35 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libc/include/FilesystemForwards.hpp" + +namespace { + +struct FilesystemForwardsRegistration { + FilesystemForwardsRegistration() { + static const FilesystemForwards_nid_no_patch forwards{ + sceKernelStat, + sceKernelFstat, + sceKernelOpen, + sceKernelRead, + sceKernelWrite, + sceKernelClose, + sceKernelMkdir, + sceKernelUnlink, + sceKernelRename, + sceKernelTruncate_nid_postfix, + sceKernelChmod_nid_postfix, + sceKernelFchmod, + sceKernelUtimes_nid_postfix, + stat_nid_postfix, + rmdir_nid_postfix, + }; + RegisterFilesystemForwards_nid_no_patch(&forwards); + } + ~FilesystemForwardsRegistration() { RegisterFilesystemForwards_nid_no_patch(nullptr); } + FilesystemForwardsRegistration(const FilesystemForwardsRegistration&) = delete; + FilesystemForwardsRegistration& operator=(const FilesystemForwardsRegistration&) = delete; +}; + +const FilesystemForwardsRegistration registration; + +} diff --git a/core/libs/tests/GuestSceV2FilesystemCalls.cpp b/core/libs/tests/GuestSceV2FilesystemCalls.cpp new file mode 100644 index 0000000000..261447acf1 --- /dev/null +++ b/core/libs/tests/GuestSceV2FilesystemCalls.cpp @@ -0,0 +1,193 @@ +#include "AppArchiveFixture.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libkernel/include/Api.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr int TypeNotFound = -1; +constexpr int TypeRegular = 1; +constexpr int TypeDirectory = 2; +constexpr int NoSuchFile = 2; +constexpr int NotDirectory = 20; +constexpr int IsDirectory = 21; +constexpr int Invalid = 22; +constexpr int NameTooLong = 63; +constexpr int NoSystemCall = 78; +constexpr int NotSupported = 45; +constexpr std::uint32_t KernelNoSuchFile = 0x80020002u; +constexpr std::uint32_t KernelNotDirectory = 0x80020014u; + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "sce v2 filesystem calls: %s\n", message); + std::abort(); +} + +void Write(const char* path, std::size_t bytes, unsigned seed) { + std::ofstream file(path, std::ios::binary | std::ios::trunc); + for (std::size_t index = 0; index < bytes; ++index) file.put(static_cast((index * 131 + seed) & 0xff)); +} + +std::string Read(const char* path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), {}}; +} + +FileStat Status(const char* path) { + FileStat status{}; + Require(stat_nid_postfix(path, &status) == 0, "stat of a test file"); + return status; +} + +void Prepare() { + std::error_code ignored; + for (const char* name : {"f", "g", "big", "d", "e", "ro", "new", "made", "copy", "renamed"}) { + std::filesystem::permissions(name, std::filesystem::perms::owner_all, std::filesystem::perm_options::add, ignored); + std::filesystem::remove_all(name, ignored); + } + Write("f", 7, 1); + Write("g", 3, 2); + Write("big", 200000, 4); + std::filesystem::create_directories("d/sub"); + Write("d/sub/h", 5, 5); + std::filesystem::create_directory("e"); + Write("ro", 4, 6); + std::filesystem::permissions("ro", std::filesystem::perms::owner_read | std::filesystem::perms::group_read | std::filesystem::perms::others_read); +} + +void TestStatus() { + Prepare(); + int perms = -1; + int error = -1; + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix("f", &perms, &error) == TypeRegular && error == 0, "a regular file"); + Require(perms == (Status("f").st_mode & 0xfff), "the permission bits of a regular file"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix("d", nullptr, nullptr) == TypeDirectory, "a directory without outputs"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix("d/", &perms) == TypeDirectory && perms == (Status("d").st_mode & 0xfff), "a directory with a trailing slash"); + const struct { + const char* path; + int error; + } missing[] = {{"missing", NoSuchFile}, {"f/x", NotDirectory}, {"", NoSuchFile}}; + for (const auto& entry : missing) { + perms = 0x1234; + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(entry.path, &perms, &error) == TypeNotFound && error == entry.error && perms == 0x1234, "a path that is not found"); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsE_nid_postfix(entry.path, &perms) == TypeNotFound && perms == 0x1234, "a path that is not found without an error output"); + } + const std::string longName(300, 'n'); + Require(_ZNSt8__sce_v210filesystem6_LstatEPKcPNS0_5permsEPi_nid_postfix(longName.c_str(), &perms, &error) == TypeNotFound && error == NameTooLong, "a name that is too long"); +} + +void TestSizes() { + Prepare(); + int error = 0x5678; + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("big", &error) == 200000 && error == 0x5678, "the size of a file leaves the error output alone"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix("f") == 7, "the size of a file without an error output"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("missing", &error) == UINT64_MAX && static_cast(error) == KernelNoSuchFile, "a missing file reports the kernel error"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("f/x", &error) == UINT64_MAX && static_cast(error) == KernelNotDirectory, "a file used as a directory reports the kernel error"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKcPi_nid_postfix("missing", nullptr) == UINT64_MAX, "a missing file without an error output"); + Require(_ZNSt8__sce_v210filesystem10_File_sizeEPKc_nid_postfix("missing") == UINT64_MAX, "a missing file through the short form"); +} + +void TestTimes() { + Prepare(); + const struct { + std::int64_t microseconds, seconds, nanoseconds; + } times[] = {{-1500001, -2, 499999000}, {1700000000123456, 1700000000, 123456000}, {999999, 0, 999999000}, {-1000000, -1, 0}, {-1, -1, 999999000}}; + for (const auto& time : times) { + Require(_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("f", time.microseconds), "set the write time of a file"); + std::int64_t seconds = 0; + std::int64_t nanoseconds = 0; + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix("f", &seconds, &nanoseconds) == 0, "read the write time of a file"); + Require(seconds == time.seconds && nanoseconds == time.nanoseconds, "the write time is floored to whole seconds and microseconds"); + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix("f") == time.seconds, "the write time in seconds"); + } + Require(_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("d", 1), "set the write time of a directory"); + Require(!_ZNSt8__sce_v210filesystem20_Set_last_write_timeEPKcl_nid_postfix("missing", 0), "a missing file has no write time to set"); + std::int64_t seconds = 0x1111; + std::int64_t nanoseconds = 0x2222; + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKcPlS3__nid_postfix("missing", &seconds, &nanoseconds) == NoSuchFile && seconds == 0x1111 && nanoseconds == 0x2222, "a missing file leaves the time outputs alone"); + Require(_ZNSt8__sce_v210filesystem16_Last_write_timeEPKc_nid_postfix("missing") == -1, "a missing file has no write time"); +} + +void TestDirectories() { + Prepare(); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("made", nullptr) == 1 && std::filesystem::is_directory("made"), "make a directory"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("made", nullptr) == 0, "an existing directory is not made again"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("f", nullptr) == 0, "an existing file is reported as existing"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("missing/x", nullptr) == -NoSuchFile, "a missing parent"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("f/x", nullptr) == -NotDirectory, "a file used as a parent"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("new", "missing") == -NoSuchFile && !std::filesystem::exists("new"), "a missing template stops before making the directory"); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix("new", "d") == 1 && std::filesystem::is_directory("new"), "make a directory from a template"); + const std::string longName(300, 'n'); + Require(_ZNSt8__sce_v210filesystem9_Make_dirEPKcS2__nid_postfix(longName.c_str(), nullptr) == -NameTooLong, "a name that is too long"); + Require(_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("made") && !std::filesystem::exists("made"), "remove an empty directory"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("d") && std::filesystem::exists("d/sub/h"), "a directory that is not empty stays"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("f") && std::filesystem::exists("f"), "a file is not removed as a directory"); + Require(!_ZNSt8__sce_v210filesystem11_Remove_dirEPKc_nid_postfix("missing"), "a missing directory"); +} + +void TestEntries() { + Prepare(); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("f/x") == NotDirectory, "unlink below a file"); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("f") == 0 && !std::filesystem::exists("f"), "unlink a file"); + Require(_ZNSt8__sce_v210filesystem7_UnlinkEPKc_nid_postfix("missing") == NoSuchFile, "unlink a missing file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("g", "renamed") == 0 && Read("renamed").size() == 3 && !std::filesystem::exists("g"), "rename a file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("missing", "new") == NoSuchFile, "rename a missing file"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("renamed", "d") == IsDirectory && std::filesystem::exists("renamed"), "rename a file over a directory"); + Require(_ZNSt8__sce_v210filesystem7_RenameEPKcS2__nid_postfix("renamed", "missing/x") == NoSuchFile, "rename into a missing directory"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 100000) == 0 && Status("renamed").st_size == 100000, "grow a file"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 2) == 0 && Status("renamed").st_size == 2, "shrink a file"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("renamed", 0x8000000000000000ull) == Invalid, "a size above the signed range"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("d", 0) == IsDirectory, "resize a directory"); + Require(_ZNSt8__sce_v210filesystem7_ResizeEPKcm_nid_postfix("missing", 0) == NoSuchFile, "resize a missing file"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("renamed", 0444) == 0 && (Status("renamed").st_mode & 0222) == 0, "make a file read only"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("renamed", 0x1ffff) == 0 && (Status("renamed").st_mode & 0200) != 0, "only the low sixteen permission bits are passed"); + Require(_ZNSt8__sce_v210filesystem6_ChmodEPKcNS0_5permsE_nid_postfix("missing", 0444) == NoSuchFile, "change a missing file"); +} + +void TestCopy() { + Prepare(); + const auto contents = Read("big"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("big", "copy") == 0 && Read("copy") == contents, "copy a file larger than one block"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("g", "copy") == 0 && Read("copy") == Read("g"), "copy over a longer file truncates it"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("ro", "new") == 0 && Read("new") == Read("ro"), "copy a read-only file"); + Require((Status("new").st_mode & 07777) == (Status("ro").st_mode & 07777), "the copy takes the permissions of its source"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("missing", "made") == NoSuchFile && !std::filesystem::exists("made"), "a missing source creates nothing"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("f", "missing/x") == NoSuchFile, "a target in a missing directory"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("f", "d") == IsDirectory, "a directory target"); + Require(_ZNSt8__sce_v210filesystem10_Copy_fileEPKcS2__nid_postfix("d", "made") == IsDirectory, "a directory source"); +} + +void TestUnsupported() { + std::uint64_t space[4] = {1, 2, 3, 4}; + Require(_ZNSt8__sce_v210filesystem8_StatvfsEPKcRNS0_10space_infoE_nid_postfix("f", space) == NoSystemCall, "statvfs is not supported"); + Require(space[0] == UINT64_MAX && space[1] == UINT64_MAX && space[2] == UINT64_MAX && space[3] == 4, "statvfs reports unknown space and writes three fields"); + Require(_ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix("/") == 0, "the root stays the current directory"); + for (const char* path : {"", ".", "d", "//", "/d", "f/"}) Require(_ZNSt8__sce_v210filesystem13_Current_set2EPKc_nid_postfix(path) == NotSupported, "any other current directory is not supported"); +} + +} + +int main() { + const auto directory = AppArchiveFixture::EnterFreshRoot("anyps5-sce-v2-filesystem-calls"); + TestStatus(); + TestSizes(); + TestTimes(); + TestDirectories(); + TestEntries(); + TestCopy(); + TestUnsupported(); + Prepare(); + std::error_code ignored; + std::filesystem::current_path(std::filesystem::temp_directory_path(), ignored); + std::filesystem::remove_all(directory, ignored); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 273a1dff14..8f00c0d30e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -148,6 +148,7 @@ - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [sinf, cosf, tanf and sincosf](../../../core/libs/prx/libc/src/Math.cpp) (libc) are FreeBSD msun's [`s_sinf.c`, `s_cosf.c` and `s_tanf.c`](https://github.com/freebsd/freebsd-src/tree/release/11.4.0/lib/msun/src) over its double-precision kernels and `e_rem_pio2f.c`, with `k_rem_pio2.c` (for `|x| >= 2^28 * pi/2`) ported only for one input word and `prec` 0; `sincosf` returns the two. As in the `libc.prx` that titles ship, only an infinite argument sets `errno` (`EDOM`); NaN leaves it unchanged. Floating-point exceptions are not compared. All four give the same bits and `errno` as the copies bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host. - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process +- [Versioned std::__sce_v2::filesystem calls](../../../core/libs/prx/libc/src/VersionedFilesystemCalls.cpp) (libc) - console execution, firmware and SDK coverage are unverified. `_Lstat`, `_File_size`, `_Last_write_time`, `_Set_last_write_time`, `_Make_dir`, `_Remove_dir`, `_Unlink`, `_Rename`, `_Resize`, `_Chmod`, `_Copy_file`, `_Statvfs` and `_Current_set2` follow the `libc.prx` bundled with PPSA03671 v01.001.005. libkernel registers the kernel calls they make, as libc cannot link it, and they are made in the module's order: kernel errors in the errno range give their errno, others give 1, and `_File_size` stores the kernel error code itself. As in the module, `_Statvfs` returns `ENOSYS` with unknown space and `_Current_set2` accepts only "/". `_Copy_file` copies 64 KiB at a time through a host buffer instead of the module's heap. - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. The multibyte conversions follow the C locale of the console's `libSceLibcInternal.sprx`: UTF-8 of at most three bytes, so `_Getmbcurmax` returns 3, overlong forms and encoded surrogates decode, and four-byte forms are invalid. `__mb_cur_max` stays 1 like the console's variable; `___mb_cur_max` is not in that module and also stays 1, unverified. Other locales are not supported. With a null destination, `wcstombs` returns the byte count; the console's count is wrong when the capacity argument equals the running count, because it reads past the terminator - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) with two gaps. A host symbolic link with a relative target under the guest root is expanded as `realpath1` does (a dangling link fails with `ENOENT`, more than 32 links with `ELOOP`), but a link with an absolute target is not expanded, nor is anything below it, because the target is a host path with no guest path to map to. Only Windows symbolic links are expanded there, not junctions. Expansion works on the guest path, so a host link whose text climbs above a path alias root is resolved against the guest tree, not the host one. Otherwise `..` drops the previous component lexically, as `Resolve` does for every guest path. - [Paths below a Windows symbolic link](../../../core/libs/prx/libc/include/FilesystemError.hpp) (libc) chooses `ENOENT` or `ENOTDIR` for a path below a link by reading the link text on Windows, and three cases still differ from FreeBSD 11.4's `namei`: below a chain of 33 or more links the answer is `ENOTDIR`, not `ELOOP`; a link made with the directory flag that points at a regular file, or at a link that runs through one, answers `ENOENT`, not `ENOTDIR`, because Windows fails its open with `ERROR_DIRECTORY`; and below a link loop `lstat`, `chmod`, `fchmodat`, `linkat`, `readlink`, `access`, `rename` and `truncate` answer `ENOTDIR`, not `ELOOP`.