Files
workinf_Blender_Wasm/web/protocol/external-vfont.ts
mes123456 0fe8d2bb56
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
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

211 lines
9.5 KiB
TypeScript

import type { ErrorCode } from "./error";
import { normalizeProjectAssetPath } from "./asset-path";
import type { StorageAssetPutResult, StorageAssetReadResult } from "./storage";
export const EXTERNAL_VFONT_SCHEMA_VERSION = 1 as const;
export const EXTERNAL_VFONT_MAX_BYTES = 32 * 1024 * 1024;
export type ExternalVFontFormat = "TTF" | "OTF" | "PFB";
export interface ExternalVFontImportRequestIR {
sourcePath: string;
mimeType: string;
byteLength: number;
sha256: string;
data: ArrayBuffer;
}
export interface ValidatedExternalVFontIR {
schemaVersion: typeof EXTERNAL_VFONT_SCHEMA_VERSION;
sourcePath: string;
fileName: string;
format: ExternalVFontFormat;
mimeType: string;
byteLength: number;
sha256: string;
data: ArrayBuffer;
}
export interface ExternalVFontMainImportProofIR {
schemaVersion: typeof EXTERNAL_VFONT_SCHEMA_VERSION;
projectId: string;
assetId: string;
assetPath: string;
sourcePath: string;
name: string;
format: ExternalVFontFormat;
mimeType: string;
byteLength: number;
sha256: string;
}
export interface ExternalVFontMainImportIR extends ExternalVFontMainImportProofIR {
data: ArrayBuffer;
}
export class ExternalVFontValidationError extends Error {
constructor(readonly code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "ExternalVFontValidationError";
}
}
const SHA256 = /^[0-9a-f]{64}$/;
const PROJECT_ID = /^[A-Za-z0-9_-]{1,64}$/;
const FORMAT = {
".ttf": { format: "TTF", mimeTypes: new Set(["font/ttf", "application/x-font-ttf", "application/font-sfnt"]) },
".otf": { format: "OTF", mimeTypes: new Set(["font/otf", "application/vnd.ms-opentype", "application/font-sfnt"]) },
".pfb": { format: "PFB", mimeTypes: new Set(["application/x-font-type1", "application/x-font-pfb"]) },
} as const satisfies Record<string, { format: ExternalVFontFormat; mimeTypes: ReadonlySet<string> }>;
function fail(code: ErrorCode, message: string): never {
throw new ExternalVFontValidationError(code, message);
}
function classify(sourcePath: string, mimeType: string, data: ArrayBuffer): { format: ExternalVFontFormat; mimeType: string } {
const extension = sourcePath.slice(sourcePath.lastIndexOf(".")).toLowerCase() as keyof typeof FORMAT;
const declaration = FORMAT[extension];
if (!declaration || !declaration.mimeTypes.has(mimeType as never)) {
fail("NON_MESH_BINARY_INVALID", "font extension and MIME type must agree on TTF, OTF or PFB");
}
const bytes = new Uint8Array(data);
const sfnt = bytes.byteLength >= 4 && bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00;
const otto = bytes.byteLength >= 4 && bytes[0] === 0x4f && bytes[1] === 0x54 && bytes[2] === 0x54 && bytes[3] === 0x4f;
const pfb = bytes.byteLength >= 6 && bytes[0] === 0x80 && bytes[1] === 0x01;
if (
(declaration.format === "TTF" && !sfnt) ||
(declaration.format === "OTF" && !otto) ||
(declaration.format === "PFB" && !pfb)
) {
fail("NON_MESH_BINARY_INVALID", `font bytes do not match the declared ${declaration.format} format`);
}
return { format: declaration.format, mimeType };
}
async function sha256(data: ArrayBuffer): Promise<string> {
return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", data))).map((byte) => byte.toString(16).padStart(2, "0")).join("");
}
function mainName(fileName: string, digest: string): string {
const candidate = fileName.replace(/\.[^.]+$/, "");
return candidate && new TextEncoder().encode(candidate).byteLength <= 63
? candidate
: `ExternalFont-${digest.slice(0, 12)}`;
}
export function validateExternalVFontMainImportProof(value: ExternalVFontMainImportProofIR): ExternalVFontMainImportProofIR {
if (!value || typeof value !== "object" || value.schemaVersion !== EXTERNAL_VFONT_SCHEMA_VERSION) {
fail("NON_MESH_BINARY_INVALID", "external VFont Main import proof schema is invalid");
}
if (!PROJECT_ID.test(value.projectId) || !SHA256.test(value.sha256)) {
fail("NON_MESH_BINARY_INVALID", "external VFont project or hash identity is invalid");
}
if (value.assetId !== `sha256:${value.sha256}` ||
value.assetPath !== `projects/${value.projectId}/assets/sha256/${value.sha256.slice(0, 2)}/${value.sha256}`) {
fail("NON_MESH_RESOURCE_MISSING", "external VFont must reference its verified OPFS content-addressed asset");
}
let normalized: string;
try {
normalized = normalizeProjectAssetPath(value.sourcePath);
}
catch {
fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "external VFont Main source path is invalid");
}
if (!normalized.startsWith("fonts/") || value.sourcePath !== `//${normalized}` ||
typeof value.name !== "string" || value.name.length === 0 || new TextEncoder().encode(value.name).byteLength > 63 ||
!Number.isSafeInteger(value.byteLength) || value.byteLength < 1 || value.byteLength > EXTERNAL_VFONT_MAX_BYTES) {
fail("NON_MESH_BINARY_INVALID", "external VFont Main metadata is invalid");
}
const declared = FORMAT[normalized.slice(normalized.lastIndexOf(".")).toLowerCase() as keyof typeof FORMAT];
if (!declared || declared.format !== value.format || !declared.mimeTypes.has(value.mimeType as never)) {
fail("NON_MESH_BINARY_INVALID", "external VFont Main format and MIME type do not agree");
}
return { ...value };
}
export function createExternalVFontMainImport(
validated: ValidatedExternalVFontIR,
stored: StorageAssetPutResult,
): ExternalVFontMainImportIR {
const normalized = validated.sourcePath.slice(2);
if (!stored.persisted || stored.projectId.length === 0 || stored.assetId !== `sha256:${validated.sha256}` ||
stored.sha256 !== validated.sha256 || stored.bytes !== validated.byteLength || stored.mimeType !== validated.mimeType ||
stored.sourcePath !== normalized) {
fail("ASSET_SOURCE_HASH_MISMATCH", "stored external VFont receipt does not match the validated font");
}
const proof = validateExternalVFontMainImportProof({
schemaVersion: EXTERNAL_VFONT_SCHEMA_VERSION,
projectId: stored.projectId,
assetId: stored.assetId,
assetPath: stored.path,
sourcePath: validated.sourcePath,
name: mainName(validated.fileName, validated.sha256),
format: validated.format,
mimeType: validated.mimeType,
byteLength: validated.byteLength,
sha256: validated.sha256,
});
return { ...proof, data: validated.data.slice(0) };
}
export async function validateStoredExternalVFontAsset(
projectId: string,
declaredSha256: string,
stored: StorageAssetReadResult,
): Promise<ValidatedExternalVFontIR> {
if (!PROJECT_ID.test(projectId) || !SHA256.test(declaredSha256) || !stored ||
typeof stored !== "object" || !(stored.data instanceof ArrayBuffer)) {
fail("NON_MESH_RESOURCE_MISSING", "stored external VFont asset identity is invalid");
}
const asset = stored.asset;
const expectedPath = `projects/${projectId}/assets/sha256/${declaredSha256.slice(0, 2)}/${declaredSha256}`;
if (!asset || asset.projectId !== projectId || asset.assetId !== `sha256:${declaredSha256}` ||
asset.sha256 !== declaredSha256 || asset.path !== expectedPath || typeof asset.sourcePath !== "string") {
fail("NON_MESH_RESOURCE_MISSING", "stored external VFont asset is not the declared project asset");
}
if (asset.bytes !== stored.data.byteLength) {
fail("ASSET_SOURCE_HASH_MISMATCH", "stored external VFont byte length does not match its metadata");
}
return validateExternalVFontImport({
sourcePath: `//${asset.sourcePath}`,
mimeType: asset.mimeType,
byteLength: asset.bytes,
sha256: asset.sha256,
data: stored.data,
});
}
export async function validateExternalVFontImport(request: ExternalVFontImportRequestIR): Promise<ValidatedExternalVFontIR> {
if (!request || typeof request !== "object") fail("NON_MESH_BINARY_INVALID", "font import request is missing");
let normalized: string;
try {
normalized = normalizeProjectAssetPath(request.sourcePath);
}
catch {
fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "font source must be a project-relative path without traversal or URI syntax");
}
if (!normalized.startsWith("fonts/") || new TextEncoder().encode(normalized).byteLength > 1021) {
fail("NON_MESH_RESOURCE_OUTSIDE_PROJECT", "font source must be a bounded path under the project fonts directory");
}
if (!(request.data instanceof ArrayBuffer)) fail("NON_MESH_BINARY_INVALID", "font payload must be an ArrayBuffer");
if (!Number.isSafeInteger(request.byteLength) || request.byteLength < 1 || request.byteLength > EXTERNAL_VFONT_MAX_BYTES) {
fail("NON_MESH_DATA_BUDGET_EXCEEDED", "font payload exceeds the 32 MiB import budget");
}
if (request.data.byteLength !== request.byteLength) fail("NON_MESH_BINARY_INVALID", "font declared byte length does not match its payload");
if (typeof request.mimeType !== "string" || request.mimeType.length > 128) fail("NON_MESH_BINARY_INVALID", "font MIME type is invalid");
const classified = classify(normalized, request.mimeType.toLowerCase(), request.data);
if (typeof request.sha256 !== "string" || !SHA256.test(request.sha256)) fail("ASSET_SOURCE_HASH_MISMATCH", "font SHA-256 declaration is invalid");
const actualSha256 = await sha256(request.data);
if (actualSha256 !== request.sha256) fail("ASSET_SOURCE_HASH_MISMATCH", "font bytes do not match the declared SHA-256");
return {
schemaVersion: EXTERNAL_VFONT_SCHEMA_VERSION,
sourcePath: `//${normalized}`,
fileName: normalized.slice(normalized.lastIndexOf("/") + 1),
format: classified.format,
mimeType: classified.mimeType,
byteLength: request.byteLength,
sha256: actualSha256,
data: request.data.slice(0),
};
}