Files

138 lines
4.6 KiB
TypeScript

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<Uint8Array> {
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,
);
});