From 8fc83ae05cf93f77497f1703749755c78b1cee08 Mon Sep 17 00:00:00 2001 From: Mouhand-Kaddo Date: Fri, 9 Oct 2026 00:34:38 +0400 Subject: [PATCH] feat(zlib): implement sequential libSceZlib decompression Resolve the missing system library with checksum-validating decompression. Add behavioral tests and document unsupported request semantics. --- core/libs/prx/libSceZlib/CMakeLists.txt | 23 ++++++ core/libs/prx/libSceZlib/Export.cpp | 100 ++++++++++++++++++++++++ core/libs/tests/GuestZlib.cpp | 77 ++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 201 insertions(+) create mode 100644 core/libs/prx/libSceZlib/CMakeLists.txt create mode 100644 core/libs/prx/libSceZlib/Export.cpp create mode 100644 core/libs/tests/GuestZlib.cpp diff --git a/core/libs/prx/libSceZlib/CMakeLists.txt b/core/libs/prx/libSceZlib/CMakeLists.txt new file mode 100644 index 000000000..29ab2dba4 --- /dev/null +++ b/core/libs/prx/libSceZlib/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(libSceZlib SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) +target_include_directories(libSceZlib PRIVATE ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/libjpeg-turbo/src/spng/zlib) +set_target_properties(libSceZlib PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(libSceZlib) +target_link_libraries(libSceZlib PRIVATE jpeg_turbo) + +if(BUILD_TESTING) + add_test_executable(guest_zlib_tests ${LIBS_INCLUDE_DIR}/tests/GuestZlib.cpp) + target_include_directories(guest_zlib_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_zlib_tests PRIVATE libSceZlib libc) + configure_windows_unwind(guest_zlib_tests) + if(MINGW) + target_compile_options(libSceZlib PRIVATE -fasynchronous-unwind-tables) + target_compile_options(guest_zlib_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME guest_zlib COMMAND guest_zlib_tests) + set_tests_properties(guest_zlib PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_zlib PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp new file mode 100644 index 000000000..ba1067f8b --- /dev/null +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -0,0 +1,100 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "zlib.h" +#include +#include +#include +#include +#include + +namespace { + +static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof(std::int32_t) == 4); + +constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); +constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); +constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); +constexpr std::int32_t FATAL = static_cast(0x811200FF); + +struct Result { + std::uint64_t id; + std::uint32_t produced; + std::int32_t status; + bool retrieved = false; +}; + +std::mutex serviceMutex; +bool initialized = false; +std::uint64_t nextId = 1; +std::optional result; + +} // namespace + +extern "C" { + +std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t length) { + std::lock_guard lock(serviceMutex); + if (initialized) return ALREADY_INITIALIZED; + if ((buffer == nullptr) != (length == 0)) throw std::runtime_error("sceZlibInitialize: invalid work buffer"); + initialized = true; + return 0; +} + +std::int32_t APS5_VABI sceZlibFinalize() { + std::lock_guard lock(serviceMutex); + if (!initialized) return NOT_INITIALIZED; + if (result && !result->retrieved) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); + result.reset(); + initialized = false; + return 0; +} + +std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLength, + void* destination, std::uint32_t destinationLength, + std::uint64_t* requestId) { + std::lock_guard lock(serviceMutex); + if (!initialized) return NOT_INITIALIZED; + if (!source || !sourceLength || !destination || !requestId || !destinationLength || + destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) + throw std::runtime_error("sceZlibInflate: invalid buffer or size"); + if (result && !result->retrieved) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); + if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); + z_stream stream{}; + stream.next_in = const_cast(static_cast(source)); + stream.avail_in = sourceLength; + stream.next_out = static_cast(destination); + stream.avail_out = destinationLength; + if (inflateInit(&stream) != Z_OK) throw std::runtime_error("sceZlibInflate: decoder initialization failed"); + const int decoded = inflate(&stream, Z_FINISH); + const auto produced = static_cast(stream.total_out); + const std::int32_t status = decoded == Z_STREAM_END ? 0 : + (decoded == Z_BUF_ERROR && stream.avail_out == 0 ? NO_SPACE : FATAL); + const int ended = inflateEnd(&stream); + if (ended != Z_OK) throw std::runtime_error("sceZlibInflate: decoder cleanup failed"); + result = Result{nextId++, produced, status}; + *requestId = result->id; + return 0; +} + +std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_t* timeout) { + std::lock_guard lock(serviceMutex); + if (!initialized) return NOT_INITIALIZED; + if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); + if (!requestId || !result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + *requestId = result->id; + return 0; +} + +std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* destinationLength, + std::int32_t* status) { + std::lock_guard lock(serviceMutex); + if (!initialized) return NOT_INITIALIZED; + if (!destinationLength || !status || !result || result->id != requestId) + throw std::runtime_error("sceZlibGetResult: invalid request or output"); + if (result->retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); + *destinationLength = result->produced; + *status = result->status; + result->retrieved = true; + return 0; +} + +} diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp new file mode 100644 index 000000000..1245c9169 --- /dev/null +++ b/core/libs/tests/GuestZlib.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +std::int32_t APS5_VABI sceZlibInitialize(const void*, std::uint64_t); +std::int32_t APS5_VABI sceZlibFinalize(); +std::int32_t APS5_VABI sceZlibInflate(const void*, std::uint32_t, void*, std::uint32_t, std::uint64_t*); +std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t*, std::uint32_t*); +std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t, std::uint32_t*, std::int32_t*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TAction action) { + try { action(); } catch (const std::runtime_error&) { return; } + std::abort(); +} + +int main() { + constexpr std::array compressed{0x78, 0x9c, 0xed, 0xca, 0xb1, 0xd, 0x80, 0x20, 0x10, 0x40, 0xd1, 0x9e, 0x29, 0x6e, 0x2, 0x3a, 0x6, 0x60, 0x3, 0x12, 0x26, 0x10, 0x30, 0x91, 0x88, 0x47, 0x71, 0xa1, 0xd0, 0xe9, 0x1d, 0x82, 0xf6, 0xbf, 0xfa, 0x45, 0x7d, 0x53, 0xe, 0xf2, 0x8d, 0x5e, 0xa4, 0x5e, 0x67, 0xbd, 0x6d, 0x3d, 0x72, 0x68, 0x93, 0x32, 0x97, 0x36, 0xf3, 0x2e, 0x12, 0x8, 0x4, 0x2, 0x81, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x81, 0x40, 0x20, 0xec, 0x85, 0x1f, 0xf5, 0xf3, 0xbd, 0x4c}; + constexpr char text[] = "AnyPS5 zlib checksum and bounds.\n"; + constexpr std::uint32_t expectedLength = (sizeof(text) - 1) * 128; + alignas(2048) std::array storage{}; + auto* output = storage.data() + 2048; + std::uint64_t id = 0; + std::uint32_t produced = 0; + std::int32_t status = 0; + Require(sceZlibFinalize() == static_cast(0x81120032)); + Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast(0x81120032)); + RequireThrows([&] { sceZlibInitialize(nullptr, 1); }); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibInitialize(nullptr, 0) == static_cast(0x81120033)); + RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output + 1, expectedLength, &id); }); + RequireThrows([&] { sceZlibInflate(compressed.data(), compressed.size(), output, 65537, &id); }); + RequireThrows([&] { sceZlibInflate(nullptr, compressed.size(), output, expectedLength, &id); }); + std::uint64_t previous = 0; + auto decode = [&](const void* input, std::uint32_t inputLength, std::uint32_t capacity, std::int32_t expectedStatus) { + storage.fill(0xA5); + Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); + Require(id > previous); + previous = id; + RequireThrows([&] { sceZlibInflate(input, inputLength, output, capacity, &id); }); + RequireThrows([&] { sceZlibFinalize(); }); + std::uint32_t timeout = 0; + RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); + std::uint64_t completed; + Require(sceZlibWaitForDone(&completed, nullptr) == 0); + Require(completed == id); + RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); + Require(sceZlibGetResult(id, &produced, &status) == 0); + Require(status == expectedStatus && produced <= capacity); + RequireThrows([&] { sceZlibGetResult(id, &produced, &status); }); + Require(std::all_of(storage.begin(), storage.begin() + 2048, [](auto byte) { return byte == 0xA5; })); + Require(std::all_of(storage.begin() + 2048 + capacity, storage.end(), [](auto byte) { return byte == 0xA5; })); + }; + decode(compressed.data(), compressed.size(), expectedLength, 0); + Require(produced == expectedLength); + for (std::uint32_t offset = 0; offset < produced; offset += sizeof(text) - 1) + Require(std::memcmp(output + offset, text, sizeof(text) - 1) == 0); + decode(compressed.data(), compressed.size(), expectedLength - 1, static_cast(0x8112001C)); + auto corrupt = compressed; + corrupt.back() ^= 1; + decode(corrupt.data(), corrupt.size(), expectedLength, static_cast(0x811200FF)); + decode(compressed.data(), compressed.size() - 1, 65536, static_cast(0x811200FF)); + Require(sceZlibFinalize() == 0); + Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); + std::array scratch{}; + Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0); + decode(compressed.data(), compressed.size(), 65536, 0); + Require(sceZlibFinalize() == 0); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 1b6722dac..6ce265550 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -248,6 +248,7 @@ Throughout the project, every function at every stage either **does exactly what - [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`. +- [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [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 - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [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