'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, ''); 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); });