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