import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import type { ToolcraftModelDocument } from "./model-document"; import { decodeToolcraftModelDocument, encodeToolcraftModelDocument, ToolcraftModelDocumentCodecError, type ToolcraftModelDocumentCodecFailureCode, } from "./model-document-codec"; import { digestToolcraftModelDocument } from "./model-document-digest"; import { createModelBounds, createModelMaterial, createModelNode, createModelPrimitive, createModelPrimitiveV2, createModelTexture, createValidModelDocument, createValidModelDocumentV2, } from "./model-document-test-support"; import { ToolcraftModelDocumentValidationError } from "./model-document-validation"; const HEADER_BYTES = 16; const DESCRIPTOR_BYTES = 16; const SECTION_COUNT = 4; const PAYLOAD_OFFSET = HEADER_BYTES + DESCRIPTOR_BYTES * SECTION_COUNT; function copyEncoded(document = createValidModelDocument()): Uint8Array { return encodeToolcraftModelDocument(document).slice(); } function descriptorOffset(index: number): number { return HEADER_BYTES + index * DESCRIPTOR_BYTES; } function sectionLength(bytes: Uint8Array, index: number): number { return Number( new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getBigUint64( descriptorOffset(index) + 8, true, ), ); } function sectionOffset(bytes: Uint8Array, index: number): number { let offset = PAYLOAD_OFFSET; for (let sectionIndex = 0; sectionIndex < index; sectionIndex += 1) { offset += sectionLength(bytes, sectionIndex); } return offset; } function expectCodecFailure( operation: () => unknown, code: ToolcraftModelDocumentCodecFailureCode, ): void { try { operation(); throw new Error("Expected codec operation to fail."); } catch (error) { expect(error).toBeInstanceOf(ToolcraftModelDocumentCodecError); expect(error).toMatchObject({ code }); } } function provenanceDocument(): ToolcraftModelDocument { return createValidModelDocument({ nodes: [ createModelNode({ children: ["node-2"] }), createModelNode({ id: "node-2", name: "Detail", primitiveIds: [], }), ], provenance: { adapterVersion: "gltf-adapter@7", canonicalSchemaVersion: 1, operations: ["deterministic-triangulation", "sequential-indexing"], repair: { algorithmVersion: "repair@3", operations: ["regenerate-normals", "repair-local-winding"], planDigest: "sha256:plan", recipeId: "recipe-1", }, sourceFormat: "gltf", }, }); } describe("canonical model document codec", () => { it("round-trips exact typed arrays, provenance, and hierarchy", () => { const source = provenanceDocument(); const decoded = decodeToolcraftModelDocument( encodeToolcraftModelDocument(source), ); expect(decoded).toEqual(source); expect(decoded.primitives[0]?.positions).toBeInstanceOf(Float32Array); expect(decoded.primitives[0]?.normals).toBeInstanceOf(Float32Array); expect(decoded.primitives[0]?.indices).toBeInstanceOf(Uint32Array); expect(decoded.primitives[0]?.positions.buffer).not.toBe( source.primitives[0]?.positions.buffer, ); }); it("round-trips v2 materials, textures, UVs, colors, and assignments", () => { const source = createValidModelDocumentV2({ materials: [ createModelMaterial({ id: "material-a" }), createModelMaterial({ baseColorTexture: undefined, id: "material-b", usesVertexColor: false, }), ], primitives: [createModelPrimitiveV2({ materialId: "material-a" })], textures: [createModelTexture()], }); const encoded = encodeToolcraftModelDocument(source); const view = new DataView( encoded.buffer, encoded.byteOffset, encoded.byteLength, ); const decoded = decodeToolcraftModelDocument(encoded); expect(view.getUint16(8, true)).toBe(2); expect(view.getUint16(10, true)).toBe(6); expect(decoded).toEqual(source); if (decoded.version !== 2) throw new Error("Expected v2 document"); expect(decoded.materials.map(({ id }) => id)).toEqual([ "material-a", "material-b", ]); expect(decoded.primitives[0]?.colors?.values).toBeInstanceOf(Float32Array); expect(decoded.primitives[0]?.textureCoordinates?.[0]?.values).toBeInstanceOf( Float32Array, ); expect(decoded.primitives[0]?.colors?.values.buffer).not.toBe( source.primitives[0]?.colors?.values.buffer, ); }); it("binds v2 material assignments and appearance arrays into the digest", () => { const source = createValidModelDocumentV2(); const reassigned = createValidModelDocumentV2({ materials: [ ...source.materials, createModelMaterial({ baseColorTexture: undefined, id: "material-2", usesVertexColor: false, }), ], primitives: [createModelPrimitiveV2({ materialId: "material-2" })], }); const changedUv = createValidModelDocumentV2({ primitives: [createModelPrimitiveV2({ textureCoordinates: [{ set: 0, values: new Float32Array([0, 0, 0.5, 0, 0, 1]), }], })], }); expect(digestToolcraftModelDocument(reassigned)).not.toBe( digestToolcraftModelDocument(source), ); expect(digestToolcraftModelDocument(changedUv)).not.toBe( digestToolcraftModelDocument(source), ); }); it("writes every numeric payload explicitly in little-endian order", () => { const primitive = createModelPrimitive({ bounds: createModelBounds([1.5, -2.25, 3.125], [4.5, 5.75, 3.125]), positions: new Float32Array([ 1.5, -2.25, 3.125, 4.5, -2.25, 3.125, 1.5, 5.75, 3.125, ]), }); const encoded = encodeToolcraftModelDocument( createValidModelDocument({ bounds: primitive.bounds, primitives: [primitive], }), ); const positionsOffset = sectionOffset(encoded, 1); const indicesOffset = sectionOffset(encoded, 3); expect([...encoded.slice(positionsOffset, positionsOffset + 4)]).toEqual([ 0, 0, 192, 63, ]); expect([...encoded.slice(indicesOffset, indicesOffset + 12)]).toEqual([ 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, ]); }); it("does not alias source arrays, encoded bytes, or decoded arrays", () => { const source = createValidModelDocument(); const encoded = encodeToolcraftModelDocument(source); source.primitives[0]!.positions[0] = 99; const decoded = decodeToolcraftModelDocument(encoded); expect(decoded.primitives[0]?.positions[0]).toBe(0); const encodedSnapshot = encoded.slice(); decoded.primitives[0]!.positions[0] = 77; expect(encoded).toEqual(encodedSnapshot); encoded.fill(0); expect(decoded.primitives[0]?.positions[1]).toBe(0); expect(decoded.primitives[0]?.positions[3]).toBe(2); }); it("uses stable metadata ordering regardless of object insertion order", () => { const source = provenanceDocument(); const primitive = source.primitives[0]!; const node = source.nodes[0]!; const equivalent = { version: source.version, rootNodeIds: [...source.rootNodeIds], provenance: { sourceFormat: source.provenance.sourceFormat, repair: source.provenance.repair ? { recipeId: source.provenance.repair.recipeId, planDigest: source.provenance.repair.planDigest, operations: [...source.provenance.repair.operations], algorithmVersion: source.provenance.repair.algorithmVersion, } : undefined, operations: [...source.provenance.operations], canonicalSchemaVersion: source.provenance.canonicalSchemaVersion, adapterVersion: source.provenance.adapterVersion, }, primitives: [ { positions: primitive.positions, normals: primitive.normals, indices: primitive.indices, id: primitive.id, bounds: { min: [...primitive.bounds.min], max: [...primitive.bounds.max], }, }, ], nodes: [ { primitiveIds: [...node.primitiveIds], name: node.name, localMatrix: [...node.localMatrix], id: node.id, children: [...node.children], }, source.nodes[1], ], bounds: { min: [...source.bounds.min], max: [...source.bounds.max], }, } as ToolcraftModelDocument; expect(encodeToolcraftModelDocument(equivalent)).toEqual( encodeToolcraftModelDocument(source), ); expect(digestToolcraftModelDocument(equivalent)).toBe( digestToolcraftModelDocument(source), ); }); it("digests the canonical envelope with browser-compatible SHA-256", () => { const document = provenanceDocument(); const encoded = encodeToolcraftModelDocument(document); const expected = createHash("sha256").update(encoded).digest("hex"); expect(digestToolcraftModelDocument(document)).toBe(`sha256:${expected}`); }); it("rejects bad magic and unsupported codec versions", () => { const badMagic = copyEncoded(); badMagic[0] ^= 0xff; expectCodecFailure( () => decodeToolcraftModelDocument(badMagic), "bad-magic", ); const badVersion = copyEncoded(); new DataView( badVersion.buffer, badVersion.byteOffset, badVersion.byteLength, ).setUint16(8, 3, true); expectCodecFailure( () => decodeToolcraftModelDocument(badVersion), "unsupported-codec-version", ); }); it("never reinterprets a v1 envelope as v2 after a header-only mutation", () => { const forgedVersion = copyEncoded(); new DataView( forgedVersion.buffer, forgedVersion.byteOffset, forgedVersion.byteLength, ).setUint16(8, 2, true); expectCodecFailure( () => decodeToolcraftModelDocument(forgedVersion), "invalid-section-count", ); }); it("rejects truncated envelopes and trailing bytes", () => { const encoded = copyEncoded(); expectCodecFailure( () => decodeToolcraftModelDocument(encoded.slice(0, 12)), "truncated-header", ); expectCodecFailure( () => decodeToolcraftModelDocument(encoded.slice(0, -1)), "truncated-envelope", ); const trailing = new Uint8Array(encoded.byteLength + 1); trailing.set(encoded); expectCodecFailure( () => decodeToolcraftModelDocument(trailing), "trailing-data", ); }); it("rejects duplicate and unknown sections", () => { const duplicate = copyEncoded(); const duplicateView = new DataView( duplicate.buffer, duplicate.byteOffset, duplicate.byteLength, ); duplicateView.setUint32(descriptorOffset(1), 1, true); expectCodecFailure( () => decodeToolcraftModelDocument(duplicate), "duplicate-section", ); const unknown = copyEncoded(); new DataView( unknown.buffer, unknown.byteOffset, unknown.byteLength, ).setUint32(descriptorOffset(1), 99, true); expectCodecFailure( () => decodeToolcraftModelDocument(unknown), "unknown-section", ); }); it("rejects unsafe and over-limit section lengths before geometry allocation", () => { const unsafe = copyEncoded(); new DataView( unsafe.buffer, unsafe.byteOffset, unsafe.byteLength, ).setBigUint64( descriptorOffset(0) + 8, BigInt(Number.MAX_SAFE_INTEGER) + 1n, true, ); expectCodecFailure( () => decodeToolcraftModelDocument(unsafe), "unsafe-section-length", ); const oversized = copyEncoded(); new DataView( oversized.buffer, oversized.byteOffset, oversized.byteLength, ).setBigUint64( descriptorOffset(0) + 8, BigInt(oversized.byteLength + 1), true, ); expectCodecFailure( () => decodeToolcraftModelDocument(oversized, { maxDecodedBytes: oversized.byteLength, }), "section-length-limit-exceeded", ); }); it("rejects invalid typed-payload byte cardinality", () => { const encoded = copyEncoded(); const view = new DataView( encoded.buffer, encoded.byteOffset, encoded.byteLength, ); view.setBigUint64( descriptorOffset(1) + 8, BigInt(sectionLength(encoded, 1) + 1), true, ); expectCodecFailure( () => decodeToolcraftModelDocument(encoded), "invalid-section-byte-length", ); }); it("rejects malformed metadata", () => { const encoded = copyEncoded(); encoded[sectionOffset(encoded, 0)] = "!".charCodeAt(0); expectCodecFailure( () => decodeToolcraftModelDocument(encoded), "malformed-metadata", ); }); it("structurally validates decoded typed payloads", () => { const encoded = copyEncoded(); new DataView( encoded.buffer, encoded.byteOffset, encoded.byteLength, ).setFloat32(sectionOffset(encoded, 1), Number.NaN, true); expect(() => decodeToolcraftModelDocument(encoded)).toThrow( ToolcraftModelDocumentValidationError, ); try { decodeToolcraftModelDocument(encoded); } catch (error) { expect(error).toMatchObject({ code: "non-finite-position" }); } }); });