diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc3906227..5f54dc518 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,7 @@ name: Build and Release on: push: + branches-ignore: [Pre-release-shadPS4-*] paths-ignore: - "documents/**" - "**/*.md" diff --git a/src/common/assert.cpp b/src/common/assert.cpp index bcd3aa054..e96132b2a 100644 --- a/src/common/assert.cpp +++ b/src/common/assert.cpp @@ -3,6 +3,7 @@ #include "common/arch.h" #include "common/assert.h" +#include "emulator.h" #if defined(ARCH_X86_64) #define Crash() __asm__ __volatile__("int $3") @@ -13,13 +14,12 @@ #endif void assert_fail_impl() { - Common::Log::Flush(); + Common::Singleton::Instance()->Shutdown(); Crash(); } [[noreturn]] void unreachable_impl() { - Common::Log::Flush(); - Crash(); + assert_fail_impl(); throw std::runtime_error("Unreachable code"); } diff --git a/src/core/libraries/kernel/file_system.cpp b/src/core/libraries/kernel/file_system.cpp index 1336973f7..95c4e9304 100644 --- a/src/core/libraries/kernel/file_system.cpp +++ b/src/core/libraries/kernel/file_system.cpp @@ -1323,6 +1323,7 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set_posix* readfds, fd_set_posix* wri } if (native_fd == -1) { + LOG_WARNING(Kernel_Fs, "Unsupported fd {}", i); continue; } @@ -1454,6 +1455,7 @@ s32 PS4_SYSV_ABI posix_select(s32 nfds, fd_set* readfds, fd_set* writefds, fd_se } }(); if (native_fd == -1) { + LOG_WARNING(Kernel_Fs, "Unsupported fd {}", i); continue; } host_to_guest.emplace(native_fd, i); diff --git a/src/core/libraries/kernel/threads/exception.cpp b/src/core/libraries/kernel/threads/exception.cpp index fc32ee705..0ac820c86 100644 --- a/src/core/libraries/kernel/threads/exception.cpp +++ b/src/core/libraries/kernel/threads/exception.cpp @@ -548,7 +548,7 @@ int PS4_SYSV_ABI sceKernelRaiseException(PthreadT thread, int signum) { return ret; } -s32 PS4_SYSV_ABI sceKernelDebugRaiseException(s32 error, s64 unk) { +s32 PS4_SYSV_ABI sceKernelDebugRaiseException(u32 error, s64 unk) { if (unk != 0) { return ORBIS_KERNEL_ERROR_EINVAL; } @@ -556,7 +556,7 @@ s32 PS4_SYSV_ABI sceKernelDebugRaiseException(s32 error, s64 unk) { return ORBIS_OK; } -s32 PS4_SYSV_ABI sceKernelDebugRaiseExceptionOnReleaseMode(s32 error, s64 unk) { +s32 PS4_SYSV_ABI sceKernelDebugRaiseExceptionOnReleaseMode(u32 error, s64 unk) { if (unk != 0) { return ORBIS_KERNEL_ERROR_EINVAL; } diff --git a/src/core/libraries/network/http.cpp b/src/core/libraries/network/http.cpp index 6e6961d75..1723bb522 100644 --- a/src/core/libraries/network/http.cpp +++ b/src/core/libraries/network/http.cpp @@ -1,29 +1,40 @@ // SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include #include #include #include +#include #include #include +#include #include #include #include #include #include #include - +#include +#include "common/elf_info.h" #include "common/logging/log.h" +#include "common/path_util.h" #include "core/emulator_settings.h" #include "core/libraries/error_codes.h" #include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/kernel/process.h" #include "core/libraries/libs.h" #include "core/libraries/network/http.h" #include "http_error.h" +#if __has_include() +#include +#define ORBIS_HTTP_WITH_HTTPLIB 1 +#endif + namespace Libraries::Http { enum class HttpRequestState { @@ -37,10 +48,20 @@ struct HttpSettings { u32 connect_timeout_us = 0; u32 send_timeout_us = 0; u32 recv_timeout_us = 0; + u32 resolve_timeout_us = 0; // DNS resolution timeout (sceHttpSetResolveTimeOut) + s32 resolve_retry = 0; // DNS retry count (sceHttpSetResolveRetry) + u64 recv_block_size = 0; // Hint to streaming receiver; 0 = default + u64 response_header_max = 0; // Max response-header bytes accepted; 0 = default bool auto_redirect = true; - bool inflate_gzip = true; + bool inflate_gzip = true; // Auto-decompress response body if gzipped + bool accept_encoding_gzip = true; // Send "Accept-Encoding: gzip" request header u32 ssl_flags = ORBIS_HTTPS_FLAG_SDK_DEFAULT; // SSL flag mask. Bitmask of OrbisHttpsFlags. bool nonblock = false; // false = blocking (default), true = nonblock (EAGAIN) + // (0 = no proxy, 1 = manual host:port, 2 = automatic/PAC). + std::string proxy_host; + u16 proxy_port = 0; + int proxy_http_conf = 0; + int proxy_wlan_conf = 0; }; struct Epoll { @@ -52,12 +73,14 @@ struct Epoll { }; struct HttpTemplate { + int ctx_id = 0; // Owning libhttp context std::string user_agent; int http_version; int auto_proxy_conf; HttpSettings settings; int epoll_id = 0; void* epoll_user_arg = nullptr; + std::vector> headers; }; struct HttpConnection { @@ -72,6 +95,7 @@ struct HttpConnection { HttpSettings settings; int epoll_id = 0; void* epoll_user_arg = nullptr; + std::vector> headers; }; struct HttpResponse { @@ -98,6 +122,7 @@ struct HttpRequest { // notified when state leaves Sending. int epoll_id = 0; void* epoll_user_arg = nullptr; + std::vector> headers; }; struct HttpState { @@ -105,7 +130,13 @@ struct HttpState { bool inited = false; int next_ctx_id = 0; int next_obj_id = 0; + bool default_accept_encoding_gzip = true; // Library-wide default for new templates std::unordered_set active_contexts; + // Contexts where sceHttpsLoadCert was called. We can't actually parse the + // PS4 cert blobs, but we use this as a signal that + // the game expects custom CA validation. Connections under these contexts + // bypass cpp-httplib's default verification so endpoints aren't blocked + std::unordered_set contexts_with_loaded_certs; std::unordered_map templates; std::unordered_map connections; std::unordered_map> requests; @@ -168,6 +199,38 @@ static bool ResolveEpollBinding(int id, int*& epoll_id_out, void**& user_arg_out return false; } +static bool HeaderNameMatches(std::string_view a, std::string_view b); +static std::string HttpStatusLabel(int sc); + +// JSON shape: flat object mapping endpoint -> replacement URL. Example: +// { +// "api.something.dev": "http://localhost:8080", +// "discovery.something.com:5300": "http://localhost:8080", +// "https://api.example.com:443": "http://localhost:8081", +// "*": "http://localhost:8080" +// } +// +// Keys are tried in order of specificity, most-specific first: +// 1. "scheme://host:port" - matches that exact endpoint +// 2. "host:port" - matches host+port on any scheme +// 3. "host" - matches host on any scheme/port (most common) +// 4. "*" - catch-all fallback +// +// Replacement value is a URL with scheme + host + optional port. When port is +// omitted the default for the scheme is used (80 for http, 443 for https). +// When scheme is omitted the connection's original scheme is preserved. +struct HostOverrideTarget { + std::string scheme; // "http", "https", or "" to mean "preserve original" + std::string host; + u16 port = 0; // 0 means "preserve original (or default-for-scheme if scheme changed)" +}; + +// Parse a JSON string into a hostname to target map +std::unordered_map ParseHostOverridesJson( + const std::string& json_text); + +bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure); + // Populate a response object with the shape a transport-level failure produces: // no status line, no headers, no body. Used by the no-internet path. static void SynthesizeTransportFailureResponse(HttpResponse& res) { @@ -179,6 +242,737 @@ static void SynthesizeTransportFailureResponse(HttpResponse& res) { res.all_headers_blob.clear(); } +// Parse a single replacement value into a HostOverrideTarget. Accepts forms: +// "host" preserve scheme, preserve port +// "host:port" preserve scheme, set port +// "http://host" set scheme to http, port defaults to 80 +// "https://host" set scheme to https, port defaults to 443 +// "http://host:port" set scheme + port explicitly +// "https://host:port" set scheme + port explicitly +// Bad ports fall back to port=0 +static HostOverrideTarget ParseHostOverrideTarget(std::string_view value) { + HostOverrideTarget out; + + // Pull off scheme prefix if present. + if (const auto scheme_end = value.find("://"); scheme_end != std::string_view::npos) { + std::string scheme(value.substr(0, scheme_end)); + std::transform(scheme.begin(), scheme.end(), scheme.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (scheme == "http" || scheme == "https") { + out.scheme = std::move(scheme); + } + // Unknown scheme,leave out.scheme empty + value = value.substr(scheme_end + 3); + } + + // Now value is "host" or "host:port". + if (const auto colon = value.find(':'); colon != std::string_view::npos) { + out.host.assign(value.substr(0, colon)); + try { + const unsigned long p = std::stoul(std::string(value.substr(colon + 1))); + if (p > 0 && p <= 65535) { + out.port = static_cast(p); + } + } catch (...) { + // ignore - port stays 0 + } + } else { + out.host.assign(value); + } + return out; +} + +std::unordered_map ParseHostOverridesJson( + const std::string& json_text) { + std::unordered_map out; + if (json_text.empty()) { + return out; + } + nlohmann::json root; + try { + root = nlohmann::json::parse(json_text); + } catch (const std::exception& e) { + LOG_ERROR(Lib_Http, "host overrides JSON parse failed: {}", e.what()); + return out; + } + if (!root.is_object()) { + LOG_ERROR(Lib_Http, "host overrides JSON root must be an object"); + return out; + } + for (auto it = root.begin(); it != root.end(); ++it) { + if (!it.key().empty() && it.key().front() == '_') { + continue; + } + if (!it.value().is_string()) { + LOG_ERROR(Lib_Http, "host overrides JSON: value for '{}' is not a string; skipped", + it.key()); + continue; + } + const std::string value = it.value().get(); + if (value.empty()) { + LOG_ERROR(Lib_Http, "host overrides JSON: value for '{}' is empty; skipped", it.key()); + continue; + } + out.emplace(it.key(), ParseHostOverrideTarget(value)); + } + return out; +} + +struct HostOverrideState { + bool loaded = false; + std::unordered_map entries; +}; + +static HostOverrideState LoadHostOverrideState() { + HostOverrideState s; + s.loaded = true; + std::filesystem::path path; + if (const char* path_env = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); + path_env && path_env[0]) { + // Explicit override path - useful for dev / testing. + path = path_env; + } else { + path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "host_overrides.json"; + } + std::ifstream f(path); + if (!f.is_open()) { + return s; + } + std::stringstream buf; + buf << f.rdbuf(); + s.entries = ParseHostOverridesJson(buf.str()); + LOG_INFO(Lib_Http, "loaded {} host override entries from {}", s.entries.size(), path.string()); + return s; +} + +static const HostOverrideState& GetHostOverrideState() { + static const HostOverrideState s = LoadHostOverrideState(); + return s; +} + +bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure) { + const auto& state = GetHostOverrideState(); + if (state.entries.empty()) { + return false; + } + // Look up most-specific match first. Keys can be: + // "scheme://host:port" - matches that exact endpoint + // "host:port" - matches host+port on any scheme + // "host" - matches host on any scheme/port + // "*" - catch-all fallback + const std::string full_key = scheme + "://" + host + ":" + std::to_string(port); + const std::string host_port_key = host + ":" + std::to_string(port); + + auto it = state.entries.find(full_key); + if (it == state.entries.end()) { + it = state.entries.find(host_port_key); + } + if (it == state.entries.end()) { + it = state.entries.find(host); + } + if (it == state.entries.end()) { + it = state.entries.find("*"); + } + if (it == state.entries.end()) { + return false; + } + + const std::string orig_scheme = scheme; + const std::string orig_host = host; + const u16 orig_port = port; + const HostOverrideTarget& target = it->second; + + host = target.host; + + // Scheme handling: explicit scheme in JSON wins; otherwise preserve. + if (!target.scheme.empty()) { + scheme = target.scheme; + is_secure = (target.scheme == "https"); + } + + if (target.port != 0) { + port = target.port; + } else if (!target.scheme.empty() && target.scheme != orig_scheme) { + if (orig_scheme == "https" && port == 443) { + port = 80; + } else if (orig_scheme == "http" && port == 80) { + port = 443; + } + } + + LOG_INFO(Lib_Http, "host override active: {}://{}:{} -> {}://{}:{} (matched key '{}')", + orig_scheme, orig_host, orig_port, scheme, host, port, it->first); + return true; +} + +struct SendRequestPlan { + std::string scheme; + std::string host; + u16 port = 0; + std::string path; // Path + query, e.g. "/utility/time" or "/?x=1" + s32 method = 0; + std::string method_str; // Only used when method == ORBIS_HTTP_METHOD_CUSTOM + std::string content_type; + std::vector body; + std::vector> headers; // Merged tmpl + conn + req + HttpSettings settings; + // True if the request's owning ctx had sceHttpsLoadCert called. In that + // case we bypass TLS verification because we can't load the game's CAs. + bool ctx_has_loaded_certs = false; +}; + +// Extract the path-and-query portion from a full URL +static std::string ExtractPathFromUrl(const std::string& url) { + auto scheme_end = url.find("://"); + if (scheme_end == std::string::npos) { + return "/"; + } + auto authority_start = scheme_end + 3; + auto path_start = url.find('/', authority_start); + if (path_start == std::string::npos) { + return "/"; + } + return url.substr(path_start); +} + +// Map common HTTP method codes to their name. Used for logging only. +static const char* HttpMethodName(s32 method) { + switch (method) { + case ORBIS_HTTP_METHOD_GET: + return "GET"; + case ORBIS_HTTP_METHOD_POST: + return "POST"; + case ORBIS_HTTP_METHOD_HEAD: + return "HEAD"; + case ORBIS_HTTP_METHOD_OPTIONS: + return "OPTIONS"; + case ORBIS_HTTP_METHOD_PUT: + return "PUT"; + case ORBIS_HTTP_METHOD_DELETE: + return "DELETE"; + case ORBIS_HTTP_METHOD_TRACE: + return "TRACE"; + case ORBIS_HTTP_METHOD_CONNECT: + return "CONNECT"; + case ORBIS_HTTP_METHOD_CUSTOM: + return "CUSTOM"; + default: + return "?"; + } +} + +// Dump every effective setting and the merged header list when a request is +// about to be sent. +static void LogSendRequestSettings(const HttpRequest& req, int reqId, u64 body_size) { + const HttpSettings& s = req.settings; + LOG_INFO(Lib_Http, "--- SendRequest dump reqId={} ---", reqId); + const char* method_name = "(unset)"; + if (!req.method_str.empty()) { + method_name = req.method_str.c_str(); + } else { + switch (req.method) { + case ORBIS_HTTP_METHOD_GET: + method_name = "GET"; + break; + case ORBIS_HTTP_METHOD_POST: + method_name = "POST"; + break; + case ORBIS_HTTP_METHOD_HEAD: + method_name = "HEAD"; + break; + case ORBIS_HTTP_METHOD_OPTIONS: + method_name = "OPTIONS"; + break; + case ORBIS_HTTP_METHOD_PUT: + method_name = "PUT"; + break; + case ORBIS_HTTP_METHOD_DELETE: + method_name = "DELETE"; + break; + case ORBIS_HTTP_METHOD_TRACE: + method_name = "TRACE"; + break; + case ORBIS_HTTP_METHOD_CONNECT: + method_name = "CONNECT"; + break; + case ORBIS_HTTP_METHOD_CUSTOM: + method_name = "CUSTOM"; + break; + default: + break; + } + } + LOG_INFO(Lib_Http, " method={} url={} body_size={}", method_name, + req.url.empty() ? "(unset)" : req.url.c_str(), body_size); + + // Resolve the owning connection + template (for full URL context and + // header inheritance dump). + const HttpConnection* conn = nullptr; + const HttpTemplate* tmpl = nullptr; + if (auto it = g_state.connections.find(req.conn_id); it != g_state.connections.end()) { + conn = &it->second; + if (auto tit = g_state.templates.find(conn->tmpl_id); tit != g_state.templates.end()) { + tmpl = &tit->second; + } + } + if (conn) { + LOG_INFO(Lib_Http, " connection: {}://{}:{} keep_alive={} secure={}", conn->scheme, + conn->hostname, conn->port, conn->keep_alive, conn->is_secure); + } + if (tmpl) { + LOG_INFO(Lib_Http, " template: ua=\"{}\" http_ver={} auto_proxy_conf={}", tmpl->user_agent, + tmpl->http_version, tmpl->auto_proxy_conf); + } + + // Timeouts and basic flags + LOG_INFO(Lib_Http, " timeouts: connect={}us send={}us recv={}us resolve={}us resolve_retry={}", + s.connect_timeout_us, s.send_timeout_us, s.recv_timeout_us, s.resolve_timeout_us, + s.resolve_retry); + LOG_INFO(Lib_Http, + " flags: auto_redirect={} inflate_gzip={} accept_encoding_gzip={} nonblock={}", + s.auto_redirect, s.inflate_gzip, s.accept_encoding_gzip, s.nonblock); + LOG_INFO(Lib_Http, " buffers: recv_block_size={} response_header_max={}", s.recv_block_size, + s.response_header_max); + LOG_INFO(Lib_Http, " ssl_flags={:#x}", s.ssl_flags); + if (!s.proxy_host.empty() || s.proxy_http_conf != 0 || s.proxy_wlan_conf != 0) { + LOG_INFO(Lib_Http, " proxy: {}:{} http_conf={} wlan_conf={}", + s.proxy_host.empty() ? "(empty)" : s.proxy_host.c_str(), s.proxy_port, + s.proxy_http_conf, s.proxy_wlan_conf); + } + if (req.epoll_id != 0) { + LOG_INFO(Lib_Http, " epoll: bound to epoll_id={} user_arg={}", req.epoll_id, + req.epoll_user_arg); + } + + // Merged header view: dump tmpl + conn + req lists + auto dump_headers = [&](const char* origin, + const std::vector>& h) { + if (h.empty()) { + return; + } + for (const auto& [name, value] : h) { + LOG_INFO(Lib_Http, " header[{}] {}: {}", origin, name, value); + } + }; + if (tmpl) { + dump_headers("template", tmpl->headers); + } + if (conn) { + dump_headers("connection", conn->headers); + } + dump_headers("request", req.headers); + LOG_INFO(Lib_Http, "--- end dump reqId={} ---", reqId); +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#define ORBIS_HTTP_HAS_HTTPS 1 +#else +#define ORBIS_HTTP_HAS_HTTPS 0 +#endif + +#ifdef ORBIS_HTTP_WITH_HTTPLIB +static s32 TranslateHttplibError(httplib::Error err) { + using E = httplib::Error; + switch (err) { + case E::Success: + return ORBIS_OK; + // TCP couldn't be established (DNS resolved but the host refused / no + // route / unreachable). Closest firmware code is ResolverEnohost. + case E::Connection: + return ORBIS_HTTP_ERROR_RESOLVER_ENOHOST; + // Timed out at connect time or during the request. + case E::ConnectionTimeout: + case E::Timeout: + return ORBIS_HTTP_ERROR_TIMEOUT; + // TCP set up but the stream broke mid-flight, or peer closed early. + case E::Read: + case E::Write: + case E::ConnectionClosed: + return ORBIS_HTTP_ERROR_BROKEN; + // Anything TLS-related. + case E::SSLConnection: + case E::SSLLoadingCerts: + case E::SSLServerVerification: + case E::SSLServerHostnameVerification: + return ORBIS_HTTP_ERROR_SSL; + // CONNECT-method proxy handshake failed. + case E::ProxyConnection: + return ORBIS_HTTP_ERROR_PROXY; + // User aborted or library cancelled. + case E::Canceled: + return ORBIS_HTTP_ERROR_ABORTED; + // Exceeded our redirect-loop bound + case E::ExceedRedirectCount: + return ORBIS_HTTP_ERROR_NETWORK; + // Internal-bug paths that exist across all cpp-httplib versions. + case E::Unknown: + case E::BindIPAddress: + case E::UnsupportedMultipartBoundaryChars: + case E::Compression: + return ORBIS_HTTP_ERROR_UNKNOWN; + } + return ORBIS_HTTP_ERROR_UNKNOWN; +} +#endif // ORBIS_HTTP_WITH_HTTPLIB + +constexpr int MaxRedirects = 5; + +bool IsFollowableRedirect(int status, s32 method) { + const bool status_in_set = (status >= 300 && status <= 303) || (status == 307); + if (!status_in_set) { + return false; + } + if (method == ORBIS_HTTP_METHOD_POST && status != 303) { + return false; + } + return true; +} + +// 303 changes the new method to GET unless original was HEAD +// Every other followable status preserves the original method. +s32 MethodAfterRedirect(int status, s32 original_method) { + if (status == 303 && original_method != ORBIS_HTTP_METHOD_HEAD) { + return ORBIS_HTTP_METHOD_GET; + } + return original_method; +} + +struct ResolvedRedirect { + std::string scheme; + std::string host; + u16 port; + std::string path; +}; + +// Resolve a Location value relative to the current request's authority. +// Handles absolute URLs and absolute-path-relative forms +std::optional ResolveRedirectLocation(const std::string& current_scheme, + const std::string& current_host, + u16 current_port, + std::string_view location) { + if (location.empty()) { + return std::nullopt; + } + if (const auto scheme_end = location.find("://"); scheme_end != std::string_view::npos) { + ResolvedRedirect out; + out.scheme.assign(location.substr(0, scheme_end)); + std::transform(out.scheme.begin(), out.scheme.end(), out.scheme.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (out.scheme != "http" && out.scheme != "https") { + return std::nullopt; + } + const auto authority_start = scheme_end + 3; + if (authority_start >= location.size()) { + return std::nullopt; + } + const auto path_start = location.find('/', authority_start); + const std::string_view authority = + (path_start == std::string_view::npos) + ? location.substr(authority_start) + : location.substr(authority_start, path_start - authority_start); + if (authority.empty()) { + return std::nullopt; + } + if (const auto colon = authority.find(':'); colon != std::string_view::npos) { + out.host.assign(authority.substr(0, colon)); + try { + const unsigned long p = std::stoul(std::string(authority.substr(colon + 1))); + if (p == 0 || p > 65535) { + return std::nullopt; + } + out.port = static_cast(p); + } catch (...) { + return std::nullopt; + } + } else { + out.host.assign(authority); + out.port = (out.scheme == "https") ? 443 : 80; + } + out.path = + (path_start == std::string_view::npos) ? "/" : std::string(location.substr(path_start)); + return out; + } + if (location[0] == '/') { + ResolvedRedirect out; + out.scheme = current_scheme; + out.host = current_host; + out.port = current_port; + out.path.assign(location); + return out; + } + return std::nullopt; +} + +static s32 RunRealHttpRequest(const SendRequestPlan& plan_in, HttpResponse& out_res, + u32& out_event_bits) { + out_event_bits = 0; +#ifndef ORBIS_HTTP_WITH_HTTPLIB + // Test or no-httplib build: behave like a transport failure. + SynthesizeTransportFailureResponse(out_res); + LOG_INFO(Lib_Http, + "real I/O path requested but httplib not available; " + "falling back to transport failure for {}://{}{}", + plan_in.scheme, plan_in.host, plan_in.path); + return ORBIS_HTTP_ERROR_RESOLVER_ENODNS; +#else + // Mutable copy: PS4-faithful redirect loop rewrites scheme/host/port/path/method. + SendRequestPlan plan = plan_in; + + auto pick_timeout_seconds = [](u32 us, u32 default_s) -> std::chrono::seconds { + if (us == 0) { + return std::chrono::seconds(default_s); + } + u64 secs = (static_cast(us) + 999999ull) / 1000000ull; + if (secs == 0) { + secs = 1; + } + return std::chrono::seconds(secs); + }; + + for (int depth = 0; depth <= MaxRedirects; ++depth) { + if (plan.scheme == "https" && !ORBIS_HTTP_HAS_HTTPS) { + LOG_ERROR(Lib_Http, "HTTPS request but cpp-httplib lacks OpenSSL support"); + SynthesizeTransportFailureResponse(out_res); + return ORBIS_HTTP_ERROR_SSL; + } + + std::string base_url = plan.scheme + "://" + plan.host; + if ((plan.scheme == "https" && plan.port != 443) || + (plan.scheme == "http" && plan.port != 80)) { + base_url += ":" + std::to_string(plan.port); + } + httplib::Client cli(base_url); + cli.set_connection_timeout(pick_timeout_seconds(plan.settings.connect_timeout_us, 30)); + cli.set_read_timeout(pick_timeout_seconds(plan.settings.recv_timeout_us, 120)); + cli.set_write_timeout(pick_timeout_seconds(plan.settings.send_timeout_us, 120)); + + // We always handle redirects manually per PS4 rules + cli.set_follow_location(false); + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + cli.set_decompress(plan.settings.inflate_gzip); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (plan.scheme == "https") { + const bool game_disabled_verify = + (plan.settings.ssl_flags & ORBIS_HTTPS_FLAG_SERVER_VERIFY) == 0; + bool verify_server = !game_disabled_verify; + if (verify_server && plan.ctx_has_loaded_certs) { + verify_server = false; + LOG_INFO(Lib_Http, + "{}://{}: ctx loaded custom CAs (libSceSsl integration absent) -> " + "bypassing cert verification", + plan.scheme, plan.host); + } + cli.enable_server_certificate_verification(verify_server); + if (game_disabled_verify) { + LOG_INFO(Lib_Http, "{}://{}: server cert verification disabled (ssl_flags={:#x})", + plan.scheme, plan.host, plan.settings.ssl_flags); + } + } +#endif + + httplib::Headers headers; + for (const auto& [k, v] : plan.headers) { + headers.emplace(k, v); + } + if (plan.settings.accept_encoding_gzip) { + bool already_set = false; + for (const auto& [k, v] : plan.headers) { + if (HeaderNameMatches(k, "Accept-Encoding")) { + already_set = true; + break; + } + } + if (!already_set) { + headers.emplace("Accept-Encoding", "gzip"); + } + } + + const char* body_ptr = + plan.body.empty() ? "" : reinterpret_cast(plan.body.data()); + const size_t body_size = plan.body.size(); + + auto result = [&]() { + switch (plan.method) { + case ORBIS_HTTP_METHOD_GET: + return cli.Get(plan.path, headers); + case ORBIS_HTTP_METHOD_POST: + return cli.Post(plan.path, headers, body_ptr, body_size, plan.content_type); + case ORBIS_HTTP_METHOD_HEAD: + return cli.Head(plan.path, headers); + case ORBIS_HTTP_METHOD_OPTIONS: + return cli.Options(plan.path, headers); + case ORBIS_HTTP_METHOD_PUT: + return cli.Put(plan.path, headers, body_ptr, body_size, plan.content_type); + case ORBIS_HTTP_METHOD_DELETE: + return cli.Delete(plan.path, headers, body_ptr, body_size, plan.content_type); + case ORBIS_HTTP_METHOD_CUSTOM: { + httplib::Request creq; + creq.method = plan.method_str.empty() ? "GET" : plan.method_str; + creq.path = plan.path; + creq.headers = headers; + if (body_size > 0) { + creq.body.assign(body_ptr, body_size); + } + return cli.send(creq); + } + default: + LOG_ERROR(Lib_Http, "Unsupported method {}; using GET", plan.method); + return cli.Get(plan.path, headers); + } + }(); + + if (!result) { + const int err_val = static_cast(result.error()); + LOG_ERROR(Lib_Http, "cpp-httplib failed for {} {}{}: error={} ({})", + HttpMethodName(plan.method), base_url, plan.path, err_val, + httplib::to_string(static_cast(err_val))); + SynthesizeTransportFailureResponse(out_res); + return TranslateHttplibError(result.error()); + } + + // Populate response (overwrites any prior-iteration 3xx). + out_res.status_code = result->status; + out_res.body.assign(result->body.begin(), result->body.end()); + out_res.content_length = result->body.size(); + out_res.content_length_result = 0; + out_res.read_cursor = 0; + std::string reason; + { + const std::string label = HttpStatusLabel(result->status); + const auto space = label.find(' '); + reason = (space == std::string::npos) ? label : label.substr(space + 1); + } + out_res.all_headers_blob = + "HTTP/1.1 " + std::to_string(result->status) + " " + reason + "\r\n"; + for (const auto& [k, v] : result->headers) { + out_res.all_headers_blob += k + ": " + v + "\r\n"; + } + out_res.all_headers_blob += "\r\n"; + + // -- PS4-faithful redirect decision -- + if (!plan.settings.auto_redirect) { + break; + } + if (depth >= MaxRedirects) { + LOG_INFO(Lib_Http, + "redirect depth limit ({}) reached; returning final response status={}", + MaxRedirects, out_res.status_code); + break; + } + if (!IsFollowableRedirect(out_res.status_code, plan.method)) { + break; + } + + std::string location; + for (const auto& [k, v] : result->headers) { + if (HeaderNameMatches(k, "Location")) { + location = v; + break; + } + } + if (location.empty()) { + LOG_INFO(Lib_Http, "{} response without Location header; returning as-is", + out_res.status_code); + break; + } + auto resolved = ResolveRedirectLocation(plan.scheme, plan.host, plan.port, location); + if (!resolved) { + LOG_INFO(Lib_Http, + "{} response Location='{}' unresolvable (relative or malformed); " + "returning as-is", + out_res.status_code, location); + break; + } + + const int prev_status = out_res.status_code; + const s32 prev_method = plan.method; + const s32 next_method = MethodAfterRedirect(prev_status, prev_method); + const bool host_changed = (resolved->host != plan.host) || (resolved->port != plan.port); + + plan.scheme = std::move(resolved->scheme); + plan.host = std::move(resolved->host); + plan.port = resolved->port; + plan.path = std::move(resolved->path); + plan.method = next_method; + + // 303 + non-HEAD downgrades the method to GET; drop the request body + // and the headers that describe it. + if (prev_status == 303 && prev_method != ORBIS_HTTP_METHOD_HEAD) { + plan.body.clear(); + plan.content_type.clear(); + plan.headers.erase( + std::remove_if(plan.headers.begin(), plan.headers.end(), + [](const auto& kv) { + return HeaderNameMatches(kv.first, "Content-Type") || + HeaderNameMatches(kv.first, "Content-Length"); + }), + plan.headers.end()); + } + + // Cross-host: rewrite any game-supplied Host header + // If the game didn't add one, cpp-httplib auto-generates the correct + // value from the new base_url on the next iteration. + if (host_changed) { + std::string host_value = plan.host; + const bool default_port = (plan.scheme == "https" && plan.port == 443) || + (plan.scheme == "http" && plan.port == 80); + if (!default_port) { + host_value += ":" + std::to_string(plan.port); + } + for (auto& [k, v] : plan.headers) { + if (HeaderNameMatches(k, "Host")) { + v = host_value; + } + } + } + + LOG_INFO(Lib_Http, "redirect: depth={} status={} {} -> {} {}://{}:{}{}", depth, prev_status, + HttpMethodName(prev_method), HttpMethodName(next_method), plan.scheme, plan.host, + plan.port, plan.path); + } + + out_event_bits = + ORBIS_HTTP_NB_EVENT_IN | ORBIS_HTTP_NB_EVENT_OUT | ORBIS_HTTP_NB_EVENT_RESOLVED; + return 0; +#endif // ORBIS_HTTP_WITH_HTTPLIB +} + +// Case-insensitive ASCII comparison of two HTTP header names. +static bool HeaderNameMatches(std::string_view a, std::string_view b) { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (std::tolower(static_cast(a[i])) != + std::tolower(static_cast(b[i]))) { + return false; + } + } + return true; +} + +// Resolve the headers vector for a template/connection/request id. Returns +// nullptr if id is invalid +static std::vector>* ResolveHeaders(int id, + const char*& level) { + if (auto it = g_state.templates.find(id); it != g_state.templates.end()) { + level = "template"; + return &it->second.headers; + } + if (auto it = g_state.connections.find(id); it != g_state.connections.end()) { + level = "connection"; + return &it->second.headers; + } + if (auto it = g_state.requests.find(id); it != g_state.requests.end()) { + level = "request"; + return &it->second->headers; + } + return nullptr; +} + // Map common HTTP status codes to strings for logs. static std::string HttpStatusLabel(int sc) { switch (sc) { @@ -313,12 +1107,6 @@ int PS4_SYSV_ABI sceHttpAddQuery() { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpAddRequestHeader(int id, const char* name, const char* value, s32 mode) { - LOG_INFO(Lib_Http, "(STUBBED) called id={}, name={}, value={}, mode={}", id, - name ? name : "(null)", value ? value : "(null)", mode); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpAddRequestHeaderRaw() { LOG_ERROR(Lib_Http, "(STUBBED) called"); return ORBIS_OK; @@ -387,16 +1175,19 @@ int PS4_SYSV_ABI sceHttpCreateConnection(int tmplId, const char* serverName, con return sc; } - const std::string scheme_str = is_secure ? "https" : "http"; + std::string scheme_str = is_secure ? "https" : "http"; + std::string host_str = serverName; + u16 effective_port = port; + ApplyHostOverride(scheme_str, host_str, effective_port, is_secure); const int conn_id = ++g_state.next_obj_id; HttpConnection conn; conn.tmpl_id = tmplId; conn.scheme = scheme_str; - conn.hostname = serverName; - conn.port = port; + conn.hostname = host_str; + conn.port = effective_port; conn.keep_alive = (isEnableKeepalive != 0); conn.is_secure = is_secure; - conn.url = scheme_str + "://" + serverName + ":" + std::to_string(port); + conn.url = scheme_str + "://" + host_str + ":" + std::to_string(effective_port); if (auto tmpl_it = g_state.templates.find(tmplId); tmpl_it != g_state.templates.end()) { conn.settings = tmpl_it->second.settings; conn.epoll_id = tmpl_it->second.epoll_id; @@ -454,6 +1245,8 @@ int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int tmplId, const char* url, boo scheme_str = is_secure ? "https" : "http"; u16 port = parsed.port; + std::string host_str = parsed.hostname; + ApplyHostOverride(scheme_str, host_str, port, is_secure); std::lock_guard lock(g_state.m_mutex); if (!g_state.inited) { @@ -470,7 +1263,7 @@ int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int tmplId, const char* url, boo conn.tmpl_id = tmplId; conn.url = url; conn.scheme = scheme_str; - conn.hostname = parsed.hostname; + conn.hostname = host_str; conn.port = port; conn.keep_alive = enableKeepalive; conn.is_secure = is_secure; @@ -572,9 +1365,11 @@ int PS4_SYSV_ABI sceHttpCreateTemplate(int libhttpCtxId, const char* userAgent, } const int tmpl_id = ++g_state.next_obj_id; HttpTemplate tmpl; + tmpl.ctx_id = libhttpCtxId; tmpl.user_agent = userAgent ? userAgent : ""; tmpl.http_version = httpVer; tmpl.auto_proxy_conf = isAutoProxyConf; + tmpl.settings.accept_encoding_gzip = g_state.default_accept_encoding_gzip; g_state.templates.emplace(tmpl_id, std::move(tmpl)); LOG_INFO(Lib_Http, "created template tmplId={}", tmpl_id); return tmpl_id; @@ -620,11 +1415,6 @@ int PS4_SYSV_ABI sceHttpDbgShowStat() { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int id, int* isEnable) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, isEnable={}", id, fmt::ptr(isEnable)); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpGetAuthEnabled(int id, int* isEnable) { LOG_ERROR(Lib_Http, "(STUBBED) called id={}, isEnable={}", id, fmt::ptr(isEnable)); return ORBIS_OK; @@ -695,11 +1485,6 @@ int PS4_SYSV_ABI sceHttpRedirectCacheFlush(int libhttpCtxId) { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, name={}", id, name ? name : "(null)"); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpRequestGetAllHeaders() { LOG_ERROR(Lib_Http, "(STUBBED) called"); return ORBIS_OK; @@ -708,6 +1493,7 @@ int PS4_SYSV_ABI sceHttpRequestGetAllHeaders() { int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size) { LOG_INFO(Lib_Http, "called reqId={}, postData={}, size={}", reqId, fmt::ptr(postData), size); std::shared_ptr req_ptr; + SendRequestPlan plan; { std::lock_guard lock(g_state.m_mutex); if (!g_state.inited) { @@ -731,48 +1517,108 @@ int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size) { // Created to Sending. Worker thread will move to Sent. req.state = HttpRequestState::Sending; req_ptr = it->second; + LogSendRequestSettings(req, reqId, size); + + plan.method = req.method; + plan.method_str = req.method_str; + plan.path = ExtractPathFromUrl(req.url); + plan.settings = req.settings; + if (auto conn_it = g_state.connections.find(req.conn_id); + conn_it != g_state.connections.end()) { + plan.scheme = conn_it->second.scheme; + plan.host = conn_it->second.hostname; + plan.port = static_cast(conn_it->second.port); + // Inherit headers in tmpl to conn to req order + if (auto tmpl_it = g_state.templates.find(conn_it->second.tmpl_id); + tmpl_it != g_state.templates.end()) { + plan.headers = tmpl_it->second.headers; + // Check if the owning context loaded custom CAs - if so the + // worker will bypass TLS verification. + plan.ctx_has_loaded_certs = + g_state.contexts_with_loaded_certs.contains(tmpl_it->second.ctx_id); + } + for (const auto& h : conn_it->second.headers) { + plan.headers.push_back(h); + } + } + for (const auto& h : req.headers) { + plan.headers.push_back(h); + } + // Pull Content-Type out of headers + for (const auto& [k, v] : plan.headers) { + if (HeaderNameMatches(k, "Content-Type")) { + plan.content_type = v; + break; + } + } + if (postData && size > 0) { + plan.body.assign(static_cast(postData), + static_cast(postData) + size); + } } - LOG_INFO(Lib_Http, "reqId={} dispatched to async worker [{}]", reqId, - EmulatorSettings.IsConnectedToNetwork() ? "ONLINE (TODO real I/O)" - : "OFFLINE no-internet path"); - std::thread([req_ptr, reqId]() { + const bool online = EmulatorSettings.IsConnectedToNetwork(); + LOG_INFO(Lib_Http, "reqId={} dispatched to async worker [{} {} {}://{}:{}{}]", reqId, + online ? "ONLINE" : "OFFLINE", HttpMethodName(plan.method), plan.scheme, plan.host, + plan.port, plan.path); + + std::thread([req_ptr, reqId, plan = std::move(plan), online]() { HttpResponse local_res; - if (!EmulatorSettings.IsConnectedToNetwork()) { + s32 worker_errno = 0; + u32 success_event_bits = 0; // 0 = no event (offline path uses failure bits) + + if (!online) { SynthesizeTransportFailureResponse(local_res); + worker_errno = ORBIS_HTTP_ERROR_RESOLVER_ENODNS; } else { - // TODO: real network I/O path but for now return the same so switching doesn't affect - // something - SynthesizeTransportFailureResponse(local_res); + worker_errno = RunRealHttpRequest(plan, local_res, success_event_bits); } + std::lock_guard lock(g_state.m_mutex); if (g_state.shutting_down.load() || req_ptr->deleted || req_ptr->state == HttpRequestState::Aborted) { + const char* reason = g_state.shutting_down.load() ? "shutdown" + : req_ptr->deleted ? "deleted" + : "aborted"; + LOG_INFO(Lib_Http, + "reqId={} worker finished but request was {} before completion; " + "dropping result (would have been status={}, errno={:#x})", + reqId, reason, local_res.status_code, static_cast(worker_errno)); req_ptr->cv.notify_all(); return; } req_ptr->res = std::move(local_res); req_ptr->state = HttpRequestState::Sent; - req_ptr->last_errno = ORBIS_HTTP_ERROR_RESOLVER_ENODNS; - LOG_INFO(Lib_Http, "(TRANSPORT FAIL) reqId={} -> 0 (body 0 bytes, errno={:#x})", reqId, - static_cast(req_ptr->last_errno)); + req_ptr->last_errno = worker_errno; + if (worker_errno == 0) { + LOG_INFO(Lib_Http, "(SUCCESS) reqId={} status={} body={} bytes", reqId, + req_ptr->res.status_code, req_ptr->res.body.size()); + } else { + LOG_INFO(Lib_Http, "(TRANSPORT FAIL) reqId={} -> {} (body {} bytes, errno={:#x})", + reqId, req_ptr->res.status_code, req_ptr->res.body.size(), + static_cast(worker_errno)); + } req_ptr->cv.notify_all(); - // If this request is bound to an epoll, push a failure-shaped event so - // sceHttpWaitRequest callers see the completion (with errored bits). + + // Push an epoll event so sceHttpWaitRequest callers see completion. if (req_ptr->epoll_id != 0) { auto epoll_it = g_state.epolls.find(req_ptr->epoll_id); if (epoll_it != g_state.epolls.end() && !epoll_it->second->destroyed) { - constexpr u32 FailureEventBits = - ORBIS_HTTP_NB_EVENT_RESOLVER_ERR | ORBIS_HTTP_NB_EVENT_HUP; + u32 event_bits; + if (worker_errno == 0) { + event_bits = success_event_bits; + } else { + event_bits = ORBIS_HTTP_NB_EVENT_RESOLVER_ERR | ORBIS_HTTP_NB_EVENT_HUP; + } OrbisHttpNBEvent ev{}; - ev.events = FailureEventBits; - ev.eventDetail = FailureEventBits; + ev.events = event_bits; + ev.eventDetail = event_bits; ev.id = reqId; ev.userArg = req_ptr->epoll_user_arg; epoll_it->second->events.push_back(ev); epoll_it->second->cv.notify_all(); - LOG_DEBUG(Lib_Http, "pushed failure epoll event for reqId={} on epoll={}", reqId, - req_ptr->epoll_id); + LOG_DEBUG(Lib_Http, "pushed epoll event for reqId={} on epoll={} bits={:#x}", reqId, + req_ptr->epoll_id, event_bits); } } }).detach(); @@ -780,11 +1626,6 @@ int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size) { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int id, int isEnable) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, isEnable={}", id, isEnable); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpSetAuthEnabled(int id, int isEnable) { LOG_ERROR(Lib_Http, "(STUBBED) called id={}, isEnable={}", id, isEnable); return ORBIS_OK; @@ -841,11 +1682,6 @@ int PS4_SYSV_ABI sceHttpSetCookieTotalMaxSize(int libhttpCtxId, u32 size) { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int libhttpCtxId, int isEnable) { - LOG_ERROR(Lib_Http, "(STUBBED) called libhttpCtxId={}, isEnable={}", libhttpCtxId, isEnable); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpSetDelayBuildRequestEnabled(int id, int isEnable) { LOG_ERROR(Lib_Http, "(STUBBED) called id={}, isEnable={}", id, isEnable); return ORBIS_OK; @@ -871,16 +1707,6 @@ int PS4_SYSV_ABI sceHttpSetPriorityOption() { return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpSetProxy() { - LOG_ERROR(Lib_Http, "(STUBBED) called"); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int id, u32 blockSize) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, blockSize={}", id, blockSize); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpSetRedirectCallback(int id, OrbisHttpRedirectCallback cbfunc, void* userArg) { LOG_ERROR(Lib_Http, "(STUBBED) called id={}, cbfunc={}, userArg={}", id, @@ -895,36 +1721,47 @@ int PS4_SYSV_ABI sceHttpSetRequestStatusCallback(int id, OrbisHttpRequestStatusC return ORBIS_OK; } -int PS4_SYSV_ABI sceHttpSetResolveRetry(int id, int retry) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, retry={}", id, retry); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int id, u32 usec) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, usec={}", id, usec); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int id, u64 headerSize) { - LOG_ERROR(Lib_Http, "(STUBBED) called id={}, headerSize={}", id, headerSize); - return ORBIS_OK; -} - int PS4_SYSV_ABI sceHttpSetSocketCreationCallback() { LOG_ERROR(Lib_Http, "(STUBBED) called"); return ORBIS_OK; } int PS4_SYSV_ABI sceHttpsFreeCaList(int libhttpCtxId, OrbisHttpsCaList* caList) { - LOG_ERROR(Lib_Http, "(STUBBED) called libhttpCtxId={}, caList={}", libhttpCtxId, - fmt::ptr(caList)); + LOG_INFO(Lib_Http, "called libhttpCtxId={}, caList={}", libhttpCtxId, fmt::ptr(caList)); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!g_state.active_contexts.contains(libhttpCtxId)) { + LOG_ERROR(Lib_Http, "Invalid libhttpCtxId={}", libhttpCtxId); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + if (!caList) { + LOG_ERROR(Lib_Http, "caList is null"); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + caList->certsNum = 0; return ORBIS_OK; } int PS4_SYSV_ABI sceHttpsGetCaList(int httpCtxId, OrbisHttpsCaList* list) { LOG_INFO(Lib_Http, "called httpCtxId={}, list={}", httpCtxId, fmt::ptr(list)); - LOG_ERROR(Lib_Http, "(DUMMY) returning empty CA list"); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!g_state.active_contexts.contains(httpCtxId)) { + LOG_ERROR(Lib_Http, "Invalid httpCtxId={}", httpCtxId); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + if (!list) { + LOG_ERROR(Lib_Http, "list output pointer is null"); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } list->certsNum = 0; + LOG_ERROR(Lib_Http, "returning empty CA list (libSceSsl integration not implemented)"); return ORBIS_OK; } @@ -936,9 +1773,28 @@ int PS4_SYSV_ABI sceHttpsGetSslError(int id, int* errNum, u32* detail) { int PS4_SYSV_ABI sceHttpsLoadCert(int libhttpCtxId, int caCertNum, const void** caList, const void* cert, const void* privKey) { + LOG_INFO(Lib_Http, "called libhttpCtxId={}, caCertNum={}, caList={}, cert={}, privKey={}", + libhttpCtxId, caCertNum, fmt::ptr(caList), fmt::ptr(cert), fmt::ptr(privKey)); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!g_state.active_contexts.contains(libhttpCtxId)) { + LOG_ERROR(Lib_Http, "Invalid libhttpCtxId={}", libhttpCtxId); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + // Firmware would hand caList/cert/privKey to libSceSsl, which would parse + // them into an X509_STORE used by the TLS layer. We don't implement that + // pipeline. Instead, we record that this context expects custom CA-based + // verification, and at TLS-handshake time we bypass cpp-httplib's default + // CA verification so the game's private-CA-signed endpoints aren't blocked + // by our system CA store not knowing about them. + g_state.contexts_with_loaded_certs.insert(libhttpCtxId); LOG_ERROR(Lib_Http, - "(STUBBED) called libhttpCtxId={}, caCertNum={}, caList={}, cert={}, privKey={}", - libhttpCtxId, caCertNum, fmt::ptr(caList), fmt::ptr(cert), fmt::ptr(privKey)); + "ctxId={} marked as using custom CAs; subsequent HTTPS requests on this " + "context will bypass cert verification (libSceSsl integration not implemented)", + libhttpCtxId); return ORBIS_OK; } @@ -959,43 +1815,18 @@ int PS4_SYSV_ABI sceHttpsSetSslVersion(int id, int version) { } int PS4_SYSV_ABI sceHttpsUnloadCert(int libhttpCtxId) { - LOG_ERROR(Lib_Http, "(STUBBED) called libhttpCtxId={}", libhttpCtxId); - return ORBIS_OK; -} - -int PS4_SYSV_ABI sceHttpTerm(int libhttpCtxId) { LOG_INFO(Lib_Http, "called libhttpCtxId={}", libhttpCtxId); std::lock_guard lock(g_state.m_mutex); if (!g_state.inited) { LOG_ERROR(Lib_Http, "Not initialized"); return ORBIS_HTTP_ERROR_BEFORE_INIT; } - if (g_state.active_contexts.erase(libhttpCtxId) == 0) { - LOG_ERROR(Lib_Http, "Invalid or already-terminated ctxId={}", libhttpCtxId); + if (!g_state.active_contexts.contains(libhttpCtxId)) { + LOG_ERROR(Lib_Http, "Invalid libhttpCtxId={}", libhttpCtxId); return ORBIS_HTTP_ERROR_INVALID_ID; } - if (g_state.active_contexts.empty()) { - // Last context torn down - wipe all dependent objects. - LOG_INFO(Lib_Http, "last context terminated, clearing state"); - g_state.shutting_down.store(true); - for (auto& [id, req_ptr] : g_state.requests) { - req_ptr->deleted = true; - req_ptr->state = HttpRequestState::Aborted; - req_ptr->cv.notify_all(); // wake blocked waiters before wiping the map - } - for (auto& [id, epoll_ptr] : g_state.epolls) { - epoll_ptr->destroyed = true; - epoll_ptr->cv.notify_all(); // wake any sceHttpWaitRequest blocker - } - g_state.requests.clear(); - g_state.connections.clear(); - g_state.templates.clear(); - g_state.epolls.clear(); - g_state.inited = false; - } else { - LOG_INFO(Lib_Http, "ctxId={} terminated, {} contexts still active", libhttpCtxId, - g_state.active_contexts.size()); - } + g_state.contexts_with_loaded_certs.erase(libhttpCtxId); + LOG_INFO(Lib_Http, "ctxId={} cleared custom-CA marker", libhttpCtxId); return ORBIS_OK; } @@ -1085,6 +1916,158 @@ int PS4_SYSV_ABI sceHttpUriCopy() { return ORBIS_OK; } +//*********************************** +// Init/Terminate functions +//*********************************** +int PS4_SYSV_ABI sceHttpTerm(int libhttpCtxId) { + LOG_INFO(Lib_Http, "called libhttpCtxId={}", libhttpCtxId); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (g_state.active_contexts.erase(libhttpCtxId) == 0) { + LOG_ERROR(Lib_Http, "Invalid or already-terminated ctxId={}", libhttpCtxId); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + g_state.contexts_with_loaded_certs.erase(libhttpCtxId); + if (g_state.active_contexts.empty()) { + // Last context torn down - wipe all dependent objects. + LOG_INFO(Lib_Http, "last context terminated, clearing state"); + g_state.shutting_down.store(true); + size_t in_flight = 0; + for (auto& [id, req_ptr] : g_state.requests) { + if (req_ptr->state == HttpRequestState::Sending) { + ++in_flight; + } + } + if (in_flight > 0) { + LOG_INFO(Lib_Http, "Term: {} request(s) still in flight,results will be abandoned", + in_flight); + } + for (auto& [id, req_ptr] : g_state.requests) { + req_ptr->deleted = true; + req_ptr->state = HttpRequestState::Aborted; + req_ptr->cv.notify_all(); // wake blocked waiters before wiping the map + } + for (auto& [id, epoll_ptr] : g_state.epolls) { + epoll_ptr->destroyed = true; + epoll_ptr->cv.notify_all(); // wake any sceHttpWaitRequest blocker + } + g_state.requests.clear(); + g_state.connections.clear(); + g_state.templates.clear(); + g_state.epolls.clear(); + g_state.contexts_with_loaded_certs.clear(); + g_state.inited = false; + } else { + LOG_INFO(Lib_Http, "ctxId={} terminated, {} contexts still active", libhttpCtxId, + g_state.active_contexts.size()); + } + return ORBIS_OK; +} + +//*********************************** +// Misc functions +//*********************************** +int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int id, u32 blockSize) { + LOG_INFO(Lib_Http, "called id={}, blockSize={}", id, blockSize); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->recv_block_size = blockSize; + LOG_INFO(Lib_Http, "set {} id={} recv_block_size={}", level, id, blockSize); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpSetProxy(int id, int httpProxyConf, int wlanProxyConf, const char* host, + u16 port) { + LOG_INFO(Lib_Http, "called id={}, httpProxyConf={}, wlanProxyConf={}, host={}, port={}", id, + httpProxyConf, wlanProxyConf, host ? host : "(null)", port); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!host) { + LOG_ERROR(Lib_Http, "host is null"); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->proxy_http_conf = httpProxyConf; + s->proxy_wlan_conf = wlanProxyConf; + s->proxy_host = host; + s->proxy_port = port; + LOG_INFO(Lib_Http, "set {} id={} proxy={}:{} (httpConf={}, wlanConf={})", level, id, host, port, + httpProxyConf, wlanProxyConf); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int id, int* isEnable) { + LOG_INFO(Lib_Http, "called id={}", id); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!isEnable) { + LOG_ERROR(Lib_Http, "isEnable output pointer is null"); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + *isEnable = s->accept_encoding_gzip ? 1 : 0; + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int libhttpCtxId, int isEnable) { + LOG_INFO(Lib_Http, "called libhttpCtxId={}, isEnable={}", libhttpCtxId, isEnable); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + g_state.default_accept_encoding_gzip = (isEnable != 0); + LOG_INFO(Lib_Http, "set library default accept_encoding_gzip={}", + g_state.default_accept_encoding_gzip); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int id, int isEnable) { + LOG_INFO(Lib_Http, "called id={}, isEnable={}", id, isEnable); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->accept_encoding_gzip = (isEnable != 0); + LOG_INFO(Lib_Http, "set {} id={} accept_encoding_gzip={}", level, id, s->accept_encoding_gzip); + return ORBIS_OK; +} + //*********************************** // Non-blocking processing functions //*********************************** @@ -1304,7 +2287,7 @@ int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size) { } auto it = g_state.requests.find(reqId); if (it == g_state.requests.end()) { - LOG_ERROR(Lib_Http, "Invalid reqId={}", reqId); + LOG_DEBUG(Lib_Http, "Invalid reqId={}", reqId); return ORBIS_HTTP_ERROR_INVALID_ID; } auto& req = *it->second; @@ -1368,7 +2351,7 @@ int PS4_SYSV_ABI sceHttpsDisableOption(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_BEFORE_INIT; } if ((sslFlags & ~ORBIS_HTTPS_FLAG_PUBLIC_VALID) != 0) { - LOG_ERROR(Lib_Http, "sslFlags=0x{:x} contains unknown bits 0x{:x}", sslFlags, + LOG_ERROR(Lib_Http, "sslFlags={:#x} contains unknown bits {:#x}", sslFlags, sslFlags & ~ORBIS_HTTPS_FLAG_PUBLIC_VALID); return ORBIS_HTTP_ERROR_INVALID_VALUE; } @@ -1379,7 +2362,7 @@ int PS4_SYSV_ABI sceHttpsDisableOption(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_INVALID_ID; } s->ssl_flags &= ~sslFlags; - LOG_INFO(Lib_Http, "ssl_flags now 0x{:x} at {} level (id={})", s->ssl_flags, level, id); + LOG_INFO(Lib_Http, "ssl_flags now {:#x} at {} level (id={})", s->ssl_flags, level, id); return ORBIS_OK; } @@ -1393,7 +2376,7 @@ int PS4_SYSV_ABI sceHttpsDisableOptionPrivate(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_BEFORE_INIT; } if ((sslFlags & ~ORBIS_HTTPS_FLAG_PRIVATE_VALID) != 0) { - LOG_ERROR(Lib_Http, "sslFlags=0x{:x} contains unknown bits 0x{:x}", sslFlags, + LOG_ERROR(Lib_Http, "sslFlags={:#x} contains unknown bits {:#x}", sslFlags, sslFlags & ~ORBIS_HTTPS_FLAG_PRIVATE_VALID); return ORBIS_HTTP_ERROR_INVALID_VALUE; } @@ -1404,7 +2387,7 @@ int PS4_SYSV_ABI sceHttpsDisableOptionPrivate(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_INVALID_ID; } s->ssl_flags &= ~sslFlags; - LOG_INFO(Lib_Http, "ssl_flags now 0x{:x} at {} level (id={})", s->ssl_flags, level, id); + LOG_INFO(Lib_Http, "ssl_flags now {:#x} at {} level (id={})", s->ssl_flags, level, id); return ORBIS_OK; } @@ -1416,7 +2399,7 @@ int PS4_SYSV_ABI sceHttpsEnableOption(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_BEFORE_INIT; } if ((sslFlags & ~ORBIS_HTTPS_FLAG_PUBLIC_VALID) != 0) { - LOG_ERROR(Lib_Http, "sslFlags=0x{:x} contains unknown bits 0x{:x}", sslFlags, + LOG_ERROR(Lib_Http, "sslFlags={:#x} contains unknown bits {:#x}", sslFlags, sslFlags & ~ORBIS_HTTPS_FLAG_PUBLIC_VALID); return ORBIS_HTTP_ERROR_INVALID_VALUE; } @@ -1427,7 +2410,7 @@ int PS4_SYSV_ABI sceHttpsEnableOption(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_INVALID_ID; } s->ssl_flags |= sslFlags; - LOG_INFO(Lib_Http, "ssl_flags now 0x{:x} at {} level (id={})", s->ssl_flags, level, id); + LOG_INFO(Lib_Http, "ssl_flags now {:#x} at {} level (id={})", s->ssl_flags, level, id); return ORBIS_OK; } @@ -1441,7 +2424,7 @@ int PS4_SYSV_ABI sceHttpsEnableOptionPrivate(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_BEFORE_INIT; } if ((sslFlags & ~ORBIS_HTTPS_FLAG_PRIVATE_VALID) != 0) { - LOG_ERROR(Lib_Http, "sslFlags=0x{:x} contains unknown bits 0x{:x}", sslFlags, + LOG_ERROR(Lib_Http, "sslFlags={:#x} contains unknown bits {:#x}", sslFlags, sslFlags & ~ORBIS_HTTPS_FLAG_PRIVATE_VALID); return ORBIS_HTTP_ERROR_INVALID_VALUE; } @@ -1452,13 +2435,55 @@ int PS4_SYSV_ABI sceHttpsEnableOptionPrivate(int id, u32 sslFlags) { return ORBIS_HTTP_ERROR_INVALID_ID; } s->ssl_flags |= sslFlags; - LOG_INFO(Lib_Http, "ssl_flags now 0x{:x} at {} level (id={})", s->ssl_flags, level, id); + LOG_INFO(Lib_Http, "ssl_flags now {:#x} at {} level (id={})", s->ssl_flags, level, id); return ORBIS_OK; } //*********************************** // Response Information functions //*********************************** +int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int id, u32 usec) { + LOG_INFO(Lib_Http, "called id={}, usec={}", id, usec); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + s32 sdk_ver = Common::ElfInfo::FW_100; + ::Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver); + if (sdk_ver >= Common::ElfInfo::FW_170 && usec <= 999999u) { + LOG_ERROR(Lib_Http, "Invalid usec={} (sdk_ver={:#x})", usec, sdk_ver); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->resolve_timeout_us = usec; + LOG_INFO(Lib_Http, "set {} id={} resolve_timeout_us={}", level, id, usec); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int id, u64 headerSize) { + LOG_INFO(Lib_Http, "called id={}, headerSize={}", id, headerSize); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->response_header_max = headerSize; + LOG_INFO(Lib_Http, "set {} id={} response_header_max={}", level, id, headerSize); + return ORBIS_OK; +} + int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int reqId, char** header, u64* headerSize) { LOG_INFO(Lib_Http, "called reqId={}, header={}, headerSize={}", reqId, fmt::ptr(header), fmt::ptr(headerSize)); @@ -1473,7 +2498,7 @@ int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int reqId, char** header, u64* hea } auto it = g_state.requests.find(reqId); if (it == g_state.requests.end()) { - LOG_ERROR(Lib_Http, "Invalid reqId={}", reqId); + LOG_DEBUG(Lib_Http, "Invalid reqId={}", reqId); return ORBIS_HTTP_ERROR_INVALID_ID; } auto& req = *it->second; @@ -1489,9 +2514,11 @@ int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int reqId, char** header, u64* hea if (req.res.all_headers_blob.empty()) { *header = nullptr; *headerSize = 0; + LOG_INFO(Lib_Http, "reqId={} returning empty headers blob", reqId); } else { *header = const_cast(req.res.all_headers_blob.c_str()); *headerSize = req.res.all_headers_blob.size(); + LOG_INFO(Lib_Http, "reqId={} returning {} bytes of headers", reqId, *headerSize); } return ORBIS_OK; } @@ -1510,7 +2537,7 @@ int PS4_SYSV_ABI sceHttpGetResponseContentLength(int reqId, int* result, u64* co } auto it = g_state.requests.find(reqId); if (it == g_state.requests.end()) { - LOG_ERROR(Lib_Http, "Invalid reqId={}", reqId); + LOG_DEBUG(Lib_Http, "Invalid reqId={}", reqId); return ORBIS_HTTP_ERROR_INVALID_ID; } auto& req = *it->second; @@ -1525,11 +2552,13 @@ int PS4_SYSV_ABI sceHttpGetResponseContentLength(int reqId, int* result, u64* co } *result = req.res.content_length_result; *contentLength = req.res.content_length; + LOG_INFO(Lib_Http, "reqId={} result={:#x} contentLength={}", reqId, static_cast(*result), + *contentLength); return ORBIS_OK; } int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode) { - LOG_INFO(Lib_Http, "called reqId={}", reqId); + LOG_DEBUG(Lib_Http, "called reqId={}", reqId); std::unique_lock lock(g_state.m_mutex); if (!g_state.inited) { LOG_ERROR(Lib_Http, "Not initialized"); @@ -1541,7 +2570,7 @@ int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode) { } auto it = g_state.requests.find(reqId); if (it == g_state.requests.end()) { - LOG_ERROR(Lib_Http, "Invalid reqId={}", reqId); + LOG_DEBUG(Lib_Http, "Invalid reqId={}", reqId); return ORBIS_HTTP_ERROR_INVALID_ID; } auto& req = *it->second; @@ -1590,6 +2619,72 @@ int PS4_SYSV_ABI sceHttpSetInflateGZIPEnabled(int id, int isEnable) { //*********************************** // Http Header setting functions //*********************************** +int PS4_SYSV_ABI sceHttpAddRequestHeader(int id, const char* name, const char* value, s32 mode) { + LOG_INFO(Lib_Http, "called id={}, name={}, value={}, mode={}", id, name ? name : "(null)", + value ? value : "(null)", mode); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (mode != ORBIS_HTTP_HEADER_OVERWRITE && mode != ORBIS_HTTP_HEADER_ADD) { + LOG_ERROR(Lib_Http, "Invalid mode={} (must be OVERWRITE=0 or ADD=1)", mode); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + if (!name || !value) { + LOG_ERROR(Lib_Http, "name or value is null"); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + const char* level = ""; + auto* headers = ResolveHeaders(id, level); + if (!headers) { + LOG_ERROR(Lib_Http, "Invalid id={} (not a template, connection, or request)", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + if (mode == ORBIS_HTTP_HEADER_OVERWRITE) { + headers->erase( + std::remove_if(headers->begin(), headers->end(), + [&](const auto& kv) { return HeaderNameMatches(kv.first, name); }), + headers->end()); + } + headers->emplace_back(name, value); + LOG_INFO(Lib_Http, "added header at {} id={}: {}: {} (mode={}, total now {})", level, id, name, + value, mode, headers->size()); + return ORBIS_OK; +} + +int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name) { + LOG_INFO(Lib_Http, "called id={}, name={}", id, name ? name : "(null)"); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (!name) { + LOG_ERROR(Lib_Http, "name is null"); + return ORBIS_HTTP_ERROR_NOT_FOUND; + } + const char* level = ""; + auto* headers = ResolveHeaders(id, level); + if (!headers) { + LOG_ERROR(Lib_Http, "Invalid id={} (not a template, connection, or request)", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + // Remove ALL entries with this name (case-insensitive) + auto new_end = std::remove_if(headers->begin(), headers->end(), [&](const auto& kv) { + return HeaderNameMatches(kv.first, name); + }); + if (new_end == headers->end()) { + LOG_INFO(Lib_Http, "no header '{}' found at {} id={}", name, level, id); + return ORBIS_HTTP_ERROR_NOT_FOUND; + } + size_t removed = std::distance(new_end, headers->end()); + headers->erase(new_end, headers->end()); + LOG_INFO(Lib_Http, "removed {} occurrence(s) of '{}' from {} id={} (total now {})", removed, + name, level, id, headers->size()); + return ORBIS_OK; +} + int PS4_SYSV_ABI sceHttpSetRequestContentLength(int id, u64 contentLength) { LOG_INFO(Lib_Http, "called id={}, contentLength={}", id, contentLength); std::lock_guard lock(g_state.m_mutex); @@ -1655,6 +2750,28 @@ int PS4_SYSV_ABI sceHttpSetAutoRedirect(int id, int isEnable) { //*********************************** // Timeout setting functions //*********************************** +int PS4_SYSV_ABI sceHttpSetResolveRetry(int id, int retry) { + LOG_INFO(Lib_Http, "called id={}, retry={}", id, retry); + std::lock_guard lock(g_state.m_mutex); + if (!g_state.inited) { + LOG_ERROR(Lib_Http, "Not initialized"); + return ORBIS_HTTP_ERROR_BEFORE_INIT; + } + if (retry < 0) { + LOG_ERROR(Lib_Http, "Invalid retry={} (must be >= 0)", retry); + return ORBIS_HTTP_ERROR_INVALID_VALUE; + } + const char* level = ""; + HttpSettings* s = ResolveSettings(id, level); + if (!s) { + LOG_ERROR(Lib_Http, "Invalid id={}", id); + return ORBIS_HTTP_ERROR_INVALID_ID; + } + s->resolve_retry = retry; + LOG_INFO(Lib_Http, "set {} id={} resolve_retry={}", level, id, retry); + return ORBIS_OK; +} + int PS4_SYSV_ABI sceHttpSetConnectTimeOut(int id, u32 usec) { LOG_INFO(Lib_Http, "called id={}, usec={}", id, usec); std::lock_guard lock(g_state.m_mutex); @@ -1756,6 +2873,12 @@ int PS4_SYSV_ABI sceHttpDeleteRequest(int reqId) { return ORBIS_HTTP_ERROR_INVALID_ID; } auto req_ptr = it->second; + if (req_ptr->state == HttpRequestState::Created) { + LOG_INFO(Lib_Http, + "reqId={} abandoned before sceHttpSendRequest (state=Created); " + "{} headers, content_length={} were prepared but never transmitted", + reqId, req_ptr->headers.size(), req_ptr->content_length); + } req_ptr->deleted = true; req_ptr->state = HttpRequestState::Aborted; req_ptr->cv.notify_all(); @@ -1903,7 +3026,7 @@ int PS4_SYSV_ABI sceHttpGetLastErrno(int reqId, int* errNum) { } auto it = g_state.requests.find(reqId); if (it == g_state.requests.end()) { - LOG_ERROR(Lib_Http, "Invalid reqId={}", reqId); + LOG_DEBUG(Lib_Http, "Invalid reqId={}", reqId); return ORBIS_HTTP_ERROR_INVALID_ID; } *errNum = it->second->last_errno; @@ -2158,9 +3281,7 @@ int PS4_SYSV_ABI sceHttpParseResponseHeader(const char* header, u64 headerLen, c //*********************************** int PS4_SYSV_ABI sceHttpUriBuild(char* out, u64* require, u64 prepare, const OrbisHttpUriElement* srcElement, u32 option) { - LOG_INFO(Lib_Http, - "sceHttpUriBuild: called out={}, require={}, prepare={}, " - "srcElement={}, option=0x{:x}", + LOG_INFO(Lib_Http, "called out={}, require={}, prepare={}, srcElement={}, option={:#x}", fmt::ptr(out), fmt::ptr(require), prepare, fmt::ptr(srcElement), option); if (srcElement == nullptr) { diff --git a/src/core/libraries/network/http.h b/src/core/libraries/network/http.h index 9931de034..82d692af9 100644 --- a/src/core/libraries/network/http.h +++ b/src/core/libraries/network/http.h @@ -54,6 +54,12 @@ enum OrbisHttpMethod : s32 { ORBIS_HTTP_METHOD_CUSTOM = 8, }; +// mode argument for sceHttpAddRequestHeader. +enum OrbisHttpHeaderModes : s32 { + ORBIS_HTTP_HEADER_OVERWRITE = 0, + ORBIS_HTTP_HEADER_ADD = 1, +}; + enum OrbisUriBuild : s32 { ORBIS_HTTP_URI_BUILD_WITH_SCHEME = 0x01, ORBIS_HTTP_URI_BUILD_WITH_HOSTNAME = 0x02, @@ -141,7 +147,6 @@ using OrbisHttpsCaList = Libraries::Ssl::OrbisSslCaList; int PS4_SYSV_ABI sceHttpAddCookie(int libhttpCtxId, const char* url, const char* cookie, u64 cookieLength); int PS4_SYSV_ABI sceHttpAddQuery(); -int PS4_SYSV_ABI sceHttpAddRequestHeader(int id, const char* name, const char* value, s32 mode); int PS4_SYSV_ABI sceHttpAddRequestHeaderRaw(); int PS4_SYSV_ABI sceHttpAuthCacheExport(); int PS4_SYSV_ABI sceHttpAuthCacheFlush(int libhttpCtxId); @@ -167,7 +172,6 @@ int PS4_SYSV_ABI sceHttpDbgShowConnectionStat(); int PS4_SYSV_ABI sceHttpDbgShowMemoryPoolStat(); int PS4_SYSV_ABI sceHttpDbgShowRequestStat(); int PS4_SYSV_ABI sceHttpDbgShowStat(); -int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int id, int* isEnable); int PS4_SYSV_ABI sceHttpGetAuthEnabled(int id, int* isEnable); int PS4_SYSV_ABI sceHttpGetConnectionStat(); int PS4_SYSV_ABI sceHttpGetCookie(int libhttpCtxId, const char* url, char* cookie, u64* required, @@ -179,10 +183,8 @@ int PS4_SYSV_ABI sceHttpGetMemoryPoolStats(int libhttpCtxId, OrbisHttpMemoryPool int PS4_SYSV_ABI sceHttpGetRegisteredCtxIds(); int PS4_SYSV_ABI sceHttpInit(int libnetMemId, int libsslCtxId, u64 poolSize); int PS4_SYSV_ABI sceHttpRedirectCacheFlush(int libhttpCtxId); -int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name); int PS4_SYSV_ABI sceHttpRequestGetAllHeaders(); int PS4_SYSV_ABI sceHttpSendRequest(int reqId, const void* postData, u64 size); -int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int id, int isEnable); int PS4_SYSV_ABI sceHttpSetAuthEnabled(int id, int isEnable); int PS4_SYSV_ABI sceHttpSetAuthInfoCallback(int id, OrbisHttpAuthInfoCallback cbfunc, void* userArg); @@ -203,15 +205,10 @@ int PS4_SYSV_ABI sceHttpSetEpollId(); int PS4_SYSV_ABI sceHttpSetHttp09Enabled(int id, int isEnable); int PS4_SYSV_ABI sceHttpSetPolicyOption(); int PS4_SYSV_ABI sceHttpSetPriorityOption(); -int PS4_SYSV_ABI sceHttpSetProxy(); -int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int id, u32 blockSize); int PS4_SYSV_ABI sceHttpSetRedirectCallback(int id, OrbisHttpRedirectCallback cbfunc, void* userArg); int PS4_SYSV_ABI sceHttpSetRequestStatusCallback(int id, OrbisHttpRequestStatusCallback cbfunc, void* userArg); -int PS4_SYSV_ABI sceHttpSetResolveRetry(int id, int retry); -int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int id, u32 usec); -int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int id, u64 headerSize); int PS4_SYSV_ABI sceHttpSetSocketCreationCallback(); int PS4_SYSV_ABI sceHttpsFreeCaList(int libhttpCtxId, OrbisHttpsCaList* caList); int PS4_SYSV_ABI sceHttpsGetCaList(int httpCtxId, OrbisHttpsCaList* list); @@ -222,11 +219,23 @@ int PS4_SYSV_ABI sceHttpsSetMinSslVersion(int id, int version); int PS4_SYSV_ABI sceHttpsSetSslCallback(int id, OrbisHttpsCallback cbfunc, void* userArg); int PS4_SYSV_ABI sceHttpsSetSslVersion(int id, int version); int PS4_SYSV_ABI sceHttpsUnloadCert(int libhttpCtxId); -int PS4_SYSV_ABI sceHttpTerm(int libhttpCtxId); int PS4_SYSV_ABI sceHttpWaitRequest(OrbisHttpEpollHandle eh, OrbisHttpNBEvent* nbev, int maxevents, int timeout); int PS4_SYSV_ABI sceHttpUriCopy(); //*********************************** +// Init/Terminate functions +//*********************************** +int PS4_SYSV_ABI sceHttpTerm(int libhttpCtxId); +//*********************************** +// Misc functions +//*********************************** +int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int id, u32 blockSize); +int PS4_SYSV_ABI sceHttpSetProxy(int id, int httpProxyConf, int wlanProxyConf, const char* host, + u16 port); +int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int id, int* isEnable); +int PS4_SYSV_ABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int libhttpCtxId, int isEnable); +int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int id, int isEnable); +//*********************************** // Non-blocking processing functions //*********************************** int PS4_SYSV_ABI sceHttpCreateEpoll(int libhttpCtxId, OrbisHttpEpollHandle* eh); @@ -254,6 +263,8 @@ int PS4_SYSV_ABI sceHttpsEnableOptionPrivate(int id, u32 sslFlags); //*********************************** // Response Information functions //*********************************** +int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int id, u32 usec); +int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int id, u64 headerSize); int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int reqId, char** header, u64* headerSize); int PS4_SYSV_ABI sceHttpGetResponseContentLength(int reqId, int* result, u64* contentLength); int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode); @@ -261,6 +272,8 @@ int PS4_SYSV_ABI sceHttpSetInflateGZIPEnabled(int id, int isEnable); //*********************************** // Http Header setting functions //*********************************** +int PS4_SYSV_ABI sceHttpAddRequestHeader(int id, const char* name, const char* value, s32 mode); +int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name); int PS4_SYSV_ABI sceHttpSetRequestContentLength(int id, u64 contentLength); //*********************************** // Redirection setting functions @@ -270,6 +283,7 @@ int PS4_SYSV_ABI sceHttpSetAutoRedirect(int id, int isEnable); //*********************************** // Timeout settting functions //*********************************** +int PS4_SYSV_ABI sceHttpSetResolveRetry(int id, int retry); int PS4_SYSV_ABI sceHttpSetConnectTimeOut(int id, u32 usec); int PS4_SYSV_ABI sceHttpSetSendTimeOut(int id, u32 usec); int PS4_SYSV_ABI sceHttpSetRecvTimeOut(int id, u32 usec); diff --git a/src/core/libraries/np/np_error.h b/src/core/libraries/np/np_error.h index 9407d8af1..1753aac8f 100644 --- a/src/core/libraries/np/np_error.h +++ b/src/core/libraries/np/np_error.h @@ -5,7 +5,7 @@ #include "core/libraries/error_codes.h" -// Base NP errors (0x80550001 – 0x8055001F) +// Base NP errors (0x80550001 - 0x8055001F) constexpr int ORBIS_NP_ERROR_ALREADY_INITIALIZED = 0x80550001; constexpr int ORBIS_NP_ERROR_NOT_INITIALIZED = 0x80550002; constexpr int ORBIS_NP_ERROR_INVALID_ARGUMENT = 0x80550003; @@ -38,7 +38,7 @@ constexpr int ORBIS_NP_ERROR_CALLBACK_MAX = 0x8055001D; constexpr int ORBIS_NP_ERROR_INVALID_NP_TITLE_ID = 0x8055001E; constexpr int ORBIS_NP_ERROR_ONLINE_ID_CHANGED = 0x8055001F; -// NP Util (0x80550601 – 0x8055060e) +// NP Util (0x80550601 - 0x8055060e) constexpr int ORBIS_NP_UTIL_ERROR_INVALID_ARGUMENT = 0x80550601; constexpr int ORBIS_NP_UTIL_ERROR_INSUFFICIENT = 0x80550602; constexpr int ORBIS_NP_UTIL_ERROR_PARSER_FAILED = 0x80550603; @@ -50,7 +50,7 @@ constexpr int ORBIS_NP_UTIL_ERROR_NOT_MATCH = 0x80550609; constexpr int ORBIS_NP_UTIL_ERROR_INVALID_TITLEID = 0x8055060A; constexpr int ORBIS_NP_UTIL_ERROR_UNKNOWN = 0x8055060E; -// NP Auth errors (0x80550300 – 0x8055040F) +// NP Auth errors (0x80550300 - 0x8055040F) constexpr int ORBIS_NP_AUTH_ERROR_INVALID_ARGUMENT = 0x80550301; constexpr int ORBIS_NP_AUTH_ERROR_INVALID_SIZE = 0x80550302; constexpr int ORBIS_NP_AUTH_ERROR_OUT_OF_MEMORY = 0x80550303; @@ -94,7 +94,7 @@ constexpr int ORBIS_NP_AUTH_ERROR_ACCOUNT_RENEW_ACCOUNT16 = 0x8055044F; constexpr int ORBIS_NP_AUTH_ERROR_SUB_ACCOUNT_RENEW_EULA = 0x8055044F; constexpr int ORBIS_NP_AUTH_ERROR_UNKNOWN = 0x80550480; -// NP Community / Score client errors (0x80550700 – 0x8055071D) +// NP Community / Score client errors (0x80550700 - 0x8055071D) constexpr int ORBIS_NP_COMMUNITY_ERROR_ALREADY_INITIALIZED = 0x80550701; constexpr int ORBIS_NP_COMMUNITY_ERROR_NOT_INITIALIZED = 0x80550702; constexpr int ORBIS_NP_COMMUNITY_ERROR_OUT_OF_MEMORY = 0x80550703; @@ -125,7 +125,7 @@ constexpr int ORBIS_NP_COMMUNITY_ERROR_GHOST_SERVER_RETURN_INVALID_STATUS_CODE = constexpr int ORBIS_NP_COMMUNITY_ERROR_UBS_ONLINE_ID_IN_XML_CREATED_PAST_IS_DIFFERENT_FROM_CURRENT = 0x8055071D; -// NP Community / Score server errors (0x80550800 – 0x805508AB) +// NP Community / Score server errors (0x80550800 - 0x805508AB) constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_BAD_REQUEST = 0x80550801; constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_INVALID_TICKET = 0x80550802; constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_INVALID_SIGNATURE = 0x80550803; @@ -205,7 +205,7 @@ constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_UBS_MAINTENANCE = 0x805508B2; constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_BASIC_BLACKLISTED_USER_ID = 0x805508B3; constexpr int ORBIS_NP_COMMUNITY_SERVER_ERROR_UNSPECIFIED = 0x805508FF; -// NP Matching2 (0x80550c01 – 0x80550d33) +// NP Matching2 (0x80550c01 - 0x80550d33) constexpr int ORBIS_NP_MATCHING2_ERROR_OUT_OF_MEMORY = 0x80550C01; constexpr int ORBIS_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = 0x80550C02; constexpr int ORBIS_NP_MATCHING2_ERROR_NOT_INITIALIZED = 0x80550C03; @@ -307,7 +307,7 @@ constexpr int ORBIS_NP_MATCHING2_SERVER_ERROR_NAT_TYPE_MISMATCH = 0x80550D31; constexpr int ORBIS_NP_MATCHING2_SERVER_ERROR_ROOM_INCONSISTENCY = 0x80550D32; constexpr int ORBIS_NP_MATCHING2_SERVER_ERROR_BLOCKED_USER_IN_ROOM = 0x80550D33; -// NP Matching2 Signaling (0x80550e01 – 0x80550e1a) +// NP Matching2 Signaling (0x80550e01 - 0x80550e1a) constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_NOT_INITIALIZED = 0x80550E01; constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_ALREADY_INITIALIZED = 0x80550E02; constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_OUT_OF_MEMORY = 0x80550E03; @@ -335,7 +335,7 @@ constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_TERMINATED_BY_MYSELF = 0x80550E constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_MATCHING2_PEER_NOT_FOUND = 0x80550E19; constexpr int ORBIS_NP_MATCHING2_SIGNALING_ERROR_OWN_PEER_ADDRESS = 0x80550E1A; -// NP Trophy (0x80551600 – 0x805516c2) +// NP Trophy (0x80551600 - 0x805516c2) constexpr int ORBIS_NP_TROPHY_ERROR_UNKNOWN = 0x80551600; constexpr int ORBIS_NP_TROPHY_ERROR_NOT_INITIALIZED = 0x80551601; constexpr int ORBIS_NP_TROPHY_ERROR_ALREADY_INITIALIZED = 0x80551602; @@ -385,7 +385,7 @@ constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_TOO_BIG = 0x805516 constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_RETRY_COUNT_MAX = 0x80551630; constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND = 0x805516C2; -// NP Bandwidth Test (0x80551f02 – 0x80551f09) +// NP Bandwidth Test (0x80551f02 - 0x80551f09) constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_NOT_INITIALIZED = 0x80551F02; constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_BAD_RESPONSE = 0x80551F03; constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_OUT_OF_MEMORY = 0x80551F04; @@ -395,7 +395,7 @@ constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_CONTEXT_NOT_AVAILABLE = 0x80551F07; constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_ABORTED = 0x80551F08; constexpr int ORBIS_NP_BANDWIDTH_TEST_ERROR_TIMEOUT = 0x80551F09; -// NP Party (0x80552501 – 0x80552516) +// NP Party (0x80552501 - 0x80552516) constexpr int ORBIS_NP_PARTY_ERROR_UNKNOWN = 0x80552501; constexpr int ORBIS_NP_PARTY_ERROR_ALREADY_INITIALIZED = 0x80552502; constexpr int ORBIS_NP_PARTY_ERROR_NOT_INITIALIZED = 0x80552503; @@ -413,7 +413,7 @@ constexpr int ORBIS_NP_PARTY_ERROR_GAME_SESSION_NOT_ENABLED = 0x80552514; constexpr int ORBIS_NP_PARTY_ERROR_INVALID_PARTY_NO_FRIENDS = 0x80552515; constexpr int ORBIS_NP_PARTY_ERROR_INVALID_PARTY_IS_GAME_SESSION = 0x80552516; -// NP Signaling (0x80552701 – 0x8055271b) +// NP Signaling (0x80552701 - 0x8055271b) constexpr int ORBIS_NP_SIGNALING_ERROR_NOT_INITIALIZED = 0x80552701; constexpr int ORBIS_NP_SIGNALING_ERROR_ALREADY_INITIALIZED = 0x80552702; constexpr int ORBIS_NP_SIGNALING_ERROR_OUT_OF_MEMORY = 0x80552703; @@ -442,7 +442,7 @@ constexpr int ORBIS_NP_SIGNALING_ERROR_PROHIBITED_TO_USE = 0x80552719; constexpr int ORBIS_NP_SIGNALING_ERROR_EXCEED_RATE_LIMIT = 0x8055271A; constexpr int ORBIS_NP_SIGNALING_ERROR_OWN_PEER_ADDRESS = 0x8055271B; -// NP WebAPI (0x80552901 – 0x8055291f) +// NP WebAPI (0x80552901 - 0x8055291f) constexpr int ORBIS_NP_WEBAPI_ERROR_OUT_OF_MEMORY = 0x80552901; constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_ARGUMENT = 0x80552902; constexpr int ORBIS_NP_WEBAPI_ERROR_INVALID_LIB_CONTEXT_ID = 0x80552903; @@ -475,7 +475,7 @@ constexpr int ORBIS_NP_WEBAPI_ERROR_EXTD_PUSH_EVENT_CALLBACK_NOT_FOUND = 0x80552 constexpr int ORBIS_NP_WEBAPI_ERROR_AFTER_SEND = 0x8055291E; constexpr int ORBIS_NP_WEBAPI_ERROR_TIMEOUT = 0x8055291F; -// NP In-Game Message (0x80552b01 – 0x80552b09) +// NP In-Game Message (0x80552b01 - 0x80552b09) constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_OUT_OF_MEMORY = 0x80552B01; constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_INVALID_ARGUMENT = 0x80552B02; constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_LIB_CONTEXT_NOT_FOUND = 0x80552B03; @@ -486,13 +486,13 @@ constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_SIGNED_IN_USER_NOT_FOUND = 0x80552B constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_NOT_PREPARED = 0x80552B08; constexpr int ORBIS_NP_IN_GAME_MESSAGE_ERROR_EXCEED_RATE_LIMIT = 0x80552B09; -// NP ID Mapper (0x80553000 – 0x80553003) +// NP ID Mapper (0x80553000 - 0x80553003) constexpr int ORBIS_NP_ID_MAPPER_ERROR_ABORTED = 0x80553000; constexpr int ORBIS_NP_ID_MAPPER_ERROR_ACCOUNT_ID_NOT_FOUND = 0x80553001; constexpr int ORBIS_NP_ID_MAPPER_ERROR_ONLINE_ID_NOT_FOUND = 0x80553002; constexpr int ORBIS_NP_ID_MAPPER_ERROR_NP_ID_NOT_FOUND = 0x80553003; -// NP Data Communication (0x80553200 – 0x80553218) +// NP Data Communication (0x80553200 - 0x80553218) constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_UNKNOWN = 0x80553200; constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_NOT_INITIALIZED = 0x80553201; constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_ALREADY_INITIALIZED = 0x80553202; @@ -519,7 +519,7 @@ constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_SIGNALING_EXCEEDS_MAX = 0x805532 constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_DATA_CHANNEL_EXCEEDS_MAX = 0x80553217; constexpr int ORBIS_NP_DATA_COMMUNICATION_ERROR_INSUFFICIENT_BUFFER = 0x80553218; -// NP Session Signaling (0x80553301 – 0x80553312) +// NP Session Signaling (0x80553301 - 0x80553312) constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_NOT_INITIALIZED = 0x80553301; constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_ALREADY_INITIALIZED = 0x80553302; constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = 0x80553303; @@ -539,7 +539,7 @@ constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_TERMINATED_BY_MYSELF = 0x80553310 constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_TOO_MANY_CONN = 0x80553311; constexpr int ORBIS_NP_SESSION_SIGNALING_ERROR_GROUP_SIGNALING_ALREADY_ACTIVATED = 0x80553312; -// NP WEBAPI2 (0x80553401 – 0x8055341c) +// NP WEBAPI2 (0x80553401 - 0x8055341c) constexpr int ORBIS_NP_WEBAPI2_ERROR_OUT_OF_MEMORY = 0x80553401; constexpr int ORBIS_NP_WEBAPI2_ERROR_INVALID_ARGUMENT = 0x80553402; constexpr int ORBIS_NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID = 0x80553403; @@ -569,7 +569,7 @@ constexpr int ORBIS_NP_WEBAPI2_AFTER_SEND = 0x8055341a; constexpr int ORBIS_NP_WEBAPI2_TIMEOUT = 0x8055341b; constexpr int ORBIS_NP_WEBAPI2_PUSH_CONTEXT_NOT_FOUND = 0x8055341c; -// NP Session Management Client (0x80553600 – 0x8055361e) +// NP Session Management Client (0x80553600 - 0x8055361e) constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_ALREADY_INITIALIZED = 0x80553600; constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_NOT_INITIALIZED = 0x80553601; constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_OUT_OF_MEMORY = 0x80553602; @@ -602,7 +602,7 @@ constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_INVALID_VIEW_NAME = 0x805 constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_INVALID_EVENT = 0x8055361D; constexpr int ORBIS_NP_SESSION_MANAGEMENT_CLIENT_ERROR_INVALID_MEMBER_ID = 0x8055361E; -// NP Session Management Manager (0x80553700 – 0x80553736) +// NP Session Management Manager (0x80553700 - 0x80553736) constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_ALREADY_INITIALIZED = 0x80553700; constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_NOT_INITIALIZED = 0x80553701; constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_OUT_OF_MEMORY = 0x80553702; @@ -661,7 +661,7 @@ constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_BRIDGE_INFO_NOT_FOUND = constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_ITEM_NOT_FOUND = 0x80553735; constexpr int ORBIS_NP_SESSION_MANAGEMENT_MANAGER_ERROR_INVALID_SESSION_CUSTOM_DATA = 0x80553736; -// NP Game Intent (0x80553800 – 0x80553807) +// NP Game Intent (0x80553800 - 0x80553807) constexpr int ORBIS_NP_GAME_INTENT_ERROR_UNKNOWN = 0x80553800; constexpr int ORBIS_NP_GAME_INTENT_ERROR_ALREADY_INITIALIZED = 0x80553801; constexpr int ORBIS_NP_GAME_INTENT_ERROR_NOT_INITIALIZED = 0x80553802; @@ -671,6 +671,6 @@ constexpr int ORBIS_NP_GAME_INTENT_ERROR_INSUFFICIENT_BUFFER = 0x80553805; constexpr int ORBIS_NP_GAME_INTENT_ERROR_INTENT_NOT_FOUND = 0x80553806; constexpr int ORBIS_NP_GAME_INTENT_ERROR_VALUE_NOT_FOUND = 0x80553807; -// NP ASM Client (0x8055a287 – 0x8055a289) +// NP ASM Client (0x8055a287 - 0x8055a289) constexpr int ORBIS_NP_ASM_CLIENT_ERROR_ABORTED = 0x8055A287; constexpr int ORBIS_NP_ASM_CLIENT_ERROR_NP_SERVICE_LAVEL_NOT_MATCH = 0x8055A289; diff --git a/src/core/libraries/np/np_trophy.cpp b/src/core/libraries/np/np_trophy.cpp index 21a4a16f5..758c2060b 100644 --- a/src/core/libraries/np/np_trophy.cpp +++ b/src/core/libraries/np/np_trophy.cpp @@ -125,6 +125,8 @@ struct ContextKeyHash { struct TrophyContext { u32 context_id; + u32 service_label; + u32 user_id; bool registered = false; std::filesystem::path trophy_xml_path; // resolved once at CreateContext std::filesystem::path xml_dir; // .../Xml/ @@ -216,29 +218,11 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context, auto& ctx = contexts_internal[key]; ctx.context_id = *context; - - // Resolve and cache all paths once so callers never recompute them. - std::string np_comm_id; - const auto& trophyMap = Common::ElfInfo::Instance().GetTrophyIndexMap(); - auto it = trophyMap.find(service_label); - if (it != trophyMap.end()) { - np_comm_id = it->second; - } else { - LOG_ERROR(Lib_NpTrophy, "No npCommId found for trophy index/service_label: {}", - service_label); - return ORBIS_NP_TROPHY_ERROR_UNKNOWN; - } - const auto trophy_base = - Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id; - ctx.xml_save_file = - EmulatorSettings.GetHomeDir() / std::to_string(user_id) / "trophy" / (np_comm_id + ".xml"); - ctx.xml_dir = trophy_base / "Xml"; - ctx.icons_dir = trophy_base / "Icons"; - ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage()); + ctx.service_label = service_label; + ctx.user_id = user_id; LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id, service_label); - return ORBIS_OK; } @@ -836,12 +820,34 @@ int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context, ContextKey contextkey = trophy_contexts[contextId]; auto& ctx = contexts_internal[contextkey]; - if (ctx.registered) + if (ctx.registered) { return ORBIS_NP_TROPHY_ERROR_ALREADY_REGISTERED; + } - if (!std::filesystem::exists(ctx.trophy_xml_path)) { - LOG_ERROR(Lib_NpTrophy, "Could not find trophy files."); - // Stub success here to prevent issues specific to missing a trophy key. + // Resolve trophy-related paths using the context's service_label + std::string np_comm_id; + const auto& trophyMap = Common::ElfInfo::Instance().GetTrophyIndexMap(); + auto it = trophyMap.find(ctx.service_label); + if (it != trophyMap.end()) { + // If we have an NP communication ID, prepare proper trophy paths + np_comm_id = it->second; + + const auto trophy_base = + Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id; + ctx.xml_save_file = EmulatorSettings.GetHomeDir() / std::to_string(ctx.user_id) / "trophy" / + (np_comm_id + ".xml"); + ctx.xml_dir = trophy_base / "Xml"; + ctx.icons_dir = trophy_base / "Icons"; + ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage()); + + if (!std::filesystem::exists(ctx.trophy_xml_path)) { + LOG_ERROR(Lib_NpTrophy, "Could not find trophy files."); + // Stub success here to prevent issues specific to missing a trophy key. + } + } else { + LOG_ERROR(Lib_NpTrophy, "No npCommId found for trophy index/service_label: {}", + ctx.service_label); + return ORBIS_NP_UTIL_ERROR_INVALID_TITLEID; } ctx.registered = true; diff --git a/src/core/libraries/pad/pad.cpp b/src/core/libraries/pad/pad.cpp index 6b8ccfcd8..1d2039a73 100644 --- a/src/core/libraries/pad/pad.cpp +++ b/src/core/libraries/pad/pad.cpp @@ -556,18 +556,12 @@ int PS4_SYSV_ABI scePadResetLightBar(s32 handle) { s32 colour_index = u ? u->user_color - 1 : 0; Input::Colour colour{255, 0, 0}; if (colour_index >= 0 && colour_index <= 3) { - static constexpr Input::Colour colours[4]{ - {0, 0, 255}, // blue - {255, 0, 0}, // red - {0, 255, 0}, // green - {255, 0, 255}, // pink - }; - colour = colours[colour_index]; + colour = Input::g_user_colours[colour_index]; } else { LOG_ERROR(Lib_Pad, "Invalid user colour value {} for controller {}, falling back to blue", colour_index, handle); } - controller.SetLightBarRGB(colour.r, colour.g, colour.b); + controller.SetLightBarRGB(colour); return ORBIS_OK; } diff --git a/src/core/signals.cpp b/src/core/signals.cpp index c4db17808..4763c4606 100644 --- a/src/core/signals.cpp +++ b/src/core/signals.cpp @@ -7,6 +7,7 @@ #include "common/signal_context.h" #include "core/libraries/kernel/threads/exception.h" #include "core/signals.h" +#include "emulator.h" #ifdef _WIN32 #include @@ -52,10 +53,6 @@ static LONG WINAPI SignalHandler(EXCEPTION_POINTERS* pExp) noexcept { case DBG_PRINTEXCEPTION_WIDE_C: // Used by OutputDebugString functions. return EXCEPTION_CONTINUE_EXECUTION; - case EXCEPTION_BREAKPOINT: - // This is almost certainly coming from our asserts/unreachables, no need to log it again. - Common::Log::Flush(); - return EXCEPTION_CONTINUE_SEARCH; default: break; } @@ -64,8 +61,11 @@ static LONG WINAPI SignalHandler(EXCEPTION_POINTERS* pExp) noexcept { return EXCEPTION_CONTINUE_EXECUTION; } - LOG_CRITICAL(Debug, "Unhandled Exception code {:#x} at {}", code, address); - Common::Log::Flush(); + // Breakpoints almost certainly come from our asserts/unreachables, no need to log it again. + if (code != EXCEPTION_BREAKPOINT) { + LOG_CRITICAL(Debug, "Unhandled Exception code {:#x} at {}", code, address); + Common::Singleton::Instance()->Shutdown(); + } return EXCEPTION_CONTINUE_SEARCH; } diff --git a/src/emulator.cpp b/src/emulator.cpp index 3f236eecf..4bbfdafff 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -57,6 +57,8 @@ Frontend::WindowSDL* g_window = nullptr; namespace Core { +std::mutex exit_mutex{}; + Emulator::Emulator() { // Initialize NT API functions, set high priority and disable WER #ifdef _WIN32 @@ -68,10 +70,26 @@ Emulator::Emulator() { WSADATA wsaData; WSAStartup(versionWanted, &wsaData); #endif + std::at_quick_exit([]() { Common::Singleton::Instance()->Shutdown(); }); } Emulator::~Emulator() {} +void Emulator::Shutdown() { + static bool exit_done = false; + std::scoped_lock l{exit_mutex}; + if (exit_done) { + return; + } + Common::Log::Flush(); + if (controllers) { + controllers->ResetLightbarColors(); + // need to give SDL time to do this before the runtime exits + std::this_thread::sleep_for(std::chrono::milliseconds{10}); + } + exit_done = true; +} + s32 ReadCompiledSdkVersion(const std::filesystem::path& file) { Core::Loader::Elf elf; elf.Open(file); @@ -102,8 +120,13 @@ std::map ExtractTrophies(const std::filesystem::path& npbind_p } auto np_comm_ids = npbind.GetNpCommIds(); - if (!std::filesystem::exists(trophy_dir) || np_comm_ids.empty()) { - LOG_WARNING(Common_Filesystem, "Cannot extract game trophies"); + if (np_comm_ids.empty()) { + LOG_WARNING(Common_Filesystem, "No NPCommIDs in npbind.dat"); + return trophy_index_map; + } + + if (!std::filesystem::exists(trophy_dir)) { + LOG_WARNING(Common_Filesystem, "Game does not contain a trophy directory"); return trophy_index_map; } @@ -128,8 +151,13 @@ std::map ExtractTrophies(const std::filesystem::path& npbind_p continue; } - // Extract the actual trophies if they're no extracted yet + // Add the relevant trophies to our trophy index map. + // This currently assumes the order of NPCommIDs matches the order of trophies. std::string np_comm_id = np_comm_ids[trophy_index]; + trophy_index_map[trophy_index] = np_comm_id; + LOG_DEBUG(Loader, "Mapped trophy index {} to NPCommID: {}", trophy_index, np_comm_id); + + // Extract the actual trophies if they're no extracted yet const auto& trophy_output_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "trophy" / np_comm_id; if (!std::filesystem::exists(trophy_output_dir)) { @@ -153,11 +181,6 @@ std::map ExtractTrophies(const std::filesystem::path& npbind_p user_trophy_file, discard); } } - - // Add the relevant trophies to our trophy index map. - // This currently assumes the order of NPCommIDs matches the order of trophies. - trophy_index_map[trophy_index] = np_comm_id; - LOG_DEBUG(Loader, "Mapped trophy index {} to NPCommID: {}", trophy_index, np_comm_id); } } return trophy_index_map; diff --git a/src/emulator.h b/src/emulator.h index ab39019e9..3395a6c0d 100644 --- a/src/emulator.h +++ b/src/emulator.h @@ -29,6 +29,7 @@ public: void Run(std::filesystem::path file, std::vector args = {}, std::optional game_folder = {}); void UpdatePlayTime(const std::string& serial); + void Shutdown(); /** * This will kill the current process and launch a new process with the same configuration diff --git a/src/input/controller.cpp b/src/input/controller.cpp index 4042525f8..5cbf01aad 100644 --- a/src/input/controller.cpp +++ b/src/input/controller.cpp @@ -150,7 +150,7 @@ void GameController::UpdateAxisSmoothing() { m_state.UpdateAxisSmoothing(); } -void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { +void GameController::SetLightBarRGB(u8 const r, u8 const g, u8 const b) { if (override_colour.has_value()) { return; } @@ -160,6 +160,10 @@ void GameController::SetLightBarRGB(u8 r, u8 g, u8 b) { } } +void GameController::SetLightBarRGB(Colour const c) { + SetLightBarRGB(c.r, c.g, c.b); +} + Colour GameController::GetLightBarRGB() { return colour; } @@ -170,6 +174,22 @@ void GameController::PollLightColour() { } } +void GameControllers::ResetLightbarColors() { + for (auto& c : controllers) { + auto const* u = UserManagement.GetUserByID(c->user_id); + if (!u || !c->m_sdl_gamepad) { + continue; + } + auto const i = u->user_color - 1; + if (i < 0 || i > 3) { + continue; + } + auto const& col = g_user_colours[i]; + c->override_colour = std::nullopt; + c->SetLightBarRGB(col); + } +} + bool GameController::SetVibration(u8 smallMotor, u8 largeMotor) { if (m_sdl_gamepad != nullptr) { return SDL_RumbleGamepad(m_sdl_gamepad, (smallMotor / 255.0f) * 0xFFFF, diff --git a/src/input/controller.h b/src/input/controller.h index 5b65ad53c..2a2e13524 100644 --- a/src/input/controller.h +++ b/src/input/controller.h @@ -43,6 +43,12 @@ struct TouchpadEntry { struct Colour { u8 r, g, b; }; +static constexpr Input::Colour g_user_colours[4]{ + {0, 0, 255}, // blue + {255, 0, 0}, // red + {0, 255, 0}, // green + {255, 0, 255}, // pink +}; struct State { private: @@ -127,7 +133,8 @@ public: void UpdateGyro(const float gyro[3]); void UpdateAcceleration(const float acceleration[3]); void UpdateAxisSmoothing(); - void SetLightBarRGB(u8 r, u8 g, u8 b); + void SetLightBarRGB(u8 const r, u8 const g, u8 const b); + void SetLightBarRGB(Colour const c); Colour GetLightBarRGB(); void PollLightColour(); bool SetVibration(u8 smallMotor, u8 largeMotor); @@ -205,6 +212,7 @@ public: controllers[i]->SetLightBarRGB(r, g, b); controllers[i]->override_colour = {r, g, b}; } + void ResetLightbarColors(); }; } // namespace Input diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp index 4c880e4a1..6a1eb4d88 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp @@ -236,6 +236,12 @@ spv::ExecutionMode ExecutionMode(AmdGpu::TessellationPartitioning spacing) { return spv::ExecutionMode::SpacingFractionalOdd; case AmdGpu::TessellationPartitioning::FracEven: return spv::ExecutionMode::SpacingFractionalEven; + case AmdGpu::TessellationPartitioning::Pow2: + // Pow2 rounds tessellation factors to the nearest power of 2, which has no + // direct Vulkan equivalent. SpacingEqual (integer) is the closest match. + LOG_WARNING(Render_Vulkan, "Tessellation partitioning Pow2 has no Vulkan equivalent, " + "falling back to SpacingEqual"); + return spv::ExecutionMode::SpacingEqual; default: break; } diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp index 47a87958c..bee05e26c 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_context_get_set.cpp @@ -139,6 +139,10 @@ Id EmitGetAttribute(EmitContext& ctx, IR::Attribute attr, u32 comp, u32 index) { case IR::Attribute::BaryCoordNoPersp: return ctx.OpLoad(ctx.F32[1], ctx.OpAccessChain(ctx.input_f32, ctx.bary_coord_nopersp, ctx.ConstU32(comp))); + case IR::Attribute::BaryCoordNoPerspSample: + return ctx.OpLoad( + ctx.F32[1], + ctx.OpAccessChain(ctx.input_f32, ctx.bary_coord_nopersp_sample, ctx.ConstU32(comp))); default: UNREACHABLE_MSG("Read attribute {}", attr); } diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index 0c2bc2de8..aeba658cb 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -390,6 +390,16 @@ void EmitContext::DefineInputs() { spv::StorageClass::Input); } } + if (info.loads.GetAny(IR::Attribute::BaryCoordNoPerspSample)) { + if (profile.supports_amd_shader_explicit_vertex_parameter) { + bary_coord_nopersp_sample = DefineVariable( + F32[2], spv::BuiltIn::BaryCoordNoPerspSampleAMD, spv::StorageClass::Input); + } else if (profile.supports_fragment_shader_barycentric) { + bary_coord_nopersp_sample = DefineVariable( + F32[3], spv::BuiltIn::BaryCoordNoPerspKHR, spv::StorageClass::Input); + // Decorate(bary_coord_nopersp_sample, spv::Decoration::Sample); + } + } const bool has_clip_distance_inputs = runtime_info.fs_info.clip_distance_emulation; // Clip distances attribute vector is the last in inputs array diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h index 3815f824c..eb4fbd9be 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h @@ -288,6 +288,7 @@ public: Id bary_coord_smooth_centroid{}; Id bary_coord_smooth_sample{}; Id bary_coord_nopersp{}; + Id bary_coord_nopersp_sample{}; struct TextureDefinition { const VectorIds* data_types; diff --git a/src/video_core/amdgpu/pixel_format.h b/src/video_core/amdgpu/pixel_format.h index 69e082edb..963254c10 100644 --- a/src/video_core/amdgpu/pixel_format.h +++ b/src/video_core/amdgpu/pixel_format.h @@ -221,7 +221,16 @@ constexpr NumberFormat RemapNumberFormat(const NumberFormat format, const DataFo } } case NumberFormat::Srgb: - return data_format == DataFormat::FormatBc6 ? NumberFormat::Unorm : format; + switch (data_format) { + case DataFormat::FormatBc4: + case DataFormat::FormatBc5: + case DataFormat::FormatBc6: + // BC4/BC5 store non-color data (single/two-channel, used for normal maps), + // and BC6 is HDR float — none have sRGB Vulkan equivalents. + return NumberFormat::Unorm; + default: + return format; + } case NumberFormat::Uscaled: return NumberFormat::Uint; case NumberFormat::Sscaled: @@ -299,8 +308,16 @@ constexpr NumberConversion MapNumberConversion(const NumberFormat num_fmt, } } case NumberFormat::Srgb: - return data_fmt == DataFormat::FormatBc6 ? NumberConversion::SrgbToNorm - : NumberConversion::None; + switch (data_fmt) { + case DataFormat::FormatBc4: + case DataFormat::FormatBc5: + // BC4/BC5 have no sRGB variant; no conversion needed (treated as Unorm). + return NumberConversion::None; + case DataFormat::FormatBc6: + return NumberConversion::SrgbToNorm; + default: + return NumberConversion::None; + } case NumberFormat::Uscaled: return NumberConversion::UintToUscaled; case NumberFormat::Sscaled: diff --git a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp index 9a631b9a7..0057bf107 100644 --- a/src/video_core/renderer_vulkan/liverpool_to_vk.cpp +++ b/src/video_core/renderer_vulkan/liverpool_to_vk.cpp @@ -35,6 +35,20 @@ vk::StencilOp StencilOp(AmdGpu::StencilFunc op) { return vk::StencilOp::eDecrementAndWrap; case AmdGpu::StencilFunc::ReplaceOp: return vk::StencilOp::eReplace; + case AmdGpu::StencilFunc::Ones: + LOG_WARNING(Render_Vulkan, "Unsupported stencil op {}, using Replace.", + static_cast(op)); + return vk::StencilOp::eReplace; + case AmdGpu::StencilFunc::And: + case AmdGpu::StencilFunc::Or: + case AmdGpu::StencilFunc::Xor: + case AmdGpu::StencilFunc::Nand: + case AmdGpu::StencilFunc::Nor: + case AmdGpu::StencilFunc::Xnor: + // Bitwise stencil operations have no Vulkan equivalent; eKeep is the safest fallback. + LOG_WARNING(Render_Vulkan, "Unsupported bitwise stencil op {}, using Keep.", + static_cast(op)); + return vk::StencilOp::eKeep; default: UNREACHABLE(); return vk::StencilOp::eKeep; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 763782aca..14f580c16 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,6 +25,7 @@ set(SETTINGS_TEST_SOURCES # Stubs that replace dependencies stubs/common_stub.cpp + stubs/core_stub.cpp stubs/scm_rev_stub.cpp stubs/sdl_stub.cpp @@ -115,6 +116,8 @@ set(GCN_TEST_SOURCES stubs/common_stub.cpp stubs/resource_tracking_pass_stub.cpp stubs/scm_rev_stub.cpp + stubs/sdl_stub.cpp + stubs/core_stub.cpp gcn/gcn_test_runner.hpp gcn/gcn_test_runner.cpp @@ -209,7 +212,6 @@ endforeach() set(HTTP_TEST_SOURCES # Under test ${CMAKE_SOURCE_DIR}/src/core/libraries/network/http.cpp - # Required to link RegisterLib's LIB_FUNCTION calls and the logger's # access to EmulatorSettings. ${CMAKE_SOURCE_DIR}/src/core/emulator_settings.cpp @@ -227,11 +229,14 @@ set(HTTP_TEST_SOURCES stubs/scm_rev_stub.cpp stubs/sdl_stub.cpp stubs/loader_stub.cpp + stubs/core_stub.cpp + stubs/kernel_stub.cpp # Tests network/test_http_uri.cpp network/test_http_status_line.cpp network/test_http_parse_response_header.cpp + network/test_http_lifecycle.cpp ) add_executable(shadps4_http_test ${HTTP_TEST_SOURCES}) diff --git a/tests/network/test_http_lifecycle.cpp b/tests/network/test_http_lifecycle.cpp new file mode 100644 index 000000000..0f16c2d85 --- /dev/null +++ b/tests/network/test_http_lifecycle.cpp @@ -0,0 +1,2066 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#include + +#include "common/types.h" +#include "core/libraries/kernel/orbis_error.h" +#include "core/libraries/network/http.h" +#include "core/libraries/network/http_error.h" +#include "tests/stubs/kernel_stub.h" + +#ifndef ORBIS_OK +#define ORBIS_OK 0 +#endif + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpInit(int, int, u64); +int PS4_SYSV_ABI sceHttpTerm(int); +int PS4_SYSV_ABI sceHttpCreateTemplate(int, const char*, int, int); +int PS4_SYSV_ABI sceHttpDeleteTemplate(int); +int PS4_SYSV_ABI sceHttpCreateConnection(int, const char*, const char*, u16, int); +int PS4_SYSV_ABI sceHttpCreateConnectionWithURL(int, const char*, bool); +int PS4_SYSV_ABI sceHttpDeleteConnection(int); +int PS4_SYSV_ABI sceHttpCreateRequest(int, int, const char*, u64); +int PS4_SYSV_ABI sceHttpCreateRequestWithURL(int, s32, const char*, u64); +int PS4_SYSV_ABI sceHttpDeleteRequest(int); +int PS4_SYSV_ABI sceHttpSendRequest(int, const void*, u64); +int PS4_SYSV_ABI sceHttpGetLastErrno(int, int*); +int PS4_SYSV_ABI sceHttpGetStatusCode(int, int*); +int PS4_SYSV_ABI sceHttpReadData(s32, void*, u64); +int PS4_SYSV_ABI sceHttpAbortRequest(int); +int PS4_SYSV_ABI sceHttpGetAllResponseHeaders(int, char**, u64*); +} // namespace Libraries::Http + +using namespace Libraries::Http; + +namespace { + +// State machine + no-internet path.. SendRequest records ENODNS on the request and GetLastErrno +// surfaces it. + +// Drains every active context. Run before each test so previous test failures +// don't poison the static state. +static void DrainState() { + for (int i = 0; i < 1024; ++i) { + if (sceHttpTerm(i) != ORBIS_OK) { + // Either not active or library already torn down + } + } +} + +class HttpLifecycle : public ::testing::Test { +protected: + void SetUp() override { + DrainState(); + } + void TearDown() override { + DrainState(); + } +}; + +// Init returns a strictly-positive context id. +TEST_F(HttpLifecycle, InitReturnsContextId) { + int ctx = sceHttpInit(1, 2, 1024 * 16); + EXPECT_GT(ctx, 0); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); +} + +// Init with poolSize=0 returns EINVAL. +TEST_F(HttpLifecycle, InitZeroPoolReturnsEinval) { + EXPECT_EQ(sceHttpInit(0, 0, 0), static_cast(ORBIS_KERNEL_ERROR_EINVAL)); +} + +// Term before Init returns BEFORE_INIT. +TEST_F(HttpLifecycle, TermBeforeInitFails) { + EXPECT_EQ(sceHttpTerm(1), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Two Inits, two Terms both context IDs are different, both terminate. +TEST_F(HttpLifecycle, MultipleContexts) { + int a = sceHttpInit(0, 0, 4096); + int b = sceHttpInit(0, 0, 4096); + EXPECT_GT(a, 0); + EXPECT_GT(b, 0); + EXPECT_NE(a, b); + EXPECT_EQ(sceHttpTerm(a), ORBIS_OK); + EXPECT_EQ(sceHttpTerm(b), ORBIS_OK); +} + +// Term of an already-terminated context returns INVALID_ID, not BEFORE_INIT. +TEST_F(HttpLifecycle, TermTwiceReturnsInvalidId) { + int a = sceHttpInit(0, 0, 4096); + int b = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpTerm(a), ORBIS_OK); + // The library is still inited via b; another Term of a should now fail + // with INVALID_ID. + EXPECT_EQ(sceHttpTerm(a), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + EXPECT_EQ(sceHttpTerm(b), ORBIS_OK); +} + +// CreateTemplate before Init should return BEFORE_INIT. +TEST_F(HttpLifecycle, CreateTemplateBeforeInit) { + EXPECT_EQ(sceHttpCreateTemplate(1, "UA/1.0", 1, 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// CreateTemplate with bogus context id should return INVALID_ID. +TEST_F(HttpLifecycle, CreateTemplateInvalidCtx) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateTemplate(ctx + 99, "UA/1.0", 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); +} + +// SendRequest twice on the same reqId: first dispatches the worker, second +// is rejected with AFTER_SEND (state is already Sending or Sent). +TEST_F(HttpLifecycle, SendTwiceReturnsAfterSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_AFTER_SEND)); + // Drain the worker so the next test starts clean. + int sc; + sceHttpGetStatusCode(req, &sc); + sceHttpDeleteRequest(req); + sceHttpDeleteConnection(conn); + sceHttpDeleteTemplate(tmpl); + sceHttpTerm(ctx); +} + +// SendRequest on bogus id shoud return INVALID_ID. +TEST_F(HttpLifecycle, SendInvalidRequest) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSendRequest(9999, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// GetLastErrno before send returns 0 (the default, no error recorded yet). +TEST_F(HttpLifecycle, GetLastErrnoBeforeSendIsZero) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + int err = 0xDEADBEEF; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), ORBIS_OK); + EXPECT_EQ(err, 0); + sceHttpDeleteRequest(req); + sceHttpDeleteConnection(conn); + sceHttpDeleteTemplate(tmpl); + sceHttpTerm(ctx); +} + +// GetLastErrno with null output pointer should return INVALID_VALUE. +TEST_F(HttpLifecycle, GetLastErrnoNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpGetLastErrno(req, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Term tears down all dependent objects: a request id from a terminated +// context is no longer valid. +TEST_F(HttpLifecycle, TermTearsDownDependents) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpTerm(ctx), ORBIS_OK); + + // Operations after term must report BEFORE_INIT. + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + int err; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Connection with null server name should return INVALID_VALUE. +TEST_F(HttpLifecycle, CreateConnectionNullServerName) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, nullptr, "http", 80, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Two requests on the same connection get distinct IDs and isolated errnos. +TEST_F(HttpLifecycle, TwoRequestsAreIsolated) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int r1 = sceHttpCreateRequestWithURL(conn, 0, "http://x/a", 0); + int r2 = sceHttpCreateRequestWithURL(conn, 0, "http://x/b", 0); + EXPECT_NE(r1, r2); + + EXPECT_EQ(sceHttpSendRequest(r1, nullptr, 0), ORBIS_OK); + // Wait for r1's worker via a blocking getter (GetStatusCode). + int sc; + sceHttpGetStatusCode(r1, &sc); + // r2 not yet sent + int err1 = 0, err2 = 0; + EXPECT_EQ(sceHttpGetLastErrno(r1, &err1), ORBIS_OK); + EXPECT_EQ(sceHttpGetLastErrno(r2, &err2), ORBIS_OK); + EXPECT_NE(err1, 0); + EXPECT_EQ(err2, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionNullServerNameTakesPriorityOverBadScheme) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, nullptr, "ftp", 80, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionUnknownScheme) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpCreateConnection(tmpl, "example.com", "ftp", 80, 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_SCHEME)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateConnectionPortZeroAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_GT(sceHttpCreateConnection(tmpl, "example.com", "http", 0, 0), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsOptions) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, /*Options=*/3, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsConnect) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, /*Connect=*/7, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestRejectsOutOfRangeMethod) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, 9, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + EXPECT_EQ(sceHttpCreateRequestWithURL(conn, 99, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_UNKNOWN_METHOD)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestAcceptsValidMethods) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + for (int m : {0, 1, 2, 4, 5, 6, 8}) { + int r = sceHttpCreateRequestWithURL(conn, m, "http://example.com/", 0); + EXPECT_GT(r, 0) << "method " << m << " unexpectedly rejected"; + if (r > 0) + sceHttpDeleteRequest(r); + } + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateRequestWithURL2(int, const char*, const char*, u64); +} + +namespace { + +// Verifies the WithURL2 ordering +TEST_F(HttpLifecycle, WithURL2BeforeInitTakesPriorityOverNullMethod) { + // Library uninitialized (DrainState ran via SetUp). + EXPECT_EQ(sceHttpCreateRequestWithURL2(/*connId=*/1, nullptr, "http://x/", 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Initialized but bogus connId + NULL method should return INVALID_ID +TEST_F(HttpLifecycle, WithURL2InvalidIdTakesPriorityOverNullMethod) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateRequestWithURL2(/*connId=*/9999, nullptr, "http://x/", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Initialized + valid connId + NULL method should return INVALID_VALUE +TEST_F(HttpLifecycle, WithURL2NullMethodAfterValidConn) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + EXPECT_EQ(sceHttpCreateRequestWithURL2(conn, nullptr, "http://example.com/", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetAutoRedirect(int id, int isEnable); +int PS4_SYSV_ABI sceHttpGetAutoRedirect(int id, int* isEnable); +int PS4_SYSV_ABI sceHttpSetConnectTimeOut(int id, u32 usec); +} // namespace Libraries::Http + +namespace { + +// Connection snapshots template settings at creation. +TEST_F(HttpLifecycle, ConnectionSnapshotsTemplateAutoRedirectAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + + // Flip the template's default (which is true) to false. + EXPECT_EQ(sceHttpSetAutoRedirect(tmpl, 0), ORBIS_OK); + + // Connection created NOW should snapshot redirect=false. + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + // Flip the template AGAIN to true. The connection should NOT change. + EXPECT_EQ(sceHttpSetAutoRedirect(tmpl, 1), ORBIS_OK); + + int got = 99; + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 0) << "connection should keep snapshot value, not follow template"; + + // And the template's current value is now 1. + EXPECT_EQ(sceHttpGetAutoRedirect(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Request snapshots connection settings at creation. +TEST_F(HttpLifecycle, RequestSnapshotsConnectionSettingsAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + // Default auto_redirect on conn is true (inherited from tmpl default). + int got = -1; + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + // Flip the connection to false BEFORE creating the request. + EXPECT_EQ(sceHttpSetAutoRedirect(conn, 0), ORBIS_OK); + + int req = sceHttpCreateRequestWithURL(conn, /*method=*/0, "http://example.com/", 0); + ASSERT_GT(req, 0); + + // Now flip the connection back to true. Should NOT affect the request. + EXPECT_EQ(sceHttpSetAutoRedirect(conn, 1), ORBIS_OK); + + got = 99; + EXPECT_EQ(sceHttpGetAutoRedirect(req, &got), ORBIS_OK); + EXPECT_EQ(got, 0) << "request should keep snapshot, not follow connection"; + + // Connection's current value is 1. + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Default chain: template default then connection default then request default +// should all be auto_redirect=true +TEST_F(HttpLifecycle, AutoRedirectDefaultsPropagateThroughCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://example.com/", 0); + + int got = -1; + EXPECT_EQ(sceHttpGetAutoRedirect(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetAutoRedirect(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetAutoRedirect(req, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + + sceHttpTerm(ctx); +} + +// Timeouts also snapshot correctly +TEST_F(HttpLifecycle, ConnectTimeoutSnapshotsTemplateValue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + + EXPECT_EQ(sceHttpSetConnectTimeOut(tmpl, 7'000'000), ORBIS_OK); + + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + ASSERT_GT(conn, 0); + + EXPECT_EQ(sceHttpSetConnectTimeOut(tmpl, 99'000'000), ORBIS_OK); + + sceHttpTerm(ctx); +} + +} // namespace + +// Async state machine sanity tests +namespace { + +// After SendRequest, a blocking ReadData waits for the worker, then returns 0 +TEST_F(HttpLifecycle, ReadDataReturnsZeroAfterTransportFailure) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), 0); + sceHttpTerm(ctx); +} + +// ReadData on an unsent request should return BEFORE_SEND (Created state). +TEST_F(HttpLifecycle, ReadDataBeforeSendReturnsBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), + static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +// GetStatusCode on an aborted request should return ABORTED. +TEST_F(HttpLifecycle, GetStatusCodeOnAbortedRequest) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +// AbortRequest on unsent request: state goes to Aborted, subsequent Send +// returns ABORTED. +TEST_F(HttpLifecycle, AbortBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, GetAllResponseHeadersEmptyOnTransportFailure) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + char* hdr = reinterpret_cast(0xdeadbeef); + u64 hdr_size = 999; + EXPECT_EQ(sceHttpGetAllResponseHeaders(req, &hdr, &hdr_size), ORBIS_OK); + EXPECT_EQ(hdr, nullptr); + EXPECT_EQ(hdr_size, 0u); + sceHttpTerm(ctx); +} + +// GetAllResponseHeaders before Send should return BEFORE_SEND. +TEST_F(HttpLifecycle, GetAllResponseHeadersBeforeSend) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + char* hdr = nullptr; + u64 hdr_size = 0; + EXPECT_EQ(sceHttpGetAllResponseHeaders(req, &hdr, &hdr_size), + static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +// Null output pointer should return INVALID_VALUE (init OK, no reqId lookup). +TEST_F(HttpLifecycle, GetAllResponseHeadersNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpGetAllResponseHeaders(0, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, AbortAfterSendIsIdempotent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + int err = 0; + EXPECT_EQ(sceHttpGetLastErrno(req, &err), ORBIS_OK); + // Transport-failed; exact errno depends on build config. Assert non-zero. + EXPECT_NE(err, 0); + + sceHttpTerm(ctx); +} + +// Aborting twice on the same request is a no-op the second time. +TEST_F(HttpLifecycle, AbortTwiceIsIdempotent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + // Second call: idempotent OK. + EXPECT_EQ(sceHttpAbortRequest(req), ORBIS_OK); + + int sc; + EXPECT_EQ(sceHttpGetStatusCode(req, &sc), static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetNonblock(int, int); +int PS4_SYSV_ABI sceHttpGetNonblock(int, int*); +int PS4_SYSV_ABI sceHttpTrySetNonblock(int, int); +int PS4_SYSV_ABI sceHttpTryGetNonblock(int, int*); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, GetNonblockDefaultIsBlocking) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +// Set + Get round-trip. +TEST_F(HttpLifecycle, SetGetNonblockRoundTrip) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpSetNonblock(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpSetNonblock(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +// Try variants delegate to the regular ones. +TEST_F(HttpLifecycle, TryNonblockBehavesAsRegular) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpTrySetNonblock(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpTryGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +// Nonblock accepts template/connection/request IDs. +TEST_F(HttpLifecycle, SetNonblockAtRequestLevel) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSetNonblock(req, 1), ORBIS_OK); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(req, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockSnapshotsAtCreation) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetNonblock(tmpl, 1), ORBIS_OK); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + // Conn should have snapshot value = 1. + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + // Flip template to 0; conn keeps its snapshot of 1. + EXPECT_EQ(sceHttpSetNonblock(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpGetNonblock(conn, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + EXPECT_EQ(sceHttpGetNonblock(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetNonblockInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + int got = -1; + EXPECT_EQ(sceHttpGetNonblock(99999, &got), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// NULL output pointer on Get should return INVALID_VALUE. +TEST_F(HttpLifecycle, GetNonblockNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpGetNonblock(tmpl, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpGetResponseContentLength(int, int*, u64*); +} + +namespace { + +TEST_F(HttpLifecycle, NonblockReadDataAfterCompletionReturnsZero) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking GetStatusCode (default blocking mode). + int sc; + sceHttpGetStatusCode(req, &sc); + // Flip to nonblock - worker is done, nonblock check no longer fires. + sceHttpSetNonblock(req, 1); + char buf[16]; + EXPECT_EQ(sceHttpReadData(req, buf, sizeof(buf)), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockGetStatusCodeAfterCompletionWorks) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking GetStatusCode first (state Sent). + int sc; + sceHttpGetStatusCode(req, &sc); + // Flip to nonblock now; worker is done. + sceHttpSetNonblock(req, 1); + int r = sceHttpGetStatusCode(req, &sc); + // For a transport-failed request, GetStatusCode returns BEFORE_SEND + // (because last_errno != 0 and status_code == 0). + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_BEFORE_SEND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, NonblockGetResponseContentLengthAfterCompletion) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain worker via blocking GetStatusCode (request is still in default + // blocking mode). + int sc; + sceHttpGetStatusCode(req, &sc); + // Now flip to nonblock. The worker is done; subsequent getters should + // succeed (nonblock only affects the Sending state). + sceHttpSetNonblock(req, 1); + int result = -999; + u64 content_length = 999; + int r = sceHttpGetResponseContentLength(req, &result, &content_length); + EXPECT_EQ(r, ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateRequest2(int, const char*, const char*, u64); +int PS4_SYSV_ABI sceHttpCreateRequestWithURL2(int, const char*, const char*, u64); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, CreateRequest2DoesNotDeadlock) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "GET", "/path", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateRequestWithURL2DoesNotDeadlock) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequestWithURL2(conn, "POST", "http://example.com/post", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +// CreateRequest2 with non-standard method routes via CUSTOM slot. +TEST_F(HttpLifecycle, CreateRequest2CustomMethodWorks) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "PATCH", "/resource", 0); + EXPECT_GT(req, 0); + sceHttpTerm(ctx); +} + +// CreateRequest2 NULL method should return INVALID_VALUE +TEST_F(HttpLifecycle, CreateRequest2NullMethod) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, nullptr, "/path", 0); + EXPECT_EQ(req, static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// CreateRequest2 NULL path should return INVALID_VALUE. +TEST_F(HttpLifecycle, CreateRequest2NullPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "example.com", "http", 80, 0); + int req = sceHttpCreateRequest2(conn, "GET", nullptr, 0); + EXPECT_EQ(req, static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +// Epoll lifecycle tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpCreateEpoll(int, OrbisHttpEpollHandle*); +int PS4_SYSV_ABI sceHttpDestroyEpoll(int, OrbisHttpEpollHandle); +int PS4_SYSV_ABI sceHttpSetEpoll(int, OrbisHttpEpollHandle, void*); +int PS4_SYSV_ABI sceHttpGetEpoll(int, OrbisHttpEpollHandle*, void**); +int PS4_SYSV_ABI sceHttpUnsetEpoll(int); +int PS4_SYSV_ABI sceHttpWaitRequest(OrbisHttpEpollHandle, OrbisHttpNBEvent*, int, int); +int PS4_SYSV_ABI sceHttpAbortWaitRequest(OrbisHttpEpollHandle); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, CreateAndDestroyEpoll) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(ctx, &eh), ORBIS_OK); + EXPECT_NE(eh, nullptr); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, eh), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateEpollNullOut) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateEpoll(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, CreateEpollInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(99999, &eh), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, DestroyEpollNullHandle) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, DestroyEpollInvalidHandle) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpDestroyEpoll(ctx, bogus), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetGetEpollRoundTripOnTemplate) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + void* user_arg = &magic; + EXPECT_EQ(sceHttpSetEpoll(tmpl, eh, user_arg), ORBIS_OK); + + OrbisHttpEpollHandle got_eh = nullptr; + void* got_user_arg = nullptr; + EXPECT_EQ(sceHttpGetEpoll(tmpl, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, user_arg); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetEpollInvalidEpollId) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpSetEpoll(tmpl, bogus, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetEpollInvalidOwnerId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + EXPECT_EQ(sceHttpSetEpoll(99999, eh, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, EpollBindingSnapshotsFromTemplateToConnectionToRequest) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + EXPECT_EQ(sceHttpSetEpoll(tmpl, eh, &magic), ORBIS_OK); + + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + OrbisHttpEpollHandle got_eh = nullptr; + void* got_user_arg = nullptr; + EXPECT_EQ(sceHttpGetEpoll(conn, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, &magic); + + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpGetEpoll(req, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, eh); + EXPECT_EQ(got_user_arg, &magic); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnsetEpollIsRequestOnly) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpUnsetEpoll(tmpl), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpUnsetEpoll(conn), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + // Request id works. + EXPECT_EQ(sceHttpUnsetEpoll(req), ORBIS_OK); + // After unset, GetEpoll returns 0 / nullptr. + OrbisHttpEpollHandle got_eh = reinterpret_cast(uintptr_t{0xdead}); + void* got_user_arg = reinterpret_cast(uintptr_t{0xdead}); + EXPECT_EQ(sceHttpGetEpoll(req, &got_eh, &got_user_arg), ORBIS_OK); + EXPECT_EQ(got_eh, nullptr); + EXPECT_EQ(got_user_arg, nullptr); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestInvalidArgs) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + OrbisHttpNBEvent ev{}; + // maxevents <= 0 + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 0, 0), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpWaitRequest(eh, nullptr, 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpWaitRequest(nullptr, &ev, 1, 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestPollEmptyReturnsZero) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + OrbisHttpNBEvent ev{}; + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 1, 0), 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestDrainsWorkerCompletionEvent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + int magic = 0; + sceHttpSetEpoll(tmpl, eh, &magic); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + // Drain via blocking getter so the worker is guaranteed to have run. + int sc; + sceHttpGetStatusCode(req, &sc); + // Now drain the epoll queue. + OrbisHttpNBEvent ev{}; + int count = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(count, 1); + EXPECT_EQ(ev.id, req); + EXPECT_EQ(ev.userArg, &magic); + // No-internet path: failure bits (SOCK_ERR | HUP) are present. + EXPECT_NE(ev.events & 0x00020010u, 0u); // RESOLVER_ERR (0x20000) or HUP (0x10) + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AbortWaitRequestNullHandle) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAbortWaitRequest(nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AbortWaitRequestInvalidHandle) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle bogus = reinterpret_cast(uintptr_t{99999}); + EXPECT_EQ(sceHttpAbortWaitRequest(bogus), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, TermClearsEpolls) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + sceHttpTerm(ctx); + // After Term, using the epoll handle returns BEFORE_INIT. + OrbisHttpNBEvent ev{}; + EXPECT_EQ(sceHttpWaitRequest(eh, &ev, 1, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, AllEpollCallsBeforeInitReturnBeforeInit) { + OrbisHttpEpollHandle eh = nullptr; + OrbisHttpEpollHandle stub = reinterpret_cast(uintptr_t{1}); + OrbisHttpNBEvent ev{}; + void* ua = nullptr; + EXPECT_EQ(sceHttpCreateEpoll(0, &eh), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpDestroyEpoll(0, stub), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpSetEpoll(0, stub, nullptr), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpGetEpoll(0, &eh, &ua), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpUnsetEpoll(0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpWaitRequest(stub, &ev, 1, 0), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); + EXPECT_EQ(sceHttpAbortWaitRequest(stub), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +} // namespace + +namespace { + +TEST_F(HttpLifecycle, CreateEpollBadCtxAndNullOutReturnsInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpCreateEpoll(99999, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// DestroyEpoll: same firmware order. +TEST_F(HttpLifecycle, DestroyEpollBadCtxAndNullEhReturnsInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpDestroyEpoll(99999, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestAfterAbortReturnsAborted) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + EXPECT_EQ(sceHttpAbortWaitRequest(eh), ORBIS_OK); + OrbisHttpNBEvent ev{}; + int r = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, WaitRequestAbortedTakesPriorityOverQueuedEvents) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + OrbisHttpEpollHandle eh = nullptr; + sceHttpCreateEpoll(ctx, &eh); + sceHttpSetEpoll(tmpl, eh, nullptr); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpSendRequest(req, nullptr, 0), ORBIS_OK); + int sc; + sceHttpGetStatusCode(req, &sc); // drain worker (pushes a failure event) + // Abort first, then call WaitRequest. + sceHttpAbortWaitRequest(eh); + OrbisHttpNBEvent ev{}; + int r = sceHttpWaitRequest(eh, &ev, 1, 0); + EXPECT_EQ(r, static_cast(ORBIS_HTTP_ERROR_ABORTED)); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpAddRequestHeader tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpAddRequestHeader(int, const char*, const char*, s32); +} + +namespace { + +// Happy path: add a header on a template id, no errors. +TEST_F(HttpLifecycle, AddRequestHeaderTemplateHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Content-Type", "application/json", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// Headers can be added on connection ids too. +TEST_F(HttpLifecycle, AddRequestHeaderConnectionIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpAddRequestHeader(conn, "X-Custom", "foo", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// And on request ids. +TEST_F(HttpLifecycle, AddRequestHeaderRequestIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + EXPECT_EQ(sceHttpAddRequestHeader(req, "X-Custom", "bar", 1), ORBIS_OK); + sceHttpTerm(ctx); +} + +// BEFORE_INIT when library not inited. +TEST_F(HttpLifecycle, AddRequestHeaderBeforeInit) { + EXPECT_EQ(sceHttpAddRequestHeader(1, "Foo", "bar", 0), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// INVALID_VALUE for mode out of range. +TEST_F(HttpLifecycle, AddRequestHeaderInvalidMode) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Foo", "bar", 2), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "Foo", "bar", -1), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// INVALID_VALUE for null name. +TEST_F(HttpLifecycle, AddRequestHeaderNullName) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, nullptr, "bar", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// INVALID_ID for non-existent id. +TEST_F(HttpLifecycle, AddRequestHeaderInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAddRequestHeader(99999, "Foo", "bar", 0), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AddRequestHeaderModeCheckedBeforeId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpAddRequestHeader(99999, "Foo", "bar", 2), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AddRequestHeaderNullValue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "X-Empty", nullptr, 1), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpRemoveRequestHeader tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int, const char*); +} + +namespace { + +// Add then remove: succeeds. +TEST_F(HttpLifecycle, RemoveRequestHeaderHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpAddRequestHeader(tmpl, "X-Foo", "bar", 1), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Foo"), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +// Remove when the header was never added should return NOT_FOUND. +TEST_F(HttpLifecycle, RemoveRequestHeaderNotPresent) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "Nonexistent"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, RemoveRequestHeaderRemovesAllDuplicates) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v1", 1); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v2", 1); + sceHttpAddRequestHeader(tmpl, "X-Dup", "v3", 1); + sceHttpAddRequestHeader(tmpl, "X-Other", "keep", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Dup"), ORBIS_OK); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Dup"), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "X-Other"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// Case-insensitive match per HTTP semantics. +TEST_F(HttpLifecycle, RemoveRequestHeaderCaseInsensitive) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpAddRequestHeader(tmpl, "Content-Type", "application/json", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, "content-type"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// BEFORE_INIT when library not inited. +TEST_F(HttpLifecycle, RemoveRequestHeaderBeforeInit) { + EXPECT_EQ(sceHttpRemoveRequestHeader(1, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// Null nameshould return NOT_FOUND +TEST_F(HttpLifecycle, RemoveRequestHeaderNullNameReturnsNotFound) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpRemoveRequestHeader(tmpl, nullptr), + static_cast(ORBIS_HTTP_ERROR_NOT_FOUND)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, RemoveRequestHeaderInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpRemoveRequestHeader(99999, "X-Foo"), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Works on connection ids too. +TEST_F(HttpLifecycle, RemoveRequestHeaderConnectionIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + sceHttpAddRequestHeader(conn, "X-Custom", "foo", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(conn, "X-Custom"), ORBIS_OK); + sceHttpTerm(ctx); +} + +// And request ids. +TEST_F(HttpLifecycle, RemoveRequestHeaderRequestIdAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int req = sceHttpCreateRequestWithURL(conn, 0, "http://x/", 0); + sceHttpAddRequestHeader(req, "X-Custom", "bar", 1); + EXPECT_EQ(sceHttpRemoveRequestHeader(req, "X-Custom"), ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetAcceptEncodingGZIPEnabled(int, int); +int PS4_SYSV_ABI sceHttpGetAcceptEncodingGZIPEnabled(int, int*); +int PS4_SYSV_ABI sceHttpSetDefaultAcceptEncodingGZIPEnabled(int, int); +int PS4_SYSV_ABI sceHttpSetResolveTimeOut(int, u32); +int PS4_SYSV_ABI sceHttpSetResolveRetry(int, int); +int PS4_SYSV_ABI sceHttpSetRecvBlockSize(int, u32); +int PS4_SYSV_ABI sceHttpSetResponseHeaderMaxSize(int, u64); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, AcceptEncodingGZIPDefaultIsTrue) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPRoundTrip) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 0), ORBIS_OK); + int got = -1; + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 0); + EXPECT_EQ(sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 1), ORBIS_OK); + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got), ORBIS_OK); + EXPECT_EQ(got, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPGetNullPtr) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpGetAcceptEncodingGZIPEnabled(tmpl, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, AcceptEncodingGZIPSnapshotsToConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpSetAcceptEncodingGZIPEnabled(tmpl, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + int got = -1; + sceHttpGetAcceptEncodingGZIPEnabled(conn, &got); + EXPECT_EQ(got, 0); // inherits from template + sceHttpTerm(ctx); +} + +// --- SetDefault: library global affects future templates --- + +TEST_F(HttpLifecycle, SetDefaultAcceptEncodingGZIPAffectsNewTemplates) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetDefaultAcceptEncodingGZIPEnabled(ctx, 0), ORBIS_OK); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int got = -1; + sceHttpGetAcceptEncodingGZIPEnabled(tmpl, &got); + EXPECT_EQ(got, 0); // picked up the new default + // Flip back so following tests aren't affected by global state. + sceHttpSetDefaultAcceptEncodingGZIPEnabled(ctx, 1); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetDefaultAcceptEncodingGZIPBeforeInit) { + EXPECT_EQ(sceHttpSetDefaultAcceptEncodingGZIPEnabled(1, 1), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +// --- ResolveTimeOut validation --- + +TEST_F(HttpLifecycle, SetResolveTimeOutHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // SDK >= 1.70 requires usec > 999999 per firmware line 16020. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 5000000u), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutMinAccepted) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // 1000000 is one above the firmware threshold (must be > 999999). + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutSmallRejected) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // 999999 fails the strict (usec > 999999) check on SDK >= 1.70. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutBeforeInit) { + EXPECT_EQ(sceHttpSetResolveTimeOut(1, 100), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutOutOfRangeBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + // usec=100 fails the > 999999 check on SDK >= 1.70 before id is looked up. + EXPECT_EQ(sceHttpSetResolveTimeOut(99999, 100u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_PreSDK170_AcceptsSmallUsec) { + Libraries::Kernel::TestSetSdkVersion(0x1600000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 100u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 0u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_AtSDK170_AppliesStrictCheck) { + Libraries::Kernel::TestSetSdkVersion(0x1700000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +TEST_F(HttpLifecycle, SetResolveTimeOut_PreSDK170_OutOfRangeStillAccepted) { + Libraries::Kernel::TestSetSdkVersion(0x1600000); + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +// --- ResolveRetry --- + +TEST_F(HttpLifecycle, SetResolveRetryHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 0), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 3), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, 100), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResolveRetryNegativeRejected) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveRetry(tmpl, -1), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// Firmware ordering: retry < 0 checked BEFORE id lookup. +TEST_F(HttpLifecycle, SetResolveRetryNegativeBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetResolveRetry(99999, -5), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +// --- RecvBlockSize --- + +TEST_F(HttpLifecycle, SetRecvBlockSizeHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetRecvBlockSize(tmpl, 16384), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetRecvBlockSizeInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetRecvBlockSize(99999, 4096), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// --- ResponseHeaderMaxSize --- + +TEST_F(HttpLifecycle, SetResponseHeaderMaxSizeHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResponseHeaderMaxSize(tmpl, 8192), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetResponseHeaderMaxSizeInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetResponseHeaderMaxSize(99999, 8192), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +} // namespace + +TEST_F(HttpLifecycle, SetResolveTimeOutOldSdkSkipsValidation) { + Libraries::Kernel::TestSetSdkVersion(0x1000000); // pre-1.70 + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // Both small and large usec accepted on old SDK. + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 5000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +TEST_F(HttpLifecycle, SetResolveTimeOutNewSdkEnforces) { + Libraries::Kernel::TestSetSdkVersion(0x1700000); // exactly 1.70 + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 999999u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 500000u), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 1000000u), ORBIS_OK); + EXPECT_EQ(sceHttpSetResolveTimeOut(tmpl, 35000000u), ORBIS_OK); + sceHttpTerm(ctx); + Libraries::Kernel::TestResetSdkVersion(); +} + +// sceHttpSetProxy tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpSetProxy(int, int, int, const char*, u16); +} + +namespace { + +TEST_F(HttpLifecycle, SetProxyHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + // mode 1 = manual proxy, host:port specified + EXPECT_EQ(sceHttpSetProxy(tmpl, 1, 0, "proxy.example.com", 8080), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyOnConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + EXPECT_EQ(sceHttpSetProxy(conn, 1, 0, "proxy.example.com", 3128), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyBeforeInit) { + EXPECT_EQ(sceHttpSetProxy(1, 0, 0, "host", 80), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, SetProxyNullHost) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + EXPECT_EQ(sceHttpSetProxy(tmpl, 1, 0, nullptr, 8080), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyNullHostBeforeBadId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetProxy(99999, 1, 0, nullptr, 8080), + static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, SetProxyInvalidId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpSetProxy(99999, 1, 0, "host", 80), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +// Connection inherits proxy from template at creation. +TEST_F(HttpLifecycle, SetProxyInheritedByConnection) { + int ctx = sceHttpInit(0, 0, 4096); + int tmpl = sceHttpCreateTemplate(ctx, "UA", 1, 0); + sceHttpSetProxy(tmpl, 1, 0, "tmpl-proxy.example.com", 9999); + int conn = sceHttpCreateConnection(tmpl, "x", "http", 80, 0); + // Override on connection + EXPECT_EQ(sceHttpSetProxy(conn, 1, 0, "conn-proxy.example.com", 8888), ORBIS_OK); + sceHttpTerm(ctx); +} + +} // namespace + +// sceHttpsLoadCert / sceHttpsUnloadCert tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpsLoadCert(int, int, const void**, const void*, const void*); +int PS4_SYSV_ABI sceHttpsUnloadCert(int); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, LoadCertHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy_cas[2] = {nullptr, nullptr}; + EXPECT_EQ(sceHttpsLoadCert(ctx, 2, dummy_cas, nullptr, nullptr), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, LoadCertBeforeInit) { + EXPECT_EQ(sceHttpsLoadCert(1, 0, nullptr, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, LoadCertInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsLoadCert(99999, 0, nullptr, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy_cas[1] = {nullptr}; + sceHttpsLoadCert(ctx, 1, dummy_cas, nullptr, nullptr); + EXPECT_EQ(sceHttpsUnloadCert(ctx), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertWithoutLoadIsOk) { + int ctx = sceHttpInit(0, 0, 4096); + // Idempotent: erasing from the set returns 0 but we don't surface it as + // an error since context exists. + EXPECT_EQ(sceHttpsUnloadCert(ctx), ORBIS_OK); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, UnloadCertBeforeInit) { + EXPECT_EQ(sceHttpsUnloadCert(1), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, UnloadCertInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsUnloadCert(99999), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, LoadCertSurvivesTermCleanly) { + int ctx = sceHttpInit(0, 0, 4096); + const void* dummy[1] = {nullptr}; + EXPECT_EQ(sceHttpsLoadCert(ctx, 1, dummy, nullptr, nullptr), ORBIS_OK); + sceHttpTerm(ctx); + // After Term, the ctxId is invalid - LoadCert on it must fail. + EXPECT_EQ(sceHttpsLoadCert(ctx, 1, dummy, nullptr, nullptr), + static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +} // namespace + +// sceHttpsGetCaList / sceHttpsFreeCaList tests + +namespace Libraries::Http { +int PS4_SYSV_ABI sceHttpsGetCaList(int httpCtxId, OrbisHttpsCaList* list); +int PS4_SYSV_ABI sceHttpsFreeCaList(int libhttpCtxId, OrbisHttpsCaList* caList); +} // namespace Libraries::Http + +namespace { + +TEST_F(HttpLifecycle, GetCaListHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + list.certsNum = 999; // sentinel; should be reset to 0 + EXPECT_EQ(sceHttpsGetCaList(ctx, &list), ORBIS_OK); + EXPECT_EQ(list.certsNum, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetCaListBeforeInit) { + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsGetCaList(1, &list), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, GetCaListInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsGetCaList(99999, &list), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, GetCaListNullList) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsGetCaList(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListHappyPath) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + list.certsNum = 5; // anything non-zero + EXPECT_EQ(sceHttpsFreeCaList(ctx, &list), ORBIS_OK); + EXPECT_EQ(list.certsNum, 0); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListBeforeInit) { + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsFreeCaList(1, &list), static_cast(ORBIS_HTTP_ERROR_BEFORE_INIT)); +} + +TEST_F(HttpLifecycle, FreeCaListInvalidCtxId) { + int ctx = sceHttpInit(0, 0, 4096); + OrbisHttpsCaList list{}; + EXPECT_EQ(sceHttpsFreeCaList(99999, &list), static_cast(ORBIS_HTTP_ERROR_INVALID_ID)); + sceHttpTerm(ctx); +} + +TEST_F(HttpLifecycle, FreeCaListNullList) { + int ctx = sceHttpInit(0, 0, 4096); + EXPECT_EQ(sceHttpsFreeCaList(ctx, nullptr), static_cast(ORBIS_HTTP_ERROR_INVALID_VALUE)); + sceHttpTerm(ctx); +} + +} // namespace + +namespace Libraries::Http { +bool IsFollowableRedirect(int status, s32 method); +s32 MethodAfterRedirect(int status, s32 original_method); +struct ResolvedRedirect { + std::string scheme; + std::string host; + u16 port; + std::string path; +}; +std::optional ResolveRedirectLocation(const std::string& current_scheme, + const std::string& current_host, + u16 current_port, + std::string_view location); +} // namespace Libraries::Http + +namespace { + +using Libraries::Http::IsFollowableRedirect; +using Libraries::Http::MethodAfterRedirect; +using Libraries::Http::ResolveRedirectLocation; + +// --- IsFollowableRedirect: status filter --- + +TEST(Ps4Redirect, FollowsThreeHundred) { + EXPECT_TRUE(IsFollowableRedirect(300, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhOne) { + EXPECT_TRUE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhTwo) { + EXPECT_TRUE(IsFollowableRedirect(302, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhThree) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, FollowsThreeOhSeven) { + EXPECT_TRUE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhFour) { + EXPECT_FALSE(IsFollowableRedirect(304, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhFive) { + EXPECT_FALSE(IsFollowableRedirect(305, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhSix) { + EXPECT_FALSE(IsFollowableRedirect(306, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsThreeOhEight) { + EXPECT_FALSE(IsFollowableRedirect(308, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsNonRedirect) { + EXPECT_FALSE(IsFollowableRedirect(200, ORBIS_HTTP_METHOD_GET)); +} +TEST(Ps4Redirect, RejectsServerError) { + EXPECT_FALSE(IsFollowableRedirect(500, ORBIS_HTTP_METHOD_GET)); +} + +// --- IsFollowableRedirect: POST nuance (only 303 follows) --- + +TEST(Ps4Redirect, PostFollows303) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow300) { + EXPECT_FALSE(IsFollowableRedirect(300, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow301) { + EXPECT_FALSE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow302) { + EXPECT_FALSE(IsFollowableRedirect(302, ORBIS_HTTP_METHOD_POST)); +} +TEST(Ps4Redirect, PostDoesNotFollow307) { + EXPECT_FALSE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_POST)); +} + +// --- IsFollowableRedirect: HEAD always follows where status allows --- + +TEST(Ps4Redirect, HeadFollows301) { + EXPECT_TRUE(IsFollowableRedirect(301, ORBIS_HTTP_METHOD_HEAD)); +} +TEST(Ps4Redirect, HeadFollows303) { + EXPECT_TRUE(IsFollowableRedirect(303, ORBIS_HTTP_METHOD_HEAD)); +} +TEST(Ps4Redirect, HeadFollows307) { + EXPECT_TRUE(IsFollowableRedirect(307, ORBIS_HTTP_METHOD_HEAD)); +} + +// --- MethodAfterRedirect: method change rules --- + +TEST(Ps4Redirect, MethodPreservedOn301) { + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); + EXPECT_EQ(MethodAfterRedirect(301, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); +} +TEST(Ps4Redirect, MethodPreservedOn302) { + EXPECT_EQ(MethodAfterRedirect(302, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(302, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); +} +TEST(Ps4Redirect, MethodPreservedOn307) { + // RFC 7231 §6.4.7 - 307 specifically preserves method + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_POST); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); + EXPECT_EQ(MethodAfterRedirect(307, ORBIS_HTTP_METHOD_PUT), ORBIS_HTTP_METHOD_PUT); +} +TEST(Ps4Redirect, PostDowngradesToGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_POST), ORBIS_HTTP_METHOD_GET); +} +TEST(Ps4Redirect, PutDowngradesToGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_PUT), ORBIS_HTTP_METHOD_GET); +} +TEST(Ps4Redirect, HeadPreservedOn303) { + // RFC 7231 §6.4.4 carve-out: HEAD stays HEAD across 303 + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_HEAD), ORBIS_HTTP_METHOD_HEAD); +} +TEST(Ps4Redirect, GetStaysGetOn303) { + EXPECT_EQ(MethodAfterRedirect(303, ORBIS_HTTP_METHOD_GET), ORBIS_HTTP_METHOD_GET); +} + +// --- ResolveRedirectLocation: URL parsing --- + +TEST(Ps4Redirect, ResolveAbsoluteHttp) { + auto r = + ResolveRedirectLocation("https", "old.example.com", 443, "http://new.example.com/path?q=1"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "http"); + EXPECT_EQ(r->host, "new.example.com"); + EXPECT_EQ(r->port, 80u); + EXPECT_EQ(r->path, "/path?q=1"); +} +TEST(Ps4Redirect, ResolveAbsoluteHttpsExplicitPort) { + auto r = + ResolveRedirectLocation("http", "old.example.com", 80, "https://secure.example.com:8443/x"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "secure.example.com"); + EXPECT_EQ(r->port, 8443u); + EXPECT_EQ(r->path, "/x"); +} +TEST(Ps4Redirect, ResolveAbsolutePath) { + auto r = ResolveRedirectLocation("https", "example.com", 443, "/new/path?q=1"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "example.com"); + EXPECT_EQ(r->port, 443u); + EXPECT_EQ(r->path, "/new/path?q=1"); +} +TEST(Ps4Redirect, ResolveAbsoluteWithoutPath) { + // Bare "https://host" with no path - default to "/" + auto r = ResolveRedirectLocation("http", "old", 80, "https://host.example.com"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); + EXPECT_EQ(r->host, "host.example.com"); + EXPECT_EQ(r->port, 443u); + EXPECT_EQ(r->path, "/"); +} +TEST(Ps4Redirect, ResolveSchemeUppercaseLowercased) { + auto r = ResolveRedirectLocation("http", "x", 80, "HTTPS://host/p"); + ASSERT_TRUE(r.has_value()); + EXPECT_EQ(r->scheme, "https"); // canonicalised +} +TEST(Ps4Redirect, ResolveRejectsDocumentRelative) { + // "foo/bar" - no leading slash, no scheme + auto r = ResolveRedirectLocation("https", "x", 443, "foo/bar"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsEmpty) { + auto r = ResolveRedirectLocation("https", "x", 443, ""); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsUnknownScheme) { + // ftp:// is not a scheme libhttp speaks; we bail + auto r = ResolveRedirectLocation("https", "x", 443, "ftp://host/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsInvalidPort) { + auto r = ResolveRedirectLocation("https", "x", 443, "https://host:99999/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsZeroPort) { + auto r = ResolveRedirectLocation("https", "x", 443, "https://host:0/path"); + EXPECT_FALSE(r.has_value()); +} +TEST(Ps4Redirect, ResolveRejectsEmptyAuthority) { + auto r = ResolveRedirectLocation("https", "x", 443, "https:///path"); + EXPECT_FALSE(r.has_value()); +} + +} // namespace + +namespace Libraries::Http { +struct HostOverrideTarget { + std::string scheme; + std::string host; + u16 port = 0; +}; +std::unordered_map ParseHostOverridesJson( + const std::string& json_text); +bool ApplyHostOverride(std::string& scheme, std::string& host, u16& port, bool& is_secure); +} // namespace Libraries::Http + +namespace { + +using Libraries::Http::ApplyHostOverride; +using Libraries::Http::ParseHostOverridesJson; + +// --- ParseHostOverridesJson: shape parsing --- + +TEST(HostOverride, ParseEmptyStringYieldsEmptyMap) { + auto m = ParseHostOverridesJson(""); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseEmptyObjectYieldsEmptyMap) { + auto m = ParseHostOverridesJson("{}"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseSingleHostNoPortNoScheme) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "localhost"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); // preserve original + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 0); // preserve original port +} + +TEST(HostOverride, ParseSingleHostWithPortNoScheme) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 8080); +} + +TEST(HostOverride, ParseHttpSchemeWithPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "http://localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "http"); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 8080); +} + +TEST(HostOverride, ParseHttpsSchemeWithPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "https://secure.local:8443"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "https"); + EXPECT_EQ(e.host, "secure.local"); + EXPECT_EQ(e.port, 8443); +} + +TEST(HostOverride, ParseHttpSchemeNoPort) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "http://localhost"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, "http"); + EXPECT_EQ(e.host, "localhost"); + EXPECT_EQ(e.port, 0); +} + +TEST(HostOverride, ParseSchemeUppercaseLowercased) { + auto m = ParseHostOverridesJson(R"({"api.example.com": "HTTPS://Host:443"})"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").scheme, "https"); +} + +TEST(HostOverride, ParseUnknownSchemeDropsSchemeKeepsHost) { + // ftp:// is not http/https; scheme is silently dropped, host still parsed. + auto m = ParseHostOverridesJson(R"({"api.example.com": "ftp://elsewhere:21"})"); + ASSERT_EQ(m.size(), 1u); + const auto& e = m.at("api.example.com"); + EXPECT_EQ(e.scheme, ""); + EXPECT_EQ(e.host, "elsewhere"); + EXPECT_EQ(e.port, 21); +} + +TEST(HostOverride, ParseMultipleEntriesMixedForms) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "http://localhost:8080", + "analytics.example.com": "https://127.0.0.1:9090", + "static.example.com": "mock.local:7000", + "*": "http://catch-all.local" + })"); + ASSERT_EQ(m.size(), 4u); + EXPECT_EQ(m.at("api.example.com").scheme, "http"); + EXPECT_EQ(m.at("api.example.com").port, 8080); + EXPECT_EQ(m.at("analytics.example.com").scheme, "https"); + EXPECT_EQ(m.at("static.example.com").scheme, ""); // no scheme prefix + EXPECT_EQ(m.at("static.example.com").port, 7000); + EXPECT_EQ(m.at("*").scheme, "http"); + EXPECT_EQ(m.at("*").port, 0); // no port = preserve / default +} + +TEST(HostOverride, ParseCatchAllWildcard) { + auto m = ParseHostOverridesJson(R"({"*": "http://localhost:8080"})"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("*")); + EXPECT_EQ(m.at("*").host, "localhost"); + EXPECT_EQ(m.at("*").scheme, "http"); +} + +TEST(HostOverride, ParseInvalidJsonYieldsEmpty) { + auto m = ParseHostOverridesJson("this is not json"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseNonObjectRootYieldsEmpty) { + auto m = ParseHostOverridesJson(R"(["array", "not", "object"])"); + EXPECT_TRUE(m.empty()); +} + +TEST(HostOverride, ParseSkipsUnderscorePrefixedKeys) { + auto m = ParseHostOverridesJson(R"({ + "_comment": "this is a comment block", + "_section_apex": "--- Apex Legends endpoints ---", + "api.example.com": "http://localhost:8080", + "_1": "another comment", + "*": "http://catch-all.local" + })"); + ASSERT_EQ(m.size(), 2u); + EXPECT_TRUE(m.contains("api.example.com")); + EXPECT_TRUE(m.contains("*")); + EXPECT_FALSE(m.contains("_comment")); + EXPECT_FALSE(m.contains("_section_apex")); + EXPECT_FALSE(m.contains("_1")); +} + +TEST(HostOverride, ParseSkipsNonStringValues) { + auto m = ParseHostOverridesJson(R"({ + "good.example.com": "http://localhost:8080", + "bad-number.example.com": 1234, + "bad-null.example.com": null, + "bad-object.example.com": {"nested": "no"} + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("good.example.com")); +} + +TEST(HostOverride, ParseSkipsEmptyStringValues) { + auto m = ParseHostOverridesJson(R"({ + "good.example.com": "localhost", + "empty.example.com": "" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_TRUE(m.contains("good.example.com")); +} + +TEST(HostOverride, ParseBadPortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "http://localhost:not-a-number" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").scheme, "http"); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +TEST(HostOverride, ParseOutOfRangePortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "localhost:99999" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +TEST(HostOverride, ParseZeroPortKeepsHostDropsPort) { + auto m = ParseHostOverridesJson(R"({ + "api.example.com": "localhost:0" + })"); + ASSERT_EQ(m.size(), 1u); + EXPECT_EQ(m.at("api.example.com").host, "localhost"); + EXPECT_EQ(m.at("api.example.com").port, 0); +} + +// --- ApplyHostOverride: behavior with no JSON file present --- +static bool HostOverrideJsonConfigured() { + if (const char* p = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); p && p[0]) { + return true; + } + std::ifstream f("host_overrides.json"); + return f.is_open(); +} + +TEST(HostOverride, InactiveByDefaultLeavesValuesUnchanged) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "api.example.com"; + u16 port = 443; + bool is_secure = true; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_FALSE(changed); + EXPECT_EQ(scheme, "https"); + EXPECT_EQ(host, "api.example.com"); + EXPECT_EQ(port, 443); + EXPECT_TRUE(is_secure); +} + +TEST(HostOverride, InactivePreservesHttpScheme) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "http"; + std::string host = "plain.example.com"; + u16 port = 80; + bool is_secure = false; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_FALSE(changed); + EXPECT_EQ(scheme, "http"); + EXPECT_EQ(host, "plain.example.com"); + EXPECT_EQ(port, 80); + EXPECT_FALSE(is_secure); +} + +TEST(HostOverride, InactivePreservesUnusualPort) { + if (HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON configured; off-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "custom-port.example.com"; + u16 port = 5300; // Pinball FX uses this for kensho-discovery + bool is_secure = true; + ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_EQ(port, 5300); +} + +// --- ApplyHostOverride: behavior WITH the test JSON file pre-staged --- +static const char* kTestOverrideJson = R"({ + "api.example.com": "localhost:8080", + "*": "mock.local:8443" +})"; + +TEST(HostOverride, ActiveExactHostMatchFromJsonFile) { + if (!HostOverrideJsonConfigured()) { + GTEST_SKIP() << "host overrides JSON not configured; on-path test inapplicable"; + } + std::string scheme = "https"; + std::string host = "api.example.com"; + u16 port = 443; + bool is_secure = true; + const bool changed = ApplyHostOverride(scheme, host, port, is_secure); + EXPECT_TRUE(changed); + const bool any_change = (host != "api.example.com") || (port != 443); + EXPECT_TRUE(any_change); +} + +} // namespace diff --git a/tests/stubs/core_stub.cpp b/tests/stubs/core_stub.cpp new file mode 100644 index 000000000..b324d7942 --- /dev/null +++ b/tests/stubs/core_stub.cpp @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "emulator.h" + +namespace Core { + +Emulator::Emulator() {} +Emulator::~Emulator() {} +void Emulator::Shutdown() {} + +} // namespace Core diff --git a/tests/stubs/kernel_stub.cpp b/tests/stubs/kernel_stub.cpp new file mode 100644 index 000000000..7ebe9a3d7 --- /dev/null +++ b/tests/stubs/kernel_stub.cpp @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "tests/stubs/kernel_stub.h" + +#include "core/libraries/kernel/process.h" + +namespace Libraries::Kernel { + +static constexpr s32 DefaultTestSdkVersion = 0x4500000; +static s32 g_test_sdk_version = DefaultTestSdkVersion; + +void TestSetSdkVersion(s32 ver) { + g_test_sdk_version = ver; +} + +void TestResetSdkVersion() { + g_test_sdk_version = DefaultTestSdkVersion; +} + +s32 PS4_SYSV_ABI sceKernelGetCompiledSdkVersion(s32* ver) { + if (ver) { + *ver = g_test_sdk_version; + } + return 0; +} + +} // namespace Libraries::Kernel diff --git a/tests/stubs/kernel_stub.h b/tests/stubs/kernel_stub.h new file mode 100644 index 000000000..e16008e65 --- /dev/null +++ b/tests/stubs/kernel_stub.h @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Libraries::Kernel { + +void TestSetSdkVersion(s32 ver); +void TestResetSdkVersion(); + +} // namespace Libraries::Kernel diff --git a/tests/stubs/sdl_stub.cpp b/tests/stubs/sdl_stub.cpp index 859147696..8e637c91d 100644 --- a/tests/stubs/sdl_stub.cpp +++ b/tests/stubs/sdl_stub.cpp @@ -2,17 +2,23 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include "sdl_window.h" + +namespace Frontend { +WindowSDL::~WindowSDL() = default; +} extern "C" { - bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) { - if (buttonid) *buttonid = 0; // "No",skip migration - return true; - } +bool SDL_ShowMessageBox(const SDL_MessageBoxData* /* messageboxdata */, int* buttonid) { + if (buttonid) + *buttonid = 0; // "No",skip migration + return true; +} - bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */, - const char* /* message */, SDL_Window* /* window */) { - return true; - } +bool SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags /* flags */, const char* /* title */, + const char* /* message */, SDL_Window* /* window */) { + return true; +} } // extern "C"