Checkpoint web parity through Chromium input tasks
This commit is contained in:
167
web/protocol/io-format-runtime-receipt.ts
Normal file
167
web/protocol/io-format-runtime-receipt.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import type { IOFormatMatrixFormat, IOFormatMatrixOperation } from "./io-format-capability-matrix";
|
||||
|
||||
export const IO_FORMAT_RUNTIME_RECEIPT_SCHEMA = 1 as const;
|
||||
export const IO_FORMAT_RUNTIME_RECEIPT_TASK = "M12-05D" as const;
|
||||
export const IO_FORMAT_RUNTIME_FORMATS = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"] as const;
|
||||
export type IOFormatRuntimeFormat = typeof IO_FORMAT_RUNTIME_FORMATS[number];
|
||||
export type IOFormatRuntimeOperation = IOFormatMatrixOperation;
|
||||
export type IOFormatRuntimeStatus = "AVAILABLE" | "OPERATOR_UNREGISTERED";
|
||||
|
||||
export interface IOFormatRuntimeIdentityIR {
|
||||
blenderVersion: string;
|
||||
versionTuple: [number, number, number];
|
||||
buildHash: string;
|
||||
buildBranch: string;
|
||||
buildPlatform: string;
|
||||
buildType: string;
|
||||
buildDate: string;
|
||||
buildTime: string;
|
||||
buildCommitTimestamp: number;
|
||||
binarySha256: string;
|
||||
buildOptions: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export interface IOFormatRuntimeReceiptIR {
|
||||
format: IOFormatRuntimeFormat;
|
||||
family: string;
|
||||
operation: IOFormatRuntimeOperation;
|
||||
operator: string;
|
||||
registered: boolean;
|
||||
rnaIdentifier: string | null;
|
||||
buildOption: string | null;
|
||||
buildOptionEnabled: boolean | null;
|
||||
runtimeStatus: IOFormatRuntimeStatus;
|
||||
variants: string[];
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export interface IOFormatRuntimeReceiptSetIR {
|
||||
schemaVersion: typeof IO_FORMAT_RUNTIME_RECEIPT_SCHEMA;
|
||||
task: typeof IO_FORMAT_RUNTIME_RECEIPT_TASK;
|
||||
inventorySha256: string;
|
||||
runtime: IOFormatRuntimeIdentityIR;
|
||||
receipts: IOFormatRuntimeReceiptIR[];
|
||||
}
|
||||
|
||||
export interface IOFormatRuntimeRouteQuery {
|
||||
format: IOFormatRuntimeFormat;
|
||||
operation: IOFormatRuntimeOperation;
|
||||
}
|
||||
|
||||
export type IOFormatRuntimeRouteResult =
|
||||
| { status: "READY"; format: IOFormatRuntimeFormat; operation: IOFormatRuntimeOperation; operator: string; receipt: IOFormatRuntimeReceiptIR }
|
||||
| { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; format: IOFormatRuntimeFormat; operation: IOFormatRuntimeOperation };
|
||||
|
||||
export class IOFormatRuntimeReceiptError extends Error {
|
||||
readonly code = "IO_FORMAT_UNSUPPORTED" as const;
|
||||
|
||||
constructor(message: string) {
|
||||
super(`IO_FORMAT_UNSUPPORTED: ${message}`);
|
||||
this.name = "IOFormatRuntimeReceiptError";
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:\s+.*)?$/;
|
||||
const IDENTIFIER = /^[A-Za-z0-9_.:-]+$/;
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatRuntimeReceiptError(`${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 IOFormatRuntimeReceiptError(`${path} contains undeclared fields`);
|
||||
}
|
||||
|
||||
function nonEmpty(value: unknown, path: string, maximum = 256): string {
|
||||
if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new IOFormatRuntimeReceiptError(`${path} is invalid`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function sha(value: unknown, path: string): string {
|
||||
if (typeof value !== "string" || !SHA256.test(value)) throw new IOFormatRuntimeReceiptError(`${path} is not SHA-256`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseRuntime(value: unknown): IOFormatRuntimeIdentityIR {
|
||||
const input = record(value, "runtime");
|
||||
exactKeys(input, ["blenderVersion", "versionTuple", "buildHash", "buildBranch", "buildPlatform", "buildType", "buildDate", "buildTime", "buildCommitTimestamp", "binarySha256", "buildOptions"], "runtime");
|
||||
if (typeof input.blenderVersion !== "string" || !VERSION.test(input.blenderVersion)) throw new IOFormatRuntimeReceiptError("runtime.blenderVersion is invalid");
|
||||
if (!Array.isArray(input.versionTuple) || input.versionTuple.length !== 3 || input.versionTuple.some((part) => !Number.isSafeInteger(part) || (part as number) < 0)) throw new IOFormatRuntimeReceiptError("runtime.versionTuple is invalid");
|
||||
const buildOptions = record(input.buildOptions, "runtime.buildOptions");
|
||||
const parsedOptions: Record<string, boolean> = {};
|
||||
for (const key of Object.keys(buildOptions).sort()) {
|
||||
if (!IDENTIFIER.test(key) || typeof buildOptions[key] !== "boolean") throw new IOFormatRuntimeReceiptError("runtime.buildOptions is invalid");
|
||||
parsedOptions[key] = buildOptions[key] as boolean;
|
||||
}
|
||||
if (!Number.isSafeInteger(input.buildCommitTimestamp) || (input.buildCommitTimestamp as number) < 0) throw new IOFormatRuntimeReceiptError("runtime.buildCommitTimestamp is invalid");
|
||||
return {
|
||||
blenderVersion: input.blenderVersion,
|
||||
versionTuple: [...input.versionTuple] as [number, number, number],
|
||||
buildHash: nonEmpty(input.buildHash, "runtime.buildHash"),
|
||||
buildBranch: nonEmpty(input.buildBranch, "runtime.buildBranch"),
|
||||
buildPlatform: nonEmpty(input.buildPlatform, "runtime.buildPlatform"),
|
||||
buildType: nonEmpty(input.buildType, "runtime.buildType"),
|
||||
buildDate: nonEmpty(input.buildDate, "runtime.buildDate"),
|
||||
buildTime: nonEmpty(input.buildTime, "runtime.buildTime"),
|
||||
buildCommitTimestamp: input.buildCommitTimestamp as number,
|
||||
binarySha256: sha(input.binarySha256, "runtime.binarySha256"),
|
||||
buildOptions: parsedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
function parseReceipt(value: unknown, index: number): IOFormatRuntimeReceiptIR {
|
||||
const path = `receipts[${index}]`;
|
||||
const input = record(value, path);
|
||||
exactKeys(input, ["format", "family", "operation", "operator", "registered", "rnaIdentifier", "buildOption", "buildOptionEnabled", "runtimeStatus", "variants", "extensions"], path);
|
||||
if (!IO_FORMAT_RUNTIME_FORMATS.includes(input.format as IOFormatRuntimeFormat) || !["IMPORT", "EXPORT"].includes(input.operation as string)) throw new IOFormatRuntimeReceiptError(`${path} identity is invalid`);
|
||||
if (typeof input.registered !== "boolean" || !["AVAILABLE", "OPERATOR_UNREGISTERED"].includes(input.runtimeStatus as string)) throw new IOFormatRuntimeReceiptError(`${path} registration status is invalid`);
|
||||
if (input.rnaIdentifier !== null && (typeof input.rnaIdentifier !== "string" || !IDENTIFIER.test(input.rnaIdentifier))) throw new IOFormatRuntimeReceiptError(`${path}.rnaIdentifier is invalid`);
|
||||
if (input.buildOption !== null && (typeof input.buildOption !== "string" || !IDENTIFIER.test(input.buildOption))) throw new IOFormatRuntimeReceiptError(`${path}.buildOption is invalid`);
|
||||
if (input.buildOptionEnabled !== null && typeof input.buildOptionEnabled !== "boolean") throw new IOFormatRuntimeReceiptError(`${path}.buildOptionEnabled is invalid`);
|
||||
if (!Array.isArray(input.variants) || input.variants.length === 0 || input.variants.some((variant) => typeof variant !== "string" || !IDENTIFIER.test(variant))) throw new IOFormatRuntimeReceiptError(`${path}.variants are invalid`);
|
||||
if (!Array.isArray(input.extensions) || input.extensions.length === 0 || input.extensions.some((extension) => typeof extension !== "string" || !/^\.[a-z0-9]+$/.test(extension))) throw new IOFormatRuntimeReceiptError(`${path}.extensions are invalid`);
|
||||
const available = input.runtimeStatus === "AVAILABLE";
|
||||
if (available !== input.registered || (available && input.rnaIdentifier === null) || (!available && input.rnaIdentifier !== null) || (!available && input.buildOptionEnabled !== null)) throw new IOFormatRuntimeReceiptError(`${path} has inconsistent runtime receipt state`);
|
||||
return {
|
||||
format: input.format as IOFormatRuntimeFormat,
|
||||
family: nonEmpty(input.family, `${path}.family`),
|
||||
operation: input.operation as IOFormatRuntimeOperation,
|
||||
operator: nonEmpty(input.operator, `${path}.operator`),
|
||||
registered: input.registered as boolean,
|
||||
rnaIdentifier: input.rnaIdentifier as string | null,
|
||||
buildOption: input.buildOption as string | null,
|
||||
buildOptionEnabled: input.buildOptionEnabled as boolean | null,
|
||||
runtimeStatus: input.runtimeStatus as IOFormatRuntimeStatus,
|
||||
variants: [...input.variants as string[]],
|
||||
extensions: [...input.extensions as string[]],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseIOFormatRuntimeReceiptSet(value: unknown): IOFormatRuntimeReceiptSetIR {
|
||||
const input = record(value, "input");
|
||||
exactKeys(input, ["schemaVersion", "task", "inventorySha256", "runtime", "receipts"], "input");
|
||||
if (input.schemaVersion !== IO_FORMAT_RUNTIME_RECEIPT_SCHEMA || input.task !== IO_FORMAT_RUNTIME_RECEIPT_TASK) throw new IOFormatRuntimeReceiptError("receipt set header is invalid");
|
||||
const receipts = Array.isArray(input.receipts) ? input.receipts.map(parseReceipt) : (() => { throw new IOFormatRuntimeReceiptError("input.receipts must be an array"); })();
|
||||
if (typeof input.inventorySha256 !== "string" || !SHA256.test(input.inventorySha256)) throw new IOFormatRuntimeReceiptError("input.inventorySha256 is invalid");
|
||||
if (receipts.length !== IO_FORMAT_RUNTIME_FORMATS.length * 2) throw new IOFormatRuntimeReceiptError("receipt set must contain one import and export receipt per format");
|
||||
const identities = receipts.map((receipt) => `${receipt.format}:${receipt.operation}`);
|
||||
if (new Set(identities).size !== identities.length || IO_FORMAT_RUNTIME_FORMATS.some((format) => !["IMPORT", "EXPORT"].every((operation) => identities.includes(`${format}:${operation}`)))) throw new IOFormatRuntimeReceiptError("receipt identities are incomplete or duplicated");
|
||||
return { schemaVersion: IO_FORMAT_RUNTIME_RECEIPT_SCHEMA, task: IO_FORMAT_RUNTIME_RECEIPT_TASK, inventorySha256: input.inventorySha256, runtime: parseRuntime(input.runtime), receipts };
|
||||
}
|
||||
|
||||
export function validateIOFormatRuntimeReceiptSet(value: unknown, expectedInventorySha256: string): IOFormatRuntimeReceiptSetIR {
|
||||
if (!SHA256.test(expectedInventorySha256)) throw new IOFormatRuntimeReceiptError("expected inventory SHA-256 is invalid");
|
||||
const parsed = parseIOFormatRuntimeReceiptSet(value);
|
||||
if (parsed.inventorySha256 !== expectedInventorySha256) throw new IOFormatRuntimeReceiptError("runtime receipt inventory identity does not match");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function resolveIOFormatRuntimeRoute(receiptSet: IOFormatRuntimeReceiptSetIR, query: IOFormatRuntimeRouteQuery): IOFormatRuntimeRouteResult {
|
||||
const receipt = receiptSet.receipts.find((candidate) => candidate.format === query.format && candidate.operation === query.operation);
|
||||
if (!receipt || receipt.runtimeStatus !== "AVAILABLE" || receipt.registered !== true || receipt.rnaIdentifier === null || receipt.buildOptionEnabled === false) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", format: query.format, operation: query.operation };
|
||||
return { status: "READY", format: query.format, operation: query.operation, operator: receipt.operator, receipt };
|
||||
}
|
||||
Reference in New Issue
Block a user