import { describe, expect, it } from 'vitest'; import type { ImportDocument } from '../../src/domain/import-intermediate'; import { ImportError } from '../../src/shared/errors'; import { resolveImportResourcePolicy, validateDecodedTextResources, validateImportDocumentResources, validateImportSourceResources } from '../../src/importers/resource-policy'; function zipWithDeclaredSizes(compressed: number, uncompressed: number, name = '', flags = 0): Uint8Array { const encodedName = new TextEncoder().encode(name); const endOffset = 46 + encodedName.length; const bytes = new Uint8Array(endOffset + 22); const view = new DataView(bytes.buffer); view.setUint32(0, 0x02014b50, true); view.setUint16(8, flags, true); view.setUint16(10, 8, true); view.setUint32(20, compressed, true); view.setUint32(24, uncompressed, true); view.setUint16(28, encodedName.length, true); bytes.set(encodedName, 46); view.setUint32(endOffset, 0x06054b50, true); view.setUint16(endOffset + 8, 1, true); view.setUint16(endOffset + 10, 1, true); view.setUint32(endOffset + 12, 46 + encodedName.length, true); view.setUint32(endOffset + 16, 0, true); return bytes; } function errorCode(action: () => void): string | undefined { try { action(); } catch (error) { return error instanceof ImportError ? error.diagnostics[0]?.code : undefined; } return undefined; } describe('import resource policy', () => { it('rejects byte mismatches, oversized files and suspicious ZIP compression before parsing', () => { const policy = resolveImportResourcePolicy({ maximumFileBytes: 10, maximumZipCompressionRatio: 10 }); expect(errorCode(() => validateImportSourceResources({ metadata: { name: 'x.csv', sizeBytes: 2, lastModified: 0 }, bytes: new Uint8Array(1) }, policy))).toBe('file-size-mismatch'); expect(errorCode(() => validateImportSourceResources({ metadata: { name: 'x.csv', sizeBytes: 11, lastModified: 0 }, bytes: new Uint8Array(11) }, policy))).toBe('file-size-limit'); const zip = zipWithDeclaredSizes(1, 1_000); expect(errorCode(() => validateImportSourceResources({ metadata: { name: 'bomb.xlsx', sizeBytes: zip.length, lastModified: 0 }, bytes: zip }, { ...policy, maximumFileBytes: 10_000, maximumZipEntryUncompressedBytes: 10_000, maximumZipUncompressedBytes: 10_000 }))) .toBe('zip-compression-ratio-limit'); }); it('rejects encrypted, escaping and VBA-bearing Excel archives', () => { const policy = resolveImportResourcePolicy(); for (const [name, flags, expectedCode] of [ ['xl/workbook.xml', 1, 'zip-encryption-rejected'], ['../xl/workbook.xml', 0, 'zip-entry-path-rejected'], ['xl/vbaProject.bin', 0, 'excel-vba-content-rejected'] ] as const) { const zip = zipWithDeclaredSizes(1, 1, name, flags); expect(errorCode(() => validateImportSourceResources({ metadata: { name: 'unsafe.xlsx', sizeBytes: zip.length, lastModified: 0 }, bytes: zip }, policy))).toBe(expectedCode); } }); it('blocks XML entities, deep JSON, null bytes and excessive markup before DOM or JSON parsing', () => { const policy = resolveImportResourcePolicy({ maximumJsonDepth: 2, maximumMarkupElements: 2 }); expect(errorCode(() => validateDecodedTextResources(']>&e;', 'xml', policy))) .toBe('xml-doctype-rejected'); expect(errorCode(() => validateDecodedTextResources('{"a":{"b":{"c":1}}}', 'json', policy))).toBe('json-depth-limit'); expect(errorCode(() => validateDecodedTextResources('a\0b', 'csv', policy))).toBe('null-byte-rejected'); expect(errorCode(() => validateDecodedTextResources('', 'html', policy))).toBe('markup-element-limit'); }); it('enforces table, row, column and cell limits on the common intermediate model', () => { const document: ImportDocument = { version: 1, status: 'complete', format: 'CSV', file: { name: 'x.csv', sizeBytes: 1, lastModified: 0, extension: 'csv' }, rawText: '', diagnostics: [], discardedContent: [], views: [], processing: [], tables: [{ id: 't', name: 'T', index: 0, rawText: '', headerCandidates: [], attributes: {}, columns: [{ index: 0, headerCandidates: [] }, { index: 1, headerCandidates: [] }], rows: [{ index: 0, source: { tableIndex: 0, rowIndex: 0 }, cells: [{ rawValue: 'long', decodedValue: 'long', detectedType: 'string', source: { tableIndex: 0, rowIndex: 0, columnIndex: 0 } }] }] }] }; expect(errorCode(() => validateImportDocumentResources(document, resolveImportResourcePolicy({ maximumColumnsPerTable: 1 })))) .toBe('column-limit'); expect(errorCode(() => validateImportDocumentResources(document, resolveImportResourcePolicy({ maximumRowsPerTable: 0 })))) .toBe('rows-per-table-limit'); expect(errorCode(() => validateImportDocumentResources(document, resolveImportResourcePolicy({ maximumCellCharacters: 3 })))) .toBe('cell-size-limit'); }); });