From 0a8cc7f54356b5657471834a1af4e0772c06c480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 1 Oct 2026 18:15:50 +0200 Subject: [PATCH] feat(libSceHttp): implement URI parse, build and escape sceHttpUriParse, sceHttpUriBuild and sceHttpUriEscape work on strings only and are implemented. sceHttpCreateRequestWithURL, sceHttpReadData, sceHttpSetChunkedTransferEnabled and sceHttpCreateEpoll follow the existing no-network behaviour. Co-Authored-By: Claude --- core/libs/prx/libSceHttp/CMakeLists.txt | 20 ++ core/libs/prx/libSceHttp/Export.cpp | 65 ++---- core/libs/prx/libSceHttp/src/HttpErrors.hpp | 9 + core/libs/prx/libSceHttp/src/Uri.cpp | 236 ++++++++++++++++++++ core/libs/tests/GuestHttp.cpp | 94 ++++++++ 5 files changed, 380 insertions(+), 44 deletions(-) create mode 100644 core/libs/prx/libSceHttp/CMakeLists.txt create mode 100644 core/libs/prx/libSceHttp/src/HttpErrors.hpp create mode 100644 core/libs/prx/libSceHttp/src/Uri.cpp create mode 100644 core/libs/tests/GuestHttp.cpp diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt new file mode 100644 index 000000000..0093dd1c0 --- /dev/null +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceHttp SHARED EXCLUDE_FROM_ALL Export.cpp src/Uri.cpp) +target_include_directories(libSceHttp PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHttp) + +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_http_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp.cpp) + target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_tests PRIVATE libSceHttp) + add_test(NAME guest_http COMMAND guest_http_tests) + if(WIN32) + set_tests_properties(guest_http PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index d3292924e..85681b975 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -2,11 +2,11 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" #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(0x80431063); static std::atomic g_nextHandle{1}; extern "C" { @@ -41,10 +41,10 @@ int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int e } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - (void)http_ctx_id; - (void)eh; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)http_ctx_id; + if (!eh) return ERROR_INVALID_VALUE; + *eh = new HttpEpoll{}; + return 0; } int APS5_VABI sceHttpCreateRequest(int conn_id, int method, const char* path, uint64_t content_length) { @@ -88,7 +88,7 @@ int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { (void)http_ctx_id; - (void)eh; + delete eh; return 0; } @@ -216,35 +216,6 @@ int APS5_VABI sceHttpUnsetEpoll(int id) { return 0; } -int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const SceHttpUriElement* src_element, uint32_t option) { - (void)out; - (void)require; - (void)prepare; - (void)src_element; - (void)option; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const char* in) { - (void)out; - (void)require; - (void)prepare; - (void)in; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* pool, size_t* require, size_t prepare) { - (void)out; - (void)src_url; - (void)pool; - (void)require; - (void)prepare; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { (void)eh; (void)nbev; @@ -253,19 +224,25 @@ int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxe return ERROR_NETWORK; } -int APS5_VABI sceHttpCreateRequestWithURL() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { + (void)conn_id; + (void)method; + (void)url; + (void)content_length; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); } -int APS5_VABI sceHttpReadData() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpReadData(int request_id, void* data, size_t size) { + (void)request_id; + (void)data; + (void)size; + return ERROR_NETWORK; } -int APS5_VABI sceHttpSetChunkedTransferEnabled() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpSetChunkedTransferEnabled(int id, int enable) { + (void)id; + (void)enable; + return 0; } } diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp new file mode 100644 index 000000000..66d70c7e4 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -0,0 +1,9 @@ +#ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP +#define CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP + +inline constexpr int ERROR_OUT_OF_MEMORY = static_cast(0x80431022); +inline constexpr int ERROR_NETWORK = static_cast(0x80431063); +inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); +inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); + +#endif diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp new file mode 100644 index 000000000..a98ace599 --- /dev/null +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -0,0 +1,236 @@ +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" + +namespace { + +constexpr uint32_t URI_BUILD_WITH_SCHEME = 0x01; +constexpr uint32_t URI_BUILD_WITH_HOSTNAME = 0x02; +constexpr uint32_t URI_BUILD_WITH_PORT = 0x04; +constexpr uint32_t URI_BUILD_WITH_PATH = 0x08; +constexpr uint32_t URI_BUILD_WITH_USERNAME = 0x10; +constexpr uint32_t URI_BUILD_WITH_PASSWORD = 0x20; +constexpr uint32_t URI_BUILD_WITH_QUERY = 0x40; +constexpr uint32_t URI_BUILD_WITH_FRAGMENT = 0x80; + +struct UriParts { + bool opaque = false; + std::string_view scheme; + std::string_view username; + std::string_view password; + std::string_view hostname; + std::string path; + std::string_view query; + std::string_view fragment; + uint16_t port = 0; +}; + +bool equalsIgnoreCase(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + for (size_t i = 0; i < left.size(); ++i) { + if (std::tolower(static_cast(left[i])) != std::tolower(static_cast(right[i]))) return false; + } + return true; +} + +uint16_t defaultPort(std::string_view scheme) { + if (equalsIgnoreCase(scheme, "http")) return 80; + if (equalsIgnoreCase(scheme, "https")) return 443; + return 0; +} + +bool isHostCharacter(char c) { + return std::isalnum(static_cast(c)) || c == '-' || c == '.' || c == '_'; +} + +std::string removeDotSegments(std::string_view input) { + std::string output; + while (!input.empty()) { + if (input.starts_with("../")) input.remove_prefix(3); + else if (input.starts_with("./")) input.remove_prefix(2); + else if (input.starts_with("/./")) input.remove_prefix(2); + else if (input == "/.") input = "/"; + else if (input.starts_with("/../") || input == "/..") { + input = input.size() == 3 ? std::string_view("/") : input.substr(3); + const size_t slash = output.rfind('/'); + output.erase(slash == std::string::npos ? 0 : slash); + } else if (input == "." || input == "..") input = {}; + else { + const size_t end = input.find('/', 1); + const size_t length = end == std::string_view::npos ? input.size() : end; + output.append(input.substr(0, length)); + input.remove_prefix(length); + } + } + return output; +} + +int parseUri(std::string_view uri, UriParts& parts) { + size_t schemeEnd = 0; + if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { + schemeEnd = 1; + while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' + || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; + } + if (schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':') { + parts.scheme = uri.substr(0, schemeEnd); + uri.remove_prefix(schemeEnd + 1); + } + + parts.opaque = !uri.starts_with("//"); + if (!parts.opaque) uri.remove_prefix(2); + + std::string_view authority = uri.substr(0, uri.find_first_of("/?#")); + uri.remove_prefix(authority.size()); + + const size_t at = authority.rfind('@'); + if (at != std::string_view::npos) { + const std::string_view userinfo = authority.substr(0, at); + const size_t colon = userinfo.find(':'); + parts.username = userinfo.substr(0, colon); + if (colon != std::string_view::npos) parts.password = userinfo.substr(colon + 1); + authority.remove_prefix(at + 1); + } + + std::string_view port; + if (authority.starts_with('[')) { + const size_t close = authority.find(']'); + if (close == std::string_view::npos) return ERROR_INVALID_URL; + parts.hostname = authority.substr(1, close - 1); + authority.remove_prefix(close + 1); + if (!authority.empty() && authority[0] != ':') return ERROR_INVALID_URL; + if (!authority.empty()) port = authority.substr(1); + for (const char c : parts.hostname) { + if (!isHostCharacter(c) && c != ':') return ERROR_INVALID_URL; + } + } else { + const size_t colon = authority.find(':'); + parts.hostname = authority.substr(0, colon); + if (colon != std::string_view::npos) port = authority.substr(colon + 1); + for (const char c : parts.hostname) { + if (!isHostCharacter(c)) return ERROR_INVALID_URL; + } + } + + if (port.size() > 5) return ERROR_INVALID_URL; + uint32_t portValue = 0; + for (const char c : port) { + if (!std::isdigit(static_cast(c))) return ERROR_INVALID_URL; + portValue = portValue * 10 + static_cast(c - '0'); + } + if (portValue > 0xFFFF) return ERROR_INVALID_URL; + parts.port = port.empty() ? defaultPort(parts.scheme) : static_cast(portValue); + + const std::string_view path = uri.substr(0, uri.find_first_of("?#")); + parts.path = removeDotSegments(path); + uri.remove_prefix(path.size()); + + if (uri.starts_with('?')) { + parts.query = uri.substr(0, uri.find('#')); + uri.remove_prefix(parts.query.size()); + } + parts.fragment = uri; + return 0; +} + +int copyOut(const std::string& value, char* out, size_t* require, size_t prepare) { + if (require) *require = value.size() + 1; + if (!out) return 0; + if (prepare < value.size() + 1) return ERROR_OUT_OF_MEMORY; + std::memcpy(out, value.c_str(), value.size() + 1); + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const SceHttpUriElement* src_element, uint32_t option) { + if (!src_element) return ERROR_INVALID_URL; + if (!out && !require) return ERROR_INVALID_VALUE; + + auto field = [](const char* value) { return value ? std::string_view(value) : std::string_view(); }; + const std::string_view scheme = field(src_element->scheme); + const std::string_view username = option & URI_BUILD_WITH_USERNAME ? field(src_element->username) : std::string_view(); + const std::string_view password = option & URI_BUILD_WITH_PASSWORD ? field(src_element->password) : std::string_view(); + const std::string_view hostname = option & URI_BUILD_WITH_HOSTNAME ? field(src_element->hostname) : std::string_view(); + + std::string uri; + if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); + if (!src_element->opaque && option & URI_BUILD_WITH_HOSTNAME) uri.append("//"); + uri.append(username); + if (!password.empty()) uri.append(":").append(password); + if (!username.empty() || !password.empty()) uri.push_back('@'); + if (hostname.find(':') != std::string_view::npos) uri.append("[").append(hostname).append("]"); + else uri.append(hostname); + if (option & URI_BUILD_WITH_PORT && src_element->port != 0 && src_element->port != defaultPort(scheme)) { + uri.append(":").append(std::to_string(src_element->port)); + } + if (option & URI_BUILD_WITH_PATH) uri.append(field(src_element->path)); + if (option & URI_BUILD_WITH_QUERY) uri.append(field(src_element->query)); + if (option & URI_BUILD_WITH_FRAGMENT) uri.append(field(src_element->fragment)); + return copyOut(uri, out, require, prepare); +} + +int APS5_VABI sceHttpUriEscape(char* out, size_t* require, size_t prepare, const char* in) { + if (!in) return ERROR_INVALID_VALUE; + if (!out && !require) return ERROR_INVALID_VALUE; + + static constexpr char hex[] = "0123456789ABCDEF"; + std::string escaped; + for (const char* c = in; *c; ++c) { + const auto value = static_cast(*c); + if (std::isalnum(value) || value == '-' || value == '_' || value == '.' || value == '~') { + escaped.push_back(*c); + } else { + escaped.push_back('%'); + escaped.push_back(hex[value >> 4]); + escaped.push_back(hex[value & 0xF]); + } + } + return copyOut(escaped, out, require, prepare); +} + +int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* pool, size_t* require, size_t prepare) { + if (!src_url) return ERROR_INVALID_URL; + const bool write = out && pool; + if (!write && !require) return ERROR_INVALID_VALUE; + + UriParts parts; + if (const int result = parseUri(src_url, parts); result != 0) return result; + + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; + size_t size = 0; + for (const std::string_view field : fields) size += field.size() + 1; + if (require) *require = size; + if (!write) return 0; + if (prepare < size) return ERROR_OUT_OF_MEMORY; + + char* next = static_cast(pool); + char* strings[7]; + for (size_t i = 0; i < 7; ++i) { + strings[i] = next; + std::memcpy(next, fields[i].data(), fields[i].size()); + next[fields[i].size()] = '\0'; + next += fields[i].size() + 1; + } + *out = {}; + out->opaque = parts.opaque; + out->scheme = strings[0]; + out->username = strings[1]; + out->password = strings[2]; + out->hostname = strings[3]; + out->path = strings[4]; + out->query = strings[5]; + out->fragment = strings[6]; + out->port = parts.port; + return 0; +} + +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp new file mode 100644 index 000000000..a772f65fd --- /dev/null +++ b/core/libs/tests/GuestHttp.cpp @@ -0,0 +1,94 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); +int APS5_VABI sceHttpUriBuild(char*, std::size_t*, std::size_t, const SceHttpUriElement*, std::uint32_t); +int APS5_VABI sceHttpUriEscape(char*, std::size_t*, std::size_t, const char*); +int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); +int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); +int APS5_VABI sceHttpReadData(int, void*, std::size_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Equal(const char* left, const char* right) { return std::strcmp(left, right) == 0; } + +int main() { + constexpr int outOfMemory = static_cast(0x80431022); + constexpr int invalidValue = static_cast(0x804311FE); + constexpr int invalidUrl = static_cast(0x80433060); + constexpr int network = static_cast(0x80431063); + constexpr std::uint32_t buildAll = 0xFF; + + const char* url = "https://user:secret@example.com:8443/a/./b/../c?x=1&y=2#top"; + std::size_t required = 0; + Require(sceHttpUriParse(nullptr, url, nullptr, &required, 0) == 0); + Require(required == 6 + 5 + 7 + 12 + 5 + 9 + 5); + + char pool[256]; + SceHttpUriElement element; + Require(sceHttpUriParse(&element, url, pool, nullptr, required - 1) == outOfMemory); + Require(sceHttpUriParse(&element, url, pool, nullptr, required) == 0); + Require(element.opaque == 0); + Require(Equal(element.scheme, "https") && Equal(element.username, "user") && Equal(element.password, "secret")); + Require(Equal(element.hostname, "example.com") && element.port == 8443); + Require(Equal(element.path, "/a/c") && Equal(element.query, "?x=1&y=2") && Equal(element.fragment, "#top")); + + char built[256]; + Require(sceHttpUriBuild(nullptr, &required, 0, &element, buildAll) == 0); + Require(required == std::strlen("https://user:secret@example.com:8443/a/c?x=1&y=2#top") + 1); + Require(sceHttpUriBuild(built, nullptr, required - 1, &element, buildAll) == outOfMemory); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); + Require(Equal(built, "https://user:secret@example.com:8443/a/c?x=1&y=2#top")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x08 | 0x40) == 0); + Require(Equal(built, "/a/c?x=1&y=2")); + + Require(sceHttpUriParse(&element, "HTTP://Example.com/", pool, nullptr, sizeof(pool)) == 0); + Require(element.port == 80 && Equal(element.username, "") && Equal(element.query, "")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); + Require(Equal(built, "HTTP://Example.com/")); + + Require(sceHttpUriParse(&element, "http://[::1]:8080/index.html", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.hostname, "::1") && element.port == 8080 && Equal(element.path, "/index.html")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); + Require(Equal(built, "http://[::1]:8080/index.html")); + + Require(sceHttpUriParse(&element, "mailto:someone@example.com", pool, nullptr, sizeof(pool)) == 0); + Require(element.opaque != 0 && element.port == 0); + Require(Equal(element.username, "someone") && Equal(element.hostname, "example.com")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); + Require(Equal(built, "mailto:someone@example.com")); + + Require(sceHttpUriParse(&element, "/path/only?q", pool, nullptr, sizeof(pool)) == 0); + Require(Equal(element.scheme, "") && Equal(element.hostname, "") && Equal(element.path, "/path/only")); + + Require(sceHttpUriParse(&element, nullptr, pool, nullptr, sizeof(pool)) == invalidUrl); + Require(sceHttpUriParse(nullptr, url, nullptr, nullptr, 0) == invalidValue); + Require(sceHttpUriParse(nullptr, "http://host:65536/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://host:80a/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://[::1/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriParse(nullptr, "http://bad host/", nullptr, &required, 0) == invalidUrl); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), nullptr, buildAll) == invalidUrl); + Require(sceHttpUriBuild(nullptr, nullptr, 0, &element, buildAll) == invalidValue); + + char escaped[64]; + Require(sceHttpUriEscape(nullptr, &required, 0, "a b/~\xC3\xA9") == 0); + Require(required == std::strlen("a%20b%2F~%C3%A9") + 1); + Require(sceHttpUriEscape(escaped, nullptr, required - 1, "a b/~\xC3\xA9") == outOfMemory); + Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), "a b/~\xC3\xA9") == 0); + Require(Equal(escaped, "a%20b%2F~%C3%A9")); + Require(sceHttpUriEscape(escaped, nullptr, sizeof(escaped), nullptr) == invalidValue); + + HttpEpollHandle epoll = nullptr; + Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); + Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); + Require(sceHttpDestroyEpoll(1, epoll) == 0); + + char data[16]; + Require(sceHttpReadData(1, data, sizeof(data)) == network); +}