Files
workinf_Blender_Wasm/web/protocol/asset-catalog-v2.ts
mes123456 5a11045ca5
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 Blender 5.2 web parity through M12-03D
2026-08-17 17:30:27 -04:00

475 lines
24 KiB
TypeScript

import type { ErrorCode } from "./error";
export const ASSET_CATALOG_SCHEMA = 2 as const;
export const ASSET_CATALOG_V2_BUDGET = Object.freeze({
maxCatalogs: 10_000,
maxAssets: 100_000,
maxLibraries: 1_024,
maxDependenciesPerLibrary: 1_024,
maxCatalogPathBytes: 4_096,
maxCatalogSimpleNameBytes: 63,
maxLibraryIdentifierBytes: 1_024,
maxRelativeAssetIdentifierBytes: 4_096,
maxAssetNameBytes: 1_024,
maxMetadataTextBytes: 64 * 1024,
maxTagBytes: 63,
maxTagsPerAsset: 128,
maxCustomPropertiesPerAsset: 256,
maxCustomPropertyNameBytes: 63,
maxCustomPropertyStringBytes: 64 * 1024,
maxCustomPropertyArrayItems: 4_096,
maxTotalTextBytes: 64 * 1024 * 1024,
});
export const ASSET_CATALOG_V2_ID_TYPES = ["ACTION", "COLLECTION", "IMAGE", "MATERIAL", "NODE_GROUP", "OBJECT", "WORLD"] as const;
export type AssetCatalogV2IDType = typeof ASSET_CATALOG_V2_ID_TYPES[number];
export type AssetCatalogV2ImportMethod = "LINK" | "APPEND" | "PACK";
export type AssetCatalogV2CustomProperty =
| { name: string; type: "BOOL"; value: boolean }
| { name: string; type: "INT"; value: number }
| { name: string; type: "FLOAT"; value: number }
| { name: string; type: "STRING"; value: string }
| { name: string; type: "INT_ARRAY"; value: number[] }
| { name: string; type: "FLOAT_ARRAY"; value: number[] }
| { name: string; type: "STRING_ARRAY"; value: string[] };
export interface AssetCatalogV2IR {
catalogId: string;
path: string;
simpleName: string;
parentPath: string | null;
}
export interface AssetCatalogV2EntryIR {
assetId: string;
assetLibraryIdentifier: string | null;
relativeAssetIdentifier: string;
idType: AssetCatalogV2IDType;
name: string;
catalogId: string | null;
catalogSimpleName: string;
author: string;
description: string;
copyright: string;
license: string;
tags: string[];
activeTag: number;
usePreferredImportMethod: boolean;
preferredImportMethod: AssetCatalogV2ImportMethod;
customProperties: AssetCatalogV2CustomProperty[];
sourceSha256: string | null;
sourcePath: string | null;
preview: AssetCatalogV2PreviewIR | null;
}
export interface AssetCatalogV2PreviewIR {
sha256: string;
mimeType: "image/png" | "image/webp";
width: number;
height: number;
byteLength: number;
}
export interface AssetCatalogV2LibraryIR {
libraryId: string;
name: string;
sourcePath: string;
sourceSha256: string;
dependencyIds: string[];
readOnly: boolean;
}
export interface AssetCatalogManifestV2IR {
schemaVersion: typeof ASSET_CATALOG_SCHEMA;
revision: number;
catalogs: AssetCatalogV2IR[];
assets: AssetCatalogV2EntryIR[];
libraries: AssetCatalogV2LibraryIR[];
}
export class AssetCatalogV2ValidationError extends Error {
readonly code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "AssetCatalogV2ValidationError";
this.code = code;
}
}
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
const ASSET_ID = /^asset:[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const encoder = new TextEncoder();
const ID_TYPE_DIRECTORY: Readonly<Record<AssetCatalogV2IDType, string>> = Object.freeze({
ACTION: "Action",
COLLECTION: "Collection",
IMAGE: "Image",
MATERIAL: "Material",
NODE_GROUP: "NodeTree",
OBJECT: "Object",
WORLD: "World",
});
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function exactKeys(value: Record<string, unknown>, allowed: readonly string[], name: string): void {
const allowedSet = new Set(allowed);
if (Object.keys(value).some((key) => !allowedSet.has(key))) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} contains undeclared fields`);
}
}
function boundedInteger(value: unknown, name: string, minimum: number, maximum: number): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} is outside the bounded integer range`);
}
return value;
}
interface TextBudgetState { total: number }
function boundedText(
value: unknown,
name: string,
maximumBytes: number,
state: TextBudgetState,
allowEmpty = false,
): string {
if (typeof value !== "string") {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be text`);
}
const bytes = encoder.encode(value).byteLength;
if ((!allowEmpty && bytes === 0) || bytes > maximumBytes) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${name} exceeds its UTF-8 byte budget`);
}
state.total += bytes;
if (!Number.isSafeInteger(state.total) || state.total > ASSET_CATALOG_V2_BUDGET.maxTotalTextBytes) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", "catalog manifest exceeds the aggregate text budget");
}
return value;
}
function canonicalUUID(value: unknown, name: string): string {
if (typeof value !== "string" || !UUID.test(value)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be a canonical RFC4122 UUID`);
}
return value;
}
function parseCatalogPath(value: unknown, name: string, state: TextBudgetState): string {
const result = boundedText(value, name, ASSET_CATALOG_V2_BUDGET.maxCatalogPathBytes, state);
const components = result.split("/");
if (components.some((component) => component.length === 0 || component === "." || component === ".." || component.trim() !== component)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} is not a cleaned Blender catalog path`);
}
return result;
}
function expectedParentPath(path: string): string | null {
const separator = path.lastIndexOf("/");
return separator === -1 ? null : path.slice(0, separator);
}
function parseCatalog(value: unknown, index: number, state: TextBudgetState): AssetCatalogV2IR {
const name = `catalogs[${index}]`;
if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be an object`);
exactKeys(value, ["catalogId", "path", "simpleName", "parentPath"], name);
const path = parseCatalogPath(value.path, `${name}.path`, state);
const parentPath = value.parentPath === null ? null : parseCatalogPath(value.parentPath, `${name}.parentPath`, state);
if (parentPath !== expectedParentPath(path)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name}.parentPath does not match the Blender path hierarchy`);
}
return {
catalogId: canonicalUUID(value.catalogId, `${name}.catalogId`),
path,
simpleName: boundedText(value.simpleName, `${name}.simpleName`, ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, state),
parentPath,
};
}
function digest(value: unknown, name: string): string {
if (typeof value !== "string" || !SHA256.test(value)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be a lowercase SHA-256 digest`);
}
return value;
}
function parsePreview(value: unknown, path: string): AssetCatalogV2PreviewIR {
if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`);
exactKeys(value, ["sha256", "mimeType", "width", "height", "byteLength"], path);
if (value.mimeType !== "image/png" && value.mimeType !== "image/webp") {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.mimeType is unsupported`);
}
return {
sha256: digest(value.sha256, `${path}.sha256`),
mimeType: value.mimeType,
width: boundedInteger(value.width, `${path}.width`, 1, 4_096),
height: boundedInteger(value.height, `${path}.height`, 1, 4_096),
byteLength: boundedInteger(value.byteLength, `${path}.byteLength`, 1, 64 * 1024 * 1024),
};
}
function parseLibrary(value: unknown, index: number, state: TextBudgetState): AssetCatalogV2LibraryIR {
const path = `libraries[${index}]`;
if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`);
exactKeys(value, ["libraryId", "name", "sourcePath", "sourceSha256", "dependencyIds", "readOnly"], path);
if (!Array.isArray(value.dependencyIds) || value.dependencyIds.length > ASSET_CATALOG_V2_BUDGET.maxDependenciesPerLibrary) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.dependencyIds exceeds the entry budget`);
}
if (typeof value.readOnly !== "boolean") throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.readOnly must be boolean`);
return {
libraryId: boundedText(value.libraryId, `${path}.libraryId`, ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes, state),
name: boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxAssetNameBytes, state),
sourcePath: boundedText(value.sourcePath, `${path}.sourcePath`, ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes, state),
sourceSha256: digest(value.sourceSha256, `${path}.sourceSha256`),
dependencyIds: value.dependencyIds.map((item, dependencyIndex) => boundedText(item, `${path}.dependencyIds[${dependencyIndex}]`, ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes, state)),
readOnly: value.readOnly,
};
}
function assertLibraryGraph(libraries: readonly AssetCatalogV2LibraryIR[]): void {
const byId = new Map(libraries.map((library) => [library.libraryId, library]));
if (byId.size !== libraries.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate library IDs");
const active = new Set<string>();
const complete = new Set<string>();
const visit = (id: string): void => {
if (active.has(id)) throw new AssetCatalogV2ValidationError("LIBRARY_DEPENDENCY_CYCLE", `library dependency cycle includes ${id}`);
if (complete.has(id)) return;
const library = byId.get(id);
if (!library) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `library dependency ${id} is missing`);
active.add(id);
for (const dependency of library.dependencyIds) visit(dependency);
active.delete(id);
complete.add(id);
};
for (const id of byId.keys()) visit(id);
}
function parseCustomProperty(value: unknown, path: string, state: TextBudgetState): AssetCatalogV2CustomProperty {
if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`);
exactKeys(value, ["name", "type", "value"], path);
const name = boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyNameBytes, state);
switch (value.type) {
case "BOOL":
if (typeof value.value !== "boolean") throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must be boolean`);
return { name, type: "BOOL", value: value.value };
case "INT":
return { name, type: "INT", value: boundedInteger(value.value, `${path}.value`, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) };
case "FLOAT":
if (typeof value.value !== "number" || !Number.isFinite(value.value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must be finite`);
return { name, type: "FLOAT", value: value.value };
case "STRING":
return { name, type: "STRING", value: boundedText(value.value, `${path}.value`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyStringBytes, state, true) };
case "INT_ARRAY":
case "FLOAT_ARRAY":
case "STRING_ARRAY": {
if (!Array.isArray(value.value) || value.value.length > ASSET_CATALOG_V2_BUDGET.maxCustomPropertyArrayItems) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.value exceeds the array budget`);
}
if (value.type === "STRING_ARRAY") {
return { name, type: value.type, value: value.value.map((item, index) => boundedText(item, `${path}.value[${index}]`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyStringBytes, state, true)) };
}
if (value.type === "INT_ARRAY") {
return { name, type: value.type, value: value.value.map((item, index) => boundedInteger(item, `${path}.value[${index}]`, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)) };
}
if (value.value.some((item) => typeof item !== "number" || !Number.isFinite(item))) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must contain finite numbers`);
}
return { name, type: value.type, value: [...value.value] as number[] };
}
default:
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.type is unsupported`);
}
}
function parseAssetIdentity(
value: { assetLibraryIdentifier: unknown; relativeAssetIdentifier: unknown },
state: TextBudgetState,
path: string,
): { assetLibraryIdentifier: string | null; relativeAssetIdentifier: string } {
const assetLibraryIdentifier = value.assetLibraryIdentifier === null ? null : boundedText(
value.assetLibraryIdentifier,
`${path}.assetLibraryIdentifier`,
ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes,
state,
);
const relativeAssetIdentifier = boundedText(
value.relativeAssetIdentifier,
`${path}.relativeAssetIdentifier`,
ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes,
state,
);
return { assetLibraryIdentifier, relativeAssetIdentifier };
}
async function sha256(value: string): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
export async function createAssetCatalogV2StableId(identityValue: unknown): Promise<string> {
if (!record(identityValue)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "asset identity must be an object");
exactKeys(identityValue, ["assetLibraryIdentifier", "relativeAssetIdentifier"], "asset identity");
const identity = parseAssetIdentity({
assetLibraryIdentifier: identityValue.assetLibraryIdentifier,
relativeAssetIdentifier: identityValue.relativeAssetIdentifier,
}, { total: 0 }, "asset identity");
const canonical = JSON.stringify({
schema: "BLENDER_ASSET_WEAK_REFERENCE_V1",
assetLibraryIdentifier: identity.assetLibraryIdentifier,
relativeAssetIdentifier: identity.relativeAssetIdentifier,
});
return `asset:${await sha256(canonical)}`;
}
async function parseAsset(value: unknown, index: number, catalogIds: ReadonlySet<string>, state: TextBudgetState): Promise<AssetCatalogV2EntryIR> {
const path = `assets[${index}]`;
if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`);
exactKeys(value, [
"assetId", "assetLibraryIdentifier", "relativeAssetIdentifier", "idType", "name", "catalogId",
"catalogSimpleName", "author", "description", "copyright", "license", "tags", "activeTag",
"usePreferredImportMethod", "preferredImportMethod", "customProperties",
"sourceSha256", "sourcePath", "preview",
], path);
if (!ASSET_CATALOG_V2_ID_TYPES.includes(value.idType as AssetCatalogV2IDType)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.idType is unsupported`);
}
const idType = value.idType as AssetCatalogV2IDType;
const identity = parseAssetIdentity({
assetLibraryIdentifier: value.assetLibraryIdentifier,
relativeAssetIdentifier: value.relativeAssetIdentifier,
}, state, path);
if (!identity.relativeAssetIdentifier.startsWith(`${ID_TYPE_DIRECTORY[idType]}/`)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.relativeAssetIdentifier disagrees with idType`);
}
if (typeof value.assetId !== "string" || !ASSET_ID.test(value.assetId) || value.assetId !== await createAssetCatalogV2StableId(identity)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.assetId does not bind the Blender weak reference`);
}
const catalogId = value.catalogId === null ? null : canonicalUUID(value.catalogId, `${path}.catalogId`);
if (catalogId !== null && !catalogIds.has(catalogId)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.catalogId is not declared`);
}
if (!Array.isArray(value.tags) || value.tags.length > ASSET_CATALOG_V2_BUDGET.maxTagsPerAsset) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.tags exceeds the entry budget`);
}
const tags = value.tags.map((tag, tagIndex) => boundedText(tag, `${path}.tags[${tagIndex}]`, ASSET_CATALOG_V2_BUDGET.maxTagBytes, state));
if (!Array.isArray(value.customProperties) || value.customProperties.length > ASSET_CATALOG_V2_BUDGET.maxCustomPropertiesPerAsset) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.customProperties exceeds the entry budget`);
}
const customProperties = value.customProperties.map((item, propertyIndex) => parseCustomProperty(item, `${path}.customProperties[${propertyIndex}]`, state));
const activeTag = boundedInteger(value.activeTag, `${path}.activeTag`, 0, Math.max(tags.length - 1, 0));
if (typeof value.usePreferredImportMethod !== "boolean" || !["LINK", "APPEND", "PACK"].includes(value.preferredImportMethod as string)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} has invalid import method metadata`);
}
return {
assetId: value.assetId,
...identity,
idType,
name: boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxAssetNameBytes, state),
catalogId,
catalogSimpleName: boundedText(value.catalogSimpleName, `${path}.catalogSimpleName`, ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, state, true),
author: boundedText(value.author, `${path}.author`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true),
description: boundedText(value.description, `${path}.description`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true),
copyright: boundedText(value.copyright, `${path}.copyright`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true),
license: boundedText(value.license, `${path}.license`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true),
tags,
activeTag,
usePreferredImportMethod: value.usePreferredImportMethod,
preferredImportMethod: value.preferredImportMethod as AssetCatalogV2ImportMethod,
customProperties,
sourceSha256: value.sourceSha256 === null ? null : digest(value.sourceSha256, `${path}.sourceSha256`),
sourcePath: value.sourcePath === null ? null : boundedText(value.sourcePath, `${path}.sourcePath`, ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes, state),
preview: value.preview === null ? null : parsePreview(value.preview, `${path}.preview`),
};
}
export async function parseAssetCatalogManifestV2(value: unknown): Promise<AssetCatalogManifestV2IR> {
if (!record(value) || value.schemaVersion !== ASSET_CATALOG_SCHEMA) {
throw new AssetCatalogV2ValidationError("PROTOCOL_MISMATCH", "unsupported asset catalog schema");
}
exactKeys(value, ["schemaVersion", "revision", "catalogs", "assets", "libraries"], "manifest");
if (!Array.isArray(value.catalogs) || value.catalogs.length > ASSET_CATALOG_V2_BUDGET.maxCatalogs ||
!Array.isArray(value.assets) || value.assets.length > ASSET_CATALOG_V2_BUDGET.maxAssets ||
!Array.isArray(value.libraries) || value.libraries.length > ASSET_CATALOG_V2_BUDGET.maxLibraries) {
throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", "manifest exceeds the catalog, asset, or library entry budget");
}
const state = { total: 0 };
const catalogs = value.catalogs.map((item, index) => parseCatalog(item, index, state));
const catalogIds = new Set(catalogs.map((item) => item.catalogId));
if (catalogIds.size !== catalogs.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate catalog IDs");
const assets = await Promise.all(value.assets.map((item, index) => parseAsset(item, index, catalogIds, state)));
if (new Set(assets.map((item) => item.assetId)).size !== assets.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate asset IDs");
const libraries = value.libraries.map((item, index) => parseLibrary(item, index, state));
assertLibraryGraph(libraries);
return {
schemaVersion: ASSET_CATALOG_SCHEMA,
revision: boundedInteger(value.revision, "revision", 0, Number.MAX_SAFE_INTEGER),
catalogs,
assets,
libraries,
};
}
function desktopProperty(value: unknown, path: string): AssetCatalogV2CustomProperty {
if (!record(value) || typeof value.name !== "string" || typeof value.type !== "string") {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} is invalid`);
}
if (value.type === "STR") return { name: value.name, type: "STRING", value: value.value as string };
if (value.type === "IDPROPERTYARRAY") {
return { name: value.name, type: "FLOAT_ARRAY", value: value.value as number[] };
}
if (["BOOL", "INT", "FLOAT"].includes(value.type)) {
return { name: value.name, type: value.type as "BOOL", value: value.value as boolean } as AssetCatalogV2CustomProperty;
}
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.type is unsupported`);
}
export async function createAssetCatalogManifestV2FromDesktop(value: unknown, revision = 0): Promise<AssetCatalogManifestV2IR> {
if (!record(value) || value.schemaVersion !== 1 || value.task !== "M12-01B" || !record(value.catalogDefinition) ||
!Array.isArray(value.catalogDefinition.records) || !Array.isArray(value.assets)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "desktop catalog canonical report is invalid");
}
const catalogs = value.catalogDefinition.records.map((item, index) => {
if (!record(item)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop catalog ${index} is invalid`);
return { catalogId: item.catalogId, path: item.path, simpleName: item.simpleName, parentPath: item.parentPath };
});
const catalogIds = new Set(catalogs.map((item) => item.catalogId));
const assets = await Promise.all(value.assets.map(async (item, index) => {
if (!record(item) || !ASSET_CATALOG_V2_ID_TYPES.includes(item.idType as AssetCatalogV2IDType) || !Array.isArray(item.tags) || !Array.isArray(item.customProperties)) {
throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop asset ${index} is invalid`);
}
const idType = item.idType as AssetCatalogV2IDType;
const assetLibraryIdentifier = null;
const relativeAssetIdentifier = `${ID_TYPE_DIRECTORY[idType]}/${item.name}`;
const desktopCatalogId = item.catalogId === NIL_UUID ? null : item.catalogId;
if (desktopCatalogId !== null && !catalogIds.has(desktopCatalogId)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop asset ${index} references an unknown catalog`);
return {
assetId: await createAssetCatalogV2StableId({ assetLibraryIdentifier, relativeAssetIdentifier }),
assetLibraryIdentifier,
relativeAssetIdentifier,
idType,
name: item.name,
catalogId: desktopCatalogId,
catalogSimpleName: item.catalogSimpleName,
author: item.author,
description: item.description,
copyright: item.copyright,
license: item.license,
tags: item.tags,
activeTag: item.activeTag,
usePreferredImportMethod: item.usePreferredImportMethod,
preferredImportMethod: item.preferredImportMethod,
customProperties: item.customProperties.map((property, propertyIndex) => desktopProperty(property, `desktop asset ${index} property ${propertyIndex}`)),
sourceSha256: null,
sourcePath: null,
preview: null,
};
}));
const manifest = { schemaVersion: ASSET_CATALOG_SCHEMA, revision, catalogs, assets, libraries: [] };
return parseAssetCatalogManifestV2(manifest);
}