mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-05-26 13:50:37 +00:00
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:
+1145
-449
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,18 @@ class SymbolsResolver;
|
||||
|
||||
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 {
|
||||
ORBIS_HTTP_URI_BUILD_WITH_SCHEME = 0x01,
|
||||
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 sceHttpGetStatusCode(int reqId, int* statusCode);
|
||||
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 sceHttpRedirectCacheFlush(int libhttpCtxId);
|
||||
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 sceHttpWaitRequest(OrbisHttpEpollHandle eh, OrbisHttpNBEvent* nbev, int maxevents,
|
||||
int timeout);
|
||||
int PS4_SYSV_ABI sceHttpUriBuild(char* out, u64* require, u64 prepare,
|
||||
const OrbisHttpUriElement* srcElement, u32 option);
|
||||
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
|
||||
//***********************************
|
||||
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 sceHttpUriMerge(char* mergedUrl, char* url, char* relativeUri, u64* require,
|
||||
u64 prepare, u32 option);
|
||||
|
||||
@@ -199,3 +199,91 @@ foreach(t ${TEST_TARGETS})
|
||||
PROPERTIES TIMEOUT 60
|
||||
)
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user