Checkpoint web parity through Chromium input tasks
This commit is contained in:
115
web/protocol/io-format-receipt-binding.ts
Normal file
115
web/protocol/io-format-receipt-binding.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { IOFormatRuntimeIdentityIR, IOFormatRuntimeReceiptIR, IOFormatRuntimeReceiptSetIR } from "./io-format-runtime-receipt";
|
||||
|
||||
export const IO_FORMAT_RECEIPT_BINDING_SCHEMA = 1 as const;
|
||||
export const IO_FORMAT_RECEIPT_BINDING_TASK = "M12-05E" as const;
|
||||
|
||||
export interface IOFormatReceiptBindingIR {
|
||||
sourceSha256: string;
|
||||
settingsSha256: string;
|
||||
runtimeSha256: string;
|
||||
}
|
||||
|
||||
export interface IOFormatBoundRuntimeReceiptIR extends IOFormatRuntimeReceiptIR, IOFormatReceiptBindingIR {}
|
||||
|
||||
export interface IOFormatBoundRuntimeReceiptSetIR {
|
||||
schemaVersion: typeof IO_FORMAT_RECEIPT_BINDING_SCHEMA;
|
||||
task: typeof IO_FORMAT_RECEIPT_BINDING_TASK;
|
||||
parentReceiptSetSha256: string;
|
||||
inventorySha256: string;
|
||||
runtime: IOFormatRuntimeIdentityIR;
|
||||
receipts: IOFormatBoundRuntimeReceiptIR[];
|
||||
}
|
||||
|
||||
export class IOFormatReceiptBindingError extends Error {
|
||||
readonly code = "IO_FORMAT_UNSUPPORTED" as const;
|
||||
|
||||
constructor(message: string) {
|
||||
super(`IO_FORMAT_UNSUPPORTED: ${message}`);
|
||||
this.name = "IOFormatReceiptBindingError";
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const FORMAT_ORDER = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"] as const;
|
||||
|
||||
function assertSha(value: unknown, path: string): string {
|
||||
if (typeof value !== "string" || !SHA256.test(value)) throw new IOFormatReceiptBindingError(`${path} is not SHA-256`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatReceiptBindingError(`${path} must be an object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function exactKeys(value: Record<string, unknown>, expected: readonly string[], path: string): void {
|
||||
const actual = Object.keys(value).sort();
|
||||
const allowed = [...expected].sort();
|
||||
if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatReceiptBindingError(`${path} contains undeclared fields`);
|
||||
}
|
||||
|
||||
function parseReceipt(value: unknown, index: number): IOFormatBoundRuntimeReceiptIR {
|
||||
const path = `receipts[${index}]`;
|
||||
const input = record(value, path);
|
||||
exactKeys(input, ["format", "family", "operation", "operator", "registered", "rnaIdentifier", "buildOption", "buildOptionEnabled", "runtimeStatus", "variants", "extensions", "sourceSha256", "settingsSha256", "runtimeSha256"], path);
|
||||
if (typeof input.format !== "string" || !FORMAT_ORDER.includes(input.format as typeof FORMAT_ORDER[number])) throw new IOFormatReceiptBindingError(`${path}.format is invalid`);
|
||||
if (input.operation !== "IMPORT" && input.operation !== "EXPORT") throw new IOFormatReceiptBindingError(`${path}.operation is invalid`);
|
||||
if (typeof input.family !== "string" || !input.family || typeof input.operator !== "string" || !input.operator) throw new IOFormatReceiptBindingError(`${path} identity is invalid`);
|
||||
if (typeof input.registered !== "boolean" || (input.rnaIdentifier !== null && typeof input.rnaIdentifier !== "string") || (input.buildOption !== null && typeof input.buildOption !== "string") || (input.buildOptionEnabled !== null && typeof input.buildOptionEnabled !== "boolean")) throw new IOFormatReceiptBindingError(`${path} runtime fields are invalid`);
|
||||
if (input.runtimeStatus !== "AVAILABLE" && input.runtimeStatus !== "OPERATOR_UNREGISTERED") throw new IOFormatReceiptBindingError(`${path}.runtimeStatus is invalid`);
|
||||
if (!Array.isArray(input.variants) || !Array.isArray(input.extensions) || input.variants.length === 0 || input.extensions.length === 0 || input.variants.some((item) => typeof item !== "string") || input.extensions.some((item) => typeof item !== "string")) throw new IOFormatReceiptBindingError(`${path} variants/extensions are invalid`);
|
||||
return {
|
||||
format: input.format as IOFormatBoundRuntimeReceiptIR["format"],
|
||||
family: input.family,
|
||||
operation: input.operation as IOFormatBoundRuntimeReceiptIR["operation"],
|
||||
operator: input.operator,
|
||||
registered: input.registered,
|
||||
rnaIdentifier: input.rnaIdentifier as string | null,
|
||||
buildOption: input.buildOption as string | null,
|
||||
buildOptionEnabled: input.buildOptionEnabled as boolean | null,
|
||||
runtimeStatus: input.runtimeStatus as IOFormatBoundRuntimeReceiptIR["runtimeStatus"],
|
||||
variants: [...input.variants as string[]],
|
||||
extensions: [...input.extensions as string[]],
|
||||
sourceSha256: assertSha(input.sourceSha256, `${path}.sourceSha256`),
|
||||
settingsSha256: assertSha(input.settingsSha256, `${path}.settingsSha256`),
|
||||
runtimeSha256: assertSha(input.runtimeSha256, `${path}.runtimeSha256`),
|
||||
};
|
||||
}
|
||||
|
||||
function parseRuntime(value: unknown): IOFormatRuntimeIdentityIR {
|
||||
const input = record(value, "runtime");
|
||||
if (!Array.isArray(input.versionTuple) || input.versionTuple.length !== 3 || input.versionTuple.some((item) => !Number.isSafeInteger(item))) throw new IOFormatReceiptBindingError("runtime.versionTuple is invalid");
|
||||
if (typeof input.binarySha256 !== "string" || !SHA256.test(input.binarySha256)) throw new IOFormatReceiptBindingError("runtime.binarySha256 is invalid");
|
||||
if (typeof input.blenderVersion !== "string" || typeof input.buildHash !== "string" || typeof input.buildBranch !== "string" || typeof input.buildPlatform !== "string" || typeof input.buildType !== "string" || typeof input.buildDate !== "string" || typeof input.buildTime !== "string" || !Number.isSafeInteger(input.buildCommitTimestamp) || typeof input.buildOptions !== "object" || input.buildOptions === null || Array.isArray(input.buildOptions)) throw new IOFormatReceiptBindingError("runtime identity is invalid");
|
||||
return input as unknown as IOFormatRuntimeIdentityIR;
|
||||
}
|
||||
|
||||
export function canonicalReceiptSource(receipt: IOFormatRuntimeReceiptIR): Record<string, unknown> {
|
||||
return { format: receipt.format, family: receipt.family, operation: receipt.operation, operator: receipt.operator, registered: receipt.registered, rnaIdentifier: receipt.rnaIdentifier };
|
||||
}
|
||||
|
||||
export function canonicalReceiptSettings(receipt: IOFormatRuntimeReceiptIR): Record<string, unknown> {
|
||||
return { buildOption: receipt.buildOption, buildOptionEnabled: receipt.buildOptionEnabled, variants: receipt.variants, extensions: receipt.extensions };
|
||||
}
|
||||
|
||||
export function canonicalRuntimeIdentity(runtime: IOFormatRuntimeIdentityIR): IOFormatRuntimeIdentityIR {
|
||||
return runtime;
|
||||
}
|
||||
|
||||
export function validateIOFormatBoundReceiptSet(value: unknown, expectedParentReceiptSetSha256: string, expectedInventorySha256: string): IOFormatBoundRuntimeReceiptSetIR {
|
||||
const input = record(value, "input");
|
||||
exactKeys(input, ["schemaVersion", "task", "parentReceiptSetSha256", "inventorySha256", "runtime", "receipts"], "input");
|
||||
if (input.schemaVersion !== IO_FORMAT_RECEIPT_BINDING_SCHEMA || input.task !== IO_FORMAT_RECEIPT_BINDING_TASK) throw new IOFormatReceiptBindingError("receipt binding header is invalid");
|
||||
if (!SHA256.test(expectedParentReceiptSetSha256) || !SHA256.test(expectedInventorySha256) || input.parentReceiptSetSha256 !== expectedParentReceiptSetSha256 || input.inventorySha256 !== expectedInventorySha256) throw new IOFormatReceiptBindingError("receipt binding parent identity drifted");
|
||||
if (!Array.isArray(input.receipts) || input.receipts.length !== FORMAT_ORDER.length * 2) throw new IOFormatReceiptBindingError("bound receipt count is invalid");
|
||||
const receipts = input.receipts.map(parseReceipt);
|
||||
const identities = receipts.map((receipt) => `${receipt.format}:${receipt.operation}`);
|
||||
if (new Set(identities).size !== identities.length || FORMAT_ORDER.some((format) => !["IMPORT", "EXPORT"].every((operation) => identities.includes(`${format}:${operation}`)))) throw new IOFormatReceiptBindingError("bound receipt identities are incomplete or duplicated");
|
||||
return { schemaVersion: IO_FORMAT_RECEIPT_BINDING_SCHEMA, task: IO_FORMAT_RECEIPT_BINDING_TASK, parentReceiptSetSha256: input.parentReceiptSetSha256, inventorySha256: input.inventorySha256, runtime: parseRuntime(input.runtime), receipts };
|
||||
}
|
||||
|
||||
export function resolveBoundReceipt(receiptSet: IOFormatBoundRuntimeReceiptSetIR, format: IOFormatBoundRuntimeReceiptIR["format"], operation: IOFormatBoundRuntimeReceiptIR["operation"]): IOFormatBoundRuntimeReceiptIR {
|
||||
const receipt = receiptSet.receipts.find((candidate) => candidate.format === format && candidate.operation === operation);
|
||||
if (!receipt || !SHA256.test(receipt.sourceSha256) || !SHA256.test(receipt.settingsSha256) || !SHA256.test(receipt.runtimeSha256)) throw new IOFormatReceiptBindingError("bound runtime receipt is unavailable");
|
||||
return receipt;
|
||||
}
|
||||
Reference in New Issue
Block a user