83 lines
5.2 KiB
JavaScript
83 lines
5.2 KiB
JavaScript
"use strict";
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const crypto = require("crypto");
|
|
|
|
const SCHEMA = "osm-asset-package/v1";
|
|
const CATEGORIES = new Set(["scene", "roads", "buildings", "vegetation", "water"]);
|
|
const ROLES = new Set(["scene", "layer"]);
|
|
|
|
function relativeUri(uri) {
|
|
if (typeof uri !== "string" || !uri || uri.startsWith("/") || path.isAbsolute(uri) || uri.includes("\\") || uri.split("/").includes("..")) {
|
|
throw new Error(`Asset URI must be a package-relative forward-slash path: ${uri}`);
|
|
}
|
|
return uri;
|
|
}
|
|
|
|
function validateManifest(manifest, packageDir, options = {}) {
|
|
const errors = [];
|
|
const fail = (message) => errors.push(message);
|
|
if (!manifest || manifest.schema !== SCHEMA) fail(`schema must be '${SCHEMA}'`);
|
|
if (typeof manifest?.packageVersion !== "string" || !manifest.packageVersion) fail("packageVersion is required");
|
|
if (typeof manifest?.areaId !== "string" || !manifest.areaId) fail("areaId is required");
|
|
const coordinateSystem = manifest?.coordinateSystem || {};
|
|
if (coordinateSystem.axes !== "ENU" || coordinateSystem.units !== "meters" || coordinateSystem.x !== "east" || coordinateSystem.y !== "north" || coordinateSystem.z !== "up") fail("coordinateSystem must declare ENU meters (east/north/up)");
|
|
const placement = manifest?.placement || {};
|
|
for (const key of ["longitude", "latitude", "height", "headingCorrectionDegrees"]) if (!Number.isFinite(placement[key])) fail(`placement.${key} must be finite`);
|
|
if (Number.isFinite(placement.longitude) && (placement.longitude < -180 || placement.longitude > 180)) fail("placement.longitude is out of range");
|
|
if (Number.isFinite(placement.latitude) && (placement.latitude < -90 || placement.latitude > 90)) fail("placement.latitude is out of range");
|
|
const bounds = manifest?.bounds || {};
|
|
for (const key of ["minLon", "minLat", "maxLon", "maxLat"]) if (!Number.isFinite(bounds[key])) fail(`bounds.${key} must be finite`);
|
|
if (Number.isFinite(bounds.minLon) && Number.isFinite(bounds.maxLon) && bounds.minLon > bounds.maxLon) fail("bounds longitude order is invalid");
|
|
if (Number.isFinite(bounds.minLat) && Number.isFinite(bounds.maxLat) && bounds.minLat > bounds.maxLat) fail("bounds latitude order is invalid");
|
|
if (!Array.isArray(manifest?.assets) || !manifest.assets.length) fail("assets must be a non-empty array");
|
|
const ids = new Set(); let sceneCount = 0;
|
|
for (const asset of manifest?.assets || []) {
|
|
if (!asset || typeof asset.id !== "string" || !asset.id) { fail("asset id is required"); continue; }
|
|
if (ids.has(asset.id)) fail(`duplicate asset id '${asset.id}'`); ids.add(asset.id);
|
|
if (!ROLES.has(asset.role)) fail(`asset '${asset.id}' has invalid role`);
|
|
if (!CATEGORIES.has(asset.category)) fail(`asset '${asset.id}' has invalid category`);
|
|
if (typeof asset.defaultLoad !== "boolean") fail(`asset '${asset.id}' defaultLoad must be boolean`);
|
|
try { relativeUri(asset.uri); } catch (error) { fail(error.message); continue; }
|
|
if (asset.role === "scene") {
|
|
sceneCount += 1;
|
|
if (asset.category !== "scene" || !asset.defaultLoad) fail("scene asset must be category scene and defaultLoad true");
|
|
} else if (asset.category === "scene" || asset.defaultLoad) fail("layer assets must use a semantic category and defaultLoad false");
|
|
if (packageDir && options.requireFiles !== false) {
|
|
const resolved = path.resolve(packageDir, asset.uri);
|
|
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`asset '${asset.id}' is missing from package: ${asset.uri}`);
|
|
}
|
|
}
|
|
if (manifest.runtime !== undefined && !Array.isArray(manifest.runtime)) fail("runtime must be an array");
|
|
for (const runtime of manifest?.runtime || []) {
|
|
if (!runtime || typeof runtime.id !== "string" || !runtime.id) { fail("runtime asset id is required"); continue; }
|
|
if (ids.has(runtime.id)) fail(`duplicate asset id '${runtime.id}'`); ids.add(runtime.id);
|
|
if (typeof runtime.type !== "string" || !runtime.type) fail(`runtime '${runtime.id}' type is required`);
|
|
try { relativeUri(runtime.uri); } catch (error) { fail(error.message); continue; }
|
|
if (packageDir && options.requireFiles !== false) {
|
|
const resolved = path.resolve(packageDir, runtime.uri);
|
|
if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`) || !fs.existsSync(resolved)) fail(`runtime '${runtime.id}' is missing from package: ${runtime.uri}`);
|
|
}
|
|
}
|
|
if (sceneCount !== 1) fail("exactly one scene asset is required");
|
|
if (errors.length) throw new Error(`Invalid asset package manifest: ${errors.join("; ")}`);
|
|
return manifest;
|
|
}
|
|
|
|
function addIntegrity(manifest, packageDir) {
|
|
for (const asset of manifest.assets) {
|
|
const file = path.resolve(packageDir, asset.uri);
|
|
const buffer = fs.readFileSync(file);
|
|
asset.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
|
|
}
|
|
for (const runtime of manifest.runtime || []) {
|
|
const file = path.resolve(packageDir, runtime.uri);
|
|
const buffer = fs.readFileSync(file);
|
|
runtime.integrity = { bytes: buffer.length, sha256: crypto.createHash("sha256").update(buffer).digest("hex") };
|
|
}
|
|
return manifest;
|
|
}
|
|
|
|
module.exports = { SCHEMA, CATEGORIES, ROLES, relativeUri, validateManifest, addIntegrity };
|