diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 5bd1b5553..bf657c237 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1243,6 +1243,14 @@ struct Http2AsyncResult { void* reserved; }; +struct Http2AsyncOption { + KernelEqueue equeue; + int user_event_id; + std::uint8_t padding[4]; + void* user_data; + void* reserved; +}; + struct NpTitleId { char data[13]; char pad[3]; }; struct NpTitleSecret { std::uint8_t data[128]; }; struct NpContentRestriction { std::uint8_t opaque[128]; }; diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt index 45ae68e16..7fbaa4014 100644 --- a/core/libs/prx/libSceHttp2/CMakeLists.txt +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -1,11 +1,12 @@ add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceHttp2 PRIVATE libc libkernel) configure_windows_unwind(libSceHttp2) if(BUILD_TESTING) add_test_executable(guest_http2_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2.cpp) target_include_directories(guest_http2_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libc) + target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libkernel libc) configure_windows_unwind(guest_http2_tests) add_test(NAME guest_http2 COMMAND guest_http2_tests) if(WIN32) diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 56f59ebea..0bbcf1b1e 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -2,12 +2,35 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Equeue/Equeue.hpp" #include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static constexpr int ERROR_NETWORK = static_cast(0x80436063); static std::atomic g_nextHandle{1}; +static std::mutex g_completionsMutex; +static std::unordered_map> g_completions; + +static void CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { + if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function); + if (option != nullptr) NotImplemented_nid_no_patch(function); + { + std::lock_guard lock(g_completionsMutex); + Http2AsyncResult completion{}; + completion.req_id = req_id; + completion.result = ERROR_NETWORK; + g_completions[req_id].push_back(completion); + } + if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { + std::lock_guard lock(g_completionsMutex); + g_completions[req_id].pop_back(); + NotImplemented_nid_no_patch(function); + } +} extern "C" { @@ -36,7 +59,8 @@ int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agen } int APS5_VABI sceHttp2DeleteRequest(int req_id) { - (void)req_id; + std::lock_guard lock(g_completionsMutex); + g_completions.erase(req_id); return 0; } @@ -96,13 +120,11 @@ int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size return ERROR_NETWORK; } -int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, void* kqueue_option, void* option) { - (void)req_id; +int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) { (void)post_data; (void)size; - (void)kqueue_option; - (void)option; - return ERROR_NETWORK; + CompleteAsync(__func__, req_id, kqueue_option, option); + return 0; } int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) { @@ -203,11 +225,14 @@ int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { } int APS5_VABI sceHttp2WaitAsync(int req_id, Http2AsyncResult* result, uint32_t* timeout, void* option) { - (void)req_id; - (void)result; (void)timeout; - (void)option; - return ERROR_NETWORK; + if (result == nullptr || option != nullptr) NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_completionsMutex); + const auto pending = g_completions.find(req_id); + if (pending == g_completions.end() || pending->second.empty()) NotImplemented_nid_no_patch("sceHttp2WaitAsync: waiting for an operation that has not completed"); + *result = pending->second.front(); + pending->second.pop_front(); + return 0; } int APS5_VABI sceHttp2AbortRequest(int req_id) { diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index 7be4800b5..265610306 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -1,6 +1,7 @@ #include "SceTypes.hpp" #include #include +#include extern "C" { int APS5_VABI sceHttp2Init(int, int, std::size_t, int); @@ -11,11 +12,31 @@ int APS5_VABI sceHttp2SetCookieBox(int, int); int APS5_VABI sceHttp2CookieFlush(int); int APS5_VABI sceHttp2SetRequestNoContentLength(int); int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); +int APS5_VABI sceHttp2SendRequestAsync(int, const void*, std::size_t, Http2AsyncOption*, void*); +int APS5_VABI sceHttp2WaitAsync(int, Http2AsyncResult*, std::uint32_t*, void*); +int APS5_VABI sceHttp2DeleteRequest(int); int APS5_VABI sceHttp2Term(int); +int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); +int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); +int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo); +int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id); +int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id); +uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev); +void* APS5_VABI sceKernelGetEventUserData(const KernelEvent* ev); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::exception&) { + return true; + } + return false; +} + int main() { const int context = sceHttp2Init(1, 1, 0x10000, 4); Require(context > 0); @@ -32,5 +53,31 @@ int main() { Require(sceHttp2SetRequestNoContentLength(request) == 0); Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "http2") == 0); + Require(sceKernelAddUserEventEdge(eq, request) == 0); + int tag = 0; + Http2AsyncOption option{}; + option.equeue = eq; + option.user_event_id = request; + option.user_data = &tag; + Require(sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr) == 0); + KernelEvent event{}; + int count = 0; + const KernelUseconds timeout = 1000000; + Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == 0 && count == 1); + Require(sceKernelGetEventId(&event) == static_cast(request)); + Require(sceKernelGetEventUserData(&event) == &tag); + Http2AsyncResult result{}; + Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0); + Require(result.req_id == request && result.result == static_cast(0x80436063)); + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr); })); + Require(sceKernelDeleteUserEvent(eq, request) == 0); + Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); })); + Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(request) == 0); + Require(sceKernelDeleteEqueue(eq) == 0); Require(sceHttp2Term(context) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0efd38423..907a13f47 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -221,6 +221,7 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [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 - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection