import assert from 'node:assert/strict' import { writeFile } from 'node:fs/promises' import { performance } from 'node:perf_hooks' import { strToU8, zipSync } from 'fflate' import { inspectFcstdArchive } from '../src/facade/fcstd' type FuzzCase = { name: string archive: Uint8Array expected: 'accept' | 'reject' limits?: Parameters[1] } let state = 0x5f3759df const random = () => { state ^= state << 13 state ^= state >>> 17 state ^= state << 5 return state >>> 0 } const token = () => `Object_${random().toString(16).padStart(8, '0')}` const archive = (documentXml: string | Uint8Array, guiXml?: string, extra: Record = {}) => zipSync({ 'Document.xml': typeof documentXml === 'string' ? strToU8(documentXml) : documentXml, ...(guiXml === undefined ? {} : { 'GuiDocument.xml': strToU8(guiXml) }), ...extra, }) const createCase = (index: number): FuzzCase => { const name = token() switch (index % 13) { case 0: return { name: 'empty-document', archive: archive(''), expected: 'accept' } case 1: return { name: 'proxy-object', archive: archive(``), expected: 'accept' } case 2: return { name: 'malformed-close', archive: archive(''), expected: 'reject' } case 3: return { name: 'wrong-document-root', archive: archive(''), expected: 'reject' } case 4: return { name: 'duplicate-object-id', archive: archive(``), expected: 'reject' } case 5: return { name: 'empty-object-id', archive: archive(''), expected: 'reject' } case 6: return { name: 'depth-limit', archive: archive(`${''.repeat(12)}${''.repeat(12)}`), expected: 'reject', limits: { maxXmlDepth: 8 } } case 7: return { name: 'node-limit', archive: archive(`${Array.from({ length: 32 }, (_, item) => ``).join('')}`), expected: 'reject', limits: { maxXmlNodes: 20 } } case 8: return { name: 'entity-declaration', archive: archive(']>&x;'), expected: 'reject' } case 9: return { name: 'invalid-utf8', archive: archive(new Uint8Array([0x3c, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3e, 0xc3, 0x28, 0x3c, 0x2f, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x3e])), expected: 'reject' } case 10: return { name: 'invalid-gui-root', archive: archive('', ''), expected: 'reject' } case 11: return { name: 'opaque-resource', archive: archive('', '', { [`Resources/${name}.bin`]: new Uint8Array([random() & 0xff, random() & 0xff]) }), expected: 'accept' } default: return { name: 'traversal-path', archive: archive('', undefined, { [`../${name}.bin`]: new Uint8Array([1]) }), expected: 'reject' } } } const durations: number[] = [] const counts = { accepted: 0, rejected: 0, validCases: 0, invalidCases: 0 } const categories = new Map() for (let index = 0; index < 1000; index += 1) { const fuzzCase = createCase(index) categories.set(fuzzCase.name, (categories.get(fuzzCase.name) ?? 0) + 1) const started = performance.now() let outcome: 'accept' | 'reject' = 'accept' let caught: unknown try { inspectFcstdArchive(fuzzCase.archive, fuzzCase.limits) } catch (error) { outcome = 'reject' caught = error } durations.push(performance.now() - started) if (outcome === 'accept') counts.accepted += 1 else counts.rejected += 1 if (fuzzCase.expected === 'accept') counts.validCases += 1 else counts.invalidCases += 1 assert.equal(outcome, fuzzCase.expected, `FCStd fuzz category ${fuzzCase.name} produced ${outcome}, expected ${fuzzCase.expected}.`) if (outcome === 'reject') assert.ok(caught instanceof Error, `FCStd fuzz category ${fuzzCase.name} rejected with a non-Error value.`) } durations.sort((left, right) => left - right) const p95Ms = durations[Math.floor(durations.length * 0.95)] assert.ok(p95Ms < 50, `FCStd parser fuzz p95 exceeded 50 ms: ${p95Ms.toFixed(3)} ms.`) const report = { status: 'fcstd-parser-fuzz-pass', seed: '0x5f3759df', cases: durations.length, counts, categories: Object.fromEntries([...categories].sort(([left], [right]) => left.localeCompare(right))), timing: { p95Ms: Number(p95Ms.toFixed(3)), maxMs: Number(durations.at(-1)?.toFixed(3)) }, } await writeFile(new URL('../config/qa04-fcstd-fuzz-verification.json', import.meta.url), `${JSON.stringify(report, null, 2)}\n`) console.log(JSON.stringify(report, null, 2))