bundle.js (9840B)
1 export const BUNDLE_LIMIT = 16_777_216; 2 // Separate bounded profile for the explicitly selected compiler, not packages. 3 export const COMPILER_BUNDLE_LIMIT = 2_097_152; 4 const utf8 = new TextDecoder('utf-8', { fatal: true }); 5 class Reader { 6 bytes; 7 at = 0; 8 view; 9 constructor(bytes) { 10 this.bytes = bytes; 11 this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); 12 } 13 take(count) { 14 if (!Number.isSafeInteger(count) || count < 0 || count > this.bytes.length - this.at) 15 throw new Error('Truncated bundle field'); 16 const value = this.bytes.subarray(this.at, this.at + count); 17 this.at += count; 18 return value; 19 } 20 u8() { return this.take(1)[0]; } 21 u16() { const at = this.at; this.take(2); return this.view.getUint16(at); } 22 u32() { const at = this.at; this.take(4); return this.view.getUint32(at); } 23 u64() { 24 const at = this.at; 25 this.take(8); 26 const n = this.view.getBigUint64(at); 27 if (n > BigInt(Number.MAX_SAFE_INTEGER)) 28 throw new Error('Oversized u64'); 29 return Number(n); 30 } 31 string(limit = 4096) { 32 const length = this.u32(); 33 if (length > limit) 34 throw new Error('Bundle string limit exceeded'); 35 return utf8.decode(this.take(length)); 36 } 37 count(limit = 4096) { 38 const n = this.u32(); 39 if (n > limit) 40 throw new Error('Bundle count limit exceeded'); 41 return n; 42 } 43 end() { if (this.at !== this.bytes.length) 44 throw new Error('Trailing section bytes'); } 45 } 46 function requireValue(actual, expected, label) { 47 if (actual !== expected) 48 throw new Error(`Unsupported/invalid ${label}`); 49 } 50 export function bundleHash(value) { 51 if (typeof value !== 'string' || !/^[0-9a-f]{64}$/u.test(value)) 52 throw new Error('Expected 64 lowercase hexadecimal bundle hash characters'); 53 return value; 54 } 55 export async function sha256(bytes) { 56 // Copy into an owned ArrayBuffer: no SharedArrayBuffer boundary or mutation 57 // can change the bytes being checked while validation/loading proceeds. 58 const result = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes)); 59 return bundleHash(Array.from(new Uint8Array(result), b => b.toString(16).padStart(2, '0')).join('')); 60 } 61 export async function fetchBytes(url, limit = BUNDLE_LIMIT) { 62 const response = await fetch(url, { redirect: 'error', credentials: 'omit' }); 63 if (!response.ok) 64 throw new Error(`Bundle fetch: HTTP ${response.status}`); 65 const length = response.headers.get('content-length'); 66 if (length && (!/^\d+$/u.test(length) || Number(length) > limit)) { 67 await response.body?.cancel(); 68 throw new Error('Bundle download limit exceeded'); 69 } 70 if (!response.body) 71 throw new Error('Missing response body'); 72 const reader = response.body.getReader(), chunks = []; 73 let size = 0; 74 try { 75 for (;;) { 76 const item = await reader.read(); 77 if (item.done) 78 break; 79 size += item.value.length; 80 if (size > limit) 81 throw new Error('Bundle download limit exceeded'); 82 chunks.push(item.value); 83 } 84 } 85 catch (error) { 86 await reader.cancel().catch(() => { }); 87 throw error; 88 } 89 finally { 90 reader.releaseLock(); 91 } 92 const bytes = new Uint8Array(size); 93 let offset = 0; 94 for (const chunk of chunks) { 95 bytes.set(chunk, offset); 96 offset += chunk.length; 97 } 98 return bytes; 99 } 100 export async function fetchBundle(url, expected, byteLimit = BUNDLE_LIMIT) { 101 const bytes = await fetchBytes(url, byteLimit); 102 if (await sha256(bytes) !== expected) 103 throw new Error('Bundle SHA-256 mismatch'); 104 return decodeBundle(bytes, byteLimit); 105 } 106 // Validate completely before touching the graph heap. All loops count physical 107 // records/bytes, never unfolded tree occurrences. No source/JS/WASM sections run. 108 export function decodeBundle(bytes, byteLimit = BUNDLE_LIMIT) { 109 if (bytes.length > byteLimit) 110 throw new Error('Bundle byte limit exceeded'); 111 const r = new Reader(bytes); 112 requireValue(utf8.decode(r.take(8)), 'ARBORICX', 'bundle magic'); 113 requireValue(r.u16(), 1, 'container major'); 114 requireValue(r.u16(), 0, 'container minor'); 115 const count = r.count(32); 116 requireValue(r.u64(), 0, 'header flags'); 117 requireValue(r.u64(), 32, 'directory offset'); 118 const sections = new Map(), ranges = []; 119 for (let i = 0; i < count; i++) { 120 const type = r.u32(), version = r.u16(), flags = r.u16(), compression = r.u16(); 121 requireValue(r.u16(), 0, 'section reserved'); 122 const start = r.u64(), size = r.u64(); 123 requireValue(r.u32(), 0, 'section padding'); 124 if (start < 32 + count * 32 || start > bytes.length || size > bytes.length - start) 125 throw new Error('Section out of bounds'); 126 if (sections.has(type)) 127 throw new Error('Duplicate section'); 128 if (flags & ~1) 129 throw new Error('Unknown section flags'); 130 if (type === 1 || type === 2) { 131 requireValue(version, 1, 'section version'); 132 requireValue(compression, 0, 'compression'); 133 } 134 else if (flags & 1) 135 throw new Error('Unknown critical section'); 136 ranges.push([start, start + size]); 137 sections.set(type, bytes.subarray(start, start + size)); 138 } 139 ranges.sort((a, b) => a[0] - b[0]); 140 let end = 32 + count * 32; 141 for (const [start, stop] of ranges) { 142 if (start !== end) 143 throw new Error('Overlapping/gapped sections'); 144 end = stop; 145 } 146 if (end !== bytes.length) 147 throw new Error('Trailing container bytes'); 148 const manifest = sections.get(1), nodeBytes = sections.get(2); 149 if (!manifest || !nodeBytes) 150 throw new Error('Missing required sections'); 151 const m = new Reader(manifest); 152 requireValue(utf8.decode(m.take(8)), 'ARBMNFST', 'manifest magic'); 153 requireValue(m.u16(), 1, 'manifest major'); 154 requireValue(m.u16(), 1, 'manifest minor'); 155 for (const expected of ['arboricx.bundle.manifest.v1', 'tree-calculus-executable-object', 156 'tree-calculus.v1', 'indexed', 'arboricx.indexed.node.v1', 'arboricx.indexed.payload.v1', 157 'tree-calculus.v1', 'normal-order', 'arboricx.abi.tree.v1']) 158 requireValue(m.string(), expected, 'manifest semantics'); 159 requireValue(m.u32(), 0, 'runtime capabilities'); 160 requireValue(m.u8(), 0, 'complete closure'); 161 const roots = []; 162 for (let i = m.count(); i > 0; i--) { 163 roots.push(m.u32()); 164 m.string(); 165 } 166 const exports = [], names = new Set(); 167 for (let i = m.count(); i > 0; i--) { 168 const name = m.string(512), root = m.u32(); 169 requireValue(m.string(), 'term', 'export kind'); 170 requireValue(m.string(), 'arboricx.abi.tree.v1', 'export ABI'); 171 if (!name || names.has(name)) 172 throw new Error('Empty/duplicate export name'); 173 names.add(name); 174 exports.push({ name, root, kind: 'term', abi: 'arboricx.abi.tree.v1' }); 175 } 176 if (!roots.length || !exports.length) 177 throw new Error('Bundle requires roots/exports'); 178 const metadata = Object.create(null); 179 const tags = { 1: 'package', 2: 'version', 3: 'description', 4: 'license', 5: 'createdBy' }; 180 for (let i = m.count(); i > 0; i--) { 181 const tag = m.u16(), length = m.u32(); 182 if (length > 16_384) 183 throw new Error('Metadata limit exceeded'); 184 const value = m.take(length), key = tags[tag]; 185 if (key) { 186 if (Object.hasOwn(metadata, key)) 187 throw new Error('Duplicate metadata'); 188 metadata[key] = utf8.decode(value); 189 } 190 } 191 for (let i = m.count(); i > 0; i--) { 192 m.u16(); 193 m.take(m.u32()); 194 } 195 m.end(); 196 const n = new Reader(nodeBytes), total = n.u64(); 197 if (!total || total > 100_000) 198 throw new Error('Bundle node count limit exceeded'); 199 const nodes = [], keys = new Set(); 200 for (let i = 0; i < total; i++) { 201 const p = new Reader(n.take(n.u32())), tag = p.u8(); 202 let node, key; 203 const child = () => { const id = p.u32(); if (id >= i) 204 throw new Error('Non-backward child reference'); return id; }; 205 if (tag === 0) { 206 node = { kind: 'leaf' }; 207 key = '0'; 208 } 209 else if (tag === 1) { 210 const id = child(); 211 node = { kind: 'stem', child: id }; 212 key = `1,${id}`; 213 } 214 else if (tag === 2) { 215 const left = child(), right = child(); 216 node = { kind: 'fork', left, right }; 217 key = `2,${left},${right}`; 218 } 219 else 220 throw new Error('Unknown node tag'); 221 p.end(); 222 if (keys.has(key)) 223 throw new Error('Duplicate indexed node'); 224 keys.add(key); 225 nodes.push(node); 226 } 227 n.end(); 228 for (const root of [...roots, ...exports.map(e => e.root)]) 229 if (root >= total) 230 throw new Error('Root index out of bounds'); 231 return { roots, exports, nodes, metadata }; 232 } 233 export function loadBundle(graph, bundle) { 234 const handles = []; 235 for (const node of bundle.nodes) { 236 graph.tick(); 237 switch (node.kind) { 238 case 'leaf': 239 handles.push(graph.leaf); 240 break; 241 case 'stem': 242 handles.push(graph.stem(handles[node.child])); 243 break; 244 case 'fork': 245 handles.push(graph.fork(handles[node.left], handles[node.right])); 246 break; 247 } 248 } 249 return new Map(bundle.exports.map(e => [e.name, handles[e.root]])); 250 }