From 7a771d2d89a8a5d70d80e4370fb96c1deaee3cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Ortellado?= Date: Thu, 8 Oct 2026 11:44:33 -0300 Subject: [PATCH] feat(posix): implement mkstemp and isatty --- .../prx/libScePosixForWebKit/CMakeLists.txt | 12 +- .../prx/libScePosixForWebKit/Filesystem.cpp | 85 +++++++++++ core/libs/tests/GuestPosixFilesystem.cpp | 140 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libScePosixForWebKit/Filesystem.cpp create mode 100644 core/libs/tests/GuestPosixFilesystem.cpp diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index 5ab1740e0..741c2f331 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libScePosixForWebKit SHARED EXCLUDE_FROM_ALL Export.cpp Resolver.cpp) +add_library(libScePosixForWebKit SHARED EXCLUDE_FROM_ALL Export.cpp Resolver.cpp Filesystem.cpp) target_include_directories(libScePosixForWebKit PRIVATE ${LIBS_INCLUDE_DIR}) if(WIN32) target_link_libraries(libScePosixForWebKit PRIVATE ws2_32) @@ -6,6 +6,16 @@ endif() configure_windows_unwind(libScePosixForWebKit) if(BUILD_TESTING) + add_test_executable(guest_posix_filesystem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixFilesystem.cpp) + target_include_directories(guest_posix_filesystem_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_posix_filesystem_tests PRIVATE libScePosixForWebKit libkernel libc) + configure_windows_unwind(guest_posix_filesystem_tests) + add_test(NAME guest_posix_filesystem COMMAND guest_posix_filesystem_tests) + set_tests_properties(guest_posix_filesystem PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_posix_filesystem PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_resolver_tests ${LIBS_INCLUDE_DIR}/tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit) diff --git a/core/libs/prx/libScePosixForWebKit/Filesystem.cpp b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp new file mode 100644 index 000000000..0bdf7aa37 --- /dev/null +++ b/core/libs/prx/libScePosixForWebKit/Filesystem.cpp @@ -0,0 +1,85 @@ +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#else +#include +#endif + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI open_nid_postfix(const char*, int, int); +} + +namespace { +constexpr char Characters[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +int Failure(int error) { + *__error_nid_postfix() = error; + return -1; +} + +#ifdef _WIN32 +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif +} + +extern "C" int APS5_VABI mkstemp_nid_postfix(char* pattern) { + if (!pattern) return Failure(14); + const auto length = std::strlen(pattern); + if (length == 0) return Failure(22); + if (length >= 1024) return Failure(63); + auto start = length; + while (start > 0 && pattern[start - 1] == 'X') --start; + const GuestArena::HostWrite destination(pattern + start, length - start); + if (!destination.Open()) return Failure(14); + if (start != length) { + std::random_device random; + std::uniform_int_distribution character(0, sizeof(Characters) - 2); + for (auto position = start; position < length; ++position) + pattern[position] = Characters[character(random)]; + } + const std::string initial(pattern + start, length - start); + for (;;) { + const int descriptor = open_nid_postfix(pattern, SCE_KERNEL_O_RDWR | SCE_KERNEL_O_CREAT | SCE_KERNEL_O_EXCL, 0600); + if (descriptor >= 0 || *__error_nid_postfix() != 17) return descriptor; + auto position = start; + for (; position < length; ++position) { + const auto index = std::strchr(Characters, pattern[position]) - Characters; + pattern[position] = Characters[(index + 1) % (sizeof(Characters) - 1)]; + if (pattern[position] != initial[position - start]) break; + } + if (position == length) return -1; + } +} + +extern "C" int APS5_VABI isatty_nid_postfix(int descriptor) { + if (descriptor >= GuestSockets::FirstDescriptor) { + Failure(GuestSockets::IsOpen(descriptor) ? 25 : 9); + return 0; + } +#ifdef _WIN32 + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + _set_thread_local_invalid_parameter_handler(previous); + if (handle == INVALID_HANDLE_VALUE) { + Failure(9); + return 0; + } + DWORD mode = 0; + if (::GetConsoleMode(handle, &mode)) return 1; + Failure(25); +#else + if (::isatty(descriptor)) return 1; + Failure(errno == EBADF ? 9 : 25); +#endif + return 0; +} diff --git a/core/libs/tests/GuestPosixFilesystem.cpp b/core/libs/tests/GuestPosixFilesystem.cpp new file mode 100644 index 000000000..540ca69d5 --- /dev/null +++ b/core/libs/tests/GuestPosixFilesystem.cpp @@ -0,0 +1,140 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#endif + +extern "C" { +int APS5_VABI mkstemp_nid_postfix(char*); +int APS5_VABI isatty_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socket_nid_postfix(int, int, int); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); +std::int64_t APS5_VABI write_nid_postfix(int, const void*, std::size_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "POSIX filesystem check failed at line %u (guest errno %d)\n", location.line(), *__error_nid_postfix()); + std::abort(); + } +} + +int main() { + std::random_device random; + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-posix-test-" + std::to_string(random()) + "-" + std::to_string(random())); + Require(std::filesystem::create_directory(root)); + AddPathAlias_nid_no_patch("/anyps5-posix-test", root.string().c_str()); + const std::string prefix = "/anyps5-posix-test/file-"; + std::string pattern = prefix + "XXXXXXXXX"; + const int descriptor = mkstemp_nid_postfix(pattern.data()); + Require(descriptor >= 0); + Require(pattern.starts_with(prefix) && pattern.size() == prefix.size() + 9); + Require(pattern.substr(prefix.size()).find_first_not_of("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") == std::string::npos); + Require(std::filesystem::file_size(ResolvePath_nid_no_patch(pattern.c_str())) == 0); +#ifndef _WIN32 + struct stat status{}; + Require(::fstat(descriptor, &status) == 0 && (status.st_mode & 0777) == 0600); +#endif + const char payload[] = {'a', '\0', '\r', '\n', '\x1a', 'z'}; + Require(write_nid_postfix(descriptor, payload, sizeof(payload)) == sizeof(payload)); + Require(lseek_nid_postfix(descriptor, 0, SEEK_SET) == 0); + std::array received{}; + Require(read_nid_postfix(descriptor, received.data(), received.size()) == static_cast(received.size())); + Require(std::memcmp(payload, received.data(), sizeof(payload)) == 0); + Require(isatty_nid_postfix(descriptor) == 0 && *__error_nid_postfix() == 25); + Require(close_nid_postfix(descriptor) == 0); + Require(isatty_nid_postfix(descriptor) == 0 && *__error_nid_postfix() == 9); + std::string second = prefix + "XXXXXXXXX"; + const int other = mkstemp_nid_postfix(second.data()); + Require(other >= 0 && second != pattern); + Require(close_nid_postfix(other) == 0); + const std::string candidates = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + for (const char character : candidates) { + std::ofstream stream(root / (std::string("occupied-") + character)); + stream << "preserved"; + Require(stream.good()); + } + std::string occupied = "/anyps5-posix-test/occupied-X"; + Require(mkstemp_nid_postfix(occupied.data()) == -1 && *__error_nid_postfix() == 17); + for (const char character : candidates) { + std::ifstream stream(root / (std::string("occupied-") + character)); + std::string contents; + stream >> contents; + Require(contents == "preserved"); + } + Require(std::filesystem::remove(root / "occupied-7")); + occupied = "/anyps5-posix-test/occupied-X"; + const int collision = mkstemp_nid_postfix(occupied.data()); + Require(collision >= 0 && occupied == "/anyps5-posix-test/occupied-7"); + Require(close_nid_postfix(collision) == 0); + std::string fixed = "/anyps5-posix-test/fixed"; + const int unchanged = mkstemp_nid_postfix(fixed.data()); + Require(unchanged >= 0 && fixed == "/anyps5-posix-test/fixed"); + Require(close_nid_postfix(unchanged) == 0); + Require(mkstemp_nid_postfix(fixed.data()) == -1 && *__error_nid_postfix() == 17); + std::string missing = "/anyps5-posix-test/missing/file-XXXXXX"; + Require(mkstemp_nid_postfix(missing.data()) == -1 && *__error_nid_postfix() == 2); + std::string notDirectory = fixed + "/file-XXXXXX"; + Require(mkstemp_nid_postfix(notDirectory.data()) == -1); + Require(*__error_nid_postfix() == 20 || *__error_nid_postfix() == 2); + std::string empty; + Require(mkstemp_nid_postfix(empty.data()) == -1 && *__error_nid_postfix() == 22); + Require(mkstemp_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 14); + std::string tooLong(1024, 'X'); + Require(mkstemp_nid_postfix(tooLong.data()) == -1 && *__error_nid_postfix() == 63); + Require(tooLong == std::string(1024, 'X')); + Require(isatty_nid_postfix(-1) == 0 && *__error_nid_postfix() == 9); + Require(isatty_nid_postfix(100000) == 0 && *__error_nid_postfix() == 9); + int pipes[2]{}; + Require(pipe_nid_postfix(pipes) == 0); + for (const int pipe : pipes) { + Require(isatty_nid_postfix(pipe) == 0 && *__error_nid_postfix() == 25); + Require(close_nid_postfix(pipe) == 0); + } + const int socket = socket_nid_postfix(2, 2, 0); + Require(socket >= GuestSockets::FirstDescriptor); + Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 25); + Require(close_nid_postfix(socket) == 0); + Require(isatty_nid_postfix(socket) == 0 && *__error_nid_postfix() == 9); +#ifdef _WIN32 + const int nullDevice = ::_open("NUL", _O_RDWR); + Require(nullDevice >= 0); + Require(isatty_nid_postfix(nullDevice) == 0 && *__error_nid_postfix() == 25); + Require(::_close(nullDevice) == 0); + const int console = ::_open("CONIN$", _O_RDONLY); + if (console >= 0) { + *__error_nid_postfix() = 123; + Require(isatty_nid_postfix(console) == 1 && *__error_nid_postfix() == 123); + Require(::_close(console) == 0); + } +#else + const int terminal = ::posix_openpt(O_RDWR | O_NOCTTY); + Require(terminal >= 0 && ::grantpt(terminal) == 0 && ::unlockpt(terminal) == 0); + const int slave = ::open(::ptsname(terminal), O_RDWR | O_NOCTTY); + Require(slave >= 0); + *__error_nid_postfix() = 123; + Require(isatty_nid_postfix(slave) == 1 && *__error_nid_postfix() == 123); + Require(::close(slave) == 0 && ::close(terminal) == 0); +#endif + RemovePathAlias_nid_no_patch("/anyps5-posix-test"); + Require(std::filesystem::remove_all(root) > 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 202e3461a..7c8bc4b34 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -236,6 +236,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. +- [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [Wide-character conversions](../../core/libs/prx/libc/src/Strings.cpp) (libc) - `wcsrtombs` and [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still pass guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, they misread and miswrite guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits