/* * End-to-end PS5 stream-simulation test. * * Pairs the PS5 installer simulator (ps5_sim.c) against the REAL stream * server (stream_server.c) running on the host — no PS5 required. This is * the "mock the PS5 on the PC" setup: it proves the server speaks the exact * HTTP contract the console expects (206 ranges, strict 404 for the .crc * sidecar) and that our replayed request pattern is a valid client. * * Two modes: * test_stream_sim run under `make test`; asserts, returns 0/1. * test_stream_sim --demo build a fixture, replay the PS5 pattern * against a live server, and print a * reference-format replay log + summary. */ #include #include #include #include #include #include #include #include #include #include #include #include "stream_server.h" #include "installer.h" #include "ps5_sim.h" #include "test_fixture.h" #define FIX_DIR "/tmp/test_stream_sim" #define FIX_PKG FIX_DIR "/pkg.pkg" #define SESSION "package-test-1.pkg" #define PATH "/stream/install/package-test-1.pkg" #define CONTENT_ID "EP9000-PPSA90012_00-TESTSIM00000001" #define PKG_SIZE (5ULL * 1024 * 1024) /* 5 MiB: header + bulk chunks */ /* ---- raw GET used for independent byte-integrity checks ----------------- */ static int fetch_range(const char *host, int port, const char *path, uint64_t start, uint64_t end, uint8_t **out_body, size_t *out_len) { char range[96]; snprintf(range, sizeof(range), "Range: bytes=%llu-%llu\r\n", (unsigned long long)start, (unsigned long long)end); char req[2048]; snprintf(req, sizeof(req), "GET %s%s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n%s\r\n\r\n", path, PS5_SIM_DEFAULT_QUERY, range); struct addrinfo hints, *res = NULL, *rp; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; char ps[16]; snprintf(ps, sizeof(ps), "%d", port); if (getaddrinfo(host, ps, &hints, &res) != 0) return -1; int fd = -1; for (rp = res; rp; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd >= 0 && connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; close(fd); fd = -1; } freeaddrinfo(res); if (fd < 0) return -1; /* Read headers byte-by-byte so a response can never have its body bytes * absorbed into the header buffer (which would under-read the body past * the connection close and give a wrong Content-Length match). */ char hdr[8192]; size_t hlen = 0; send(fd, req, strlen(req), 0); for (;;) { char c; ssize_t r = recv(fd, &c, 1, 0); if (r <= 0) break; if (hlen + 1 >= sizeof(hdr)) break; hdr[hlen++] = c; if (hlen >= 4 && hdr[hlen - 4] == '\r' && hdr[hlen - 3] == '\n' && hdr[hlen - 2] == '\r' && hdr[hlen - 1] == '\n') break; } uint64_t clen = 0; for (char *s = hdr; (s = strstr(s, "\r\n")) != NULL; ) { s += 2; if (strncasecmp(s, "content-length:", 15) == 0) clen = strtoull(s + 15, NULL, 10); } uint8_t *buf = (uint8_t *)malloc(clen ? clen : 1); size_t got = 0; while (got < clen) { ssize_t r = recv(fd, buf + got, clen - got, 0); if (r <= 0) break; got += (size_t)r; } close(fd); *out_body = buf; *out_len = clen ? got : 0; return 0; } static int fetch_icon(const char *host, int port, const char *path, uint8_t **out_body, size_t *out_len, int *out_status, char *out_ct, size_t out_ct_sz) { char req[1024]; snprintf(req, sizeof(req), "GET %s HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n", path); struct addrinfo hints, *res = NULL, *rp; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; char ps[16]; snprintf(ps, sizeof(ps), "%d", port); if (getaddrinfo(host, ps, &hints, &res) != 0) return -1; int fd = -1; for (rp = res; rp; rp = rp->ai_next) { fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (fd >= 0 && connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; close(fd); fd = -1; } freeaddrinfo(res); if (fd < 0) return -1; char hdr[8192]; size_t hlen = 0; send(fd, req, strlen(req), 0); for (;;) { char c; ssize_t r = recv(fd, &c, 1, 0); if (r <= 0) break; if (hlen + 1 >= sizeof(hdr)) break; hdr[hlen++] = c; if (hlen >= 4 && hdr[hlen - 4] == '\r' && hdr[hlen - 3] == '\n' && hdr[hlen - 2] == '\r' && hdr[hlen - 1] == '\n') break; } hdr[hlen] = '\0'; int status = 0; sscanf(hdr, "HTTP/1.%*d %d", &status); if (out_status) *out_status = status; uint64_t clen = 0; for (char *s = hdr; (s = strstr(s, "\r\n")) != NULL; ) { s += 2; if (strncasecmp(s, "content-length:", 15) == 0) clen = strtoull(s + 15, NULL, 10); if (out_ct && out_ct_sz > 0 && strncasecmp(s, "content-type:", 13) == 0) { const char *val = s + 13; while (*val == ' ') val++; const char *eol = strstr(val, "\r\n"); size_t vlen = eol ? (size_t)(eol - val) : strlen(val); if (vlen >= out_ct_sz) vlen = out_ct_sz - 1; strncpy(out_ct, val, vlen); out_ct[vlen] = '\0'; } } uint8_t *buf = (uint8_t *)malloc(clen ? clen : 1); size_t got = 0; while (got < clen) { ssize_t r = recv(fd, buf + got, clen - got, 0); if (r <= 0) break; got += (size_t)r; } close(fd); *out_body = buf; *out_len = clen ? got : 0; return 0; } static void write_fixture(void) { system("rm -rf " FIX_DIR " && mkdir -p " FIX_DIR); assert(fixture_write_ps5_pkg(FIX_PKG, "PPSA90012", "SimGame", "gd", "01.000.000", 1) == 0); assert(fixture_grow_file(FIX_PKG, PKG_SIZE) == 0); } /* Verify the server serves the fixture bytes for a few offsets. */ static void verify_integrity(void) { FILE *f = fopen(FIX_PKG, "rb"); assert(f); uint8_t sample[65536]; assert(fread(sample, 1, sizeof(sample), f) == sizeof(sample)); uint8_t *b; size_t len; /* Header chunk: server bytes [0, 65536) must match the fixture file. */ assert(fetch_range("127.0.0.1", STREAM_SERVER_PORT, PATH, 0, 65535, &b, &len) == 0); assert(len == 65536 && memcmp(b, sample, 65536) == 0); free(b); /* First bulk chunk, straight after the header: server bytes * [65536, 65536+4096) must match the fixture file at the same offset. * (Read the expected bytes from the file rather than past the end of * `sample`, which only holds the first 64 KiB.) */ uint8_t bulk[4096]; assert(fread(bulk, 1, sizeof(bulk), f) == sizeof(bulk)); assert(fetch_range("127.0.0.1", STREAM_SERVER_PORT, PATH, 65536, 65536 + 4095, &b, &len) == 0); assert(len == 4096 && memcmp(b, bulk, 4096) == 0); free(b); fclose(f); } static int build_target(const ps5_sim_target_t *base, ps5_sim_target_t *t) { *t = *base; t->host = "127.0.0.1"; t->port = STREAM_SERVER_PORT; t->path = PATH; t->content_id = CONTENT_ID; t->check_crc = 1; t->total_size = PKG_SIZE; return 0; } /* Run the full PS5 replay and assert the model holds. */ static void run_scenario(const char *name, ps5_sim_target_t base, int expect_fail) { ps5_sim_target_t t; build_target(&base, &t); ps5_sim_stats_t st; int rc = ps5_sim_replay(&t, &st, NULL); printf(" scenario '%s': replay_rc=%d header_ok=%d bulk_ok=%d crc_404=%d " "failures=%d conns=%d reqs=%u bytes=%llu\n", name, rc, st.header_ok, st.bulk_ok, st.crc_404, st.failures, st.conns_opened, (unsigned)st.requests_made, (unsigned long long)st.total_delivered); if (expect_fail) { assert(rc != 0 || st.failures != 0); return; } assert(rc == 0); assert(st.header_ok == 1); assert(st.bulk_ok == 1); assert(st.crc_404 == 1); assert(st.failures == 0); assert(st.conns_opened >= base.header_repeats); } static void test_end_to_end(void) { printf("=== End-to-end: real stream_server.c + simulated PS5 client ===\n"); write_fixture(); /* Start the real stream server pinned to our session filename. */ assert(stream_server_session_start_ex(FIX_PKG, SESSION) == 0); assert(stream_server_is_running() == 1); /* Sanity: the .crc sidecar must NOT be served (server pins *.pkg name). */ { uint8_t *b; size_t len; assert(fetch_range("127.0.0.1", STREAM_SERVER_PORT, "/stream/install/EP9000-PPSA90012_00-TESTSIM00000001.crc", 0, 0, &b, &len) == 0); assert(len == 0); /* 404 -> empty body */ free(b); } /* Scenario 1: default-ish pattern (2 parallel bulk connections). */ { ps5_sim_target_t base; memset(&base, 0, sizeof(base)); base.header_repeats = 5; base.reqs_per_header = 2; base.parallel = 2; base.chunk_size = PS5_SIM_DEFAULT_CHUNK; run_scenario("default-2parallel", base, 0); } /* Scenario 2: single bulk connection (smaller install behavior). */ { ps5_sim_target_t base; memset(&base, 0, sizeof(base)); base.header_repeats = 3; base.reqs_per_header = 2; base.parallel = 1; base.chunk_size = PS5_SIM_DEFAULT_CHUNK; run_scenario("single-conn", base, 0); } /* Scenario 3: minimal header reads (1 repeat, 1 req). */ { ps5_sim_target_t base; memset(&base, 0, sizeof(base)); base.header_repeats = 1; base.reqs_per_header = 1; base.parallel = 2; base.chunk_size = PS5_SIM_DEFAULT_CHUNK; run_scenario("minimal-headers", base, 0); } verify_integrity(); stream_server_session_stop(); assert(stream_server_is_running() == 0); printf(" -> real stream server passed the PS5 replay and stopped cleanly\n"); } static void test_missing_server(void) { /* Against a closed port the client must report failures (not hang/crash). */ printf("=== Negative: no server listening (port 1) ===\n"); ps5_sim_target_t base; memset(&base, 0, sizeof(base)); base.header_repeats = 2; base.parallel = 1; ps5_sim_target_t t; build_target(&base, &t); t.host = "127.0.0.1"; t.port = 1; /* almost certainly closed */ ps5_sim_stats_t st; int rc = ps5_sim_replay(&t, &st, NULL); printf(" replay_rc=%d failures=%d (expected nonzero)\n", rc, st.failures); assert(rc != 0 || st.failures != 0); } static void test_stream_icon(void) { printf("=== Testing stream server icon serving ===\n"); write_fixture(); assert(stream_server_session_start_ex(FIX_PKG, SESSION) == 0); /* 1. Before icon is set, GET icon returns 404 */ { uint8_t *body = NULL; size_t len = 0; int status = 0; char content_type[64] = {0}; assert(fetch_icon("127.0.0.1", STREAM_SERVER_PORT, "/stream/install/icon-test.png", &body, &len, &status, content_type, sizeof(content_type)) == 0); assert(status == 404); free(body); } /* 2. Set icon and fetch: returns 200 OK, image/png, and matching bytes */ uint8_t fake_png[16] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4, 5, 6, 7, 8}; stream_server_set_icon(fake_png, sizeof(fake_png), "icon-test.png"); { uint8_t *body = NULL; size_t len = 0; int status = 0; char content_type[64] = {0}; assert(fetch_icon("127.0.0.1", STREAM_SERVER_PORT, "/stream/install/icon-test.png", &body, &len, &status, content_type, sizeof(content_type)) == 0); assert(status == 200); assert(len == sizeof(fake_png)); assert(memcmp(body, fake_png, sizeof(fake_png)) == 0); assert(strstr(content_type, "image/png") != NULL); free(body); } /* 3. Unknown icon name returns 404 */ { uint8_t *body = NULL; size_t len = 0; int status = 0; char content_type[64] = {0}; assert(fetch_icon("127.0.0.1", STREAM_SERVER_PORT, "/stream/install/icon-other.png", &body, &len, &status, content_type, sizeof(content_type)) == 0); assert(status == 404); free(body); } stream_server_session_stop(); assert(stream_server_is_running() == 0); printf(" -> stream server icon serving verified\n"); } int main(int argc, char **argv) { setvbuf(stdout, NULL, _IOLBF, 0); /* flush progress as we go */ setvbuf(stderr, NULL, _IOLBF, 0); int demo = 0; const char *pkg = FIX_PKG; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--demo") == 0) { demo = 1; } else if (strcmp(argv[i], "--pkg") == 0 && i + 1 < argc) { pkg = argv[++i]; } } if (demo) { printf("=== PS5 stream-simulation DEMO (real stream server) ===\n"); if (strcmp(pkg, FIX_PKG) == 0) write_fixture(); FILE *replay = fopen("/tmp/test_stream_sim/replay.log", "w"); assert(replay); assert(stream_server_session_start_ex(pkg, SESSION) == 0); ps5_sim_target_t base; memset(&base, 0, sizeof(base)); base.header_repeats = 5; base.reqs_per_header = 2; base.parallel = 2; base.chunk_size = PS5_SIM_DEFAULT_CHUNK; base.check_crc = 1; base.content_id = CONTENT_ID; ps5_sim_stats_t st; ps5_sim_target_t t; build_target(&base, &t); ps5_sim_replay(&t, &st, replay); fclose(replay); printf("\n----- replay log (stream_debug reference format) -----\n"); FILE *fp = fopen("/tmp/test_stream_sim/replay.log", "r"); assert(fp); char line[1024]; while (fgets(line, sizeof(line), fp)) fputs(line, stdout); fclose(fp); printf("\n----- summary -----\n"); printf("connections=%d requests=%u bytes_delivered=%llu " "header_ok=%d bulk_ok=%d crc_404=%d failures=%d\n", st.conns_opened, (unsigned)st.requests_made, (unsigned long long)st.total_delivered, st.header_ok, st.bulk_ok, st.crc_404, st.failures); stream_server_session_stop(); printf("\n>>> DEMO COMPLETE <<<\n"); return 0; } printf("==============================================\n"); printf(">>> RUNNING PS5 STREAM SIMULATION TEST <<<\n"); printf("==============================================\n"); test_end_to_end(); test_missing_server(); test_stream_icon(); printf("\n>>> ALL STREAM SIMULATION TESTS PASSED! <<<\n"); return 0; }