feat(zlib): implement sequential libSceZlib decompression

Resolve the missing system library with checksum-validating decompression. Add behavioral tests and document unsupported request semantics.
This commit is contained in:
Mouhand-Kaddo 2026-10-09 00:34:38 +04:00 committed by Mouhand Kaddo
parent 0ccdf15f18
commit 8fc83ae05c
4 changed files with 201 additions and 0 deletions

View File

@ -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:$<TARGET_FILE_DIR:libSceZlib>")
endif()
endif()

View File

@ -0,0 +1,100 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "zlib.h"
#include <cstdint>
#include <limits>
#include <mutex>
#include <optional>
#include <stdexcept>
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<std::int32_t>(0x81120032);
constexpr std::int32_t ALREADY_INITIALIZED = static_cast<std::int32_t>(0x81120033);
constexpr std::int32_t NO_SPACE = static_cast<std::int32_t>(0x8112001C);
constexpr std::int32_t FATAL = static_cast<std::int32_t>(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> 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<std::uintptr_t>(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<std::uint64_t>::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted");
z_stream stream{};
stream.next_in = const_cast<Bytef*>(static_cast<const Bytef*>(source));
stream.avail_in = sourceLength;
stream.next_out = static_cast<Bytef*>(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<std::uint32_t>(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;
}
}

View File

@ -0,0 +1,77 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
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<typename TAction>
static void RequireThrows(TAction action) {
try { action(); } catch (const std::runtime_error&) { return; }
std::abort();
}
int main() {
constexpr std::array<std::uint8_t, 77> 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<std::uint8_t, 2048 + 65536 + 2048> 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<std::int32_t>(0x81120032));
Require(sceZlibInflate(nullptr, 0, nullptr, 0, nullptr) == static_cast<std::int32_t>(0x81120032));
RequireThrows([&] { sceZlibInitialize(nullptr, 1); });
Require(sceZlibInitialize(nullptr, 0) == 0);
Require(sceZlibInitialize(nullptr, 0) == static_cast<std::int32_t>(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<std::int32_t>(0x8112001C));
auto corrupt = compressed;
corrupt.back() ^= 1;
decode(corrupt.data(), corrupt.size(), expectedLength, static_cast<std::int32_t>(0x811200FF));
decode(compressed.data(), compressed.size() - 1, 65536, static_cast<std::int32_t>(0x811200FF));
Require(sceZlibFinalize() == 0);
Require(sceZlibGetResult(id, &produced, &status) == static_cast<std::int32_t>(0x81120032));
std::array<std::uint8_t, 65536> scratch{};
Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0);
decode(compressed.data(), compressed.size(), 65536, 0);
Require(sceZlibFinalize() == 0);
}

View File

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