diff --git a/tests/placesPipeline.test.js b/tests/placesPipeline.test.js index 8dc8cec..6d78011 100644 --- a/tests/placesPipeline.test.js +++ b/tests/placesPipeline.test.js @@ -1,8 +1,9 @@ +import { isAbsolute } from "node:path"; import { beforeAll, describe, expect, it } from "vitest"; import { Document } from "@gltf-transform/core"; import sharp from "sharp"; import { EXT_MATERIALS_BUMP, EXTMaterialsBump } from "../tools/assets/bumpExtension.mjs"; -import { FAR_TEXTURE_SIZE, NEAR_TEXTURE_SIZE, optimizePlace } from "../tools/assets/places.mjs"; +import { FAR_TEXTURE_SIZE, NEAR_TEXTURE_SIZE, optimizePlace, placeTarget } from "../tools/assets/places.mjs"; /** 1100 px große Streifen. seed macht die Bilder verschieden, sonst legt dedup() sie zusammen. */ function stripes(seed) { @@ -95,3 +96,21 @@ describe("optimizePlace", () => { expect(used).not.toContain("EXT_mesh_gpu_instancing"); }); }); + +describe("placeTarget", () => { + // Bruch: ein Pfad relativ zum Arbeitsverzeichnis statt zum Projekt — bei einem Aufruf aus einem + // anderen Ordner (z. B. aus einem Skript) landete die Datei am falschen Ort oder mkdir schlug fehl. + it("liefert den absoluten Zielpfad unter public/models/places/", () => { + const target = placeTarget("hood-yard"); + + expect(isAbsolute(target)).toBe(true); + expect(target.endsWith("public/models/places/hood-yard.glb")).toBe(true); + }); + + // Bruch: eine ID mit Pfadtrennzeichen oder Großbuchstaben schreibt außerhalb von + // public/models/places/ oder unter einem Namen, den PLACES so nie referenziert. + it("lehnt IDs mit Pfadtrennzeichen oder Großbuchstaben ab", () => { + expect(() => placeTarget("../x")).toThrow(); + expect(() => placeTarget("Hood Yard")).toThrow(); + }); +}); diff --git a/tools/assets/places.mjs b/tools/assets/places.mjs index 1557111..1c8f008 100644 --- a/tools/assets/places.mjs +++ b/tools/assets/places.mjs @@ -8,7 +8,7 @@ // verwirft EXT_materials_bump, und darin steckt das Wellblech der Container (bumpExtension.mjs). import { mkdir, stat } from "node:fs/promises"; import { dirname } from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { ExtensionProperty, NodeIO, PropertyType } from "@gltf-transform/core"; import { ALL_EXTENSIONS, EXTTextureWebP } from "@gltf-transform/extensions"; import { compressTexture, dedup, flatten, getBounds, join, listTextureSlots, meshopt, prune, @@ -119,13 +119,27 @@ export function describePlace(document) { }; } +/** + * Zielpfad für ein aufbereitetes Modell, absolut und unabhängig vom Arbeitsverzeichnis (public/ + * models/places/.glb, relativ zu dieser Datei aufgelöst statt zu process.cwd()). id landet auch + * in src/world/places.js (PLACES) und im Dateinamen — nur Kleinbuchstaben, Ziffern und Bindestrich, + * damit sie auf jedem Dateisystem gleich aussieht und nicht aus public/models/places/ herausführt. + */ +export function placeTarget(id) { + if (!/^[a-z0-9-]+$/.test(id)) { + throw new Error(`Ungültige Orts-ID "${id}": erlaubt sind nur Kleinbuchstaben, Ziffern und Bindestrich.`); + } + const dir = new URL("../../public/models/places/", import.meta.url); + return fileURLToPath(new URL(`${id}.glb`, dir)); +} + async function main() { const [source, id] = process.argv.slice(2); if (!source || !id) { - console.error("Aufruf: node tools/assets/places.mjs "); + console.error("Aufruf: node tools/assets/places.mjs "); process.exit(1); } - const target = `public/models/places/${id}.glb`; + const target = placeTarget(id); const io = new NodeIO() .registerExtensions([...ALL_EXTENSIONS, EXTMaterialsBump]) .registerDependencies({ "meshopt.encoder": MeshoptEncoder });