Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,320 @@
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const JSZip = require('jszip');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { SettingsStore } = require('../../src/persistence/settings-store');
|
||||
const { createEcosystemService, patchIni, iniDiff, SERVICE_DEFINITIONS, parseRepoArchive, parseItemzflowLog, parsePkgbuild } = require('../../src/services/ecosystem-service');
|
||||
|
||||
function fixture() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-ecosystem-'));
|
||||
const paths = Object.fromEntries(['backups', 'cache', 'tmp'].map(name => [name, path.join(root, name)]));
|
||||
for (const directory of Object.values(paths)) fs.mkdirSync(directory, { recursive: true });
|
||||
const settings = new SettingsStore(path.join(root, 'config', 'settings.json'));
|
||||
settings.set('ftp.host', '127.0.0.1');
|
||||
const secretValues = new Map();
|
||||
const secrets = {
|
||||
set(name, value) { if (value) secretValues.set(name, value); else secretValues.delete(name); },
|
||||
has: name => secretValues.has(name),
|
||||
get: name => secretValues.get(name) || '',
|
||||
};
|
||||
const remote = new Map([
|
||||
['/data/etaHEN/config.ini', Buffer.from('# preserve\nFTP=1\nPS5Debug=0\nUnknownFutureKey=42\n')],
|
||||
['/data/homebrew/Demo/eboot.elf', Buffer.from('ELF')],
|
||||
['/data/homebrew/Demo/sce_sys/icon0.png', Buffer.from('PNG')],
|
||||
['/data/etaHEN/plugins/example.elf', Buffer.from('binary TEST00001 version 1.23')],
|
||||
['/mnt/usb0/etaHEN/plugins/example.elf', Buffer.from('usb TEST00001 version 1.24')],
|
||||
]);
|
||||
const directories = new Set([
|
||||
'/data', '/data/etaHEN', '/data/etaHEN/plugins', '/data/homebrew', '/data/homebrew/Demo', '/data/homebrew/Demo/sce_sys',
|
||||
'/mnt', '/mnt/usb0', '/mnt/usb0/etaHEN', '/mnt/usb0/etaHEN/plugins',
|
||||
]);
|
||||
const children = directory => {
|
||||
const rows = new Map();
|
||||
for (const candidate of directories) {
|
||||
if (candidate === directory || path.posix.dirname(candidate) !== directory) continue;
|
||||
rows.set(path.posix.basename(candidate), { name: path.posix.basename(candidate), isDir: true, size: 0 });
|
||||
}
|
||||
for (const [candidate, content] of remote) {
|
||||
if (path.posix.dirname(candidate) !== directory) continue;
|
||||
rows.set(path.posix.basename(candidate), { name: path.posix.basename(candidate), isDir: false, size: content.length });
|
||||
}
|
||||
return [...rows.values()];
|
||||
};
|
||||
const ftp = {
|
||||
isConnected: () => true,
|
||||
hasCredentials: () => true,
|
||||
listDir: async directory => {
|
||||
if (!directories.has(directory)) throw new Error('missing directory');
|
||||
return children(directory);
|
||||
},
|
||||
downloadFile: async (remotePath, localPath) => {
|
||||
if (!remote.has(remotePath)) throw new Error('missing file');
|
||||
fs.writeFileSync(localPath, remote.get(remotePath));
|
||||
},
|
||||
uploadFile: async (localPath, remotePath) => {
|
||||
remote.set(remotePath, fs.readFileSync(localPath));
|
||||
let current = path.posix.dirname(remotePath);
|
||||
while (current && current !== '.') { directories.add(current); if (current === '/') break; current = path.posix.dirname(current); }
|
||||
},
|
||||
deleteRemote: async remotePath => remote.delete(remotePath),
|
||||
renameRemote: async (source, destination) => {
|
||||
if (!remote.has(source)) throw new Error('missing source');
|
||||
remote.set(destination, remote.get(source)); remote.delete(source);
|
||||
},
|
||||
};
|
||||
const db = {
|
||||
listGames: () => [{ game_id: 'CUSA00001', title: 'Test Game',
|
||||
availability: { confirmedInstallCount: 1 },
|
||||
consoleInstalls: [{ installState: 'observed', installedVersion: '1.00' }] }],
|
||||
};
|
||||
const service = createEcosystemService({ db, ftp, settings, secrets, paths, logger: { info() {}, warn() {}, error() {} } });
|
||||
return { root, paths, settings, secrets, remote, service };
|
||||
}
|
||||
|
||||
test('etaHEN INI patching preserves comments and unknown future keys', () => {
|
||||
const before = '# comment\nFTP=1\nFutureSetting=abc\n';
|
||||
const after = patchIni(before, { FTP: 0, Klog: 1 });
|
||||
assert.match(after, /^# comment/m);
|
||||
assert.match(after, /^FutureSetting=abc/m);
|
||||
assert.match(after, /^FTP=0/m);
|
||||
assert.match(after, /^Klog=1/m);
|
||||
assert.deepEqual(iniDiff(before, after).map(item => item.key).sort(), ['FTP', 'Klog']);
|
||||
assert.throws(() => patchIni(before, { UnknownSetting: 1 }), /Onbekende etaHEN-instelling/);
|
||||
});
|
||||
|
||||
test('service matrix covers every Chimera PS5 integration on unique ports', () => {
|
||||
const ports = SERVICE_DEFINITIONS.map(service => service.port);
|
||||
assert.equal(new Set(ports).size, ports.length);
|
||||
for (const required of [1337, 2121, 9021, 6969, 8080, 8082, 8084, 9090, 9081, 9112, 3232, 12800]) {
|
||||
assert.ok(ports.includes(required), `missing service port ${required}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('GitHub branch archives are parsed with their generated root removed', async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file('PS5_Cheats-main/json/PPSA00001_1.00.json', '{"mods":[]}');
|
||||
zip.file('PS5_Cheats-main/README.md', '# Cheats');
|
||||
const entries = await parseRepoArchive(await zip.generateAsync({ type: 'nodebuffer' }));
|
||||
assert.deepEqual(entries.map(entry => entry.path).sort(), ['README.md', 'json/PPSA00001_1.00.json']);
|
||||
assert.equal(await entries.find(entry => entry.path.endsWith('.json')).readText(), '{"mods":[]}');
|
||||
await assert.rejects(() => entries.find(entry => entry.path.endsWith('.json')).readText(2), /te groot/);
|
||||
});
|
||||
|
||||
test('Itemzflow parser separates jobs, warnings and real errors without false positives', () => {
|
||||
const parsed = parseItemzflowLog([
|
||||
'[2026-07-16 12:00:00] Installing package 25%',
|
||||
'[2026-07-16 12:00:01] Daemon returned with no errors',
|
||||
'[2026-07-16 12:00:02] WARNING mount retry',
|
||||
'[2026-07-16 12:00:03] ERROR dump failed',
|
||||
].join('\n'), { id: 'app', label: 'App', path: '/app.log' });
|
||||
assert.equal(parsed.lines, 4);
|
||||
assert.equal(parsed.errorCount, 1);
|
||||
assert.equal(parsed.warningCount, 1);
|
||||
assert.equal(parsed.latestProgress.progress, 25);
|
||||
assert.ok(parsed.events.some(event => event.type === 'install'));
|
||||
assert.ok(parsed.events.some(event => event.type === 'error'));
|
||||
});
|
||||
|
||||
test('PacBrew PKGBUILD metadata is parsed as inert text', () => {
|
||||
const metadata = parsePkgbuild(`pkgname=ps5-payload-websrv\npkgver=git\npkgdesc="A web server"\nlicense=('GPLv3')\ndepends=('ps5-payload-libmicrohttpd')\nmakedepends=('ps5-payload-sdk' 'ps5-payload-libsmb2')\nsource=("git+https://github.com/ps5-payload-dev/websrv.git")\n`);
|
||||
assert.equal(metadata.pkgname, 'ps5-payload-websrv');
|
||||
assert.equal(metadata.description, 'A web server');
|
||||
assert.deepEqual(metadata.licenses, ['GPLv3']);
|
||||
assert.deepEqual(metadata.dependencies, ['ps5-payload-libmicrohttpd']);
|
||||
assert.deepEqual(metadata.buildDependencies, ['ps5-payload-sdk', 'ps5-payload-libsmb2']);
|
||||
});
|
||||
|
||||
test('Itemzflow log center combines every available official log and tolerates missing logs', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
f.remote.set('/user/app/ITEM00001/logs/itemzflow_app.log', Buffer.from('Installing package 50%\nDone\n'));
|
||||
f.remote.set('/data/itemzflow_daemon/daemon.log', Buffer.from('WARNING NFS mount retry\n'));
|
||||
const packet = await f.service.itemzflowStatus();
|
||||
assert.equal(packet.online, true);
|
||||
assert.equal(packet.logs.length, 5);
|
||||
assert.equal(packet.health.available, 2);
|
||||
assert.equal(packet.health.missing, 3);
|
||||
assert.equal(packet.health.errors, 0);
|
||||
assert.equal(packet.health.warnings, 1);
|
||||
assert.equal(packet.latestProgress.progress, 50);
|
||||
assert.ok(packet.events.some(event => event.source === 'daemon'));
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('etaHEN profile changes are snapshotted, uploaded and verified', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const current = await f.service.getEtaHenConfig();
|
||||
const preview = await f.service.previewEtaHen({ raw: current.raw, patch: { PS5Debug: 1, Klog: 1 } });
|
||||
assert.equal(preview.diff.length, 2);
|
||||
assert.match(preview.proposed, /UnknownFutureKey=42/);
|
||||
const result = await f.service.applyEtaHen({ proposed: preview.proposed, expectedHash: current.hash, label: 'Test' });
|
||||
assert.equal(result.changed, 2);
|
||||
assert.equal(f.service.etaHistory().length, 1);
|
||||
assert.match(f.remote.get('/data/etaHEN/config.ini').toString(), /PS5Debug=1/);
|
||||
assert.match(f.remote.get('/data/etaHEN/config.ini').toString(), /Klog=1/);
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('homebrew and plugins are inventoried with icons, priority and safe toggles', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const homebrew = await f.service.scanHomebrew();
|
||||
assert.equal(homebrew.items.length, 1);
|
||||
assert.equal(homebrew.items[0].name, 'Demo');
|
||||
assert.match(homebrew.items[0].iconUrl, /^\/pork-cache\/homebrew-icons\//);
|
||||
|
||||
let plugins = await f.service.scanPlugins();
|
||||
assert.equal(plugins.items.length, 2);
|
||||
assert.equal(plugins.items.filter(item => item.duplicate).length, 2);
|
||||
assert.equal(plugins.items.find(item => item.location === 'usb0').effective, true);
|
||||
assert.equal(plugins.skippedUnmountedUsbRoots, 6);
|
||||
const internal = plugins.items.find(item => item.location === 'internal');
|
||||
const inspected = await f.service.inspectPlugin({ id: internal.id });
|
||||
assert.deepEqual(inspected.titleIds, ['TEST00001']);
|
||||
const toggled = await f.service.togglePlugin({ id: internal.id, enabled: false });
|
||||
assert.equal(toggled.ok, true);
|
||||
assert.equal(f.remote.has('/data/etaHEN/plugins/example.elf.disabled'), true);
|
||||
plugins = toggled.inventory;
|
||||
assert.equal(plugins.items.find(item => item.location === 'internal').enabled, false);
|
||||
|
||||
const pluginBytes = Buffer.from('plugin TEST00002 version 2.00');
|
||||
const installed = await f.service.installPlugin({
|
||||
filename: 'new-plugin.prx',
|
||||
data: pluginBytes,
|
||||
targetRoot: '/data/etaHEN/plugins',
|
||||
});
|
||||
assert.equal(installed.path, '/data/etaHEN/plugins/new-plugin.prx');
|
||||
assert.equal(installed.sha256, crypto.createHash('sha256').update(pluginBytes).digest('hex'));
|
||||
assert.deepEqual(f.remote.get(installed.path), pluginBytes);
|
||||
assert.ok(installed.inventory.items.some(item => item.filename === 'new-plugin.prx'));
|
||||
await assert.rejects(
|
||||
() => f.service.installPlugin({ filename: '../unsafe.prx', data: pluginBytes, targetRoot: '/data/etaHEN/plugins' }),
|
||||
/valid \.elf or \.prx plugin file/
|
||||
);
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('cheat catalog is filtered and paginated server-side', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const items = Array.from({ length: 135 }, (_, index) => ({
|
||||
key: `cheat-${index}`,
|
||||
title: `Catalog Game ${String(index).padStart(3, '0')}`,
|
||||
gameId: index === 0 ? 'CUSA00001' : `PPSA${String(index).padStart(5, '0')}`,
|
||||
sourceName: 'Test Source',
|
||||
sourceId: 'test',
|
||||
path: `/json/cheat-${index}.json`,
|
||||
platform: index % 2 ? 'ps5' : 'ps4',
|
||||
kind: 'cheat',
|
||||
format: 'json',
|
||||
}));
|
||||
const cacheFile = path.join(f.paths.cache, 'ecosystem', 'cheats-v2.json');
|
||||
fs.writeFileSync(cacheFile, JSON.stringify({ schema: 1, syncedAt: 123, items, errors: [] }));
|
||||
|
||||
const first = f.service.cheatIndex({ limit: 50 });
|
||||
assert.equal(first.total, 135);
|
||||
assert.equal(first.items.length, 50);
|
||||
assert.equal(first.offset, 0);
|
||||
assert.equal(first.nextOffset, 50);
|
||||
assert.equal(first.previousOffset, null);
|
||||
|
||||
const last = f.service.cheatIndex({ limit: 50, offset: first.nextOffset });
|
||||
assert.equal(last.items.length, 50);
|
||||
assert.equal(last.previousOffset, 0);
|
||||
assert.equal(last.nextOffset, 100);
|
||||
|
||||
const installed = f.service.cheatIndex({ installedOnly: true, limit: 50 });
|
||||
assert.equal(installed.total, 1);
|
||||
assert.equal(installed.items[0].installedTitle, 'Test Game');
|
||||
|
||||
const searched = f.service.cheatIndex({ query: 'Catalog Game 134', limit: 50 });
|
||||
assert.equal(searched.total, 1);
|
||||
assert.equal(searched.items[0].key, 'cheat-134');
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('remote source passwords never leave the encrypted secret interface', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const sources = f.service.saveRemoteSource({ label: 'Unraid', protocol: 'smb', host: 'server.example.test', path: '/Media/roms', username: 'chimera', password: 'secret' });
|
||||
assert.equal(sources.length, 1);
|
||||
assert.equal(sources[0].passwordConfigured, true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(sources[0], 'password'), false);
|
||||
const removed = f.service.removeRemoteSource({ id: sources[0].id });
|
||||
assert.equal(removed.length, 0);
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('PS5 Linux Lab is a read-only preflight and warns about active HEN components', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
f.settings.set('operations.compatibility', { firmware: '7.61', hen: 'etaHEN', henVersion: '2.5B', kstuffVersion: '1.07' });
|
||||
let report = f.service.linuxLab();
|
||||
assert.equal(report.status, 'compatible');
|
||||
assert.equal(report.normalizedFirmware, '7.61');
|
||||
assert.equal(report.policy.payloadSendDisabled, true);
|
||||
assert.equal(report.policy.blockDeviceWritesDisabled, true);
|
||||
assert.equal(report.requirements.find(item => item.id === 'hen').status, 'warning');
|
||||
assert.equal(report.requirements.find(item => item.id === 'kstuff').status, 'warning');
|
||||
|
||||
f.settings.set('operations.compatibility', { firmware: '8.00' });
|
||||
report = f.service.linuxLab();
|
||||
assert.equal(report.status, 'incompatible');
|
||||
assert.equal(report.requirements.find(item => item.id === 'firmware').status, 'incompatible');
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('Save Vault restore verification rejects tampering and accepts only bounded PS5 title archives', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const titleId = 'PPSA03016';
|
||||
const id = '1700000000000-test';
|
||||
const directory = path.join(f.paths.backups, 'save-vault', titleId);
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
const zip = new JSZip();
|
||||
zip.file(`${titleId}/sdimg_profile`, Buffer.from([2, 4, 6, 8]));
|
||||
zip.file(`${titleId}/sdimg_sce_bu_profile`, Buffer.from([1, 3, 5, 7]));
|
||||
const archive = await zip.generateAsync({ type: 'nodebuffer', compression: 'STORE' });
|
||||
const filename = `${id}.zip`;
|
||||
fs.writeFileSync(path.join(directory, filename), archive);
|
||||
fs.writeFileSync(path.join(directory, `${id}.json`), JSON.stringify({
|
||||
schema: 1, id, titleId, createdAt: Date.now(), file: filename, bytes: archive.length,
|
||||
sha256: crypto.createHash('sha256').update(archive).digest('hex'), source: 'test',
|
||||
}));
|
||||
|
||||
const verified = await f.service.verifySaveSnapshot({ id });
|
||||
assert.equal(verified.ok, true);
|
||||
assert.equal(verified.archive.titleId, titleId);
|
||||
assert.equal(verified.archive.entries.length, 2);
|
||||
assert.equal(verified.archive.entries.filter(entry => entry.backup).length, 1);
|
||||
|
||||
fs.appendFileSync(path.join(directory, filename), Buffer.from('tamper'));
|
||||
const rejected = await f.service.verifySaveSnapshot({ id });
|
||||
assert.equal(rejected.ok, false);
|
||||
assert.match(rejected.error, /SHA-256/);
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('Console Timeline persistence is opt-in, bounded, searchable and redacted', () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
const configured = f.service.configureTimeline({ enabled: true, retentionDays: 7, maxEvents: 100 });
|
||||
assert.equal(configured.options.enabled, true);
|
||||
f.service.saveRemoteSource({ protocol: 'smb', host: 'nas.local', port: 445, path: '/roms', label: 'password=supersecret' });
|
||||
const result = f.service.timelineStatus({ search: 'sources', limit: 10 });
|
||||
assert.equal(result.filtered, 1);
|
||||
assert.doesNotMatch(result.events[0].message, /supersecret/);
|
||||
assert.match(result.events[0].message, /REDACTED/);
|
||||
const exported = f.service.exportTimeline({ limit: 100 });
|
||||
assert.doesNotMatch(exported, /supersecret/);
|
||||
assert.equal(JSON.parse(exported).redacted, true);
|
||||
f.service.stop();
|
||||
assert.equal(fs.existsSync(path.join(f.paths.backups, 'ecosystem', 'timeline', 'events.jsonl')), true);
|
||||
} finally { f.service.stop(); fs.rmSync(f.root, { recursive: true, force: true }); }
|
||||
});
|
||||
Reference in new issue
Block a user