Files
MineGeo/tests/map-sharing.test.ts

171 lines
5.4 KiB
TypeScript

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<string, Uint8Array> = { "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<string, Buffer>();
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/,
);
});