feat(http): implement response header parsing
This commit is contained in:
parent
f00750e7bd
commit
669c7a40fa
|
|
@ -13,8 +13,13 @@ if(BUILD_TESTING)
|
|||
target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_http_tests PRIVATE libSceHttp)
|
||||
add_test(NAME guest_http COMMAND guest_http_tests)
|
||||
add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp)
|
||||
target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_http_response_header_tests PRIVATE libSceHttp libc)
|
||||
configure_windows_unwind(guest_http_response_header_tests)
|
||||
add_test(NAME guest_http_response_header COMMAND guest_http_response_header_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_http PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libSceHttp/src/HttpErrors.hpp"
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
// No network is emulated: contexts, templates and requests can be created, but any request
|
||||
// that would touch the network fails with the library's network error.
|
||||
|
|
@ -257,9 +260,66 @@ int APS5_VABI sceHttpSetRequestStatusCallback(void) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceHttpParseResponseHeader(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerLen, const char* fieldStr,
|
||||
const char** fieldValue, std::size_t* valueLen) {
|
||||
constexpr int invalidResponse = static_cast<int>(0x80432060);
|
||||
constexpr int invalidValue = static_cast<int>(0x804321FE);
|
||||
constexpr int notFound = static_cast<int>(0x80432025);
|
||||
if (!header) return invalidResponse;
|
||||
if (!fieldStr || !fieldValue || !valueLen) return invalidValue;
|
||||
|
||||
std::size_t fieldLen = 0;
|
||||
while (fieldLen < 0xfff && fieldStr[fieldLen] != '\0') ++fieldLen;
|
||||
const std::string_view input(header, headerLen);
|
||||
const auto isSpace = [](char c) { return c == ' ' || (c >= '\t' && c <= '\r'); };
|
||||
const auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; };
|
||||
std::size_t start = 0;
|
||||
bool found = false;
|
||||
while (start < headerLen) {
|
||||
if (!isSpace(input[start]) && fieldLen < headerLen - start && input[start + fieldLen] == ':') {
|
||||
std::size_t i = 0;
|
||||
while (i < fieldLen && lower(input[start + i]) == lower(fieldStr[i])) ++i;
|
||||
if (i == fieldLen) {
|
||||
start += fieldLen + 1;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const auto newline = input.find('\n', start);
|
||||
if (newline == std::string_view::npos) break;
|
||||
start = newline + 1;
|
||||
}
|
||||
if (!found) return notFound;
|
||||
|
||||
while (start < headerLen && isSpace(input[start])) {
|
||||
if (input[start++] == '\n') break;
|
||||
}
|
||||
std::size_t end = headerLen;
|
||||
std::size_t consumed = headerLen;
|
||||
std::size_t scan = start;
|
||||
while (scan < headerLen) {
|
||||
const auto newline = input.find('\n', scan);
|
||||
if (newline == std::string_view::npos) break;
|
||||
const auto next = newline + 1;
|
||||
if (next < headerLen && (input[next] == ' ' || input[next] == '\t')) {
|
||||
scan = next;
|
||||
continue;
|
||||
}
|
||||
end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline;
|
||||
consumed = next;
|
||||
break;
|
||||
}
|
||||
if (end == start) {
|
||||
*fieldValue = nullptr;
|
||||
*valueLen = 0;
|
||||
return 0;
|
||||
}
|
||||
if (consumed > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
|
||||
throw std::out_of_range("sceHttpParseResponseHeader: consumed byte count exceeds int");
|
||||
}
|
||||
*fieldValue = header + start;
|
||||
*valueLen = end - start;
|
||||
return static_cast<int>(consumed);
|
||||
}
|
||||
|
||||
int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
extern "C" int APS5_VABI sceHttpParseResponseHeader(const char*, std::size_t, const char*, const char**, std::size_t*);
|
||||
|
||||
static void Require(bool value) {
|
||||
if (!value) {
|
||||
std::fputs("HTTP response header check failed\n", stderr);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
static void Check(std::string_view header, const char* field, std::size_t offset, std::string_view expected, int consumed) {
|
||||
const char* value = nullptr;
|
||||
std::size_t length = 999;
|
||||
const int result = sceHttpParseResponseHeader(header.data(), header.size(), field, &value, &length);
|
||||
if (result != consumed) {
|
||||
std::fprintf(stderr, "Field %s: consumed %d bytes, expected %d\n", field, result, consumed);
|
||||
}
|
||||
Require(result == consumed);
|
||||
Require(length == expected.size());
|
||||
Require(value == (expected.empty() ? nullptr : header.data() + offset));
|
||||
if (length != 0) Require(std::memcmp(value, expected.data(), length) == 0);
|
||||
}
|
||||
|
||||
static void CheckGuardPage() {
|
||||
#ifdef _WIN32
|
||||
SYSTEM_INFO info;
|
||||
GetSystemInfo(&info);
|
||||
const std::size_t pageSize = info.dwPageSize;
|
||||
auto* pages = static_cast<char*>(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
Require(pages != nullptr);
|
||||
DWORD oldProtection = 0;
|
||||
Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0);
|
||||
#else
|
||||
const long hostPageSize = sysconf(_SC_PAGESIZE);
|
||||
Require(hostPageSize > 0);
|
||||
const std::size_t pageSize = static_cast<std::size_t>(hostPageSize);
|
||||
auto* pages = static_cast<char*>(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
|
||||
Require(pages != MAP_FAILED);
|
||||
Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0);
|
||||
#endif
|
||||
constexpr std::string_view text = "X: abc\r\n\tdef\r\n";
|
||||
char* header = pages + pageSize - text.size();
|
||||
std::memcpy(header, text.data(), text.size());
|
||||
Check({header, text.size()}, "x", 3, "abc\r\n\tdef", static_cast<int>(text.size()));
|
||||
Check({header, 6}, "x", 3, "abc", 6);
|
||||
const char* value = header;
|
||||
std::size_t length = 123;
|
||||
Require(sceHttpParseResponseHeader(header, text.size(), "not-present", &value, &length) == static_cast<int>(0x80432025));
|
||||
Require(value == header && length == 123);
|
||||
Require(sceHttpParseResponseHeader(pages + pageSize, 0, "x", &value, &length) == static_cast<int>(0x80432025));
|
||||
pages[pageSize - 1] = 'X';
|
||||
Require(sceHttpParseResponseHeader(pages + pageSize - 1, 1, "x", &value, &length) == static_cast<int>(0x80432025));
|
||||
#ifdef _WIN32
|
||||
Require(VirtualFree(pages, 0, MEM_RELEASE) != 0);
|
||||
#else
|
||||
Require(munmap(pages, 2 * pageSize) == 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
int main() {
|
||||
constexpr int invalidResponse = static_cast<int>(0x80432060);
|
||||
constexpr int invalidValue = static_cast<int>(0x804321FE);
|
||||
constexpr int notFound = static_cast<int>(0x80432025);
|
||||
Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 30);
|
||||
Check("X: first\r\nX: second\r\n", "X", 3, "first", 10);
|
||||
Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 17);
|
||||
Check("X: no-newline", "X", 3, "no-newline", 13);
|
||||
Check("X: value\r", "X", 3, "value\r", 9);
|
||||
Check("X:", "X", 2, "", 0);
|
||||
Check("X: \t", "X", 4, "", 0);
|
||||
Check("X:\r\n\r\n", "X", 5, "", 0);
|
||||
Check("X:\r\nnext\r\n", "X", 4, "next", 10);
|
||||
Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right", 43);
|
||||
Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b", 3), 7);
|
||||
|
||||
const char* marker = "unchanged";
|
||||
const char* value = marker;
|
||||
std::size_t length = 456;
|
||||
Require(sceHttpParseResponseHeader(nullptr, 1, "X", &value, &length) == invalidResponse);
|
||||
Require(sceHttpParseResponseHeader(nullptr, 0, nullptr, nullptr, nullptr) == invalidResponse);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 6, nullptr, &value, &length) == invalidValue);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 6, "X", nullptr, &length) == invalidValue);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 6, "X", &value, nullptr) == invalidValue);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 0, "X", &value, &length) == notFound);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 1, "X", &value, &length) == notFound);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 6, "Longer-Field", &value, &length) == notFound);
|
||||
Require(sceHttpParseResponseHeader("X: yes", 6, "X:", &value, &length) == notFound);
|
||||
Require(value == marker && length == 456);
|
||||
|
||||
std::string field(0xfff, 'A');
|
||||
std::string header = field + ": value\n";
|
||||
field += "ignored";
|
||||
Check(header, field.c_str(), 0x1000 + 1, "value", static_cast<int>(header.size()));
|
||||
CheckGuardPage();
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps
|
||||
- [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts
|
||||
- [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted
|
||||
- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature
|
||||
- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping.
|
||||
- [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read
|
||||
- [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature
|
||||
- [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature
|
||||
|
|
|
|||
Loading…
Reference in New Issue