'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const test = require('node:test'); const { createPs5FpkgReadinessService, combinePreservationAndReadiness, } = require('../../src/services/ps5-fpkg-readiness-service'); const TITLE_ID = 'PPSA12345'; const CONTENT_ID = 'UP0001-PPSA12345_00-CHIMERAFPKG00001'; const ELF = Buffer.concat([Buffer.from([0x7f, 0x45, 0x4c, 0x46]), Buffer.alloc(32, 0x11)]); const SELF = Buffer.concat([Buffer.from([0x54, 0x14, 0xf5, 0xee]), Buffer.alloc(32, 0x22)]); function fixture({ param = true, eboot = ELF } = {}) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-fpkg-readiness-')); if (param) { fs.mkdirSync(path.join(root, 'sce_sys'), { recursive: true }); fs.writeFileSync(path.join(root, 'sce_sys', 'param.json'), JSON.stringify({ titleId: TITLE_ID, contentId: CONTENT_ID })); } if (eboot) fs.writeFileSync(path.join(root, 'eboot.bin'), eboot); const service = createPs5FpkgReadinessService({ allowedRoots: [root] }); return { root, service }; } test('raw ELF base is ready with immutable tree and module provenance', async t => { const f = fixture(); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); const result = await f.service.analyze({ sourcePath: f.root, componentType: 'base', expectedTitleId: TITLE_ID, expectedContentId: CONTENT_ID }); assert.equal(result.schema, 'chimera.ps5-fpkg-readiness.v1'); assert.equal(result.state, 'ready'); assert.equal(result.ready, true); assert.match(result.sourceTreeSha256, /^[a-f0-9]{64}$/); assert.equal(result.modules.total, 1); assert.equal(result.modules.plaintext, 1); assert.equal(result.modules.unresolved, 0); assert.equal(result.moduleEvidence[0].provenance, 'raw-elf-from-source'); }); test('provider-decrypted stream claims become evidence only after local ELF reinspection', async t => { const f = fixture(); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); const service = createPs5FpkgReadinessService({ allowedRoots: [f.root], providerEvidence: { files: { 'eboot.bin': { decryptionVerified: true, provider: 'pinned-capture-adapter' } } }, }); const result = await service.analyze({ sourcePath: f.root, componentType: 'base' }); assert.equal(result.moduleEvidence[0].provenance, 'provider-decrypted-stream'); fs.writeFileSync(path.join(f.root, 'eboot.bin'), SELF); const blocked = await service.analyze({ sourcePath: f.root, componentType: 'base' }); assert.equal(blocked.moduleEvidence[0].provenance, 'signed-unresolved'); assert.equal(blocked.ready, false); }); test('SELF with a matching decrypted ELF counterpart is ready and substituted', async t => { const f = fixture({ eboot: SELF }); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); fs.mkdirSync(path.join(f.root, 'decrypted'), { recursive: true }); fs.writeFileSync(path.join(f.root, 'decrypted', 'eboot.bin'), ELF); const result = await f.service.analyze({ sourcePath: f.root, componentType: 'base' }); assert.equal(result.state, 'ready'); assert.equal(result.modules.substitutable, 1); assert.equal(result.moduleEvidence[0].provenance, 'decrypted-mirror-substitute'); assert.match(result.moduleEvidence[0].counterpartSha256, /^[a-f0-9]{64}$/); }); test('opaque executable wrapper with an exact decrypted ELF mirror is ready and substituted', async t => { const opaque = Buffer.concat([Buffer.from([0x01, 0x02, 0x03, 0x04]), Buffer.alloc(32, 0x55)]); const f = fixture({ eboot: opaque }); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); fs.mkdirSync(path.join(f.root, 'decrypted'), { recursive: true }); fs.writeFileSync(path.join(f.root, 'decrypted', 'eboot.bin'), ELF); const result = await f.service.analyze({ sourcePath: f.root, componentType: 'base' }); assert.equal(result.state, 'ready'); assert.equal(result.modules.substitutable, 1); assert.equal(result.moduleEvidence[0].format, 'opaque-executable'); assert.equal(result.moduleEvidence[0].provenance, 'decrypted-mirror-substitute'); }); test('SELF without suitable decrypted input blocks fPKG but not preservation', async t => { const f = fixture({ eboot: SELF }); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); const readiness = await f.service.analyze({ sourcePath: f.root, componentType: 'base' }); assert.equal(readiness.state, 'blocked'); assert.equal(readiness.modules.unresolved, 1); assert.equal(readiness.issues.some(issue => issue.code === 'PS5_FPKG_MODULE_UNRESOLVED'), true); const combined = combinePreservationAndReadiness({ complete: true, verified: true }, readiness); assert.equal(combined.preservation.complete, true); assert.equal(combined.preservation.verified, true); assert.equal(combined.fpkgReadiness.state, 'blocked'); assert.equal(combined.backupFailed, false); }); test('missing eboot, missing param.json, param.sfo and identity mismatch remain distinct blockers', async t => { const noEboot = fixture({ eboot: null }); t.after(() => fs.rmSync(noEboot.root, { recursive: true, force: true })); assert.equal((await noEboot.service.analyze({ sourcePath: noEboot.root, componentType: 'base' })).issues.some(item => item.code === 'PS5_FPKG_EBOOT_REQUIRED'), true); const noParam = fixture({ param: false }); t.after(() => fs.rmSync(noParam.root, { recursive: true, force: true })); fs.mkdirSync(path.join(noParam.root, 'sce_sys'), { recursive: true }); fs.writeFileSync(path.join(noParam.root, 'sce_sys', 'param.sfo'), 'legacy'); const missing = await noParam.service.analyze({ sourcePath: noParam.root, componentType: 'base' }); assert.equal(missing.issues.some(item => item.code === 'PS5_FPKG_PARAM_REQUIRED'), true); assert.equal(missing.issues.some(item => item.code === 'PS5_FPKG_PARAM_SFO_UNSUPPORTED'), true); const mismatch = fixture(); t.after(() => fs.rmSync(mismatch.root, { recursive: true, force: true })); const identity = await mismatch.service.analyze({ sourcePath: mismatch.root, componentType: 'base', expectedTitleId: 'PPSA99999' }); assert.equal(identity.issues.some(item => item.code === 'PS5_FPKG_TITLE_MISMATCH'), true); }); test('updates are preserved but unsupported for native fPKG conversion', async t => { const f = fixture(); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); const result = await f.service.analyze({ sourcePath: f.root, componentType: 'update' }); assert.equal(result.state, 'unsupported'); assert.equal(result.ready, false); assert.equal(result.issues[0].code, 'PS5_FPKG_UPDATE_UNSUPPORTED'); }); test('symlinks are rejected and same-size source mutation invalidates prior evidence', async t => { const f = fixture(); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); const first = await f.service.analyze({ sourcePath: f.root, componentType: 'base' }); fs.writeFileSync(path.join(f.root, 'eboot.bin'), Buffer.concat([Buffer.from([0x7f, 0x45, 0x4c, 0x46]), Buffer.alloc(32, 0x44)])); await assert.rejects(() => f.service.assertUnchanged(first), error => error.code === 'PS5_FPKG_SOURCE_CHANGED'); if (process.platform !== 'win32') { fs.symlinkSync('/tmp', path.join(f.root, 'escape')); await assert.rejects(() => f.service.analyze({ sourcePath: f.root, componentType: 'base' }), error => error.code === 'PS5_FPKG_SOURCE_UNSAFE'); } }); test('unsafe expected identity and paths fail closed instead of becoming readiness issues', async t => { const f = fixture(); t.after(() => fs.rmSync(f.root, { recursive: true, force: true })); await assert.rejects(() => f.service.analyze({ sourcePath: path.join(f.root, '..'), componentType: 'base' }), error => error.code === 'PS5_FPKG_SOURCE_UNSAFE'); await assert.rejects(() => f.service.analyze({ sourcePath: f.root, componentType: 'base', expectedContentId: '../PPSA12345' }), error => error.code === 'PS5_FPKG_IDENTITY_INVALID'); }); test('console staging integrates local readiness without treating provider defaults as decryption proof', () => { const staging = fs.readFileSync('src/services/ps5-console-staging-service.js', 'utf8'); const registry = fs.readFileSync('src/server/create-registry.js', 'utf8'); assert.match(staging, /selfDecryption: 'unproven-local-reinspection-required'/); assert.match(staging, /await readinessService\.analyze/); assert.match(staging, /preservation = \{ complete: true, verified: true/); assert.match(staging, /executableProvenance: 'locally-reinspected-after-download'/); assert.match(registry, /createPs5FpkgReadinessService/); assert.match(registry, /readinessService: ps5FpkgReadiness/); });