module-identity.ts (3826B)
1 // Native indexed import identity (ContentStore.Bundle / Wire.extractBundleRoot). 2 // Hash stored DAG records in left-before-right physical-edge order. 3 import { bundleHash, sha256, type Bundle, type BundleHash } from './bundle.js'; 4 export type ModuleHash = string & { readonly __moduleHash: unique symbol }; 5 export function moduleHash(value: unknown): ModuleHash { return bundleHash(value) as string as ModuleHash; } 6 const utf8 = new TextEncoder(); 7 function join(parts: Uint8Array[]): Uint8Array { 8 const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); 9 let at = 0; for (const p of parts) { out.set(p, at); at += p.length; } return out; 10 } 11 function word(n: number, size = 4): Uint8Array { 12 const out = new Uint8Array(size), view = new DataView(out.buffer); 13 if (size === 8) view.setBigUint64(0, BigInt(n)); 14 else if (size === 2) view.setUint16(0, n); else view.setUint32(0, n); 15 return out; 16 } 17 const string = (s: string): Uint8Array => { const b = utf8.encode(s); return join([word(b.length), b]); }; 18 export function indexedObject(bundle: Bundle, root: number): Uint8Array { 19 const ids = new Map<number, number>(), rows: Uint8Array[] = [], stack: [number, boolean][] = [[root, false]]; 20 while (stack.length) { 21 const [id, leave] = stack.pop()!; 22 if (ids.has(id)) continue; 23 const node = bundle.nodes[id]; if (!node) throw new Error('Missing indexed node'); 24 const children = node.kind === 'leaf' ? [] : node.kind === 'stem' ? [node.child] : [node.left, node.right]; 25 if (children.some(c => c >= id || c < 0)) throw new Error('Invalid indexed edge'); 26 if (!leave) { stack.push([id, true]); for (const c of children.reverse()) stack.push([c, false]); } 27 else { 28 const payload = join([new Uint8Array([children.length]), ...children.map(c => word(ids.get(c)!))]); 29 ids.set(id, rows.length); rows.push(join([word(payload.length), payload])); 30 } 31 } 32 const index = ids.get(root)!; 33 const manifest = join([ 34 utf8.encode('ARBMNFST'), word(1, 2), word(1, 2), 35 ...['arboricx.bundle.manifest.v1', 'tree-calculus-executable-object', 'tree-calculus.v1', 'indexed', 36 'arboricx.indexed.node.v1', 'arboricx.indexed.payload.v1', 'tree-calculus.v1', 'normal-order', 'arboricx.abi.tree.v1'].map(string), 37 word(0), new Uint8Array([0]), word(1), word(index), string('default'), 38 word(1), string('root'), word(index), string('term'), string('arboricx.abi.tree.v1'), 39 word(1), word(5, 2), string('arboricx'), word(0), 40 ]); 41 const nodes = join([word(rows.length, 8), ...rows]); 42 const section = (type: number, at: number, size: number): Uint8Array => join([ 43 word(type), word(1, 2), word(1, 2), word(0, 2), word(0, 2), word(at, 8), word(size, 8), word(0), 44 ]); 45 return join([utf8.encode('ARBORICX'), word(1, 2), word(0, 2), word(2), word(0, 8), word(32, 8), 46 section(1, 96, manifest.length), section(2, 96 + manifest.length, nodes.length), manifest, nodes]); 47 } 48 export async function moduleIdentity(bundle: Bundle): Promise<ModuleHash> { 49 if (!bundle.exports.length || bundle.exports.some(e => !/^[A-Za-z_][A-Za-z0-9_?$%'.-]*$/u.test(e.name))) { 50 throw new Error('Package requires a nonempty native module interface'); 51 } 52 const hashes = new Map<number, BundleHash>(); 53 let manifest = 'arboricx.module-manifest.v1\n'; 54 const escape = (s: string): string => s.replace(/%/g, '%25').replace(/\t/g, '%09').replace(/\n/g, '%0A').replace(/\r/g, '%0D'); 55 for (const e of bundle.exports) { 56 let hash = hashes.get(e.root); 57 if (!hash) { hash = await sha256(join([utf8.encode('arboricx.indexed-tree.v1\0'), indexedObject(bundle, e.root)])); hashes.set(e.root, hash); } 58 manifest += `export\t${escape(e.name)}\tarboricx.indexed-tree.v1\t${hash}\t${e.abi}\n`; 59 } 60 return moduleHash(await sha256(utf8.encode('arboricx.module-manifest.v1\0' + manifest))); 61 }