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

58 lines
4.9 KiB
TypeScript

import assert from 'node:assert/strict'
import { strToU8, zipSync } from 'fflate'
import { collectGeometryImportText } from '../src/facade/geometryRuntime'
import { inspectFcstdArchive } from '../src/facade/fcstd'
import { createMockFacade } from '../src/facade/mockFacade'
const documentXml = (body = '') => `<Document SchemaVersion="4" Label="Hardening"><Objects Count="0"/><ObjectData Count="0"/>${body}</Document>`
const oneMiB = new Uint8Array(1024 * 1024)
for (let index = 0; index < oneMiB.length; index += 1) oneMiB[index] = (index * 31 + 17) & 0xff
const withinLimit = zipSync({ 'Document.xml': strToU8(documentXml()), 'Resources/payload.bin': oneMiB }, { level: 0 })
const accepted = inspectFcstdArchive(withinLimit, { maxArchiveBytes: 2 * 1024 * 1024, maxEntryBytes: 2 * 1024 * 1024, maxTotalUncompressedBytes: 2 * 1024 * 1024 })
assert.equal(accepted.entries.length, 2)
assert.equal(accepted.entries.find((entry) => entry.path === 'Resources/payload.bin')?.uncompressedBytes, oneMiB.byteLength)
const rejected: Record<string, string> = {}
const expectReject = (name: string, action: () => unknown, pattern: RegExp) => {
let caught: unknown
try { action() } catch (error) { caught = error }
assert.ok(caught instanceof Error && pattern.test(caught.message), `${name} did not fail explicitly: ${caught instanceof Error ? caught.message : String(caught)}`)
rejected[name] = caught.message
}
expectReject('archive-bytes', () => inspectFcstdArchive(withinLimit, { maxArchiveBytes: withinLimit.byteLength - 1 }), /archive exceeds/)
expectReject('entry-bytes', () => inspectFcstdArchive(withinLimit, { maxEntryBytes: oneMiB.byteLength - 1 }), /entry .* exceeds/)
expectReject('total-uncompressed', () => inspectFcstdArchive(withinLimit, { maxTotalUncompressedBytes: oneMiB.byteLength }), /archive exceeds .* uncompressed bytes/)
expectReject('entry-count', () => inspectFcstdArchive(withinLimit, { maxEntries: 1 }), /archive exceeds 1 entries/)
const compressed = zipSync({ 'Document.xml': strToU8(documentXml()), 'Resources/compressible.bin': new Uint8Array(256 * 1024) }, { level: 9 })
expectReject('compression-ratio', () => inspectFcstdArchive(compressed, { maxCompressionRatio: 2 }), /maximum compression ratio/)
expectReject('truncated-zip', () => inspectFcstdArchive(withinLimit.slice(0, -16)), /ZIP archive|central directory|end-of-central-directory/)
const deepXml = zipSync({ 'Document.xml': strToU8(documentXml(`${'<Group>'.repeat(10)}${'</Group>'.repeat(10)}`)) })
expectReject('xml-depth', () => inspectFcstdArchive(deepXml, { maxXmlDepth: 8 }), /nesting depth/)
const wideXml = zipSync({ 'Document.xml': strToU8(documentXml(Array.from({ length: 32 }, (_, index) => `<Node id="${index}"/>`).join(''))) })
expectReject('xml-nodes', () => inspectFcstdArchive(wideXml, { maxXmlNodes: 20 }), /XML nodes/)
const malformedXml = zipSync({ 'Document.xml': strToU8('<Document><Objects></Document>') })
expectReject('malformed-xml', () => inspectFcstdArchive(malformedXml), /XML|closing tag|Document/i)
const streamChunks = [new TextEncoder().encode('ISO-10303-21;\n'), new TextEncoder().encode('DATA;\n'), new TextEncoder().encode('ENDSEC;\nEND-ISO-10303-21;')]
async function* successfulStream() { for (const chunk of streamChunks) yield chunk }
const streamed = await collectGeometryImportText(successfulStream(), undefined, 1024)
assert.equal(streamed, 'ISO-10303-21;\nDATA;\nENDSEC;\nEND-ISO-10303-21;')
const controller = new AbortController()
async function* cancelledStream() { yield streamChunks[0]; controller.abort(); yield streamChunks[1] }
await assert.rejects(() => collectGeometryImportText(cancelledStream(), controller.signal, 1024), (error: unknown) => error instanceof DOMException && error.name === 'AbortError')
async function* invalidUtf8() { yield new Uint8Array([0xff]) }
await assert.rejects(() => collectGeometryImportText(invalidUtf8(), undefined, 1024), /UTF-8|encoding|encoded/i)
async function* overLimitStream() { yield new Uint8Array(4); yield new Uint8Array(5) }
await assert.rejects(() => collectGeometryImportText(overLimitStream(), undefined, 8), /exceeds 8 bytes/)
const facade = createMockFacade()
await facade.project.save()
facade.app.document.setProperty({ objectId: 'pad', propertyName: 'Length', value: 31 })
await facade.project.save()
const recovery = await facade.project.recovery('doc-pump-housing')
assert.equal(recovery.integrity, 'ok')
assert.equal(recovery.lastSavedVersion, facade.app.document.getActive().version)
assert.ok(recovery.checkpoints.length >= 2)
console.log(JSON.stringify({ status: 'fcstd-hardening-pass', accepted: { archiveBytes: withinLimit.byteLength, entryCount: accepted.entries.length, payloadBytes: oneMiB.byteLength }, rejected, streaming: { chunks: streamChunks.length, bytes: new TextEncoder().encode(streamed).byteLength, cancellation: 'AbortError', invalidUtf8: 'rejected', overLimit: 'rejected' }, recovery: { integrity: recovery.integrity, lastSavedVersion: recovery.lastSavedVersion, checkpointCount: recovery.checkpoints.length } }, null, 2))