Files
chimera-Public/test/server/ch-1101-ps4-package-backup.test.js
T

166 lines
11 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const test = require('node:test');
const { createPs4PackageBackupService } = require('../../src/services/ps4-package-backup-service');
const { PINNED_PS4_BUILDER, createPs4PkgBuilderProvider } = require('../../src/services/ps4-pkg-builder-provider');
const database = require('../../src/database');
const { registerPs4PackageManifest } = require('../../src/services/ps4-package-registration-service');
function sfo(values) {
const entries = Object.entries(values);
const keys = Buffer.from(`${entries.map(([key]) => key).join('\0')}\0`);
const encoded = entries.map(([, value]) => Buffer.from(`${value}\0`));
const data = Buffer.concat(encoded);
const table = Buffer.alloc(entries.length * 16);
let keyOffset = 0;
let dataOffset = 0;
entries.forEach(([key], index) => {
const value = encoded[index];
table.writeUInt16LE(keyOffset, index * 16);
table.writeUInt16LE(0x0204, index * 16 + 2);
table.writeUInt32LE(value.length, index * 16 + 4);
table.writeUInt32LE(value.length, index * 16 + 8);
table.writeUInt32LE(dataOffset, index * 16 + 12);
keyOffset += Buffer.byteLength(key) + 1;
dataOffset += value.length;
});
const header = Buffer.alloc(20);
header.writeUInt32LE(0x46535000, 0);
header.writeUInt32LE(0x00000101, 4);
header.writeUInt32LE(20 + table.length, 8);
header.writeUInt32LE(20 + table.length + keys.length, 12);
header.writeUInt32LE(entries.length, 16);
return Buffer.concat([header, table, keys, data]);
}
function writeDump(root, name, { titleId = 'CUSA12345', category = 'gd', version = '01.00', keystone = category === 'gd' } = {}) {
const folder = path.join(root, name);
fs.mkdirSync(path.join(folder, 'sce_sys'), { recursive: true });
fs.writeFileSync(path.join(folder, 'eboot.bin'), 'verified executable');
fs.writeFileSync(path.join(folder, 'sce_sys', 'param.sfo'), sfo({
TITLE: 'Test Game', TITLE_ID: titleId, CATEGORY: category, APP_VER: version,
CONTENT_ID: `UP0001-${titleId}_00-TESTGAME00000001`,
}));
if (keystone) fs.writeFileSync(path.join(folder, 'sce_sys', 'keystone'), 'key');
return folder;
}
function fixture(t, overrides = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps4-package-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const libraryRoot = path.join(root, 'library');
const outputRoot = path.join(root, 'packages');
fs.mkdirSync(libraryRoot, { recursive: true });
const registrations = [];
const provider = overrides.provider || {
probe: async () => ({ available: true, ...PINNED_PS4_BUILDER }),
build: async input => {
fs.mkdirSync(path.dirname(input.outputPath), { recursive: true });
fs.writeFileSync(input.outputPath, `pkg:${input.component.type}:${input.component.titleId}`);
return { provider: PINNED_PS4_BUILDER, outputPath: input.outputPath };
},
};
const service = createPs4PackageBackupService({
allowedRoots: () => [libraryRoot, outputRoot], outputRoot, stateFile: path.join(root, 'state', 'jobs.json'),
provider, registrar: async manifest => { registrations.push(manifest); return { packageSetId: 77 }; },
idFactory: overrides.idFactory || (() => 'job-1'), now: overrides.now || (() => 1_700_000_000_000),
});
return { root, libraryRoot, outputRoot, registrations, service };
}
test('validates base and patch dumps from SFO evidence rather than folder names', async t => {
const f = fixture(t);
const base = writeDump(f.libraryRoot, 'anything', { category: 'gd' });
const patch = writeDump(f.libraryRoot, 'also-not-trusted', { category: 'gp', version: '01.23', keystone: false });
const baseResult = await f.service.inspect({ sourcePath: base, componentType: 'base', expectedTitleId: 'CUSA12345' });
const patchResult = await f.service.inspect({ sourcePath: patch, componentType: 'update', expectedTitleId: 'CUSA12345' });
assert.equal(baseResult.ready, true);
assert.deepEqual({ titleId: patchResult.titleId, version: patchResult.version, category: patchResult.category }, { titleId: 'CUSA12345', version: '01.23', category: 'gp' });
});
test('rejects incomplete, mismatched and symlink-escaped dumps', async t => {
const f = fixture(t);
const missingKeystone = writeDump(f.libraryRoot, 'missing-key', { keystone: false });
await assert.rejects(() => f.service.inspect({ sourcePath: missingKeystone, componentType: 'base' }), error => error.code === 'PS4_DUMP_KEYSTONE_REQUIRED');
const wrong = writeDump(f.libraryRoot, 'wrong-title');
await assert.rejects(() => f.service.inspect({ sourcePath: wrong, componentType: 'base', expectedTitleId: 'CUSA99999' }), error => error.code === 'PS4_DUMP_TITLE_MISMATCH');
const outside = path.join(f.root, 'outside'); fs.mkdirSync(outside); fs.writeFileSync(path.join(outside, 'payload.bin'), 'x');
const linked = writeDump(f.libraryRoot, 'linked');
fs.symlinkSync(outside, path.join(linked, 'escape'), process.platform === 'win32' ? 'junction' : 'dir');
await assert.rejects(() => f.service.inspect({ sourcePath: linked, componentType: 'base' }), error => error.code === 'PS4_DUMP_SYMLINK');
});
test('builds a deterministic ordered package set, hashes outputs and registers only after verification', async t => {
const f = fixture(t);
const base = writeDump(f.libraryRoot, 'base');
const update = writeDump(f.libraryRoot, 'update', { category: 'gp', version: '01.10', keystone: false });
const created = await f.service.create({ expectedTitleId: 'CUSA12345', sources: [{ sourcePath: update, componentType: 'update' }, { sourcePath: base, componentType: 'base' }] });
assert.equal(created.status, 'queued');
const completed = await f.service.run(created.id);
assert.equal(completed.status, 'completed');
assert.deepEqual(completed.children.map(item => item.type), ['base', 'update']);
assert.equal(f.registrations.length, 1);
assert.deepEqual(f.registrations[0].recipe.installOrder.map(item => item.componentId), ['base', 'update']);
for (const component of f.registrations[0].components) {
assert.match(component.sha256, /^[a-f0-9]{64}$/);
assert.equal(component.sha256, crypto.createHash('sha256').update(fs.readFileSync(component.absolutePath)).digest('hex'));
}
});
test('active jobs become interrupted after restart and require explicit resume', async t => {
const f = fixture(t);
const base = writeDump(f.libraryRoot, 'base');
const job = await f.service.create({ sources: [{ sourcePath: base, componentType: 'base' }] });
const stored = JSON.parse(fs.readFileSync(path.join(f.root, 'state', 'jobs.json'), 'utf8'));
stored[0].status = 'building'; stored[0].children[0].status = 'building';
fs.writeFileSync(path.join(f.root, 'state', 'jobs.json'), JSON.stringify(stored));
const restarted = createPs4PackageBackupService({ allowedRoots: () => [f.libraryRoot, f.outputRoot], outputRoot: f.outputRoot, stateFile: path.join(f.root, 'state', 'jobs.json'), provider: { probe: async () => ({ available: true }), build: async () => ({}) }, registrar: async () => ({}) });
assert.equal(restarted.get(job.id).status, 'interrupted');
assert.equal(restarted.resume(job.id).status, 'queued');
});
test('creates a short-lived hash-bound package handoff without exposing arbitrary files', async t => {
const f = fixture(t);
const pkg = path.join(f.outputRoot, 'verified.pkg'); fs.writeFileSync(pkg, 'verified package');
const sha256 = crypto.createHash('sha256').update('verified package').digest('hex');
const exposed = await f.service.expose({ filePath: pkg, sha256, size: 16, origin: 'https://chimera.example.test' });
assert.match(exposed.url, /^https:\/\/chimera\.example\.test\/ps4-package\/[A-Za-z0-9_-]{43}\/verified\.pkg$/);
assert.equal(f.service.resolvePackageHandoff(new URL(exposed.url).pathname).filePath, fs.realpathSync(pkg));
await assert.rejects(() => f.service.expose({ filePath: pkg, sha256: '0'.repeat(64), size: 16, origin: 'https://chimera.example.test' }), error => error.code === 'PS4_PACKAGE_HANDOFF_HASH');
await assert.rejects(() => f.service.expose({ filePath: pkg, sha256, size: 16, origin: 'http://chimera.example.test' }), error => error.code === 'PS4_PACKAGE_ORIGIN');
});
test('pinned provider invokes only the fixed executable without a shell or caller-controlled arguments', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps4-builder-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const executable = path.join(root, PINNED_PS4_BUILDER.executable);
fs.writeFileSync(executable, 'tool');
const projectPath = path.join(root, 'work', 'project.gp4'); const outputPath = path.join(root, 'work', 'out.pkg'); const tempPath = path.join(root, 'work', 'tmp');
fs.mkdirSync(path.dirname(projectPath), { recursive: true }); fs.writeFileSync(projectPath, '<psproject />');
const calls = [];
const sourcePath = path.join(root, 'source'); fs.mkdirSync(sourcePath);
const provider = createPs4PkgBuilderProvider({ toolRoot: root, allowedWorkRoots: [path.join(root, 'work')], allowedSourceRoots: [sourcePath], runner: async input => { calls.push(input); fs.writeFileSync(outputPath, 'pkg'); return { code: 0, output: 'ok' }; } });
assert.equal((await provider.probe()).available, true);
await provider.build({ projectPath, outputPath, tempPath, sourcePath, component: { type: 'base', titleId: 'CUSA12345' } });
assert.equal(calls[0].command, fs.realpathSync(executable));
assert.equal(calls[0].shell, false);
assert.equal(calls[0].cwd, fs.realpathSync(sourcePath));
assert.deepEqual(calls[0].args.slice(0, 5), ['img_create', '--no_progress_bar', '--oformat', 'pkg', '--tmp_path']);
await assert.rejects(() => provider.build({ projectPath: path.join(fixture(t).root, 'evil.gp4'), outputPath, tempPath, sourcePath, component: { type: 'base' } }), error => error.code === 'PS4_BUILDER_PATH');
});
test('registers verified outputs as a package set and available local copy without marking a console install', async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ps4-registration-'));
database.configure({ dataDir: path.join(root, 'db'), logger: { info() {}, warn() {}, error() {} } }); await database.initialize(); t.after(() => { database.close(); fs.rmSync(root, { recursive: true, force: true }); });
const outputRoot = path.join(root, 'vault'); const folder = path.join(outputRoot, 'CUSA12345', 'job-1'); fs.mkdirSync(folder, { recursive: true });
const pkg = path.join(folder, 'base.pkg'); fs.writeFileSync(pkg, 'pkg');
const manifest = { schemaVersion: 1, kind: 'chimera.ps4-package-backup.v1', createdAt: 100, title: { platform: 'ps4', titleId: 'CUSA12345', name: 'Test Game' }, components: [{ id: 'base', type: 'base', version: '01.00', contentId: 'UP0001-CUSA12345_00-TESTGAME00000001', size: 3, sha256: crypto.createHash('sha256').update('pkg').digest('hex'), absolutePath: pkg, originalRelativePath: 'base.pkg', normalizedRelativePath: 'base/base.pkg', required: true, order: 1, adapterHint: 'ps4-package', provenance: [] }], dependencies: [], recipe: { installOrder: [{ componentId: 'base', order: 1 }] } };
const registered = registerPs4PackageManifest({ db: database, outputRoot, manifest });
assert.ok(registered.packageSetId); assert.equal(database.runQuery('SELECT COUNT(*) AS count FROM local_copies')[0].count, 1); assert.equal(database.runQuery('SELECT COUNT(*) AS count FROM console_installs')[0].count, 0);
});