import test from "node:test"; import assert from "node:assert/strict"; import { projectArchive, unpackProject } from "../engine/archive.ts"; import { validateProject } from "../engine/schema.ts"; import { project, node, findNode, triangleGlb } from "./fixtures.ts"; import { zipSync, strToU8 } from "fflate"; async function toBytes(result: any): Promise { if (result instanceof Uint8Array) return result; if (result instanceof ArrayBuffer) return new Uint8Array(result); if (typeof result?.arrayBuffer === "function") return new Uint8Array(await result.arrayBuffer()); throw new TypeError("Archive builder did not return bytes or a Blob"); } const decodeData = (uri: string) => Buffer.from(uri.slice(uri.indexOf(",") + 1), "base64"); test("project archive restores scripts, exposed properties, geometry and transforms", async () => { const p = project([ node("model", null, { mesh: { assetId: "geometry_test" }, script: { scriptId: "spin", params: { speed: 2 } }, }), ]); p.scenes[0].entities[0].transform.position = [4, 1, -7]; p.assets = [ { id: "geometry_test", name: "Triangle", kind: "geometry", geometry: { positions: [0, 0, 0, 1, 0, 0, 0, 1, 0], indices: [0, 1, 2] }, }, ]; p.scripts = [ { id: "spin", name: "Spin", source: "({ update(api, dt) { api.rotate(0, dt * api.params.speed, 0); } })", fields: { speed: { type: "number", default: 1 } }, }, ]; const restored = await unpackProject(await toBytes(await projectArchive(p))); validateProject(restored); assert.deepEqual(restored, p); assert.deepEqual(findNode(restored, "model").transform.position, [4, 1, -7]); }); test("GLB bytes survive project export and reimport without a network dependency", async () => { const model = triangleGlb(true); const p = project([ node("triangle", null, { mesh: { assetId: "triangle_model" } }), ]); p.assets = [ { id: "triangle_model", name: "Triangle", kind: "model", uri: "data:model/gltf-binary;base64," + model.toString("base64"), }, ]; const restored = await unpackProject(await toBytes(await projectArchive(p))); validateProject(restored); assert.match(restored.assets[0].uri!, /^data:/); assert.deepEqual(decodeData(restored.assets[0].uri!), model); }); test("archive import rejects a missing referenced asset instead of silently losing it", async () => { const p = project([node("triangle", null, { mesh: { assetId: "model" } })]); p.assets = [ { id: "model", name: "Missing", kind: "model", uri: "assets/missing.glb" }, ]; const archive = zipSync({ "project.forma.json": strToU8(JSON.stringify(p)) }); await assert.rejects(async () => unpackProject(archive)); }); test("archive rejects asset references that escape its asset directory", async () => { const model = triangleGlb(); for (const uri of [ "../outside.glb", "assets/../../outside.glb", "/etc/passwd", "assets\\..\\outside.glb", ]) { const p = project([ node("model_node", null, { mesh: { assetId: "model" } }), ]); p.assets = [{ id: "model", name: "Unsafe", kind: "model", uri }]; const archive = zipSync({ "project.forma.json": strToU8(JSON.stringify(p)), [uri]: new Uint8Array(model), }); await assert.rejects( async () => unpackProject(archive), "must reject " + uri, ); } }); test("foreign or malformed archives fail with an explicit error", async () => { for (const bytes of [ new Uint8Array([1, 2, 3]), zipSync({ "readme.txt": strToU8("not a project") }), strToU8('{"format":"forma","version":999}'), ]) { await assert.rejects(async () => unpackProject(bytes)); } }); test("asset IDs that normalize to the same file name cannot corrupt an export", async () => { const a = triangleGlb(true); const b = triangleGlb(); const p = project(); p.assets = [ { id: "asset/a", name: "A", kind: "model", uri: "data:model/gltf-binary;base64," + a.toString("base64"), }, { id: "asset_a", name: "B", kind: "model", uri: "data:model/gltf-binary;base64," + b.toString("base64"), }, ]; // Rejecting unsafe IDs is valid; accepting them must preserve distinct content. try { validateProject(p); } catch { return; } const restored = await unpackProject(await toBytes(await projectArchive(p))); assert.deepEqual( decodeData(restored.assets.find((x: any) => x.id === "asset/a")!.uri!), a, ); assert.deepEqual( decodeData(restored.assets.find((x: any) => x.id === "asset_a")!.uri!), b, ); });