test(scum): verify live data asset digests

This commit is contained in:
npc0-hue
2026-08-11 18:32:46 +08:00
parent 05ddb3babf
commit 06bf22996a
3 changed files with 46 additions and 10 deletions
+23 -2
View File
@@ -1,4 +1,5 @@
import fs from "node:fs";
import crypto from "node:crypto";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -13,6 +14,10 @@ function readJson(filePath: string): unknown {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function sha256FileDigest(filePath: string): string {
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
}
function formatErrors(prefix: string, errors: ErrorObject[] | null | undefined): string[] {
return (errors ?? []).map((error) => `${prefix}${error.instancePath}: ${error.message}`);
}
@@ -1054,7 +1059,7 @@ export function validateRuntimeLogEventCatalog(manifest: unknown): string[] {
return errors;
}
export function validateSCUMLiveDataManifest(manifest: unknown): string[] {
export function validateSCUMLiveDataManifest(manifest: unknown, manifestDir?: string): string[] {
if (typeof manifest !== "object" || manifest === null) return [];
type Gate = { capability?: string; gate?: string; adapterVersion?: string; requiredSchemaFingerprint?: string; requiredAssetDigests?: string[]; evidenceStatus?: string; safeReason?: string };
type Probe = { capability?: string; targetKey?: string; bounds?: { maxSampleRows?: number; timeoutMs?: number; maxResultBytes?: number } };
@@ -1127,6 +1132,21 @@ export function validateSCUMLiveDataManifest(manifest: unknown): string[] {
if (!asset.assetPath || !isSafeRelativePathRef(asset.assetPath)) errors.push(`${assetLocation}.assetPath: must be a contained package-relative path`);
if (asset.assetPath && !assetFiles.has(asset.assetPath)) errors.push(`${assetLocation}.assetPath: must be declared in manifest.assetFiles`);
if (!digestPattern.test(asset.digest ?? "")) errors.push(`${assetLocation}.digest: must be a sha256 digest`);
if (asset.assetPath && asset.digest) validateSCUMAssetDigest(asset.assetPath, asset.digest, `${assetLocation}.digest`);
};
const validateSCUMAssetDigest = (assetPath: string, expectedDigest: string, digestLocation: string): void => {
if (!manifestDir || !isSafeRelativePathRef(assetPath) || !digestPattern.test(expectedDigest)) return;
const absoluteAssetPath = path.resolve(manifestDir, assetPath);
if (!fs.existsSync(absoluteAssetPath) || !fs.statSync(absoluteAssetPath).isFile()) {
errors.push(`${digestLocation}: missing packaged asset file ${assetPath}`);
return;
}
const relativeRealPath = path.relative(fs.realpathSync(manifestDir), fs.realpathSync(absoluteAssetPath));
if (relativeRealPath === ".." || relativeRealPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeRealPath)) {
errors.push(`${digestLocation}: asset file must remain inside the plugin manifest directory`);
return;
}
if (sha256FileDigest(absoluteAssetPath) !== expectedDigest) errors.push(`${digestLocation}: digest does not match packaged asset content`);
};
const requireUniqueAssetKeys = (assets: Asset[] | undefined, collection: string): void => {
const seenKeys = new Set<string>();
@@ -1218,6 +1238,7 @@ export function validateSCUMLiveDataManifest(manifest: unknown): string[] {
if (!mapAsset.transformAssetPath || !isSafeRelativePathRef(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be a contained package-relative path`);
if (mapAsset.transformAssetPath && !assetFiles.has(mapAsset.transformAssetPath)) errors.push(`${itemLocation}.transformAssetPath: must be declared in manifest.assetFiles`);
if (!digestPattern.test(mapAsset.transformDigest ?? "")) errors.push(`${itemLocation}.transformDigest: must be a sha256 digest`);
if (mapAsset.transformAssetPath && mapAsset.transformDigest) validateSCUMAssetDigest(mapAsset.transformAssetPath, mapAsset.transformDigest, `${itemLocation}.transformDigest`);
const bounds = mapAsset.worldBounds;
if (!bounds || !Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY) || !Number.isFinite(bounds.maxX) || !Number.isFinite(bounds.maxY) || (bounds.minX ?? 0) >= (bounds.maxX ?? 0) || (bounds.minY ?? 0) >= (bounds.maxY ?? 0)) errors.push(`${itemLocation}.worldBounds: must define finite increasing bounds`);
if (!Number.isInteger(mapAsset.image?.width) || (mapAsset.image?.width ?? 0) < 1 || !Number.isInteger(mapAsset.image?.height) || (mapAsset.image?.height ?? 0) < 1) errors.push(`${itemLocation}.image: width and height must be positive integers`);
@@ -1554,7 +1575,7 @@ export function validateManifestFile(manifestPath: string): string[] {
errors.push(...validateClientManagerProfiles(manifest));
errors.push(...validateDLLExtensionProfiles(manifest));
errors.push(...validateGameClientBridgeCatalog(manifest));
errors.push(...validateSCUMLiveDataManifest(manifest));
errors.push(...validateSCUMLiveDataManifest(manifest, manifestDir));
errors.push(...validateGameClientBridgeSchemaFiles(manifest, manifestDir));
errors.push(...validateGameClientBridgeCompanionConfig(manifest, manifestDir));
errors.push(...validateRuntimeLogEventCatalog(manifest));