Checkpoint web parity through Chromium input tasks
This commit is contained in:
149
web/protocol/io-format-ui-gate.ts
Normal file
149
web/protocol/io-format-ui-gate.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type {
|
||||
IOFormatCapabilityMatrixIR,
|
||||
IOFormatMatrixExecution,
|
||||
IOFormatMatrixFormat,
|
||||
IOFormatMatrixOperation,
|
||||
} from "./io-format-capability-matrix";
|
||||
|
||||
export const IO_FORMAT_UI_GATE_SCHEMA = 1 as const;
|
||||
export const IO_FORMAT_UI_TASK = "M12-05C" as const;
|
||||
export const IO_FORMAT_PROJECT_ACCEPT = ".blend,application/octet-stream" as const;
|
||||
|
||||
export interface IOFormatUIRouteIR {
|
||||
format: IOFormatMatrixFormat;
|
||||
operation: IOFormatMatrixOperation;
|
||||
execution: IOFormatMatrixExecution;
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export interface IOFormatUIRegistryIR {
|
||||
schemaVersion: typeof IO_FORMAT_UI_GATE_SCHEMA;
|
||||
task: typeof IO_FORMAT_UI_TASK;
|
||||
parentMatrixSha256: string;
|
||||
projectFileAccept: typeof IO_FORMAT_PROJECT_ACCEPT;
|
||||
importRoutes: IOFormatUIRouteIR[];
|
||||
exportRoutes: IOFormatUIRouteIR[];
|
||||
}
|
||||
|
||||
export interface IOFormatUICommandRef {
|
||||
format: IOFormatMatrixFormat;
|
||||
operation: IOFormatMatrixOperation;
|
||||
execution: IOFormatMatrixExecution;
|
||||
}
|
||||
|
||||
export class IOFormatUIGateError extends Error {
|
||||
readonly code = "IO_FORMAT_UNSUPPORTED" as const;
|
||||
|
||||
constructor(message: string) {
|
||||
super(`IO_FORMAT_UNSUPPORTED: ${message}`);
|
||||
this.name = "IOFormatUIGateError";
|
||||
}
|
||||
}
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const FORMAT_EXTENSIONS: Record<IOFormatMatrixFormat, readonly string[]> = {
|
||||
GLTF: [".gltf"],
|
||||
GLB: [".glb"],
|
||||
OBJ: [".obj"],
|
||||
STL: [".stl"],
|
||||
PLY: [".ply"],
|
||||
USD: [".usd", ".usda", ".usdc"],
|
||||
ALEMBIC: [".abc"],
|
||||
};
|
||||
|
||||
function routeFor(
|
||||
matrix: IOFormatCapabilityMatrixIR,
|
||||
operation: IOFormatMatrixOperation,
|
||||
execution: IOFormatMatrixExecution,
|
||||
): IOFormatUIRouteIR[] {
|
||||
return matrix.formats
|
||||
.filter((entry) => {
|
||||
const route = entry.operations[operation][execution.toLowerCase() as "local" | "server"];
|
||||
const runtimeStatus = operation === "IMPORT" ? entry.runtimeImportStatus : entry.runtimeExportStatus;
|
||||
return runtimeStatus === "AVAILABLE" && route.status === "READY" && route.execution === execution;
|
||||
})
|
||||
.map((entry) => ({
|
||||
format: entry.format,
|
||||
operation,
|
||||
execution,
|
||||
extensions: [...FORMAT_EXTENSIONS[entry.format]],
|
||||
}));
|
||||
}
|
||||
|
||||
export function buildIOFormatUIRegistry(matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR {
|
||||
if (!SHA256.test(parentMatrixSha256)) throw new IOFormatUIGateError("parent matrix SHA-256 is invalid");
|
||||
return {
|
||||
schemaVersion: IO_FORMAT_UI_GATE_SCHEMA,
|
||||
task: IO_FORMAT_UI_TASK,
|
||||
parentMatrixSha256,
|
||||
projectFileAccept: IO_FORMAT_PROJECT_ACCEPT,
|
||||
importRoutes: routeFor(matrix, "IMPORT", "LOCAL"),
|
||||
exportRoutes: routeFor(matrix, "EXPORT", "LOCAL"),
|
||||
};
|
||||
}
|
||||
|
||||
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 IOFormatUIGateError(`${path} contains undeclared fields`);
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatUIGateError(`${path} must be an object`);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function parseRoute(value: unknown, path: string, operation: IOFormatMatrixOperation): IOFormatUIRouteIR {
|
||||
const input = record(value, path);
|
||||
exactKeys(input, ["format", "operation", "execution", "extensions"], path);
|
||||
if (typeof input.format !== "string" || !(input.format in FORMAT_EXTENSIONS) || input.operation !== operation || input.execution !== "LOCAL") throw new IOFormatUIGateError(`${path} route identity is invalid`);
|
||||
if (!Array.isArray(input.extensions) || input.extensions.length !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat].length || input.extensions.some((extension, index) => extension !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat][index])) throw new IOFormatUIGateError(`${path}.extensions are invalid`);
|
||||
return { format: input.format as IOFormatMatrixFormat, operation, execution: "LOCAL", extensions: [...input.extensions as string[]] };
|
||||
}
|
||||
|
||||
export function parseIOFormatUIRegistry(value: unknown): IOFormatUIRegistryIR {
|
||||
const input = record(value, "input");
|
||||
exactKeys(input, ["schemaVersion", "task", "parentMatrixSha256", "projectFileAccept", "importRoutes", "exportRoutes"], "input");
|
||||
if (input.schemaVersion !== IO_FORMAT_UI_GATE_SCHEMA || input.task !== IO_FORMAT_UI_TASK || typeof input.parentMatrixSha256 !== "string" || !SHA256.test(input.parentMatrixSha256) || input.projectFileAccept !== IO_FORMAT_PROJECT_ACCEPT) throw new IOFormatUIGateError("registry header is invalid");
|
||||
if (!Array.isArray(input.importRoutes) || !Array.isArray(input.exportRoutes)) throw new IOFormatUIGateError("registry routes are invalid");
|
||||
const importRoutes = input.importRoutes.map((route, index) => parseRoute(route, `importRoutes[${index}]`, "IMPORT"));
|
||||
const exportRoutes = input.exportRoutes.map((route, index) => parseRoute(route, `exportRoutes[${index}]`, "EXPORT"));
|
||||
const identities = [...importRoutes, ...exportRoutes].map((route) => `${route.operation}:${route.execution}:${route.format}`);
|
||||
if (new Set(identities).size !== identities.length) throw new IOFormatUIGateError("registry contains duplicate route identities");
|
||||
return { schemaVersion: IO_FORMAT_UI_GATE_SCHEMA, task: IO_FORMAT_UI_TASK, parentMatrixSha256: input.parentMatrixSha256, projectFileAccept: IO_FORMAT_PROJECT_ACCEPT, importRoutes, exportRoutes };
|
||||
}
|
||||
|
||||
function routeIdentity(route: IOFormatUIRouteIR): string {
|
||||
return `${route.operation}:${route.execution}:${route.format}:${route.extensions.join("|")}`;
|
||||
}
|
||||
|
||||
export function validateIOFormatUIRegistry(value: unknown, matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR {
|
||||
const parsed = parseIOFormatUIRegistry(value);
|
||||
const expected = buildIOFormatUIRegistry(matrix, parentMatrixSha256);
|
||||
const actualRoutes = [...parsed.importRoutes, ...parsed.exportRoutes].map(routeIdentity);
|
||||
const expectedRoutes = [...expected.importRoutes, ...expected.exportRoutes].map(routeIdentity);
|
||||
if (parsed.parentMatrixSha256 !== parentMatrixSha256 || actualRoutes.length !== expectedRoutes.length || actualRoutes.some((route, index) => route !== expectedRoutes[index])) throw new IOFormatUIGateError("registry route is not declared by the capability matrix");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function routeMatches(registry: IOFormatUIRegistryIR, command: IOFormatUICommandRef): boolean {
|
||||
const routes = command.operation === "IMPORT" ? registry.importRoutes : registry.exportRoutes;
|
||||
return routes.some((route) => route.format === command.format && route.execution === command.execution);
|
||||
}
|
||||
|
||||
export function filterIOFormatOperatorCommands<T extends { ioFormat?: IOFormatUICommandRef }>(commands: readonly T[], registry: IOFormatUIRegistryIR): T[] {
|
||||
return commands.filter((command) => !command.ioFormat || routeMatches(registry, command.ioFormat));
|
||||
}
|
||||
|
||||
export function gateIOFormatFileSelection(fileName: string, registry: IOFormatUIRegistryIR): { status: "READY"; kind: "BLEND" } | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; extension: string } {
|
||||
const extension = fileName.trim().toLowerCase().match(/\.[a-z0-9]+$/)?.[0] ?? "";
|
||||
if (extension === ".blend") return { status: "READY", kind: "BLEND" };
|
||||
const route = registry.importRoutes.find((candidate) => candidate.extensions.includes(extension));
|
||||
if (route) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension };
|
||||
return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension };
|
||||
}
|
||||
|
||||
export function ioFormatUIAccept(registry: IOFormatUIRegistryIR): string {
|
||||
const importExtensions = registry.importRoutes.flatMap((route) => route.extensions);
|
||||
return [registry.projectFileAccept, ...importExtensions].join(",");
|
||||
}
|
||||
Reference in New Issue
Block a user