import { tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { PerformanceTracker } from '../performance.tracker.js'; const performanceTracker = new PerformanceTracker('EDGE-09: Corrupted ZIP Containers'); tap.test('EDGE-09: Corrupted ZIP Containers - should handle corrupted ZIP/container files', async (t) => { const einvoice = new EInvoice(); // Test 1: Invalid ZIP headers const invalidZipHeaders = await performanceTracker.measureAsync( 'invalid-zip-headers', async () => { const corruptHeaders = [ { name: 'wrong-magic-bytes', data: Buffer.from('NOTAZIP\x00\x00\x00\x00'), description: 'Invalid ZIP signature' }, { name: 'partial-header', data: Buffer.from('PK\x03'), description: 'Incomplete ZIP header' }, { name: 'corrupted-local-header', data: Buffer.concat([ Buffer.from('PK\x03\x04'), // Local file header signature Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), // Corrupted version/flags Buffer.alloc(20, 0) // Rest of header ]), description: 'Corrupted local file header' }, { name: 'missing-central-directory', data: Buffer.concat([ Buffer.from('PK\x03\x04'), // Local file header Buffer.alloc(26, 0), // Header data Buffer.from('PK\x07\x08'), // Data descriptor Buffer.alloc(12, 0), // Descriptor data // Missing central directory ]), description: 'Missing central directory' } ]; const results = []; for (const corrupt of corruptHeaders) { try { const extracted = await einvoice.extractFromContainer(corrupt.data); results.push({ type: corrupt.name, recovered: !!extracted, filesExtracted: extracted?.files?.length || 0, error: null }); } catch (error) { results.push({ type: corrupt.name, recovered: false, error: error.message, isZipError: error.message.toLowerCase().includes('zip') || error.message.toLowerCase().includes('archive') }); } } return results; } ); invalidZipHeaders.forEach(result => { t.ok(!result.recovered || result.isZipError, `Invalid header ${result.type} should fail or be detected`); }); // Test 2: Truncated ZIP files const truncatedZipFiles = await performanceTracker.measureAsync( 'truncated-zip-files', async () => { // Create a valid ZIP structure and truncate at different points const validZip = createValidZipStructure(); const truncationPoints = [ { point: 10, name: 'header-truncated' }, { point: 50, name: 'file-data-truncated' }, { point: validZip.length - 50, name: 'directory-truncated' }, { point: validZip.length - 10, name: 'eocd-truncated' }, { point: validZip.length - 1, name: 'last-byte-missing' } ]; const results = []; for (const truncation of truncationPoints) { const truncated = validZip.slice(0, truncation.point); try { const recovery = await einvoice.recoverFromCorruptedZip(truncated, { attemptPartialRecovery: true }); results.push({ truncation: truncation.name, size: truncated.length, recovered: recovery?.success || false, filesRecovered: recovery?.recoveredFiles || 0, dataRecovered: recovery?.recoveredBytes || 0 }); } catch (error) { results.push({ truncation: truncation.name, size: truncated.length, recovered: false, error: error.message }); } } return results; } ); truncatedZipFiles.forEach(result => { t.ok(result.recovered === false || result.filesRecovered < 1, `Truncated ZIP at ${result.truncation} should have limited recovery`); }); // Test 3: CRC errors const crcErrors = await performanceTracker.measureAsync( 'crc-checksum-errors', async () => { const scenarios = [ { name: 'single-bit-flip', corruption: (data: Buffer) => { const copy = Buffer.from(data); // Flip a bit in the compressed data if (copy.length > 100) { copy[100] ^= 0x01; } return copy; } }, { name: 'data-corruption', corruption: (data: Buffer) => { const copy = Buffer.from(data); // Corrupt a chunk of data for (let i = 50; i < Math.min(100, copy.length); i++) { copy[i] = 0xFF; } return copy; } }, { name: 'wrong-crc-stored', corruption: (data: Buffer) => { const copy = Buffer.from(data); // Find and corrupt CRC values const crcOffset = findCRCOffset(copy); if (crcOffset > 0) { copy.writeUInt32LE(0xDEADBEEF, crcOffset); } return copy; } } ]; const results = []; for (const scenario of scenarios) { const validZip = createZipWithInvoice(); const corrupted = scenario.corruption(validZip); try { const extraction = await einvoice.extractFromContainer(corrupted, { ignoreCRCErrors: false }); results.push({ scenario: scenario.name, extracted: true, crcValidated: extraction?.crcValid || false, dataIntegrity: extraction?.integrityCheck || 'unknown' }); } catch (error) { results.push({ scenario: scenario.name, extracted: false, error: error.message, isCRCError: error.message.toLowerCase().includes('crc') || error.message.toLowerCase().includes('checksum') }); } } return results; } ); crcErrors.forEach(result => { t.ok(!result.extracted || !result.crcValidated || result.isCRCError, `CRC error ${result.scenario} should be detected`); }); // Test 4: Compression method issues const compressionMethodIssues = await performanceTracker.measureAsync( 'compression-method-issues', async () => { const compressionTests = [ { name: 'unsupported-method', method: 99, // Invalid compression method description: 'Unknown compression algorithm' }, { name: 'store-but-compressed', method: 0, // Store (no compression) compressed: true, description: 'Stored method but data is compressed' }, { name: 'deflate-corrupted', method: 8, // Deflate corrupted: true, description: 'Deflate stream corrupted' }, { name: 'bzip2-in-zip', method: 12, // Bzip2 (not standard in ZIP) description: 'Non-standard compression method' } ]; const results = []; for (const test of compressionTests) { const zipData = createZipWithCompressionMethod(test.method, test); try { const extracted = await einvoice.extractFromContainer(zipData); results.push({ test: test.name, method: test.method, extracted: true, filesFound: extracted?.files?.length || 0, decompressed: extracted?.decompressed || false }); } catch (error) { results.push({ test: test.name, method: test.method, extracted: false, error: error.message, isCompressionError: error.message.includes('compress') || error.message.includes('method') }); } } return results; } ); compressionMethodIssues.forEach(result => { if (result.method === 0 || result.method === 8) { t.ok(result.extracted || result.isCompressionError, `Standard compression ${result.test} should be handled`); } else { t.notOk(result.extracted, `Non-standard compression ${result.test} should fail`); } }); // Test 5: Nested/recursive ZIP bombs const nestedZipBombs = await performanceTracker.measureAsync( 'nested-zip-bombs', async () => { const bombTypes = [ { name: 'deep-nesting', depth: 10, description: 'ZIP within ZIP, 10 levels deep' }, { name: 'exponential-expansion', copies: 10, description: 'Each level contains 10 copies' }, { name: 'circular-reference', circular: true, description: 'ZIP contains itself' }, { name: 'compression-ratio-bomb', ratio: 1000, description: 'Extreme compression ratio' } ]; const results = []; for (const bomb of bombTypes) { const bombZip = createZipBomb(bomb); const startTime = Date.now(); const startMemory = process.memoryUsage(); try { const extraction = await einvoice.extractFromContainer(bombZip, { maxDepth: 5, maxExpandedSize: 100 * 1024 * 1024, // 100MB limit maxFiles: 1000 }); const endTime = Date.now(); const endMemory = process.memoryUsage(); results.push({ type: bomb.name, handled: true, timeTaken: endTime - startTime, memoryUsed: endMemory.heapUsed - startMemory.heapUsed, depthReached: extraction?.maxDepth || 0, stopped: extraction?.limitReached || false }); } catch (error) { results.push({ type: bomb.name, handled: true, prevented: true, error: error.message, isBombDetected: error.message.includes('bomb') || error.message.includes('depth') || error.message.includes('limit') }); } } return results; } ); nestedZipBombs.forEach(result => { t.ok(result.prevented || result.stopped, `ZIP bomb ${result.type} should be prevented or limited`); }); // Test 6: Character encoding in filenames const filenameEncodingIssues = await performanceTracker.measureAsync( 'filename-encoding-issues', async () => { const encodingTests = [ { name: 'utf8-bom-filename', filename: '\uFEFFファイル.xml', encoding: 'utf8' }, { name: 'cp437-extended', filename: 'Ñoño_español.xml', encoding: 'cp437' }, { name: 'mixed-encoding', filename: 'Test_文件_файл.xml', encoding: 'mixed' }, { name: 'null-bytes', filename: 'file\x00.xml', encoding: 'binary' }, { name: 'path-traversal', filename: '../../../etc/passwd', encoding: 'ascii' } ]; const results = []; for (const test of encodingTests) { const zipData = createZipWithFilename(test.filename, test.encoding); try { const extracted = await einvoice.extractFromContainer(zipData); const files = extracted?.files || []; results.push({ test: test.name, extracted: true, fileCount: files.length, filenamePreserved: files.some(f => f.name === test.filename), filenameNormalized: files[0]?.name || null, securityCheck: !files.some(f => f.name.includes('..')) }); } catch (error) { results.push({ test: test.name, extracted: false, error: error.message }); } } return results; } ); filenameEncodingIssues.forEach(result => { t.ok(result.securityCheck, `Filename ${result.test} should pass security checks`); }); // Test 7: Factur-X/ZUGFeRD specific corruptions const facturXCorruptions = await performanceTracker.measureAsync( 'facturx-zugferd-corruptions', async () => { const corruptionTypes = [ { name: 'missing-metadata', description: 'PDF/A-3 without required metadata' }, { name: 'wrong-attachment-relationship', description: 'XML not marked as Alternative' }, { name: 'multiple-xml-versions', description: 'Both Factur-X and ZUGFeRD XML present' }, { name: 'corrupted-xml-stream', description: 'XML attachment stream corrupted' } ]; const results = []; for (const corruption of corruptionTypes) { const corruptedPDF = createCorruptedFacturX(corruption.name); try { const extraction = await einvoice.extractFromPDF(corruptedPDF); results.push({ corruption: corruption.name, extracted: !!extraction, hasValidXML: extraction?.xml && isValidXML(extraction.xml), hasMetadata: !!extraction?.metadata, conformance: extraction?.conformanceLevel || 'unknown' }); } catch (error) { results.push({ corruption: corruption.name, extracted: false, error: error.message }); } } return results; } ); facturXCorruptions.forEach(result => { t.ok(result.extracted || result.error, `Factur-X corruption ${result.corruption} was handled`); }); // Test 8: Recovery strategies const recoveryStrategies = await performanceTracker.measureAsync( 'zip-recovery-strategies', async () => { const strategies = [ { name: 'scan-for-headers', description: 'Scan for local file headers' }, { name: 'reconstruct-central-dir', description: 'Rebuild central directory' }, { name: 'raw-deflate-extraction', description: 'Extract raw deflate streams' }, { name: 'pattern-matching', description: 'Find XML by pattern matching' } ]; const corruptedZip = createSeverelyCorruptedZip(); const results = []; for (const strategy of strategies) { try { const recovered = await einvoice.attemptZipRecovery(corruptedZip, { strategy: strategy.name }); results.push({ strategy: strategy.name, success: recovered?.success || false, filesRecovered: recovered?.files?.length || 0, xmlFound: recovered?.files?.some(f => f.name.endsWith('.xml')) || false, confidence: recovered?.confidence || 0 }); } catch (error) { results.push({ strategy: strategy.name, success: false, error: error.message }); } } return results; } ); recoveryStrategies.forEach(result => { t.ok(result.success || result.error, `Recovery strategy ${result.strategy} was attempted`); }); // Test 9: Multi-part archive issues const multiPartArchiveIssues = await performanceTracker.measureAsync( 'multi-part-archive-issues', async () => { const multiPartTests = [ { name: 'missing-parts', parts: ['part1.zip', null, 'part3.zip'], description: 'Missing middle part' }, { name: 'wrong-order', parts: ['part3.zip', 'part1.zip', 'part2.zip'], description: 'Parts in wrong order' }, { name: 'mixed-formats', parts: ['part1.zip', 'part2.rar', 'part3.zip'], description: 'Different archive formats' }, { name: 'size-mismatch', parts: createMismatchedParts(), description: 'Part sizes do not match' } ]; const results = []; for (const test of multiPartTests) { try { const assembled = await einvoice.assembleMultiPartArchive(test.parts); const extracted = await einvoice.extractFromContainer(assembled); results.push({ test: test.name, assembled: true, extracted: !!extracted, complete: extracted?.isComplete || false }); } catch (error) { results.push({ test: test.name, assembled: false, error: error.message }); } } return results; } ); multiPartArchiveIssues.forEach(result => { t.ok(!result.assembled || !result.complete, `Multi-part issue ${result.test} should cause problems`); }); // Test 10: Performance with corrupted files const corruptedPerformance = await performanceTracker.measureAsync( 'corrupted-file-performance', async () => { const sizes = [ { size: 1024, name: '1KB' }, { size: 1024 * 1024, name: '1MB' }, { size: 10 * 1024 * 1024, name: '10MB' } ]; const results = []; for (const sizeTest of sizes) { // Create corrupted file of specific size const corrupted = createCorruptedZipOfSize(sizeTest.size); const startTime = Date.now(); const timeout = 10000; // 10 second timeout try { const extractPromise = einvoice.extractFromContainer(corrupted); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout) ); await Promise.race([extractPromise, timeoutPromise]); const timeTaken = Date.now() - startTime; results.push({ size: sizeTest.name, completed: true, timeTaken, timedOut: false }); } catch (error) { const timeTaken = Date.now() - startTime; results.push({ size: sizeTest.name, completed: false, timeTaken, timedOut: error.message === 'Timeout', error: error.message }); } } return results; } ); corruptedPerformance.forEach(result => { t.ok(!result.timedOut, `Corrupted file ${result.size} should not cause timeout`); }); // Print performance summary performanceTracker.printSummary(); }); // Helper functions function createValidZipStructure(): Buffer { // Simplified ZIP structure const parts = []; // Local file header parts.push(Buffer.from('PK\x03\x04')); // Signature parts.push(Buffer.alloc(26, 0)); // Header fields parts.push(Buffer.from('test.xml')); // Filename parts.push(Buffer.from('123')); // File data // Central directory parts.push(Buffer.from('PK\x01\x02')); // Signature parts.push(Buffer.alloc(42, 0)); // Header fields parts.push(Buffer.from('test.xml')); // Filename // End of central directory parts.push(Buffer.from('PK\x05\x06')); // Signature parts.push(Buffer.alloc(18, 0)); // EOCD fields return Buffer.concat(parts); } function createZipWithInvoice(): Buffer { // Create a simple ZIP with invoice XML return createValidZipStructure(); } function findCRCOffset(data: Buffer): number { // Find CRC32 field in ZIP structure const sig = Buffer.from('PK\x03\x04'); const idx = data.indexOf(sig); if (idx >= 0) { return idx + 14; // CRC32 offset in local file header } return -1; } function createZipWithCompressionMethod(method: number, options: any): Buffer { const parts = []; // Local file header with specific compression method parts.push(Buffer.from('PK\x03\x04')); const header = Buffer.alloc(26, 0); header.writeUInt16LE(method, 8); // Compression method parts.push(header); parts.push(Buffer.from('invoice.xml')); // Add compressed or uncompressed data based on method if (options.corrupted) { parts.push(Buffer.from([0xFF, 0xFE, 0xFD])); // Invalid deflate stream } else if (method === 0) { parts.push(Buffer.from('')); } else { parts.push(Buffer.from([0x78, 0x9C])); // Deflate header parts.push(Buffer.alloc(10, 0)); // Compressed data } return Buffer.concat(parts); } function createZipBomb(config: any): Buffer { // Create various types of ZIP bombs if (config.circular) { // Create a ZIP that references itself return Buffer.from('PK...[circular reference]...'); } else if (config.depth) { // Create nested ZIPs let zip = Buffer.from(''); for (let i = 0; i < config.depth; i++) { zip = wrapInZip(zip, `level${i}.zip`); } return zip; } return Buffer.from('PK'); } function wrapInZip(content: Buffer, filename: string): Buffer { // Wrap content in a ZIP file return Buffer.concat([ Buffer.from('PK\x03\x04'), Buffer.alloc(26, 0), Buffer.from(filename), content ]); } function createZipWithFilename(filename: string, encoding: string): Buffer { const parts = []; parts.push(Buffer.from('PK\x03\x04')); const header = Buffer.alloc(26, 0); // Set filename length const filenameBuffer = Buffer.from(filename, encoding === 'binary' ? 'binary' : 'utf8'); header.writeUInt16LE(filenameBuffer.length, 24); parts.push(header); parts.push(filenameBuffer); parts.push(Buffer.from('')); return Buffer.concat(parts); } function createCorruptedFacturX(type: string): Buffer { // Create corrupted Factur-X/ZUGFeRD PDFs const mockPDF = Buffer.from('%PDF-1.4\n...'); return mockPDF; } function createSeverelyCorruptedZip(): Buffer { // Create a severely corrupted ZIP for recovery testing const data = Buffer.alloc(1024); data.fill(0xFF); // Add some ZIP-like signatures at random positions data.write('PK\x03\x04', 100); data.write('', 300); return data; } function createMismatchedParts(): Buffer[] { return [ Buffer.alloc(1000, 1), Buffer.alloc(500, 2), Buffer.alloc(1500, 3) ]; } function createCorruptedZipOfSize(size: number): Buffer { const data = Buffer.alloc(size); // Fill with random data for (let i = 0; i < size; i += 4) { data.writeUInt32LE(Math.random() * 0xFFFFFFFF, i); } // Add ZIP signature at start data.write('PK\x03\x04', 0); return data; } function isValidXML(content: string): boolean { try { // Simple XML validation check return content.includes(''); } catch { return false; } } // Run the test tap.start();