Files
2026-10-10 17:34:54 +02:00

449 lines
16 KiB
C

/*
* Shared synthetic PKG fixture generator for host tests.
*
* Builds minimal but fully valid packages parseable by pkg_parser,
* pkg_scanner, pkg_cache (SMB), installer, multipart and
* tools/pkg_split.py — so no test needs real files from
* .for_reference/. All IDs/titles are fictional (CUSA/PPSA 9xxxx).
*
* Layouts mirror the structures pkg_parser.c understands:
* - PS4: raw "\x7fCNT" header + string table + param.sfo (+ icon0.png)
* - PS5: "\x7fFIH" header with CNT offset at 0x58 + CNT + param.json
*/
#ifndef TEST_FIXTURE_H
#define TEST_FIXTURE_H
#include <stdint.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Fake icon payload: real PNG magic + zero filler. */
#define FIXTURE_ICON_SIZE 1072
/* Synthetic image locations used by executable SDK regression tests. */
#define FIXTURE_SELF_OFFSET 0x20000
#define FIXTURE_SELF_SEGMENT_OFFSET 0x20
#define FIXTURE_ELF_PROC_OFFSET 0xB8
static const uint8_t kFixturePngMagic[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
static inline void fx_be32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
static inline void fx_le64(uint8_t *p, uint64_t v) {
for (int i = 0; i < 8; i++) {
p[i] = (uint8_t)(v >> (i * 8));
}
}
static inline void fx_le16(uint8_t *p, uint16_t v) {
p[0] = (uint8_t)v;
p[1] = (uint8_t)(v >> 8);
}
static inline void fx_le32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)v;
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
/* Builds a param.sfo with TITLE/TITLE_ID/CATEGORY/APP_VER. Returns size, 0 on overflow. */
static inline size_t fixture_build_sfo(uint8_t *out, size_t cap,
const char *title, const char *title_id,
const char *category, const char *app_ver) {
static const char *keys[] = {"TITLE", "TITLE_ID", "CATEGORY", "APP_VER"};
const char *vals[4];
vals[0] = title ? title : "";
vals[1] = title_id ? title_id : "";
vals[2] = category ? category : "gd";
vals[3] = app_ver ? app_ver : "01.00";
size_t key_off[4];
size_t key_total = 0;
for (int i = 0; i < 4; i++) {
key_off[i] = key_total;
key_total += strlen(keys[i]) + 1;
}
size_t data_off[4];
size_t data_total = 0;
size_t data_len[4];
for (int i = 0; i < 4; i++) {
data_off[i] = data_total;
data_len[i] = strlen(vals[i]) + 1;
data_total += data_len[i];
}
size_t key_start = 20 + 4 * 16;
size_t data_start = key_start + key_total;
size_t total = data_start + data_total;
if (total > cap) {
return 0;
}
memset(out, 0, total);
memcpy(out, "\x00PSF", 4);
out[4] = 1;
out[5] = 1;
fx_le32(out + 8, (uint32_t)key_start);
fx_le32(out + 12, (uint32_t)data_start);
fx_le32(out + 16, 4);
for (int i = 0; i < 4; i++) {
uint8_t *e = out + 20 + (size_t)i * 16;
fx_le16(e + 0, (uint16_t)key_off[i]);
e[2] = 0x04;
e[3] = 0x02; /* utf-8 string */
fx_le32(e + 4, (uint32_t)data_len[i]);
fx_le32(e + 8, (uint32_t)data_len[i]);
fx_le32(e + 12, (uint32_t)data_off[i]);
}
for (int i = 0; i < 4; i++) {
memcpy(out + key_start + key_off[i], keys[i], strlen(keys[i]) + 1);
memcpy(out + data_start + data_off[i], vals[i], data_len[i]);
}
return total;
}
/* Builds a PS5 param.json. Returns size, 0 on overflow. */
static inline size_t fixture_build_json(char *out, size_t cap,
const char *title_id, const char *title,
const char *category, const char *content_ver) {
int n = snprintf(out, cap,
"{\"titleId\":\"%s\",\"titleName\":\"%s\","
"\"category\":\"%s\",\"contentVersion\":\"%s\"}",
title_id ? title_id : "",
title ? title : "",
category ? category : "gd",
content_ver ? content_ver : "01.000.000");
if (n <= 0 || (size_t)n >= cap) {
return 0;
}
return (size_t)n;
}
static inline size_t fixture_build_multilang_json(char *out, size_t cap,
const char *title_id, const char *default_lang,
const char *category, const char *content_ver) {
int n = snprintf(out, cap,
"{\"titleId\":\"%s\","
"\"category\":\"%s\",\"contentVersion\":\"%s\","
"\"localizedParameters\":{"
"\"ar-AE\":{\"titleName\":\"Arabic Title\"},"
"\"defaultLanguage\":\"%s\","
"\"en-US\":{\"titleName\":\"English Title\"},"
"\"pl-PL\":{\"titleName\":\"Polish Title\"}"
"}}",
title_id ? title_id : "",
category ? category : "gd",
content_ver ? content_ver : "01.000.000",
default_lang ? default_lang : "en-US");
if (n <= 0 || (size_t)n >= cap) {
return 0;
}
return (size_t)n;
}
typedef struct {
const char *title_id;
const char *title;
const char *category; /* "gd" base, "gp" update, "ac" dlc */
const char *version; /* SFO "01.00" style or JSON "01.000.000" style */
int with_icon;
const char *content_id; /* NULL => derived EP0001-<tid>_00-TEST000000000001 */
int is_multilang;
const char *default_lang;
} fixture_pkg_spec_t;
/* Writes one CNT-based package. is_ps5 selects FIH+CNT vs raw CNT.
* Returns 0 on success. */
static inline int fixture_write_pkg(const char *path, int is_ps5, const fixture_pkg_spec_t *spec) {
if (!path || !spec || !spec->title_id) {
return -1;
}
uint8_t payload[8192];
size_t payload_len = 0;
const char *payload_name = NULL;
if (is_ps5) {
payload_name = "param.json";
if (spec->is_multilang) {
payload_len = fixture_build_multilang_json((char *)payload, sizeof(payload),
spec->title_id, spec->default_lang,
spec->category, spec->version);
} else {
payload_len = fixture_build_json((char *)payload, sizeof(payload),
spec->title_id, spec->title,
spec->category, spec->version);
}
} else {
payload_name = "param.sfo";
payload_len = fixture_build_sfo(payload, sizeof(payload),
spec->title, spec->title_id,
spec->category, spec->version);
}
if (payload_len == 0) {
return -1;
}
/* String table: payload name + optional icon name. */
char strtab[64];
size_t icon_name_off = 0;
size_t strtab_len = strlen(payload_name) + 1;
memcpy(strtab, payload_name, strtab_len);
if (spec->with_icon) {
icon_name_off = strtab_len;
memcpy(strtab + strtab_len, "icon0.png", 10);
strtab_len += 10;
}
int n_entries = spec->with_icon ? 3 : 2;
size_t table_size = (size_t)n_entries * 32;
size_t data_base = 0x80 + table_size;
size_t strtab_off = data_base;
size_t payload_off = strtab_off + strtab_len;
size_t icon_off = payload_off + payload_len;
FILE *f = fopen(path, "wb");
if (!f) {
return -1;
}
uint64_t cnt_offset = 0;
if (is_ps5) {
/* FIH header with CNT offset at 0x58 (0x1000-aligned). */
uint8_t fih[0x200];
memset(fih, 0, sizeof(fih));
memcpy(fih, "\x7f" "FIH", 4);
cnt_offset = 0x10000;
fx_le64(fih + 0x58, cnt_offset);
if (fwrite(fih, 1, sizeof(fih), f) != sizeof(fih)) {
fclose(f);
return -1;
}
/* Pad up to the CNT offset. */
uint8_t zero[4096];
memset(zero, 0, sizeof(zero));
size_t pad_left = (size_t)cnt_offset - sizeof(fih);
while (pad_left > 0) {
size_t w = pad_left > sizeof(zero) ? sizeof(zero) : pad_left;
if (fwrite(zero, 1, w, f) != w) {
fclose(f);
return -1;
}
pad_left -= w;
}
}
/* CNT header. */
uint8_t cnt[0x80];
memset(cnt, 0, sizeof(cnt));
memcpy(cnt, "\x7f" "CNT", 4);
fx_be32(cnt + 0x10, (uint32_t)n_entries);
fx_be32(cnt + 0x18, 0x80);
{
char cid[49];
if (spec->content_id && spec->content_id[0] != '\0') {
strncpy(cid, spec->content_id, sizeof(cid) - 1);
cid[sizeof(cid) - 1] = '\0';
} else {
snprintf(cid, sizeof(cid), "EP0001-%.9s_00-TEST000000000001", spec->title_id);
}
memcpy(cnt + 0x40, cid, strlen(cid));
}
if (fwrite(cnt, 1, sizeof(cnt), f) != sizeof(cnt)) {
fclose(f);
return -1;
}
/* Entry table: [strtab][payload][icon?]. Offsets are CNT-relative. */
for (int i = 0; i < n_entries; i++) {
uint8_t e[32];
memset(e, 0, sizeof(e));
if (i == 0) {
fx_be32(e + 0, 0x0200);
fx_be32(e + 4, 0);
fx_be32(e + 16, (uint32_t)strtab_off);
fx_be32(e + 20, (uint32_t)strtab_len);
} else if (i == 1) {
fx_be32(e + 0, is_ps5 ? 0x2000 : 0x1000);
fx_be32(e + 4, 0);
fx_be32(e + 16, (uint32_t)payload_off);
fx_be32(e + 20, (uint32_t)payload_len);
} else {
fx_be32(e + 0, 0x1200);
fx_be32(e + 4, (uint32_t)icon_name_off);
fx_be32(e + 16, (uint32_t)icon_off);
fx_be32(e + 20, FIXTURE_ICON_SIZE);
}
if (fwrite(e, 1, sizeof(e), f) != sizeof(e)) {
fclose(f);
return -1;
}
}
if (fwrite(strtab, 1, strtab_len, f) != strtab_len) {
fclose(f);
return -1;
}
if (fwrite(payload, 1, payload_len, f) != payload_len) {
fclose(f);
return -1;
}
if (spec->with_icon) {
uint8_t icon[FIXTURE_ICON_SIZE];
memset(icon, 0, sizeof(icon));
memcpy(icon, kFixturePngMagic, sizeof(kFixturePngMagic));
if (fwrite(icon, 1, sizeof(icon), f) != sizeof(icon)) {
fclose(f);
return -1;
}
}
fclose(f);
return 0;
}
static inline int fixture_write_ps4_pkg(const char *path, const char *title_id,
const char *title, const char *category,
const char *app_ver) {
fixture_pkg_spec_t spec;
memset(&spec, 0, sizeof(spec));
spec.title_id = title_id;
spec.title = title;
spec.category = category;
spec.version = app_ver;
spec.with_icon = 0;
return fixture_write_pkg(path, 0, &spec);
}
static inline int fixture_write_ps5_pkg(const char *path, const char *title_id,
const char *title, const char *category,
const char *content_ver, int with_icon) {
fixture_pkg_spec_t spec;
memset(&spec, 0, sizeof(spec));
spec.title_id = title_id;
spec.title = title;
spec.category = category;
spec.version = content_ver;
spec.with_icon = with_icon;
return fixture_write_pkg(path, 1, &spec);
}
static inline int fixture_write_ps5_pkg_multilang(const char *path, const char *title_id,
const char *default_lang, const char *category,
const char *content_ver, int with_icon) {
fixture_pkg_spec_t spec;
memset(&spec, 0, sizeof(spec));
spec.title_id = title_id;
spec.default_lang = default_lang;
spec.category = category;
spec.version = content_ver;
spec.with_icon = with_icon;
spec.is_multilang = 1;
return fixture_write_pkg(path, 1, &spec);
}
/* Grows a file to target_size with deterministic filler. Returns 0 on success. */
static inline int fixture_grow_file(const char *path, uint64_t target_size) {
FILE *f = fopen(path, "r+b");
if (!f) {
return -1;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return -1;
}
long cur = ftell(f);
if (cur < 0) {
fclose(f);
return -1;
}
static uint8_t blk[1024 * 1024];
static int blk_init = 0;
if (!blk_init) {
for (size_t i = 0; i < sizeof(blk); i++) {
blk[i] = (uint8_t)((i * 31 + 7) & 0xFF);
}
blk_init = 1;
}
uint64_t left = (target_size > (uint64_t)cur) ? (target_size - (uint64_t)cur) : 0;
while (left > 0) {
size_t w = left > sizeof(blk) ? sizeof(blk) : (size_t)left;
if (fwrite(blk, 1, w, f) != w) {
fclose(f);
return -1;
}
left -= w;
}
fclose(f);
return 0;
}
/* Sparse synthetic PS5 image with an uncompressed main SELF and a CNT table.
* Metadata and process parameters can be placed beyond 32-bit offsets. */
static inline int fixture_write_ps5_image(const char *path, int lih,
uint64_t cnt_offset, uint64_t sdk_offset,
uint32_t sdk, uint32_t content_flags) {
if (fixture_write_ps5_pkg(path, "PPSA90090", "Synthetic Image", "gd",
"07.000.004", 1) != 0) return -1;
FILE *f = fopen(path, "rb");
if (!f) return -1;
uint8_t cnt[8192];
if (fseek(f, 0x10000, SEEK_SET) != 0) { fclose(f); return -1; }
size_t cnt_size = fread(cnt, 1, sizeof(cnt), f);
fclose(f);
fx_be32(cnt + 0x78, content_flags);
uint8_t header[0x80] = {0}, inner[0x80] = {0};
memcpy(header, lih ? "\x7f" "LIH" : "\x7f" "FIH", 4);
fx_le16(header + 6, lih ? 1 : 3);
fx_le64(header + (lih ? 0x30 : 0x58), cnt_offset);
if (lih) {
fx_le64(header + 0x20, 0x10000);
memcpy(inner, "\x7f" "FIH", 4);
fx_le16(inner + 6, 3);
fx_le64(inner + 0x10, 0x10000);
} else fx_le64(header + 0x10, FIXTURE_SELF_OFFSET);
uint8_t self[8192] = {0};
fx_le32(self, 0xEEF51454);
fx_le16(self + 24, 1);
uint64_t segment_offset = sdk_offset - FIXTURE_SELF_OFFSET - 0x20;
fx_le64(self + 16, segment_offset + 0x1000);
fx_le64(self + 32, 0x804);
fx_le64(self + 40, segment_offset);
fx_le64(self + 48, 0x1000);
fx_le64(self + 56, 0x1000);
memcpy(self + 64, "\x7f" "ELF\x02\x01\x01", 7);
fx_le16(self + 64 + 18, 62);
fx_le64(self + 64 + 32, 64);
fx_le16(self + 64 + 54, 56);
fx_le16(self + 64 + 56, 2);
fx_le32(self + 128, 1);
fx_le64(self + 128 + 8, 0x1000);
fx_le64(self + 128 + 32, 0x1000);
fx_le32(self + FIXTURE_ELF_PROC_OFFSET, 0x61000001);
fx_le64(self + FIXTURE_ELF_PROC_OFFSET + 8, 0x1020);
fx_le64(self + FIXTURE_ELF_PROC_OFFSET + 32, 32);
uint8_t proc[32] = {0};
fx_le64(proc, sizeof(proc));
fx_le32(proc + 8, 0x4942524F);
fx_le32(proc + 20, sdk);
f = fopen(path, "wb");
if (!f) return -1;
int ok = fwrite(header, 1, sizeof(header), f) == sizeof(header);
if (lih) ok = ok && fseeko(f, 0x10000, SEEK_SET) == 0 &&
fwrite(inner, 1, sizeof(inner), f) == sizeof(inner);
ok = ok && fseeko(f, FIXTURE_SELF_OFFSET, SEEK_SET) == 0 &&
fwrite(self, 1, sizeof(self), f) == sizeof(self) &&
fseeko(f, (off_t)sdk_offset, SEEK_SET) == 0 && fwrite(proc, 1, sizeof(proc), f) == sizeof(proc) &&
fseeko(f, (off_t)cnt_offset, SEEK_SET) == 0 && fwrite(cnt, 1, cnt_size, f) == cnt_size;
if (fclose(f) != 0) ok = 0;
return ok ? 0 : -1;
}
#endif /* TEST_FIXTURE_H */