Http part2 (#4441)

* more uri work based on decompile and tests

* fix includes

* fix loader stubs

* cleanups

* sceHttpParseStatusLine matches decompile and tests

* sceHttpParseResponseHeader implemenation and tests

* try fixing no-internet path in sendrequest

* minimal state machine to support proper erroring of no-internet available

* more improvements

* more implementation based on stephen's comments

* some more fixes based on decompile
This commit is contained in:
georgemoralis
2026-05-17 22:54:34 +03:00
committed by GitHub
parent c7686e33a8
commit 94786d70ca
7 changed files with 2210 additions and 477 deletions
File diff suppressed because it is too large Load Diff
+23 -7
View File
@@ -13,6 +13,18 @@ class SymbolsResolver;
namespace Libraries::Http { namespace Libraries::Http {
enum OrbisHttpMethod : s32 {
ORBIS_HTTP_METHOD_GET = 0,
ORBIS_HTTP_METHOD_POST = 1,
ORBIS_HTTP_METHOD_HEAD = 2,
ORBIS_HTTP_METHOD_OPTIONS = 3,
ORBIS_HTTP_METHOD_PUT = 4,
ORBIS_HTTP_METHOD_DELETE = 5,
ORBIS_HTTP_METHOD_TRACE = 6,
ORBIS_HTTP_METHOD_CONNECT = 7,
ORBIS_HTTP_METHOD_CUSTOM = 8,
};
enum OrbisUriBuild : s32 { enum OrbisUriBuild : s32 {
ORBIS_HTTP_URI_BUILD_WITH_SCHEME = 0x01, ORBIS_HTTP_URI_BUILD_WITH_SCHEME = 0x01,
ORBIS_HTTP_URI_BUILD_WITH_HOSTNAME = 0x02, ORBIS_HTTP_URI_BUILD_WITH_HOSTNAME = 0x02,
@@ -156,11 +168,6 @@ int PS4_SYSV_ABI sceHttpGetRegisteredCtxIds();
int PS4_SYSV_ABI sceHttpGetResponseContentLength(int reqId, int* result, u64* contentLength); int PS4_SYSV_ABI sceHttpGetResponseContentLength(int reqId, int* result, u64* contentLength);
int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode); int PS4_SYSV_ABI sceHttpGetStatusCode(int reqId, int* statusCode);
int PS4_SYSV_ABI sceHttpInit(int libnetMemId, int libsslCtxId, u64 poolSize); int PS4_SYSV_ABI sceHttpInit(int libnetMemId, int libsslCtxId, u64 poolSize);
int PS4_SYSV_ABI sceHttpParseResponseHeader(const char* header, u64 headerLen, const char* fieldStr,
const char** fieldValue, u64* valueLen);
int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int32_t* httpMajorVer,
int32_t* httpMinorVer, int32_t* responseCode,
const char** reasonPhrase, u64* phraseLen);
int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size); int PS4_SYSV_ABI sceHttpReadData(s32 reqId, void* data, u64 size);
int PS4_SYSV_ABI sceHttpRedirectCacheFlush(int libhttpCtxId); int PS4_SYSV_ABI sceHttpRedirectCacheFlush(int libhttpCtxId);
int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name); int PS4_SYSV_ABI sceHttpRemoveRequestHeader(int id, const char* name);
@@ -223,12 +230,21 @@ int PS4_SYSV_ABI sceHttpTrySetNonblock(int id, int isEnable);
int PS4_SYSV_ABI sceHttpUnsetEpoll(int id); int PS4_SYSV_ABI sceHttpUnsetEpoll(int id);
int PS4_SYSV_ABI sceHttpWaitRequest(OrbisHttpEpollHandle eh, OrbisHttpNBEvent* nbev, int maxevents, int PS4_SYSV_ABI sceHttpWaitRequest(OrbisHttpEpollHandle eh, OrbisHttpNBEvent* nbev, int maxevents,
int timeout); int timeout);
int PS4_SYSV_ABI sceHttpUriBuild(char* out, u64* require, u64 prepare,
const OrbisHttpUriElement* srcElement, u32 option);
int PS4_SYSV_ABI sceHttpUriCopy(); int PS4_SYSV_ABI sceHttpUriCopy();
//***********************************
// HTTP Header Parsing functions
//***********************************
int PS4_SYSV_ABI sceHttpParseStatusLine(const char* statusLine, u64 lineLen, int32_t* httpMajorVer,
int32_t* httpMinorVer, int32_t* responseCode,
const char** reasonPhrase, u64* phraseLen);
int PS4_SYSV_ABI sceHttpParseResponseHeader(const char* header, u64 headerLen, const char* fieldStr,
const char** fieldValue, u64* valueLen);
//*********************************** //***********************************
// URI functions // URI functions
//*********************************** //***********************************
int PS4_SYSV_ABI sceHttpUriBuild(char* out, u64* require, u64 prepare,
const OrbisHttpUriElement* srcElement, u32 option);
int PS4_SYSV_ABI sceHttpUriEscape(char* out, u64* require, u64 prepare, const char* in); int PS4_SYSV_ABI sceHttpUriEscape(char* out, u64* require, u64 prepare, const char* in);
int PS4_SYSV_ABI sceHttpUriMerge(char* mergedUrl, char* url, char* relativeUri, u64* require, int PS4_SYSV_ABI sceHttpUriMerge(char* mergedUrl, char* url, char* relativeUri, u64* require,
u64 prepare, u32 option); u64 prepare, u32 option);
+88
View File
@@ -199,3 +199,91 @@ foreach(t ${TEST_TARGETS})
PROPERTIES TIMEOUT 60 PROPERTIES TIMEOUT 60
) )
endforeach() endforeach()
# ===========================================================================
# HTTP tests (libSceHttp - URI helpers and status-line parsing)
# ===========================================================================
# Self-contained block - mirrors the structure of the targets above so the
# new test does not depend on any earlier foreach iterations.
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
${CMAKE_SOURCE_DIR}/src/core/emulator_state.cpp
# Minimal common support
${CMAKE_SOURCE_DIR}/src/common/path_util.cpp
${CMAKE_SOURCE_DIR}/src/common/assert.cpp
${CMAKE_SOURCE_DIR}/src/common/error.cpp
${CMAKE_SOURCE_DIR}/src/common/string_util.cpp
${CMAKE_SOURCE_DIR}/src/common/logging/log.cpp
# Stubs that replace dependencies
stubs/common_stub.cpp
stubs/scm_rev_stub.cpp
stubs/sdl_stub.cpp
stubs/loader_stub.cpp
# Tests
network/test_http_uri.cpp
network/test_http_status_line.cpp
network/test_http_parse_response_header.cpp
)
add_executable(shadps4_http_test ${HTTP_TEST_SOURCES})
list(APPEND TEST_TARGETS shadps4_http_test)
target_include_directories(shadps4_http_test PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}
)
target_compile_features(shadps4_http_test PRIVATE cxx_std_23)
target_compile_definitions(shadps4_http_test PRIVATE BOOST_ASIO_STANDALONE)
target_link_libraries(shadps4_http_test PRIVATE
GTest::gtest_main
fmt::fmt
magic_enum::magic_enum
nlohmann_json::nlohmann_json
toml11::toml11
SDL3::SDL3
spdlog::spdlog
)
if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
include(CheckCXXSymbolExists)
check_cxx_symbol_exists(_LIBCPP_VERSION version LIBCPP)
if (LIBCPP)
target_compile_options(shadps4_http_test PRIVATE -fexperimental-library)
endif()
endif()
if (WIN32)
target_link_libraries(shadps4_http_test PRIVATE onecore)
target_compile_definitions(shadps4_http_test PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
NTDDI_VERSION=0x0A000006
_WIN32_WINNT=0x0A00
WINVER=0x0A00
)
if (MSVC)
target_compile_definitions(shadps4_http_test PRIVATE
_CRT_SECURE_NO_WARNINGS
_CRT_NONSTDC_NO_DEPRECATE
_SCL_SECURE_NO_WARNINGS
_TIMESPEC_DEFINED
)
endif()
endif()
gtest_discover_tests(shadps4_http_test
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
PROPERTIES TIMEOUT 60
)
@@ -0,0 +1,213 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
#include "common/types.h"
#include "core/libraries/network/http.h"
#include "core/libraries/network/http_error.h"
#ifndef ORBIS_OK
#define ORBIS_OK 0
#endif
using namespace Libraries::Http;
namespace {
class HttpParseResponseHeader : public ::testing::Test {
protected:
const char* value{nullptr};
u64 valueLen{0};
int Parse(std::string_view header, const char* field) {
value = nullptr;
valueLen = 0;
return sceHttpParseResponseHeader(header.data(), header.size(), field, &value, &valueLen);
}
std::string ValueAsString() const {
if (!value || valueLen == 0)
return std::string{};
return std::string(value, valueLen);
}
};
TEST_F(HttpParseResponseHeader, SimpleField) {
const char* hdr = "Content-Type: text/html\r\n";
const int ret = Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Type");
EXPECT_GT(ret, 0);
EXPECT_EQ(ret, 25); // length up to and including the '\n'
EXPECT_EQ(ValueAsString(), "text/html");
}
TEST_F(HttpParseResponseHeader, LFOnlyAccepted) {
const char* hdr = "Content-Type: text/html\n";
const int ret = Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Type");
EXPECT_GT(ret, 0);
EXPECT_EQ(ValueAsString(), "text/html");
}
TEST_F(HttpParseResponseHeader, CaseInsensitiveFieldName) {
const char* hdr = "Content-Type: text/html\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "content-type"), 0);
EXPECT_EQ(ValueAsString(), "text/html");
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "CONTENT-TYPE"), 0);
EXPECT_EQ(ValueAsString(), "text/html");
}
// Multiple headers, find second.
TEST_F(HttpParseResponseHeader, FindSecondHeader) {
const char* hdr = "Date: Mon, 01 Jan 2024 00:00:00 GMT\r\n"
"Content-Length: 1234\r\n"
"Content-Type: text/plain\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Length"), 0);
EXPECT_EQ(ValueAsString(), "1234");
}
// Field with multiple leading spaces after colon
TEST_F(HttpParseResponseHeader, MultipleLeadingSpacesStripped) {
const char* hdr = "Content-Type: text/html\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Type"), 0);
EXPECT_EQ(ValueAsString(), "text/html");
}
// No space after colon
TEST_F(HttpParseResponseHeader, NoSpaceAfterColon) {
const char* hdr = "Content-Type:text/html\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Type"), 0);
EXPECT_EQ(ValueAsString(), "text/html");
}
// Tab as separator (whitespace).
TEST_F(HttpParseResponseHeader, TabAfterColon) {
const char* hdr = "Content-Type:\ttext/html\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Content-Type"), 0);
EXPECT_EQ(ValueAsString(), "text/html");
}
// CRLF stripped from value length but LF position included in return.
TEST_F(HttpParseResponseHeader, CRStrippedFromValueLength) {
const char* hdr = "Server: nginx\r\n";
const int ret = Parse(std::string_view(hdr, std::strlen(hdr)), "Server");
EXPECT_EQ(ret, 15);
EXPECT_EQ(valueLen, 5u); // "nginx" - no trailing \r
EXPECT_EQ(ValueAsString(), "nginx");
}
TEST_F(HttpParseResponseHeader, LineFoldingWithSpace) {
// Value continues onto next line because second line starts with ' '.
const char* hdr = "X-Custom: part1\r\n part2\r\nNext-Header: x\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "X-Custom"), 0);
EXPECT_NE(value, nullptr);
EXPECT_GT(valueLen, std::strlen("part1"));
// The captured value should include the folded continuation.
const std::string got(value, valueLen);
EXPECT_NE(got.find("part1"), std::string::npos);
EXPECT_NE(got.find("part2"), std::string::npos);
}
TEST_F(HttpParseResponseHeader, LineFoldingWithTab) {
const char* hdr = "X-Custom: part1\r\n\tpart2\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "X-Custom"), 0);
const std::string got(value, valueLen);
EXPECT_NE(got.find("part1"), std::string::npos);
EXPECT_NE(got.find("part2"), std::string::npos);
}
TEST_F(HttpParseResponseHeader, FieldNotFound) {
const char* hdr = "Content-Type: text/html\r\n";
EXPECT_EQ(Parse(std::string_view(hdr, std::strlen(hdr)), "Server"),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_NOT_FOUND));
}
// Empty header buffer (headerLen == 0)
TEST_F(HttpParseResponseHeader, EmptyHeaderBuffer) {
EXPECT_EQ(Parse(std::string_view("", 0u), "X-Anything"),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_NOT_FOUND));
}
// Field name appears mid-line (not at start of line)
TEST_F(HttpParseResponseHeader, MidLineFieldNameNotMatched) {
const char* hdr = "X-Reason: not-a-Server: header\r\nServer: nginx\r\n";
const int ret = Parse(std::string_view(hdr, std::strlen(hdr)), "Server");
EXPECT_GT(ret, 0);
EXPECT_EQ(ValueAsString(), "nginx");
}
// Continuation line (starts with whitespace) must not be treated as a field
// name match start.
TEST_F(HttpParseResponseHeader, ContinuationLineNotMatched) {
const char* hdr = "X-A: v1\r\n Server: not-here\r\nServer: real\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Server"), 0);
EXPECT_EQ(ValueAsString(), "real");
}
// Field name without colon - skipped.
TEST_F(HttpParseResponseHeader, FieldNameWithoutColon) {
const char* hdr = "Servernotacolon here\r\nServer: nginx\r\n";
EXPECT_GT(Parse(std::string_view(hdr, std::strlen(hdr)), "Server"), 0);
EXPECT_EQ(ValueAsString(), "nginx");
}
TEST_F(HttpParseResponseHeader, NullHeaderReturnsInvalidResponse) {
const char* v;
u64 vl;
EXPECT_EQ(sceHttpParseResponseHeader(nullptr, 0, "X", &v, &vl),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE));
}
TEST_F(HttpParseResponseHeader, NullFieldStrReturnsInvalidValue) {
const char* v;
u64 vl;
EXPECT_EQ(sceHttpParseResponseHeader("X: y\r\n", 6, nullptr, &v, &vl),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE));
}
TEST_F(HttpParseResponseHeader, NullFieldValueReturnsInvalidValue) {
u64 vl;
EXPECT_EQ(sceHttpParseResponseHeader("X: y\r\n", 6, "X", nullptr, &vl),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE));
}
TEST_F(HttpParseResponseHeader, NullValueLenReturnsInvalidValue) {
const char* v;
EXPECT_EQ(sceHttpParseResponseHeader("X: y\r\n", 6, "X", &v, nullptr),
static_cast<int>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE));
}
TEST_F(HttpParseResponseHeader, ErrorCodesMatchDocs) {
EXPECT_EQ(static_cast<unsigned>(ORBIS_HTTP_ERROR_PARSE_HTTP_NOT_FOUND), 0x80432025u);
EXPECT_EQ(static_cast<unsigned>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE), 0x80432060u);
EXPECT_EQ(static_cast<unsigned>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE), 0x804321feu);
}
TEST_F(HttpParseResponseHeader, DocsExampleIteration) {
// Docs example loops calling the function with counter += ret to find
// multiple matching fields.
const char* hdr = "Date: Mon, 01 Jan 2024 12:00:00 GMT\r\n"
"Date: Tue, 02 Jan 2024 12:00:00 GMT\r\n"
"Server: x\r\n";
const u64 hdrLen = std::strlen(hdr);
std::vector<std::string> seen;
u64 counter = 0;
while (counter < hdrLen) {
const int ret =
sceHttpParseResponseHeader(hdr + counter, hdrLen - counter, "Date", &value, &valueLen);
if (ret <= 0)
break;
seen.emplace_back(value, valueLen);
counter += ret;
}
ASSERT_EQ(seen.size(), 2u);
EXPECT_EQ(seen[0], "Mon, 01 Jan 2024 12:00:00 GMT");
EXPECT_EQ(seen[1], "Tue, 02 Jan 2024 12:00:00 GMT");
}
} // namespace
+219
View File
@@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include <string>
#include <string_view>
#include <gtest/gtest.h>
#include "common/types.h"
#include "core/libraries/network/http.h"
#include "core/libraries/network/http_error.h"
#ifndef ORBIS_OK
#define ORBIS_OK 0
#endif
using namespace Libraries::Http;
namespace {
class HttpStatusLine : public ::testing::Test {
protected:
int32_t major{}, minor{}, code{};
const char* phrase{nullptr};
u64 phraseLen{0};
int Parse(std::string_view sv) {
major = -1;
minor = -1;
code = -1;
phrase = nullptr;
phraseLen = 0;
return sceHttpParseStatusLine(sv.data(), sv.size(), &major, &minor, &code, &phrase,
&phraseLen);
}
};
// Canonical "HTTP/1.1 200 OK\n"
TEST_F(HttpStatusLine, CanonicalParse) {
const char* line = "HTTP/1.1 200 OK\n";
EXPECT_EQ(Parse(std::string_view(line, 16)), 16);
EXPECT_EQ(major, 1);
EXPECT_EQ(minor, 1);
EXPECT_EQ(code, 200);
ASSERT_NE(phrase, nullptr);
EXPECT_EQ(phrase, line + 12);
EXPECT_EQ(phraseLen, 3u);
EXPECT_EQ(std::string(phrase, phraseLen), " OK");
}
TEST_F(HttpStatusLine, CRLFStripped) {
const char* line = "HTTP/1.1 200 OK\r\n";
EXPECT_EQ(Parse(std::string_view(line, 17)), 17);
EXPECT_EQ(phraseLen, 3u); // " OK" - '\r' stripped
EXPECT_EQ(std::string(phrase, phraseLen), " OK");
}
TEST_F(HttpStatusLine, NoSpaceAfterCodeAccepted) {
const char* line = "HTTP/1.1 200OK\n";
EXPECT_EQ(Parse(std::string_view(line, 15)), 15);
EXPECT_EQ(code, 200);
EXPECT_EQ(std::string(phrase, phraseLen), "OK");
}
TEST_F(HttpStatusLine, EmptyReasonPhrase) {
const char* line = "HTTP/1.1 200\n";
EXPECT_EQ(Parse(std::string_view(line, 13)), 13);
EXPECT_EQ(code, 200);
EXPECT_EQ(phraseLen, 0u);
EXPECT_EQ(phrase, line + 12);
}
TEST_F(HttpStatusLine, MissingNewlineRejected) {
const char* line = "HTTP/1.1 200 OK\r";
EXPECT_EQ(Parse(std::string_view(line, 16)), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Multi-digit version numbers.
TEST_F(HttpStatusLine, MultiDigitVersions) {
const char* line = "HTTP/12.34 200 X\n";
EXPECT_EQ(Parse(std::string_view(line, 17)), 17);
EXPECT_EQ(major, 12);
EXPECT_EQ(minor, 34);
}
// Null check ordering.
TEST_F(HttpStatusLine, NullStatusLine) {
int32_t a, b, c;
const char* p;
u64 l;
EXPECT_EQ(sceHttpParseStatusLine(nullptr, 0, &a, &b, &c, &p, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
TEST_F(HttpStatusLine, NullMajor) {
int32_t b, c;
const char* p;
u64 l;
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.1 200 \n", 14, nullptr, &b, &c, &p, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
}
// Too short.
TEST_F(HttpStatusLine, TooShort) {
EXPECT_EQ(Parse("HTTP/1."), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Wrong prefix.
TEST_F(HttpStatusLine, WrongPrefix) {
EXPECT_EQ(Parse("XTTP/1.1 200 OK\n"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Non-digit after HTTP/.
TEST_F(HttpStatusLine, NonDigitMajor) {
EXPECT_EQ(Parse("HTTP/A.1 200 OK\n"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Missing dot.
TEST_F(HttpStatusLine, MissingDot) {
EXPECT_EQ(Parse("HTTP/11 200 OK\n"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Missing space after minor version.
TEST_F(HttpStatusLine, MissingSpaceAfterMinor) {
EXPECT_EQ(Parse("HTTP/1.1A200 OK\n"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
TEST_F(HttpStatusLine, HighBitByteRejected) {
char line[] = "HTTP/1\x80"
"1 200 OK\n";
EXPECT_EQ(Parse(std::string_view(line, sizeof(line) - 1)),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Response code with non-digit.
TEST_F(HttpStatusLine, NonDigitResponseCode) {
EXPECT_EQ(Parse("HTTP/1.1 2X0 OK\n"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
// Not enough room for response code.
TEST_F(HttpStatusLine, ResponseCodeTruncated) {
EXPECT_EQ(Parse("HTTP/1.1 20"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
}
} // namespace
namespace {
class HttpStatusLineDocs : public ::testing::Test {
protected:
int32_t major{-1}, minor{-1}, code{-1};
const char* phrase{nullptr};
u64 phraseLen{0};
int Parse(std::string_view sv) {
return sceHttpParseStatusLine(sv.data(), sv.size(), &major, &minor, &code, &phrase,
&phraseLen);
}
};
TEST_F(HttpStatusLineDocs, ExampleReturnsLineLengthIncludingCRLF) {
const char* header = "HTTP/1.0 200 OK\r\n";
const int ret = Parse(std::string_view(header, std::strlen(header)));
EXPECT_EQ(ret, 17);
EXPECT_EQ(major, 1);
EXPECT_EQ(minor, 0);
EXPECT_EQ(code, 200);
ASSERT_NE(phrase, nullptr);
EXPECT_EQ(phrase, header + 12);
EXPECT_EQ(phraseLen, 3u);
EXPECT_EQ(std::string(phrase, phraseLen), " OK");
}
TEST_F(HttpStatusLineDocs, LFAloneAcceptedByFirmware) {
const char* header = "HTTP/1.0 200 OK\n";
EXPECT_GT(Parse(std::string_view(header, std::strlen(header))), 0);
EXPECT_EQ(major, 1);
EXPECT_EQ(minor, 0);
EXPECT_EQ(code, 200);
}
TEST_F(HttpStatusLineDocs, ExtraBytesAfterCRLFIgnored) {
const char* header = "HTTP/1.0 200 OK\r\nHost: x\r\n\r\n";
const int ret = Parse(std::string_view(header, std::strlen(header)));
EXPECT_EQ(ret, 17); // stops right after first '\n'
}
TEST_F(HttpStatusLineDocs, EmptyPhraseWithCRLF) {
const char* header = "HTTP/1.0 200\r\n";
EXPECT_EQ(Parse(std::string_view(header, std::strlen(header))), 14);
EXPECT_EQ(phraseLen, 0u);
ASSERT_NE(phrase, nullptr);
EXPECT_EQ(phrase, header + 12); // points to '\r'
}
TEST_F(HttpStatusLineDocs, ErrorCodeForInvalidResponse) {
EXPECT_EQ(Parse("HTTP/1.0 200 OK"), ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE);
EXPECT_EQ(static_cast<unsigned>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_RESPONSE), 0x80432060u);
}
TEST_F(HttpStatusLineDocs, ErrorCodeForInvalidValue) {
int32_t a, b, c;
const char* p;
u64 l;
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.0 200 OK\r\n", 17, nullptr, &b, &c, &p, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.0 200 OK\r\n", 17, &a, nullptr, &c, &p, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.0 200 OK\r\n", 17, &a, &b, nullptr, &p, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.0 200 OK\r\n", 17, &a, &b, &c, nullptr, &l),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
EXPECT_EQ(sceHttpParseStatusLine("HTTP/1.0 200 OK\r\n", 17, &a, &b, &c, &p, nullptr),
ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE);
EXPECT_EQ(static_cast<unsigned>(ORBIS_HTTP_ERROR_PARSE_HTTP_INVALID_VALUE), 0x804321feu);
}
} // namespace
+490
View File
@@ -0,0 +1,490 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <cstring>
#include <string>
#include <gtest/gtest.h>
#include "common/types.h"
#include "core/libraries/network/http.h"
#include "core/libraries/network/http_error.h"
#ifndef ORBIS_OK
#define ORBIS_OK 0
#endif
using namespace Libraries::Http;
namespace {
class HttpUri : public ::testing::Test {
protected:
static constexpr size_t kPoolSize = 4096;
static constexpr size_t kBufSize = 4096;
OrbisHttpUriElement el{};
std::array<char, kPoolSize> pool{};
std::array<char, kBufSize> buf{};
u64 require = 0;
// Compute pool size needed by sceHttpUriParse, then call it with that exact size.
int Parse(const char* uri) {
u64 needed = 0;
const int sz = sceHttpUriParse(nullptr, uri, nullptr, &needed, 0);
if (sz != ORBIS_OK) {
return sz;
}
EXPECT_LE(needed, kPoolSize);
std::memset(&el, 0, sizeof(el));
std::memset(pool.data(), 0, kPoolSize);
return sceHttpUriParse(&el, uri, pool.data(), &require, needed);
}
};
TEST_F(HttpUri, ParseHttpDefaultPort) {
ASSERT_EQ(Parse("http://example.com/path"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "http");
EXPECT_STREQ(el.hostname, "example.com");
EXPECT_STREQ(el.path, "/path");
EXPECT_EQ(el.port, 80);
EXPECT_FALSE(el.opaque);
}
TEST_F(HttpUri, ParseHttpsDefaultPort) {
ASSERT_EQ(Parse("https://example.com/foo"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "https");
EXPECT_EQ(el.port, 443);
EXPECT_FALSE(el.opaque);
}
TEST_F(HttpUri, ParseExplicitPortOverridesDefault) {
ASSERT_EQ(Parse("http://example.com:8080/foo"), ORBIS_OK);
EXPECT_STREQ(el.hostname, "example.com");
EXPECT_EQ(el.port, 8080);
EXPECT_STREQ(el.path, "/foo");
}
TEST_F(HttpUri, ParseUserAndPassword) {
ASSERT_EQ(Parse("http://alice:secret@example.com/path"), ORBIS_OK);
EXPECT_STREQ(el.username, "alice");
EXPECT_STREQ(el.password, "secret");
EXPECT_STREQ(el.hostname, "example.com");
EXPECT_STREQ(el.path, "/path");
}
TEST_F(HttpUri, ParseUserOnlyHasEmptyPassword) {
ASSERT_EQ(Parse("http://alice@example.com/"), ORBIS_OK);
EXPECT_STREQ(el.username, "alice");
EXPECT_STREQ(el.password, "");
}
TEST_F(HttpUri, ParseQueryAndFragment) {
ASSERT_EQ(Parse("http://h/p?q=1#frag"), ORBIS_OK);
EXPECT_STREQ(el.path, "/p");
EXPECT_STREQ(el.query, "?q=1");
EXPECT_STREQ(el.fragment, "#frag");
}
TEST_F(HttpUri, ParseSchemeRelativeUrl) {
ASSERT_EQ(Parse("//cdn.example.com/asset.js"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "");
EXPECT_STREQ(el.hostname, "cdn.example.com");
EXPECT_STREQ(el.path, "/asset.js");
EXPECT_FALSE(el.opaque);
}
TEST_F(HttpUri, ParsePathIsNormalizedViaSweep) {
// sceHttpUriParse calls sceHttpUriSweepPath on the path component, so /a/b/../c -> /a/c
ASSERT_EQ(Parse("http://h/a/b/../c"), ORBIS_OK);
EXPECT_STREQ(el.path, "/a/c");
}
TEST_F(HttpUri, ParseSchemePrefixMatchHttps) {
ASSERT_EQ(Parse("HTTPSFOO://x/"), ORBIS_OK);
EXPECT_EQ(el.port, 443);
}
TEST_F(HttpUri, ParseIPv6HostStripsBrackets) {
ASSERT_EQ(Parse("http://[2001:db8::1]:8080/x"), ORBIS_OK);
EXPECT_STREQ(el.hostname, "2001:db8::1");
EXPECT_EQ(el.port, 8080);
EXPECT_STREQ(el.path, "/x");
}
TEST_F(HttpUri, ParseMailtoUsesAuthorityScan) {
ASSERT_EQ(Parse("mailto:user@example.com"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "mailto");
EXPECT_TRUE(el.opaque);
EXPECT_STREQ(el.username, "user");
EXPECT_STREQ(el.hostname, "example.com");
EXPECT_STREQ(el.path, "");
}
TEST_F(HttpUri, ParseDotFirstCharProducesEmptyHost) {
ASSERT_EQ(Parse("./bar.html"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "");
EXPECT_TRUE(el.opaque);
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "./bar.html");
}
TEST_F(HttpUri, ParseOpaqueSchemeStillGetsDefaultPort) {
ASSERT_EQ(Parse("http:"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "http");
EXPECT_EQ(el.port, 80);
EXPECT_TRUE(el.opaque);
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "");
}
TEST_F(HttpUri, ParseRejectsCharAfterPortOtherThanSlashOrNull) {
u64 needed = 0;
EXPECT_EQ(sceHttpUriParse(nullptr, "http://h:80?q=1", nullptr, &needed, 0),
ORBIS_HTTP_ERROR_INVALID_URL);
}
TEST_F(HttpUri, ParseRejectsPortGreaterThanLimit) {
u64 needed = 0;
EXPECT_EQ(sceHttpUriParse(nullptr, "http://h:999999/x", nullptr, &needed, 0),
ORBIS_HTTP_ERROR_INVALID_URL);
}
TEST_F(HttpUri, ParseNullUriReturnsInvalidUrl) {
u64 needed = 0;
EXPECT_EQ(sceHttpUriParse(nullptr, nullptr, nullptr, &needed, 0), ORBIS_HTTP_ERROR_INVALID_URL);
}
TEST_F(HttpUri, ParseSizeQueryYieldsNonZeroRequirement) {
u64 needed = 0;
ASSERT_EQ(sceHttpUriParse(nullptr, "http://example.com/foo", nullptr, &needed, 0), ORBIS_OK);
EXPECT_GT(needed, 0u);
}
TEST_F(HttpUri, ParseNoOutputArgsAndNoRequireReturnsInvalidValue) {
EXPECT_EQ(sceHttpUriParse(nullptr, "http://x/", nullptr, nullptr, 0),
ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, ParseInsufficientPoolReturnsOutOfMemory) {
OrbisHttpUriElement local{};
char tiny[2] = {};
EXPECT_EQ(sceHttpUriParse(&local, "http://example.com/", tiny, &require, sizeof(tiny)),
ORBIS_HTTP_ERROR_OUT_OF_MEMORY);
}
TEST_F(HttpUri, BuildHttpWithDefaultPortOmitsPort) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("http");
src.hostname = const_cast<char*>("example.com");
src.port = 80;
src.path = const_cast<char*>("/foo");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://example.com/foo");
}
TEST_F(HttpUri, BuildHttpWithExplicitNonDefaultPort) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("http");
src.hostname = const_cast<char*>("example.com");
src.port = 8080;
src.path = const_cast<char*>("/foo");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://example.com:8080/foo");
}
TEST_F(HttpUri, BuildHttpsWithDefaultPortOmitsPort) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("https");
src.hostname = const_cast<char*>("a.b");
src.port = 443;
src.path = const_cast<char*>("/x");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "https://a.b/x");
}
TEST_F(HttpUri, BuildMailtoPortSkipIsCaseSensitive) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("MAILTO");
src.hostname = const_cast<char*>("h");
src.port = 25;
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "MAILTO://h:25");
}
TEST_F(HttpUri, BuildOpaqueUriOmitsDoubleSlash) {
OrbisHttpUriElement src{};
src.opaque = true;
src.scheme = const_cast<char*>("mailto");
src.path = const_cast<char*>("user@example.com");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "mailto:user@example.com");
}
TEST_F(HttpUri, BuildNullSourceReturnsInvalidUrl) {
EXPECT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, nullptr, 0xFF),
ORBIS_HTTP_ERROR_INVALID_URL);
}
TEST_F(HttpUri, BuildBothOutputAndRequireNullReturnsInvalidValue) {
OrbisHttpUriElement src{};
src.scheme = const_cast<char*>("http");
EXPECT_EQ(sceHttpUriBuild(nullptr, nullptr, 0, &src, 0xFF), ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, BuildSizeQueryWithNullOutPopulatesRequire) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("http");
src.hostname = const_cast<char*>("example.com");
src.path = const_cast<char*>("/foo");
EXPECT_EQ(sceHttpUriBuild(nullptr, &require, 0, &src, 0xFF), ORBIS_OK);
EXPECT_GE(require, std::strlen("http://example.com/foo") + 1);
}
TEST_F(HttpUri, BuildInsufficientBufferReturnsOutOfMemory) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("http");
src.hostname = const_cast<char*>("example.com");
src.path = const_cast<char*>("/foo");
char tiny[4] = {};
EXPECT_EQ(sceHttpUriBuild(tiny, &require, sizeof(tiny), &src, 0xFF),
ORBIS_HTTP_ERROR_OUT_OF_MEMORY);
}
TEST_F(HttpUri, BuildRespectsOptionMask) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("http");
src.hostname = const_cast<char*>("example.com");
src.path = const_cast<char*>("/foo");
src.query = const_cast<char*>("?q=1");
// Without WITH_QUERY the query is suppressed.
const u32 opt = ORBIS_HTTP_URI_BUILD_WITH_SCHEME | ORBIS_HTTP_URI_BUILD_WITH_HOSTNAME |
ORBIS_HTTP_URI_BUILD_WITH_PATH;
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, opt), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://example.com/foo");
}
TEST_F(HttpUri, SweepPathZeroSizeShortCircuitsBeforeNullCheck) {
EXPECT_EQ(sceHttpUriSweepPath(nullptr, nullptr, 0), ORBIS_OK);
}
TEST_F(HttpUri, SweepPathNullDstNonZeroSize) {
EXPECT_EQ(sceHttpUriSweepPath(nullptr, "/foo", 5), ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, SweepPathNullSrcNonZeroSize) {
char dst[16];
EXPECT_EQ(sceHttpUriSweepPath(dst, nullptr, 5), ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, SweepPathNonAbsoluteCopiesVerbatim) {
char dst[64] = {};
const char* src = "foo/../bar";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "foo/../bar");
}
TEST_F(HttpUri, SweepPathAbsoluteRemovesDotDot) {
char dst[64] = {};
const char* src = "/foo/../bar";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "/bar");
}
TEST_F(HttpUri, SweepPathAbsoluteRemovesSingleDot) {
char dst[64] = {};
const char* src = "/foo/./bar";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "/foo/bar");
}
TEST_F(HttpUri, SweepPathTrailingDotIsPreservedLiterally) {
char dst[64] = {};
const char* src = "/foo/.";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "/foo/.");
}
TEST_F(HttpUri, SweepPathTrailingDotDotIsPreservedLiterally) {
char dst[64] = {};
const char* src = "/foo/..";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "/foo/..");
}
TEST_F(HttpUri, SweepPathSdkDocExample) {
// Example from the PS4 SDK docs / sceHttpUriSweepPath reference.
char dst[128] = {};
const char* src = "/foo/bar/../foo/././../../../test/index.html";
ASSERT_EQ(sceHttpUriSweepPath(dst, src, std::strlen(src) + 1), ORBIS_OK);
EXPECT_STREQ(dst, "/test/index.html");
}
TEST_F(HttpUri, SweepPathRootOnly) {
char dst[8] = {};
ASSERT_EQ(sceHttpUriSweepPath(dst, "/", 2), ORBIS_OK);
EXPECT_STREQ(dst, "/");
}
TEST_F(HttpUri, MergeWithAbsoluteRelativeReturnsRelativeAsIs) {
char base[] = "http://foo.com/foo/index.html";
char absolute[] = "http://bar.com/other";
ASSERT_EQ(sceHttpUriMerge(buf.data(), base, absolute, &require, kBufSize, 0), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://bar.com/other");
}
TEST_F(HttpUri, MergeWithRelativeDoesNotSweepDotSegments) {
char base[] = "http://foo.com/foo/index.html";
char rel[] = "./default.html";
ASSERT_EQ(sceHttpUriMerge(buf.data(), base, rel, &require, kBufSize, 0), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://foo.com/foo/./default.html");
}
TEST_F(HttpUri, MergeWithRelativeDoesNotSweepParentSegments) {
char base[] = "http://foo.com/foo/index.html";
char up[] = "../sibling.html";
ASSERT_EQ(sceHttpUriMerge(buf.data(), base, up, &require, kBufSize, 0), ORBIS_OK);
EXPECT_STREQ(buf.data(), "http://foo.com/foo/../sibling.html");
}
TEST_F(HttpUri, MergeNullBaseReturnsInvalidValue) {
char rel[] = "./x";
EXPECT_EQ(sceHttpUriMerge(buf.data(), nullptr, rel, &require, kBufSize, 0),
ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, MergeNullRelativeReturnsInvalidValue) {
char base[] = "http://h/";
EXPECT_EQ(sceHttpUriMerge(buf.data(), base, nullptr, &require, kBufSize, 0),
ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, MergeNonZeroFlagReturnsInvalidValue) {
// Per firmware: the last param must be 0; non-zero is rejected.
char base[] = "http://h/";
char rel[] = "./x";
EXPECT_EQ(sceHttpUriMerge(buf.data(), base, rel, &require, kBufSize, 1),
ORBIS_HTTP_ERROR_INVALID_VALUE);
}
TEST_F(HttpUri, EscapeSpace) {
ASSERT_EQ(sceHttpUriEscape(buf.data(), &require, kBufSize, "hello world"), ORBIS_OK);
EXPECT_STREQ(buf.data(), "hello%20world");
}
TEST_F(HttpUri, UnescapePercentTwenty) {
ASSERT_EQ(sceHttpUriUnescape(buf.data(), &require, kBufSize, "hello%20world"), ORBIS_OK);
EXPECT_STREQ(buf.data(), "hello world");
}
TEST_F(HttpUri, EscapeUnescapeRoundtrip) {
const char* original = "a b/c?d=e&f=g h";
char escaped[128] = {};
ASSERT_EQ(sceHttpUriEscape(escaped, &require, sizeof(escaped), original), ORBIS_OK);
char unescaped[128] = {};
ASSERT_EQ(sceHttpUriUnescape(unescaped, &require, sizeof(unescaped), escaped), ORBIS_OK);
EXPECT_STREQ(unescaped, original);
}
TEST_F(HttpUri, ParseBuildRoundtripFull) {
const char* original = "http://alice:secret@host.example.com:8080/path?q=1#frag";
ASSERT_EQ(Parse(original), ORBIS_OK);
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &el, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), original);
}
TEST_F(HttpUri, ParseBuildRoundtripSimple) {
const char* original = "https://example.com/";
ASSERT_EQ(Parse(original), ORBIS_OK);
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &el, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), original);
}
} // namespace
namespace {
// "file:///etc/passwd" - canonical form with empty authority + absolute path
TEST_F(HttpUri, ParseFileUrlEmptyAuthority) {
ASSERT_EQ(Parse("file:///etc/passwd"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "file");
EXPECT_FALSE(el.opaque);
EXPECT_STREQ(el.username, "");
EXPECT_STREQ(el.password, "");
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "/etc/passwd");
EXPECT_EQ(el.port, 0);
}
TEST_F(HttpUri, ParseFileUrlSingleSlashIsOpaque) {
ASSERT_EQ(Parse("file:/etc/passwd"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "file");
EXPECT_TRUE(el.opaque);
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "/etc/passwd");
EXPECT_EQ(el.port, 0);
}
TEST_F(HttpUri, ParseFileUrlWithLocalhost) {
ASSERT_EQ(Parse("file://localhost/etc/passwd"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "file");
EXPECT_FALSE(el.opaque);
EXPECT_STREQ(el.hostname, "localhost");
EXPECT_STREQ(el.path, "/etc/passwd");
EXPECT_EQ(el.port, 0);
}
TEST_F(HttpUri, ParseFileUrlBareAuthority) {
ASSERT_EQ(Parse("file://"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "file");
EXPECT_FALSE(el.opaque);
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "");
EXPECT_EQ(el.port, 0);
}
TEST_F(HttpUri, ParseFileUrlSchemeOnly) {
ASSERT_EQ(Parse("file:"), ORBIS_OK);
EXPECT_STREQ(el.scheme, "file");
EXPECT_TRUE(el.opaque); // no slashes -> opaque
EXPECT_STREQ(el.hostname, "");
EXPECT_STREQ(el.path, "");
EXPECT_EQ(el.port, 0);
}
TEST_F(HttpUri, BuildFileUrlEmptyAuthority) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("file");
src.hostname = const_cast<char*>("");
src.path = const_cast<char*>("/etc/passwd");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "file:///etc/passwd");
}
TEST_F(HttpUri, BuildFileUrlWithHost) {
OrbisHttpUriElement src{};
src.opaque = false;
src.scheme = const_cast<char*>("file");
src.hostname = const_cast<char*>("localhost");
src.path = const_cast<char*>("/etc/passwd");
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &src, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), "file://localhost/etc/passwd");
}
TEST_F(HttpUri, ParseBuildFileUrlRoundtrip) {
const char* original = "file:///etc/passwd";
ASSERT_EQ(Parse(original), ORBIS_OK);
ASSERT_EQ(sceHttpUriBuild(buf.data(), &require, kBufSize, &el, 0xFF), ORBIS_OK);
EXPECT_STREQ(buf.data(), original);
}
} // namespace
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/types.h"
#include "core/loader/symbols_resolver.h"
namespace Core::Loader {
void SymbolsResolver::AddSymbol(const SymbolResolver& /*sym*/, u64 /*addr*/) {}
} // namespace Core::Loader