export const RNX_METRO_FINGERPRINT_SCHEMA_VERSION = 2 export const RNX_METRO_FINGERPRINT_POINTER_PATH = '/compile/metro-fingerprints.json' export const RNX_METRO_FINGERPRINT_MAX_POINTER_BYTES = 16 * 1024 export const RNX_METRO_FINGERPRINT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024 export type RNXMetroFingerprintPlatform = 'ios' | 'android' export interface RNXMetroFingerprintAtom { key: string exports: string[] literals: string[] dependencyArity: number factoryDigest: string } export interface RNXMetroFingerprintNode { atom: number internalDependencies: Array<{ position: number; node: number }> landmarkPath?: string } export interface RNXMetroFingerprintRoot { node: number } export interface RNXMetroFingerprintTemplate { packageName: string packageVersion: string sourceIntegrity: string profile: string platform: RNXMetroFingerprintPlatform nodes: RNXMetroFingerprintNode[] roots: RNXMetroFingerprintRoot[] } export interface RNXMetroFingerprintCandidate { template: number root: number } export interface RNXMetroFingerprintRegistry { schemaVersion: typeof RNX_METRO_FINGERPRINT_SCHEMA_VERSION revision: string atoms: RNXMetroFingerprintAtom[] templates: RNXMetroFingerprintTemplate[] candidatesByFactoryDigest: Record candidatesByDiscriminator: Record } export interface RNXMetroFingerprintPointer { schemaVersion: typeof RNX_METRO_FINGERPRINT_SCHEMA_VERSION revision: string integrity: string bytes: number payloadUrl: string } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function isSha256(value: unknown): value is string { return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value) } function readStringArray(value: unknown): string[] | null { if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) { return null } const entries = [...value] if ( new Set(entries).size !== entries.length || entries.some((entry, index) => index > 0 && entries[index - 1] >= entry) ) { return null } return entries } export function parseRNXMetroFingerprintPointer( value: unknown, ): RNXMetroFingerprintPointer | null { if (!isRecord(value)) return null const { schemaVersion, revision, integrity, bytes, payloadUrl } = value if ( schemaVersion !== RNX_METRO_FINGERPRINT_SCHEMA_VERSION || !isSha256(revision) || !isSha256(integrity) || typeof bytes !== 'number' || !Number.isSafeInteger(bytes) || bytes < 1 || typeof payloadUrl !== 'string' || payloadUrl.length === 0 ) { return null } return { schemaVersion, revision, integrity, bytes, payloadUrl } } export function parseRNXMetroFingerprintRegistry( value: unknown, ): RNXMetroFingerprintRegistry | null { if (!isRecord(value)) return null const { schemaVersion, revision } = value if ( schemaVersion !== RNX_METRO_FINGERPRINT_SCHEMA_VERSION || !isSha256(revision) || !Array.isArray(value.atoms) || !Array.isArray(value.templates) || !isRecord(value.candidatesByFactoryDigest) || !isRecord(value.candidatesByDiscriminator) ) { return null } const atoms: RNXMetroFingerprintAtom[] = [] for (const atomValue of value.atoms) { if (!isRecord(atomValue)) return null const exports = readStringArray(atomValue.exports) const literals = readStringArray(atomValue.literals) if ( typeof atomValue.key !== 'string' || atomValue.key.length === 0 || !exports || !literals || typeof atomValue.dependencyArity !== 'number' || !Number.isSafeInteger(atomValue.dependencyArity) || atomValue.dependencyArity < 0 || !isSha256(atomValue.factoryDigest) ) { return null } atoms.push({ key: atomValue.key, exports, literals, dependencyArity: atomValue.dependencyArity, factoryDigest: atomValue.factoryDigest, }) } const templates: RNXMetroFingerprintTemplate[] = [] for (const templateValue of value.templates) { if ( !isRecord(templateValue) || typeof templateValue.packageName !== 'string' || templateValue.packageName.length === 0 || typeof templateValue.packageVersion !== 'string' || templateValue.packageVersion.length === 0 || !isSha256(templateValue.sourceIntegrity) || typeof templateValue.profile !== 'string' || templateValue.profile.length === 0 || (templateValue.platform !== 'ios' && templateValue.platform !== 'android') || !Array.isArray(templateValue.nodes) || templateValue.nodes.length === 0 || !Array.isArray(templateValue.roots) || templateValue.roots.length === 0 ) { return null } const nodes: RNXMetroFingerprintNode[] = [] for (const nodeValue of templateValue.nodes) { if ( !isRecord(nodeValue) || typeof nodeValue.atom !== 'number' || !Number.isSafeInteger(nodeValue.atom) || nodeValue.atom < 0 || nodeValue.atom >= atoms.length || !Array.isArray(nodeValue.internalDependencies) || (nodeValue.landmarkPath !== undefined && (typeof nodeValue.landmarkPath !== 'string' || nodeValue.landmarkPath.length === 0)) ) { return null } const internalDependencies: Array<{ position: number; node: number }> = [] const positions = new Set() for (const edgeValue of nodeValue.internalDependencies) { if ( !isRecord(edgeValue) || typeof edgeValue.position !== 'number' || !Number.isSafeInteger(edgeValue.position) || edgeValue.position < 0 || positions.has(edgeValue.position) || typeof edgeValue.node !== 'number' || !Number.isSafeInteger(edgeValue.node) || edgeValue.node < 0 || edgeValue.node >= templateValue.nodes.length ) { return null } positions.add(edgeValue.position) internalDependencies.push({ position: edgeValue.position, node: edgeValue.node, }) } internalDependencies.sort((left, right) => left.position - right.position) nodes.push({ atom: nodeValue.atom, internalDependencies, ...(typeof nodeValue.landmarkPath === 'string' ? { landmarkPath: nodeValue.landmarkPath } : {}), }) } const roots: RNXMetroFingerprintRoot[] = [] for (const rootValue of templateValue.roots) { if ( !isRecord(rootValue) || typeof rootValue.node !== 'number' || !Number.isSafeInteger(rootValue.node) || rootValue.node < 0 || rootValue.node >= nodes.length || !nodes[rootValue.node].landmarkPath ) { return null } roots.push({ node: rootValue.node }) } templates.push({ packageName: templateValue.packageName, packageVersion: templateValue.packageVersion, sourceIntegrity: templateValue.sourceIntegrity, profile: templateValue.profile, platform: templateValue.platform, nodes, roots, }) } const candidatesByFactoryDigest: Record = {} for (const [factoryDigest, candidateValues] of Object.entries( value.candidatesByFactoryDigest, )) { if (!isSha256(factoryDigest) || !Array.isArray(candidateValues)) return null const candidates: RNXMetroFingerprintCandidate[] = [] for (const candidateValue of candidateValues) { if ( !isRecord(candidateValue) || typeof candidateValue.template !== 'number' || !Number.isSafeInteger(candidateValue.template) || candidateValue.template < 0 || candidateValue.template >= templates.length || typeof candidateValue.root !== 'number' || !Number.isSafeInteger(candidateValue.root) || candidateValue.root < 0 || candidateValue.root >= templates[candidateValue.template].roots.length ) { return null } const template = templates[candidateValue.template] const root = template.roots[candidateValue.root] if (atoms[template.nodes[root.node].atom].factoryDigest !== factoryDigest) { return null } candidates.push({ template: candidateValue.template, root: candidateValue.root, }) } candidatesByFactoryDigest[factoryDigest] = candidates } const candidatesByDiscriminator: Record = {} for (const [discriminator, candidateValues] of Object.entries( value.candidatesByDiscriminator, )) { if (!/^(?:e|l|ea|la|el|ela):[a-f0-9]{16}$/.test(discriminator)) return null if (!Array.isArray(candidateValues) || candidateValues.length === 0) return null const candidates: RNXMetroFingerprintCandidate[] = [] for (const candidateValue of candidateValues) { if ( !isRecord(candidateValue) || typeof candidateValue.template !== 'number' || !Number.isSafeInteger(candidateValue.template) || candidateValue.template < 0 || candidateValue.template >= templates.length || typeof candidateValue.root !== 'number' || !Number.isSafeInteger(candidateValue.root) || candidateValue.root < 0 || candidateValue.root >= templates[candidateValue.template].roots.length ) { return null } candidates.push({ template: candidateValue.template, root: candidateValue.root, }) } candidatesByDiscriminator[discriminator] = candidates } return { schemaVersion, revision, atoms, templates, candidatesByFactoryDigest, candidatesByDiscriminator, } }