'use strict'; const assert = require('node:assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const test = require('node:test'); const JSZip = require('jszip'); const { createArchiveWorkerService, detectArchiveSets } = require('../../src/services/archive-worker-service'); async function zipFile(filename, entries, options = {}) { const zip = new JSZip(); for (const entry of entries) zip.file(entry.name, entry.content || '', entry.options || {}); fs.mkdirSync(path.dirname(filename), { recursive: true }); fs.writeFileSync(filename, await zip.generateAsync({ type: 'nodebuffer', platform: options.platform || 'DOS', compression: options.compression || 'STORE' })); } function fixtureStore(payloadPath, components) { return { openForInspection: () => ({ id: 'job', state: 'quarantined', payloadPath, components }) }; } test('capabilities and multipart detection report missing dependencies and parts truthfully', () => { const worker = createArchiveWorkerService({ stagingStore: fixtureStore('unused', []) }); const capabilities = worker.capabilities(); assert.equal(capabilities.zip.available, true); assert.equal(capabilities.rar.available, false); assert.equal(capabilities.sevenZip.available, false); const sets = detectArchiveSets([ { relativePath: 'set/game.part1.rar' }, { relativePath: 'set/game.part3.rar' }, { relativePath: 'other/archive.7z.001' }, { relativePath: 'plain.zip' }, { relativePath: 'classic/data.rar' }, { relativePath: 'classic/data.r00' }, { relativePath: 'classic/data.r02' }, ]); assert.deepEqual(sets.find(set => set.key === 'set/game.rar').missingParts, [2]); assert.equal(sets.find(set => set.key === 'other/archive.7z').multipart, true); assert.deepEqual(sets.find(set => set.key === 'classic/data.rar').missingParts, [3]); assert.equal(sets.find(set => !set.multipart && set.format === 'zip').parts[0].relativePath, 'plain.zip'); }); test('hostile ZIP matrix rejects traversal, links, entry count, depth and expansion limits', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-archive-hostile-')); try { const cases = [ { name: 'traversal.zip', entries: [{ name: '../escape.txt', content: 'x' }], code: 'ARCHIVE_PATH' }, { name: 'absolute.zip', entries: [{ name: '/escape.txt', content: 'x' }], code: 'ARCHIVE_PATH' }, { name: 'link.zip', entries: [{ name: 'link', content: '../target', options: { unixPermissions: 0o120777 } }], platform: 'UNIX', code: 'ARCHIVE_LINK' }, ]; for (const item of cases) { const filename = path.join(root, item.name); await zipFile(filename, item.entries, { platform: item.platform }); const worker = createArchiveWorkerService({ stagingStore: fixtureStore(root, [{ relativePath: item.name, size: fs.statSync(filename).size }]) }); const inspected = await worker.inspectJob('job', 'alice'); assert.equal(inspected.archives[0].state, 'rejected'); assert.equal(inspected.archives[0].code, item.code); } await zipFile(path.join(root, 'many.zip'), [{ name: 'a', content: 'a' }, { name: 'b', content: 'b' }]); const many = await createArchiveWorkerService({ stagingStore: fixtureStore(root, [{ relativePath: 'many.zip' }]), limits: { maxEntries: 1 } }).inspectJob('job', 'alice'); assert.equal(many.archives[0].code, 'ARCHIVE_LIMIT'); await zipFile(path.join(root, 'deep.zip'), [{ name: 'a/b/c/file', content: 'x' }]); const deep = await createArchiveWorkerService({ stagingStore: fixtureStore(root, [{ relativePath: 'deep.zip' }]), limits: { maxDepth: 3 } }).inspectJob('job', 'alice'); assert.equal(deep.archives[0].code, 'ARCHIVE_LIMIT'); await zipFile(path.join(root, 'bomb.zip'), [{ name: 'large.txt', content: 'A'.repeat(10000) }], { compression: 'DEFLATE' }); const bomb = await createArchiveWorkerService({ stagingStore: fixtureStore(root, [{ relativePath: 'bomb.zip' }]), limits: { maxExpansionRatio: 2 } }).inspectJob('job', 'alice'); assert.equal(bomb.archives[0].code, 'ARCHIVE_LIMIT'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('corrupt and encrypted ZIPs are classified without partial extraction', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-archive-invalid-')); try { fs.writeFileSync(path.join(root, 'corrupt.zip'), 'not-a-zip'); await zipFile(path.join(root, 'encrypted.zip'), [{ name: 'file.txt', content: 'secret' }]); const encrypted = fs.readFileSync(path.join(root, 'encrypted.zip')); encrypted.writeUInt16LE(encrypted.readUInt16LE(6) | 1, 6); const central = encrypted.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])); encrypted.writeUInt16LE(encrypted.readUInt16LE(central + 8) | 1, central + 8); fs.writeFileSync(path.join(root, 'encrypted.zip'), encrypted); const worker = createArchiveWorkerService({ stagingStore: fixtureStore(root, [{ relativePath: 'corrupt.zip' }, { relativePath: 'encrypted.zip' }]) }); const result = await worker.inspectJob('job', 'alice'); assert.equal(result.archives.find(item => item.parts[0].relativePath === 'corrupt.zip').state, 'corrupt'); assert.equal(result.archives.find(item => item.parts[0].relativePath === 'encrypted.zip').state, 'encrypted'); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); test('interrupted ZIP extraction resumes verified entries and retains the original archive', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'chimera-archive-resume-')); try { const filename = path.join(root, 'bundle.zip'); await zipFile(filename, [{ name: 'one.txt', content: 'one' }, { name: 'two.txt', content: 'two' }]); const store = fixtureStore(root, [{ relativePath: 'bundle.zip', size: fs.statSync(filename).size }]); let checks = 0; const interrupted = createArchiveWorkerService({ stagingStore: store, isCancelled: () => ++checks > 1 }); await assert.rejects(interrupted.extract({ jobId: 'job', owner: 'alice', relativePath: 'bundle.zip' }), error => error.code === 'ARCHIVE_CANCELLED'); const resumed = createArchiveWorkerService({ stagingStore: store }); const result = await resumed.extract({ jobId: 'job', owner: 'alice', relativePath: 'bundle.zip' }); assert.equal(result.state, 'complete'); assert.equal(result.resumedEntries, 1); assert.equal(result.originalRetained, true); assert.equal(fs.readFileSync(filename).length, fs.statSync(filename).size); } finally { fs.rmSync(root, { recursive: true, force: true }); } });