feat(posix): implement mkstemp and isatty

This commit is contained in:
Tomás Ortellado 2026-10-08 11:44:33 -03:00 committed by Adrià Franch
parent 87b998a99a
commit 7a771d2d89
4 changed files with 237 additions and 1 deletions

View File

@ -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:$<TARGET_FILE_DIR:libScePosixForWebKit>")
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)

View File

@ -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 <cerrno>
#include <cstring>
#include <random>
#include <string>
#ifdef _WIN32
#include <windows.h>
#include <io.h>
extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler);
#else
#include <unistd.h>
#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<unsigned int> 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<HANDLE>(::_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;
}

View File

@ -0,0 +1,140 @@
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <random>
#include <source_location>
#include <string>
#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#include <windows.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#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<char, sizeof(payload)> received{};
Require(read_nid_postfix(descriptor, received.data(), received.size()) == static_cast<std::int64_t>(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);
}

View File

@ -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