import { createHash } from "node:crypto"; import { existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path"; export const P0A_RUNTIME_ASSET_ROOT_REF = "p0a_runtime_assets"; export const RUNTIME_ASSET_MANIFEST_SCHEMA = "DadaRuntimeAssets/v1"; const assetIdPattern = /^[a-z0-9][a-z0-9_-]{2,119}$/i; const mimePattern = /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i; const releasePattern = /^[a-z0-9][a-z0-9._-]{0,79}$/i; const shaPattern = /^[a-f0-9]{64}$/i; const fontMimeTypes = new Map([ [".otf", "font/otf"], [".ttf", "font/ttf"], [".woff", "font/woff"], [".woff2", "font/woff2"], ]); function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function fileSha256(path) { return sha256(readFileSync(path)); } function stableEntries(entries) { return entries.map((entry) => { if (!entry || typeof entry !== "object") throw new Error("runtime_asset_entry_invalid"); if (!assetIdPattern.test(entry.assetId)) throw new Error("runtime_asset_id_invalid"); if (!mimePattern.test(entry.mimeType)) throw new Error("runtime_asset_mime_invalid"); if (!releasePattern.test(entry.resourceVersion)) throw new Error("runtime_asset_version_invalid"); if (entry.rootRef !== P0A_RUNTIME_ASSET_ROOT_REF) throw new Error("runtime_asset_root_ref_invalid"); if (!shaPattern.test(entry.sha256)) throw new Error("runtime_asset_sha256_invalid"); if ( typeof entry.relativePath !== "string" || isAbsolute(entry.relativePath) || entry.relativePath.includes("\\") || entry.relativePath.split("/").some((part) => part === "" || part === "..") ) throw new Error("runtime_asset_relative_path_invalid"); return { ...entry, sha256: entry.sha256.toLowerCase() }; }).sort((left, right) => { const byVersion = left.resourceVersion.localeCompare(right.resourceVersion); return byVersion || left.assetId.localeCompare(right.assetId); }); } function derivedCounts(entries) { return { dynamic_fonts: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^[a-f0-9]{32}$/.test(entry.assetId)).length, dynamic_images: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^DYN\d{3}-/.test(entry.assetId)).length, font_panel_items: entries.filter((entry) => entry.resourceVersion === "p0a-complex-v1" && /^FONT\d{3}$/.test(entry.assetId)).length, static_stickers: entries.filter((entry) => entry.resourceVersion === "p0a-static-v1" && /^STK\d{3,4}$/.test(entry.assetId)).length, }; } export function createRuntimeAssetManifest({ counts, entries, sourceManifestSha256 }) { const normalizedEntries = stableEntries(entries); const keys = new Set(); const paths = new Set(); for (const entry of normalizedEntries) { const key = `${entry.resourceVersion}\u0000${entry.assetId}`; if (keys.has(key)) throw new Error("runtime_asset_id_duplicate"); if (paths.has(entry.relativePath)) throw new Error("runtime_asset_path_duplicate"); keys.add(key); paths.add(entry.relativePath); } const actualCounts = derivedCounts(normalizedEntries); if (JSON.stringify(counts) !== JSON.stringify(actualCounts)) throw new Error("runtime_asset_counts_invalid"); if (sourceManifestSha256 !== undefined && !shaPattern.test(sourceManifestSha256)) { throw new Error("runtime_asset_source_manifest_sha256_invalid"); } return { counts: actualCounts, entries: normalizedEntries, root_ref: P0A_RUNTIME_ASSET_ROOT_REF, schema_version: RUNTIME_ASSET_MANIFEST_SCHEMA, source: "external_read_only", ...(sourceManifestSha256 ? { source_manifest_sha256: sourceManifestSha256.toLowerCase() } : {}), }; } export function readRuntimeAssetManifest(path) { const value = JSON.parse(readFileSync(path, "utf8")); if ( value?.schema_version !== RUNTIME_ASSET_MANIFEST_SCHEMA || value?.source !== "external_read_only" || value?.root_ref !== P0A_RUNTIME_ASSET_ROOT_REF || !Array.isArray(value.entries) ) throw new Error("runtime_asset_manifest_invalid"); return createRuntimeAssetManifest({ counts: value.counts, entries: value.entries, ...(value.source_manifest_sha256 ? { sourceManifestSha256: value.source_manifest_sha256 } : {}), }); } export function serializeRuntimeAssetManifest(manifest) { return `${JSON.stringify(manifest, null, 2)}\n`; } export function writeRuntimeAssetManifest(path, manifest) { mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, serializeRuntimeAssetManifest(manifest)); } function targetWithinRoot(root, relativePath) { const absoluteRoot = resolve(root); const target = resolve(absoluteRoot, ...relativePath.split("/")); if (target === absoluteRoot || !target.startsWith(`${absoluteRoot}${sep}`)) throw new Error("asset_target_path_invalid"); return target; } function sameFile(left, right) { const leftStat = statSync(left); const rightStat = statSync(right); return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino; } export function deployRuntimeAssetPlan({ assetRoot, manifest, resources }) { if (!isAbsolute(assetRoot)) throw new Error("asset_root_must_be_absolute"); const normalizedManifest = createRuntimeAssetManifest({ counts: manifest.counts, entries: manifest.entries, ...(manifest.source_manifest_sha256 ? { sourceManifestSha256: manifest.source_manifest_sha256 } : {}), }); const entries = new Map(normalizedManifest.entries.map((entry) => [`${entry.resourceVersion}\u0000${entry.assetId}`, entry])); if (resources.length !== entries.size) throw new Error("asset_resource_plan_incomplete"); mkdirSync(assetRoot, { recursive: true }); for (const resource of resources) { const key = `${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`; const entry = entries.get(key); if (!entry || JSON.stringify(entry) !== JSON.stringify({ ...resource.entry, sha256: resource.entry.sha256.toLowerCase() })) { throw new Error("asset_resource_plan_mismatch"); } if (!existsSync(resource.sourcePath) || !statSync(resource.sourcePath).isFile() || lstatSync(resource.sourcePath).isSymbolicLink()) { throw new Error("asset_source_invalid"); } if (fileSha256(resource.sourcePath) !== entry.sha256) throw new Error("asset_source_hash_invalid"); const targetPath = targetWithinRoot(assetRoot, entry.relativePath); mkdirSync(dirname(targetPath), { recursive: true }); if (existsSync(targetPath)) { if (fileSha256(targetPath) !== entry.sha256) throw new Error("asset_target_conflict"); if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_target_not_hardlink"); continue; } try { linkSync(resource.sourcePath, targetPath); } catch (error) { if (error && typeof error === "object" && "code" in error && error.code === "EXDEV") { throw new Error("asset_hardlink_volume_mismatch"); } throw error; } if (!sameFile(resource.sourcePath, targetPath)) throw new Error("asset_hardlink_verification_failed"); } writeRuntimeAssetManifest(join(assetRoot, "manifest.json"), normalizedManifest); return { linked_files: resources.length, manifest: normalizedManifest, status: "ready" }; } function oneDirectoryWithPrefix(root, prefix) { const matches = readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${prefix}_`)); if (matches.length !== 1) throw new Error(`runtime_asset_source_directory_invalid:${prefix}`); return join(root, matches[0].name); } function oneSupportedFont(root) { const matches = readdirSync(root, { withFileTypes: true }) .filter((entry) => entry.isFile() && fontMimeTypes.has(extname(entry.name).toLowerCase())); if (matches.length !== 1) throw new Error(`runtime_font_source_invalid:${basename(root)}`); return join(root, matches[0].name); } function entryFor(sourcePath, assetId, resourceVersion, relativePath, mimeType) { return { assetId, mimeType, relativePath, resourceVersion, rootRef: P0A_RUNTIME_ASSET_ROOT_REF, sha256: fileSha256(sourcePath), }; } function dynamicMetadata(templateRoot, descriptor, field) { const templateDirectory = join(templateRoot, descriptor.templateId); const metadata = JSON.parse(readFileSync(join(templateDirectory, "metadata.json"), "utf8")); if (!Array.isArray(metadata?.files?.[field]) || !metadata.files[field].includes(descriptor.sourceReference)) { throw new Error(`runtime_dynamic_reference_invalid:${descriptor.assetId}`); } return templateDirectory; } export async function buildP0aRuntimeAssetPlan({ replicationRoot }) { const [{ compileStaticStickerCatalog }, registry] = await Promise.all([ import("../../packages/asset-compiler/dist/index.js"), import("../../packages/template-registry/dist/index.js"), ]); const compilerOutput = mkdtempSync(join(tmpdir(), "dada-runtime-asset-plan-")); try { const staticSourceRoot = join(replicationRoot, "sticker_normal"); const staticResult = compileStaticStickerCatalog({ outputDirectory: compilerOutput, releaseVersion: registry.P0A_STATIC_STICKER_RELEASE_VERSION, sourceRoot: staticSourceRoot, }); const resources = staticResult.catalog.items.map((item) => { const sourcePath = join(staticSourceRoot, ...item.relative_path.split("/")); const entry = entryFor( sourcePath, item.stable_id, registry.P0A_STATIC_STICKER_RELEASE_VERSION, `${registry.P0A_STATIC_STICKER_RELEASE_VERSION}/${item.stable_id}.png`, "image/png", ); if (entry.sha256 !== item.sha256.toLowerCase()) throw new Error(`static_sticker_hash_invalid:${item.stable_id}`); return { entry, sourcePath }; }); const fontPackagesRoot = join( replicationRoot, "sticker_text", "字体", "面板全量采集", "font_panel_full_20260722", "resources", "font_packages", ); for (const assetId of registry.P0A_REQUIRED_FONT_PANEL_IDS) { const packageDirectory = oneDirectoryWithPrefix(fontPackagesRoot, assetId); const sourcePath = oneSupportedFont(join(packageDirectory, "font_files")); const extension = extname(sourcePath).toLowerCase(); resources.push({ entry: entryFor( sourcePath, assetId, registry.P0A_COMPLEX_RELEASE_VERSION, `${registry.P0A_COMPLEX_RELEASE_VERSION}/${assetId}${extension}`, fontMimeTypes.get(extension), ), sourcePath, }); } const templateRoot = join(replicationRoot, "sticker_interactive", "单模板归档", "templates"); for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES) { const templateDirectory = dynamicMetadata(templateRoot, descriptor, "fonts"); const sourcePath = oneSupportedFont(join(templateDirectory, ...descriptor.sourceReference.split("/"))); const extension = extname(sourcePath).toLowerCase(); resources.push({ entry: entryFor( sourcePath, descriptor.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, `${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}${extension}`, fontMimeTypes.get(extension), ), sourcePath, }); } for (const descriptor of registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES) { const templateDirectory = dynamicMetadata(templateRoot, descriptor, "images"); const sourcePath = join(templateDirectory, ...descriptor.sourceReference.split("/")); if (!existsSync(sourcePath) || extname(sourcePath).toLowerCase() !== ".png") { throw new Error(`runtime_dynamic_image_invalid:${descriptor.assetId}`); } resources.push({ entry: entryFor( sourcePath, descriptor.assetId, registry.P0A_COMPLEX_RELEASE_VERSION, `${registry.P0A_COMPLEX_RELEASE_VERSION}/${descriptor.assetId}.png`, "image/png", ), sourcePath, }); } const manifestPath = join(replicationRoot, "sticker_web_handoff", "sticker_web_catalog_manifest.json"); const manifest = createRuntimeAssetManifest({ counts: { dynamic_fonts: registry.P0A_DYNAMIC_RUNTIME_FONT_SOURCES.length, dynamic_images: registry.P0A_DYNAMIC_RUNTIME_IMAGE_SOURCES.length, font_panel_items: registry.P0A_REQUIRED_FONT_PANEL_IDS.length, static_stickers: staticResult.catalog.count, }, entries: resources.map((resource) => resource.entry), sourceManifestSha256: fileSha256(manifestPath), }); const resourcesByKey = new Map(resources.map((resource) => [`${resource.entry.resourceVersion}\u0000${resource.entry.assetId}`, resource])); return { manifest, resources: manifest.entries.map((entry) => resourcesByKey.get(`${entry.resourceVersion}\u0000${entry.assetId}`)), }; } finally { rmSync(compilerOutput, { force: true, recursive: true }); } } export function defaultReplicationRoot(environment = process.env) { if (!environment.USERPROFILE || !isAbsolute(environment.USERPROFILE)) throw new Error("user_profile_unavailable"); return join(environment.USERPROFILE, "Desktop", "sticker_web_replication_assets"); }