Add Chromium-only Blender WebEngine parity work
This commit is contained in:
127
web/protocol/simulation-cache.ts
Normal file
127
web/protocol/simulation-cache.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { ErrorCode } from "./error";
|
||||
|
||||
export const SIMULATION_CACHE_SCHEMA = 1 as const;
|
||||
export const SIMULATION_CACHE_BLENDER_VERSION_PREFIX = "5.2." as const;
|
||||
|
||||
export interface SimulationCacheFrameIR {
|
||||
frame: number;
|
||||
byteOffset: number;
|
||||
byteLength: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface SimulationCacheManifestIR {
|
||||
schemaVersion: typeof SIMULATION_CACHE_SCHEMA;
|
||||
graphId: string;
|
||||
graphHash: string;
|
||||
sourceBlendSha256: string;
|
||||
inputHash: string;
|
||||
cacheSha256: string;
|
||||
blenderVersion: string;
|
||||
frameStart: number;
|
||||
frameEnd: number;
|
||||
byteLength: number;
|
||||
frames: SimulationCacheFrameIR[];
|
||||
}
|
||||
|
||||
export class SimulationCacheValidationError extends Error {
|
||||
readonly code: ErrorCode;
|
||||
|
||||
constructor(code: ErrorCode, message: string) {
|
||||
super(message);
|
||||
this.name = "SimulationCacheValidationError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function integer(value: unknown, name: string, minimum = 0): number {
|
||||
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", `${name} must be an integer >= ${minimum}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function digest(value: unknown, name: string): string {
|
||||
if (typeof value !== "string" || !SHA256.test(value)) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", `${name} must be a lowercase SHA-256 digest`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseSimulationCacheManifest(value: unknown): SimulationCacheManifestIR {
|
||||
if (!record(value) || value.schemaVersion !== SIMULATION_CACHE_SCHEMA) {
|
||||
throw new SimulationCacheValidationError("PROTOCOL_MISMATCH", "Unsupported SimulationCache manifest schema");
|
||||
}
|
||||
for (const name of ["graphId", "blenderVersion"] as const) {
|
||||
if (typeof value[name] !== "string" || value[name].length === 0) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", `${name} is required`);
|
||||
}
|
||||
}
|
||||
if (!(value.blenderVersion as string).startsWith(SIMULATION_CACHE_BLENDER_VERSION_PREFIX)) {
|
||||
throw new SimulationCacheValidationError("PROTOCOL_MISMATCH", `Simulation cache requires Blender ${SIMULATION_CACHE_BLENDER_VERSION_PREFIX}x`);
|
||||
}
|
||||
const frameStart = integer(value.frameStart, "frameStart", -1_000_000);
|
||||
const frameEnd = integer(value.frameEnd, "frameEnd", -1_000_000);
|
||||
const byteLength = integer(value.byteLength, "byteLength", 1);
|
||||
if (frameEnd < frameStart || frameEnd - frameStart > 100_000 || !Array.isArray(value.frames)) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", "Simulation frame range is invalid");
|
||||
}
|
||||
if (value.frames.length !== frameEnd - frameStart + 1) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_MISSING", "Simulation cache must contain every declared frame");
|
||||
}
|
||||
let nextOffset = 0;
|
||||
const frames = value.frames.map((item, index): SimulationCacheFrameIR => {
|
||||
if (!record(item)) throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", `frames[${index}] is invalid`);
|
||||
const frame = integer(item.frame, `frames[${index}].frame`, -1_000_000);
|
||||
const byteOffset = integer(item.byteOffset, `frames[${index}].byteOffset`);
|
||||
const frameByteLength = integer(item.byteLength, `frames[${index}].byteLength`, 1);
|
||||
if (frame !== frameStart + index || byteOffset !== nextOffset || byteOffset + frameByteLength > byteLength) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", `frames[${index}] is not contiguous or ordered`);
|
||||
}
|
||||
nextOffset += frameByteLength;
|
||||
return { frame, byteOffset, byteLength: frameByteLength, sha256: digest(item.sha256, `frames[${index}].sha256`) };
|
||||
});
|
||||
if (nextOffset !== byteLength) throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", "Simulation frame ranges do not cover the cache payload");
|
||||
return {
|
||||
schemaVersion: SIMULATION_CACHE_SCHEMA,
|
||||
graphId: value.graphId as string,
|
||||
graphHash: digest(value.graphHash, "graphHash"),
|
||||
sourceBlendSha256: digest(value.sourceBlendSha256, "sourceBlendSha256"),
|
||||
inputHash: digest(value.inputHash, "inputHash"),
|
||||
cacheSha256: digest(value.cacheSha256, "cacheSha256"),
|
||||
blenderVersion: value.blenderVersion as string,
|
||||
frameStart,
|
||||
frameEnd,
|
||||
byteLength,
|
||||
frames,
|
||||
};
|
||||
}
|
||||
|
||||
async function sha256(data: ArrayBuffer): Promise<string> {
|
||||
const hash = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function verifySimulationCache(manifestValue: unknown, data: ArrayBuffer): Promise<SimulationCacheManifestIR> {
|
||||
const manifest = parseSimulationCacheManifest(manifestValue);
|
||||
if (data.byteLength !== manifest.byteLength || await sha256(data) !== manifest.cacheSha256) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_HASH_MISMATCH", "Simulation cache payload does not match its manifest");
|
||||
}
|
||||
for (const frame of manifest.frames) {
|
||||
const bytes = data.slice(frame.byteOffset, frame.byteOffset + frame.byteLength);
|
||||
if (await sha256(bytes) !== frame.sha256) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_HASH_MISMATCH", `Simulation frame ${frame.frame} failed SHA-256 verification`);
|
||||
}
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export function simulationCacheKey(manifest: SimulationCacheManifestIR): string {
|
||||
return `${manifest.graphHash.slice(0, 16)}-${manifest.sourceBlendSha256.slice(0, 16)}-${manifest.inputHash.slice(0, 16)}-${manifest.frameStart}-${manifest.frameEnd}`;
|
||||
}
|
||||
Reference in New Issue
Block a user