Files
Web_FreeCAD_Bitbybit/scripts/run-fcstd-fuzz.ts

84 lines
4.8 KiB
TypeScript

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<typeof inspectFcstdArchive>[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<string, Uint8Array> = {}) => 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('<Document/>'), expected: 'accept' }
case 1: return { name: 'proxy-object', archive: archive(`<Document SchemaVersion="4"><Objects><Object name="${name}" type="Vendor::Proxy"/></Objects><ObjectData><Object name="${name}"><Properties/></Object></ObjectData></Document>`), expected: 'accept' }
case 2: return { name: 'malformed-close', archive: archive('<Document><Objects></Document>'), expected: 'reject' }
case 3: return { name: 'wrong-document-root', archive: archive('<Objects/>'), expected: 'reject' }
case 4: return { name: 'duplicate-object-id', archive: archive(`<Document><Objects><Object name="${name}" type="Part::Feature"/><Object name="${name}" type="Part::Feature"/></Objects></Document>`), expected: 'reject' }
case 5: return { name: 'empty-object-id', archive: archive('<Document><Objects><Object name="" type="Part::Feature"/></Objects></Document>'), expected: 'reject' }
case 6: return { name: 'depth-limit', archive: archive(`<Document>${'<Group>'.repeat(12)}${'</Group>'.repeat(12)}</Document>`), expected: 'reject', limits: { maxXmlDepth: 8 } }
case 7: return { name: 'node-limit', archive: archive(`<Document>${Array.from({ length: 32 }, (_, item) => `<N${item}/>`).join('')}</Document>`), expected: 'reject', limits: { maxXmlNodes: 20 } }
case 8: return { name: 'entity-declaration', archive: archive('<!DOCTYPE Document [<!ENTITY x "boom">]><Document>&x;</Document>'), 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('<Document/>', '<Views/>'), expected: 'reject' }
case 11: return { name: 'opaque-resource', archive: archive('<Document/>', '<GuiDocument/>', { [`Resources/${name}.bin`]: new Uint8Array([random() & 0xff, random() & 0xff]) }), expected: 'accept' }
default: return { name: 'traversal-path', archive: archive('<Document/>', 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<string, number>()
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))