import assert from "node:assert/strict"; import { test } from "node:test"; import { randomUUID } from "node:crypto"; import sharp from "sharp"; import { zipSync, strToU8 } from "fflate"; import { archiveSchema, importManifest, readMapArchive, sha256, archiveManifestName, } from "../shared/map-archive"; import { datasetSchema, faces, parseUploadManifest } from "../shared/protocol"; import { archiveSignature, verifyArchiveSignature, } from "../lib/map-archive-signature"; import { processFabricImages } from "../lib/fabric-images"; async function fixture(source: "fabric" | "plugin" = "plugin") { const bytes = await sharp({ create: { width: 256, height: 256, channels: 3, background: "#78ab23" }, }) .webp() .toBuffer(); const viewpoints = Array.from({ length: 5 }, (_, i) => ({ id: `p${i}`, x: i, y: 70, z: i, })); const files: Record = { "map.webp": bytes }; for (const p of viewpoints) for (const f of faces) files[`${p.id}/${f}.webp`] = bytes; const archive = archiveSchema.parse({ format: "minegeo-map/v1", name: "Shared world", description: "Map for friends", serverName: "Test server", source, dimension: "minecraft:overworld", north: "-z", minecraftVersion: "26.2", bounds: { minX: 0, minZ: 0, maxX: 32, maxZ: 32 }, viewpoints, originalMapId: randomUUID(), originalOwner: "Original explorer", signature: "0".repeat(64), assets: await Promise.all( Object.entries(files).map(async ([path, content]) => ({ path, bytes: content.length, sha256: await sha256(content), })), ), }); archive.signature = archiveSignature(archive, "test-key"); files[archiveManifestName] = strToU8(JSON.stringify(archive)); return { archive, files }; } test("ZIP roundtrip preserves source, six faces, coordinates, and signed original owner", async () => { for (const source of ["plugin", "fabric"] as const) { const { archive, files } = await fixture(source); for (const level of [0, 6] as const) { const restored = await readMapArchive(zipSync(files, { level })); assert.deepEqual(restored.archive, archive); const manifest = importManifest(restored.archive); assert.equal(manifest.source, source); assert.equal(manifest.mediaFormat, "webp"); assert.equal(manifest.assets.length, 31); assert.equal(manifest.contributionTo, undefined); assert.equal(manifest.mapPath, undefined); assert.equal(manifest.serverAddress, undefined); verifyArchiveSignature(restored.archive, "test-key"); assert.throws(() => parseUploadManifest(manifest), /Use map import/); } } }); test("ZIP import rejects corruption, missing faces, unsafe files, and false ownership", async () => { const { archive, files } = await fixture(); await assert.rejects( readMapArchive(zipSync({ ...files, "p0/px.webp": strToU8("corrupt") })), /checksum or size/, ); const missing = { ...files }; delete missing["p0/px.webp"]; await assert.rejects( readMapArchive(zipSync(missing)), /unexpected files|checksum/, ); await assert.rejects( readMapArchive(zipSync({ ...files, "../secret": strToU8("file") })), /unexpected/, ); await assert.rejects(readMapArchive(strToU8("not a zip")), /ZIP/); assert.throws( () => verifyArchiveSignature( { ...archive, originalOwner: "Impersonator" }, "test-key", ), /ownership/, ); assert.throws( () => verifyArchiveSignature({ ...archive, name: "Changed world" }, "test-key"), /ownership/, ); assert.throws(() => importManifest({ ...archive, contributionTo: randomUUID() }), ); assert.throws(() => importManifest({ ...archive, viewpoints: archive.viewpoints.map((p) => ({ ...p, assetRoot: randomUUID(), })), }), ); }); test("ZIP extraction rejects oversized entries before inflating them", async () => { const { files } = await fixture(); const zip = zipSync(files, { level: 0 }); // The first local header advertises an enormous uncompressed map. new DataView(zip.buffer).setUint32(22, 60 * 1024 * 1024, true); await assert.rejects(readMapArchive(zip), /oversized/); }); test("rendered server map imports are validated and processed without the terrain renderer", async () => { const { archive, files } = await fixture(); const manifest = importManifest(archive); const images = new Map(); await processFabricImages(manifest, { signal: new AbortController().signal, download: async (path) => Buffer.from(files[path]), save: async (path, bytes) => { images.set(path, bytes); }, progress: async () => {}, }); assert.equal(images.size, 36); assert.equal((await sharp(images.get("map.webp")).metadata()).format, "webp"); assert.equal( (await sharp(images.get("p0/preview.webp")).metadata()).width, 512, ); assert.equal(datasetSchema.parse(manifest).viewpoints.length, 5); const corrupt = Buffer.from("not an image"); const bad = datasetSchema.parse({ ...manifest, assets: manifest.assets.map((a) => a.path === "map.webp" ? { ...a, bytes: corrupt.length, sha256: "0".repeat(64) } : a, ), }); await assert.rejects( processFabricImages(bad, { signal: new AbortController().signal, download: async () => corrupt, save: async () => {}, progress: async () => {}, }), /checksum/, ); });