Files
workinf_Blender_Wasm/web/protocol/archive-metadata-first.ts
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

136 lines
7.1 KiB
TypeScript

import type { ErrorCode } from "./error";
export const ARCHIVE_METADATA_FIRST_SCHEMA = 1 as const;
export const ARCHIVE_METADATA_FORMATS = ["ZIP", "TAR"] as const;
export type ArchiveMetadataFormat = typeof ARCHIVE_METADATA_FORMATS[number];
export interface ArchiveMetadataFirstRequestIR {
schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA;
archiveId: string;
format: ArchiveMetadataFormat;
archiveByteLength: number;
metadataOffset: number;
metadataByteLength: number;
}
export interface ArchiveMetadataReadIR {
kind: "CENTRAL_DIRECTORY" | "MANIFEST";
byteOffset: number;
byteLength: number;
}
export interface ArchiveMetadataFirstPlanIR {
status: "METADATA_ONLY";
schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA;
archiveId: string;
format: ArchiveMetadataFormat;
firstRead: ArchiveMetadataReadIR;
payloadReads: [];
}
export interface ArchiveReadTraceItemIR {
sequence: number;
kind: "CENTRAL_DIRECTORY" | "MANIFEST" | "PAYLOAD";
byteOffset: number;
byteLength: number;
}
export interface ArchiveReadTraceIR {
schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA;
archiveId: string;
reads: ArchiveReadTraceItemIR[];
}
export interface ArchiveReadTraceValidationIR {
status: "VALID";
metadataFirst: true;
payloadReadsAfterMetadata: true;
}
export class ArchiveMetadataFirstError extends Error {
readonly code: ErrorCode;
readonly path?: string;
constructor(message: string, path?: string) {
super(`IO_ARCHIVE_UNSAFE: ${message}`);
this.name = "ArchiveMetadataFirstError";
this.code = "IO_ARCHIVE_UNSAFE";
this.path = path;
}
}
const ARCHIVE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const MAX_ARCHIVE_BYTES = Number.MAX_SAFE_INTEGER;
const MAX_METADATA_BYTES = 64 * 1024 * 1024;
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ArchiveMetadataFirstError(`${path} must be an object`, path);
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 ArchiveMetadataFirstError(`${path} contains undeclared fields`, path);
}
function integer(value: unknown, path: string, minimum = 0): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > MAX_ARCHIVE_BYTES) throw new ArchiveMetadataFirstError(`${path} must be a safe integer`, path);
return value;
}
function parseRequest(value: unknown): ArchiveMetadataFirstRequestIR {
const input = record(value, "input");
exactKeys(input, ["schemaVersion", "archiveId", "format", "archiveByteLength", "metadataOffset", "metadataByteLength"], "input");
if (input.schemaVersion !== ARCHIVE_METADATA_FIRST_SCHEMA) throw new ArchiveMetadataFirstError("unsupported archive metadata schema", "input.schemaVersion");
if (typeof input.archiveId !== "string" || !ARCHIVE_ID.test(input.archiveId)) throw new ArchiveMetadataFirstError("archiveId is invalid", "input.archiveId");
if (!ARCHIVE_METADATA_FORMATS.includes(input.format as ArchiveMetadataFormat)) throw new ArchiveMetadataFirstError("archive format is unsupported", "input.format");
const archiveByteLength = integer(input.archiveByteLength, "input.archiveByteLength", 1);
const metadataOffset = integer(input.metadataOffset, "input.metadataOffset");
const metadataByteLength = integer(input.metadataByteLength, "input.metadataByteLength", 1);
if (metadataByteLength > MAX_METADATA_BYTES || metadataOffset + metadataByteLength > archiveByteLength) throw new ArchiveMetadataFirstError("metadata range is outside the archive or exceeds its bound", "input.metadataByteLength");
return { schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA, archiveId: input.archiveId, format: input.format as ArchiveMetadataFormat, archiveByteLength, metadataOffset, metadataByteLength };
}
export function planArchiveMetadataRead(value: unknown): ArchiveMetadataFirstPlanIR {
const request = parseRequest(value);
return {
status: "METADATA_ONLY",
schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA,
archiveId: request.archiveId,
format: request.format,
firstRead: { kind: request.format === "ZIP" ? "CENTRAL_DIRECTORY" : "MANIFEST", byteOffset: request.metadataOffset, byteLength: request.metadataByteLength },
payloadReads: [],
};
}
function parseTraceItem(value: unknown, index: number): ArchiveReadTraceItemIR {
const path = `reads[${index}]`;
const item = record(value, path);
exactKeys(item, ["sequence", "kind", "byteOffset", "byteLength"], path);
const sequence = integer(item.sequence, `${path}.sequence`);
if (!(["CENTRAL_DIRECTORY", "MANIFEST", "PAYLOAD"] as const).includes(item.kind as ArchiveReadTraceItemIR["kind"])) throw new ArchiveMetadataFirstError(`${path}.kind is unsupported`, `${path}.kind`);
return { sequence, kind: item.kind as ArchiveReadTraceItemIR["kind"], byteOffset: integer(item.byteOffset, `${path}.byteOffset`), byteLength: integer(item.byteLength, `${path}.byteLength`, 1) };
}
function parseTrace(value: unknown): ArchiveReadTraceIR {
const input = record(value, "trace");
exactKeys(input, ["schemaVersion", "archiveId", "reads"], "trace");
if (input.schemaVersion !== ARCHIVE_METADATA_FIRST_SCHEMA) throw new ArchiveMetadataFirstError("unsupported archive trace schema", "trace.schemaVersion");
if (typeof input.archiveId !== "string" || !ARCHIVE_ID.test(input.archiveId)) throw new ArchiveMetadataFirstError("trace archiveId is invalid", "trace.archiveId");
if (!Array.isArray(input.reads) || input.reads.length === 0 || input.reads.length > 10_000) throw new ArchiveMetadataFirstError("trace reads exceeds its bound", "trace.reads");
const reads = input.reads.map(parseTraceItem).sort((left, right) => left.sequence - right.sequence);
if (reads.some((item, index) => item.sequence !== index)) throw new ArchiveMetadataFirstError("trace sequence must be contiguous and unique", "trace.reads");
return { schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA, archiveId: input.archiveId, reads };
}
export function validateArchiveReadTrace(planValue: unknown, traceValue: unknown): ArchiveReadTraceValidationIR {
const plan = planArchiveMetadataRead(planValue);
const trace = parseTrace(traceValue);
if (trace.archiveId !== plan.archiveId) throw new ArchiveMetadataFirstError("trace archiveId does not match the plan", "trace.archiveId");
const first = trace.reads[0];
if (first.kind !== plan.firstRead.kind || first.byteOffset !== plan.firstRead.byteOffset || first.byteLength !== plan.firstRead.byteLength) throw new ArchiveMetadataFirstError("payload was read before the central directory or manifest", "trace.reads[0]");
if (trace.reads.slice(1).some((item) => item.kind === plan.firstRead.kind || item.kind === (plan.format === "ZIP" ? "MANIFEST" : "CENTRAL_DIRECTORY"))) throw new ArchiveMetadataFirstError("metadata read sequence is duplicated or out of order", "trace.reads");
return { status: "VALID", metadataFirst: true, payloadReadsAfterMetadata: true };
}