diff --git a/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt b/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt new file mode 100644 index 0000000000..cdc8922b8a --- /dev/null +++ b/core/libs/prx/libSceCompanionHttpd/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceCompanionHttpd SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceCompanionHttpd PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceCompanionHttpd) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_companion_httpd_tests ${LIBS_INCLUDE_DIR}/tests/GuestCompanionHttpd.cpp) + target_include_directories(guest_companion_httpd_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_companion_httpd_tests PRIVATE libSceCompanionHttpd libc) + configure_windows_unwind(guest_companion_httpd_tests) + add_test(NAME guest_companion_httpd COMMAND guest_companion_httpd_tests) + if(WIN32) + set_tests_properties(guest_companion_httpd PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceCompanionHttpd/Export.cpp b/core/libs/prx/libSceCompanionHttpd/Export.cpp index 48240a3fe1..8d3a814f50 100644 --- a/core/libs/prx/libSceCompanionHttpd/Export.cpp +++ b/core/libs/prx/libSceCompanionHttpd/Export.cpp @@ -1,91 +1,162 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int CompanionHttpdErrorInvalidParam = static_cast(0x80E40004u); +constexpr int CompanionHttpdErrorNotInitialized = static_cast(0x80E40006u); +constexpr int CompanionHttpdErrorAlreadyInitialized = static_cast(0x80E40007u); +constexpr int CompanionHttpdErrorNoEvent = static_cast(0x80E40008u); +constexpr int CompanionHttpdErrorAlreadyStarted = static_cast(0x80E4000Au); +constexpr int CompanionHttpdErrorNotStarted = static_cast(0x80E4000Bu); +constexpr int CompanionHttpdErrorAlreadyRegistered = static_cast(0x80E4000Cu); + +struct CompanionHttpdOptParam { + std::uint64_t reserved; + void* workMemory; + std::uint64_t workMemorySize; + std::uint8_t settings[0x40]; + const void* temporaryMountPoint; +}; +static_assert(sizeof(CompanionHttpdOptParam) == 0x60); + +using CompanionHttpdRequestCallback = int (*)(int, int, const void*, void*, void*); + +struct CompanionHttpdState { + std::mutex mutex; + bool initialized = false; + bool started = false; + CompanionHttpdRequestCallback callback = nullptr; + void* callbackArgument = nullptr; +}; + +CompanionHttpdState& State() { + static CompanionHttpdState state; + return state; +} + +} + extern "C" { -int APS5_VABI sceCompanionHttpdAddHeader() { +int APS5_VABI sceCompanionHttpdOptParamInitialize(CompanionHttpdOptParam* param) { + if (param == nullptr) return CompanionHttpdErrorInvalidParam; + std::memset(param, 0, sizeof(*param)); + return 0; +} + +int APS5_VABI sceCompanionHttpdInitialize(const CompanionHttpdOptParam* param) { + if (param == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (state.initialized) return CompanionHttpdErrorAlreadyInitialized; + state.initialized = true; + return 0; +} + +int APS5_VABI sceCompanionHttpdTerminate(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.started) NotImplemented_nid_no_patch(__func__); + state.initialized = false; + state.callback = nullptr; + state.callbackArgument = nullptr; + return 0; +} + +int APS5_VABI sceCompanionHttpdRegisterRequestCallback(CompanionHttpdRequestCallback function, void* argument) { + if (function == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.callback != nullptr) return CompanionHttpdErrorAlreadyRegistered; + state.callback = function; + state.callbackArgument = argument; + return 0; +} + +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.callback == nullptr) NotImplemented_nid_no_patch(__func__); + state.callback = nullptr; + state.callbackArgument = nullptr; + return 0; +} + +int APS5_VABI sceCompanionHttpdStart(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (state.started) return CompanionHttpdErrorAlreadyStarted; + state.started = true; + return 0; +} + +int APS5_VABI sceCompanionHttpdStop(void) { + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + if (!state.started) return CompanionHttpdErrorNotStarted; + state.started = false; + return 0; +} + +int APS5_VABI sceCompanionHttpdGetEvent(void* event) { + if (event == nullptr) return CompanionHttpdErrorInvalidParam; + auto& state = State(); + std::lock_guard lock(state.mutex); + if (!state.initialized) return CompanionHttpdErrorNotInitialized; + return CompanionHttpdErrorNoEvent; +} + +int APS5_VABI sceCompanionHttpdAddHeader(const char* key, const char* value, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGet2ndScreenStatus() { +int APS5_VABI sceCompanionHttpdSetStatus(int status, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGetEvent() { +int APS5_VABI sceCompanionHttpdSetBody(const char* body, std::uint64_t size, void* response) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdGetUserId() { +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdInitialize() { +int APS5_VABI sceCompanionHttpdGetUserId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdInitialize2() { +int APS5_VABI sceCompanionHttpdInitialize2(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdOptParamInitialize() { +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback() { +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceCompanionHttpdRegisterRequestCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdRegisterRequestCallback2() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdSetBody() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdSetStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdStop() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCompanionHttpdUnregisterRequestCallback() { +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/tests/GuestCompanionHttpd.cpp b/core/libs/tests/GuestCompanionHttpd.cpp new file mode 100644 index 0000000000..e9f8990365 --- /dev/null +++ b/core/libs/tests/GuestCompanionHttpd.cpp @@ -0,0 +1,129 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCompanionHttpdOptParamInitialize(void* param); +int APS5_VABI sceCompanionHttpdInitialize(const void* param); +int APS5_VABI sceCompanionHttpdTerminate(void); +int APS5_VABI sceCompanionHttpdRegisterRequestCallback(void* function, void* argument); +int APS5_VABI sceCompanionHttpdUnregisterRequestCallback(void); +int APS5_VABI sceCompanionHttpdStart(void); +int APS5_VABI sceCompanionHttpdStop(void); +int APS5_VABI sceCompanionHttpdGetEvent(void* event); +int APS5_VABI sceCompanionHttpdAddHeader(const char* key, const char* value, void* response); +int APS5_VABI sceCompanionHttpdSetStatus(int status, void* response); +int APS5_VABI sceCompanionHttpdSetBody(const char* body, std::uint64_t size, void* response); +int APS5_VABI sceCompanionHttpdGet2ndScreenStatus(void); +int APS5_VABI sceCompanionHttpdGetUserId(void); +int APS5_VABI sceCompanionHttpdInitialize2(void); +int APS5_VABI sceCompanionHttpdRegisterRequestBodyReceptionCallback(void); +int APS5_VABI sceCompanionHttpdRegisterRequestCallback2(void); +int APS5_VABI sceCompanionHttpdUnregisterRequestBodyReceptionCallback(void); +} + +namespace { + +constexpr int InvalidParam = static_cast(0x80E40004u); +constexpr int NotInitialized = static_cast(0x80E40006u); +constexpr int AlreadyInitialized = static_cast(0x80E40007u); +constexpr int NoEvent = static_cast(0x80E40008u); +constexpr int AlreadyStarted = static_cast(0x80E4000Au); +constexpr int NotStarted = static_cast(0x80E4000Bu); +constexpr int AlreadyRegistered = static_cast(0x80E4000Cu); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "CompanionHttpd: %s\n", message); + std::abort(); + } +} + +template +void RequireNotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == name + " not implemented", "a companion function threw another error"); + return; + } + Require(false, "a companion function that should throw returned"); +} + +int APS5_VABI RequestCallback(int, int, const void*, void*, void*) { + std::fprintf(stderr, "CompanionHttpd: the request callback was called\n"); + std::abort(); +} + +void CheckOptionBlock() { + unsigned char block[0x70]; + std::memset(block, 0x5a, sizeof(block)); + Require(sceCompanionHttpdOptParamInitialize(nullptr) == InvalidParam, "a null option block must be rejected"); + Require(sceCompanionHttpdOptParamInitialize(block) == 0, "sceCompanionHttpdOptParamInitialize failed"); + for (std::size_t index = 0; index < sizeof(block); ++index) { + Require(block[index] == (index < 0x60 ? 0u : 0x5au), "the option block must be zeroed over its 0x60 bytes and nothing past them"); + } +} + +void CheckStates() { + unsigned char param[0x60]{}; + unsigned char event[0x104]; + std::memset(event, 0x5a, sizeof(event)); + unsigned char untouched[0x104]; + std::memset(untouched, 0x5a, sizeof(untouched)); + auto* callback = reinterpret_cast(&RequestCallback); + Require(sceCompanionHttpdGetEvent(event) == NotInitialized, "an event before initialization"); + Require(sceCompanionHttpdStart() == NotInitialized, "a start before initialization"); + Require(sceCompanionHttpdStop() == NotInitialized, "a stop before initialization"); + Require(sceCompanionHttpdTerminate() == NotInitialized, "a termination before initialization"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, nullptr) == NotInitialized, "a callback before initialization"); + Require(sceCompanionHttpdUnregisterRequestCallback() == NotInitialized, "an unregistration before initialization"); + Require(sceCompanionHttpdInitialize(nullptr) == InvalidParam, "a null parameter must be rejected"); + Require(sceCompanionHttpdInitialize(param) == 0, "sceCompanionHttpdInitialize failed"); + Require(sceCompanionHttpdInitialize(param) == AlreadyInitialized, "a second initialization"); + Require(sceCompanionHttpdRegisterRequestCallback(nullptr, nullptr) == InvalidParam, "a null callback must be rejected"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, param) == 0, "registering the request callback failed"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, param) == AlreadyRegistered, "a second callback"); + Require(sceCompanionHttpdGetEvent(nullptr) == InvalidParam, "a null event must be rejected"); + Require(sceCompanionHttpdGetEvent(event) == NoEvent, "no companion device connects before the start"); + Require(sceCompanionHttpdStop() == NotStarted, "a stop before the start"); + Require(sceCompanionHttpdStart() == 0, "sceCompanionHttpdStart failed"); + Require(sceCompanionHttpdStart() == AlreadyStarted, "a second start"); + Require(sceCompanionHttpdGetEvent(event) == NoEvent, "no companion device connects after the start"); + Require(std::memcmp(event, untouched, sizeof(event)) == 0, "sceCompanionHttpdGetEvent wrote the event"); + RequireNotImplemented([] { sceCompanionHttpdTerminate(); }, "sceCompanionHttpdTerminate"); + Require(sceCompanionHttpdStop() == 0, "sceCompanionHttpdStop failed"); + Require(sceCompanionHttpdUnregisterRequestCallback() == 0, "unregistering the request callback failed"); + RequireNotImplemented([] { sceCompanionHttpdUnregisterRequestCallback(); }, "sceCompanionHttpdUnregisterRequestCallback"); + Require(sceCompanionHttpdTerminate() == 0, "sceCompanionHttpdTerminate failed"); + Require(sceCompanionHttpdTerminate() == NotInitialized, "a second termination"); + Require(sceCompanionHttpdInitialize(param) == 0, "initializing again after the termination failed"); + Require(sceCompanionHttpdRegisterRequestCallback(callback, nullptr) == 0, "the termination kept the callback"); + Require(sceCompanionHttpdTerminate() == 0, "the last termination failed"); +} + +void CheckResponseBuilders() { + unsigned char response[0x20]{}; + RequireNotImplemented([&] { sceCompanionHttpdAddHeader("key", "value", response); }, "sceCompanionHttpdAddHeader"); + RequireNotImplemented([&] { sceCompanionHttpdSetStatus(200, response); }, "sceCompanionHttpdSetStatus"); + RequireNotImplemented([&] { sceCompanionHttpdSetBody("body", 4, response); }, "sceCompanionHttpdSetBody"); + RequireNotImplemented([] { sceCompanionHttpdGet2ndScreenStatus(); }, "sceCompanionHttpdGet2ndScreenStatus"); + RequireNotImplemented([] { sceCompanionHttpdGetUserId(); }, "sceCompanionHttpdGetUserId"); + RequireNotImplemented([] { sceCompanionHttpdInitialize2(); }, "sceCompanionHttpdInitialize2"); + RequireNotImplemented([] { sceCompanionHttpdRegisterRequestBodyReceptionCallback(); }, "sceCompanionHttpdRegisterRequestBodyReceptionCallback"); + RequireNotImplemented([] { sceCompanionHttpdRegisterRequestCallback2(); }, "sceCompanionHttpdRegisterRequestCallback2"); + RequireNotImplemented([] { sceCompanionHttpdUnregisterRequestBodyReceptionCallback(); }, "sceCompanionHttpdUnregisterRequestBodyReceptionCallback"); +} + +} + +int main() { + CheckOptionBlock(); + CheckStates(); + CheckResponseBuilders(); + return 0; +} diff --git a/docs/dev/debt/libSceCompanionHttpd.md b/docs/dev/debt/libSceCompanionHttpd.md index 2738a6bd6d..3ed7d72d8c 100644 --- a/docs/dev/debt/libSceCompanionHttpd.md +++ b/docs/dev/debt/libSceCompanionHttpd.md @@ -2,4 +2,10 @@ ### Unknown function info -- [sceCompanionHttpdAddHeader, sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetEvent, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize, sceCompanionHttpdInitialize2, sceCompanionHttpdOptParamInitialize, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdSetBody, sceCompanionHttpdSetStatus, sceCompanionHttpdStart, sceCompanionHttpdStop, sceCompanionHttpdTerminate, sceCompanionHttpdUnregisterRequestBodyReceptionCallback and sceCompanionHttpdUnregisterRequestCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - names and NIDs are present in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCompanionHttpd.c), but no credible prototypes were found. The declarations have no parameters solely to resolve known imports; this is not an ABI claim and callers must not call them until signatures are verified +- [sceCompanionHttpdOptParamInitialize, sceCompanionHttpdInitialize](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - the option block is taken as 0x60 bytes: titles fill a work memory pointer at +0x08, its size at +0x10 and a temporary data mount point at +0x58 after `sceCompanionHttpdOptParamInitialize`. Its other fields and the defaults the console writes are unknown, so `sceCompanionHttpdOptParamInitialize` zeroes the 0x60 bytes and `sceCompanionHttpdInitialize` reads none of them; both reject a null block with `INVALID_PARAM` +- [sceCompanionHttpdAddHeader, sceCompanionHttpdSetStatus, sceCompanionHttpdSetBody](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - imported by PS4 titles and added without an implementation. They fill the response of a request callback, which is never called here, so every call throws +- [sceCompanionHttpdGet2ndScreenStatus, sceCompanionHttpdGetUserId, sceCompanionHttpdInitialize2, sceCompanionHttpdRegisterRequestBodyReceptionCallback, sceCompanionHttpdRegisterRequestCallback2, sceCompanionHttpdUnregisterRequestBodyReceptionCallback](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - the rest of the library's exports, declared without parameters and without an implementation; their signatures are not verified, so every call throws + +### Functional + +- [Companion server state](../../../core/libs/prx/libSceCompanionHttpd/Export.cpp) (libSceCompanionHttpd) - no HTTP server listens, so no companion device ever connects: `sceCompanionHttpdGetEvent` returns `NO_EVENT` without writing the event and the request callback is never called. Initialization, callback registration and start and stop are tracked with the errors of shadPS4's [companion_error.h](https://github.com/shadps4-emu/shadPS4/blob/eb9f66c349ae7c8fef780f4f55e98b8065947e14/src/core/libraries/companion/companion_error.h) (`ALREADY_REGISTERED` is 0x80E4000C, between `NOT_STARTED` and `NOT_CONNECTED`; the header has 0x80E4000, one digit short). Unregistering with no callback and terminating a started server throw, since the console's answers are unknown. Not measured on a console. Closes when: a companion server is hosted