| 1 | // BUD-16 and BUD-17 MessagePack manifests. Go templates embed this browser |
| 2 | // source directly so the same codec serves pages and file controls. |
| 3 | (() => { |
| 4 | "use strict" ; |
| 5 | // Manifest codec and tree reader. The API encodes and validates file and |
| 6 | // directory manifests, builds chunked trees and reads them through caller |
| 7 | // supplied fetch functions. |
| 8 | const CHUNK_SIZE = 2 * 1024 * 1024; |
| 9 | const MAX_LINKS = 174; |
| 10 | const MAX_DEPTH = 64; |
| 11 | const MAX_MANIFEST_BYTES = 16 * 1024 * 1024; |
| 12 | const textEncoder = new TextEncoder(), |
| 13 | textDecoder = new TextDecoder("utf-8" , {fatal: true}); |
| 14 | const {bytes: asBytes, hex, sha256} = globalThis.tiny.util; |
| 15 | const hashBytes = h => globalThis.tiny.util.fromHex(h, "expected lowercase SHA-256 hash" ); |
| 16 | const keyBytes = k => { |
| 17 | if (k === undefined) return; |
| 18 | const b = asBytes(k); |
| 19 | if (b.length !== 32) throw new Error("manifest keys must be 32 bytes" ); |
| 20 | return b; |
| 21 | }; |
| 22 | const wellFormed = value => { |
| 23 | for (let i = 0; i < value.length; i++) { |
| 24 | const c = value.charCodeAt(i); |
| 25 | if (c >= 55296 && c <= 56319) { |
| 26 | if (++i >= value.length || value.charCodeAt(i) < 56320 || value.charCodeAt(i) > 57343) return false; |
| 27 | } else if (c >= 56320 && c <= 57343) return false; |
| 28 | } |
| 29 | return true; |
| 30 | }; |
| 31 | const nameOK = n => |
| 32 | typeof n === "string" && |
| 33 | n.length > 0 && |
| 34 | n !== "." && |
| 35 | n !== ".." && |
| 36 | wellFormed(n) && |
| 37 | !n.includes("/" ) && |
| 38 | !n.includes("\0" ) && |
| 39 | textEncoder.encode(n).length <= 1024; |
| 40 | const int = n => Number.isSafeInteger(n) && n >= 0; |
| 41 | const utf8cmp = (a, b) => { |
| 42 | const x = textEncoder.encode(a), |
| 43 | y = textEncoder.encode(b); |
| 44 | for (let i = 0; i < Math.min(x.length, y.length); i++) if (x[i] !== y[i]) return x[i] - y[i]; |
| 45 | return x.length - y.length; |
| 46 | }; |
| 47 | class MessagePackWriter { |
| 48 | constructor() { |
| 49 | this.output = []; |
| 50 | } |
| 51 | push(...x) { |
| 52 | this.output.push(...x); |
| 53 | } |
| 54 | u8(n) { |
| 55 | this.output.push(n & 255); |
| 56 | } |
| 57 | bytes() { |
| 58 | return Uint8Array.from(this.output); |
| 59 | } |
| 60 | } |
| 61 | const writeInteger = (w, n) => { |
| 62 | if (!Number.isSafeInteger(n)) throw new Error("manifest integers must be safe integers" ); |
| 63 | if (n >= 0) { |
| 64 | if (n < 128) w.u8(n); |
| 65 | else if (n < 256) w.push(204, n); |
| 66 | else if (n < 65536) w.push(205, n >> 8, n); |
| 67 | else if (n < 4294967296) w.push(206, n >>> 24, n >>> 16, n >>> 8, n); |
| 68 | else { |
| 69 | w.u8(207); |
| 70 | let x = BigInt(n); |
| 71 | for (let i = 7; i >= 0; i--) w.u8(Number((x >> BigInt(i * 8)) & 255n)); |
| 72 | } |
| 73 | } else if (n >= -32) w.u8(256 + n); |
| 74 | else if (n >= -128) w.push(208, n & 255); |
| 75 | else if (n >= -32768) w.push(209, (n >> 8) & 255, n & 255); |
| 76 | else if (n >= -2147483648) w.push(210, n >>> 24, n >>> 16, n >>> 8, n); |
| 77 | else { |
| 78 | w.u8(211); |
| 79 | let x = BigInt.asUintN(64, BigInt(n)); |
| 80 | for (let i = 7; i >= 0; i--) w.u8(Number((x >> BigInt(i * 8)) & 255n)); |
| 81 | } |
| 82 | }; |
| 83 | const writeValue = (w, v) => { |
| 84 | if (v === null) return w.u8(192); |
| 85 | if (v === false) return w.u8(194); |
| 86 | if (v === true) return w.u8(195); |
| 87 | if (typeof v === "number" ) { |
| 88 | if (Number.isInteger(v)) return writeInteger(w, v); |
| 89 | if (!Number.isFinite(v)) throw Error("metadata number must be finite" ); |
| 90 | w.u8(203); |
| 91 | const b = new DataView(new ArrayBuffer(8)); |
| 92 | b.setFloat64(0, v); |
| 93 | for (let i = 0; i < 8; i++) w.u8(b.getUint8(i)); |
| 94 | return; |
| 95 | } |
| 96 | if (typeof v === "string" ) { |
| 97 | const b = textEncoder.encode(v); |
| 98 | if (b.length < 32) w.u8(160 | b.length); |
| 99 | else if (b.length < 256) w.push(217, b.length); |
| 100 | else if (b.length < 65536) w.push(218, b.length >> 8, b.length); |
| 101 | else throw new Error("string too large" ); |
| 102 | w.push(...b); |
| 103 | return; |
| 104 | } |
| 105 | if (v instanceof Uint8Array) { |
| 106 | if (v.length < 256) w.push(196, v.length); |
| 107 | else if (v.length < 65536) w.push(197, v.length >> 8, v.length); |
| 108 | else throw new Error("binary value too large" ); |
| 109 | w.push(...v); |
| 110 | return; |
| 111 | } |
| 112 | if (Array.isArray(v)) { |
| 113 | if (v.length < 16) w.u8(144 | v.length); |
| 114 | else if (v.length < 65536) w.push(220, v.length >> 8, v.length); |
| 115 | else throw new Error("array too large" ); |
| 116 | v.forEach(x => writeValue(w, x)); |
| 117 | return; |
| 118 | } |
| 119 | if (v && typeof v === "object" ) { |
| 120 | const keys = Object.keys(v).sort(utf8cmp); |
| 121 | if (keys.length < 16) w.u8(128 | keys.length); |
| 122 | else if (keys.length < 65536) w.push(222, keys.length >> 8, keys.length); |
| 123 | else throw new Error("map too large" ); |
| 124 | keys.forEach(k => { |
| 125 | writeValue(w, k); |
| 126 | writeValue(w, v[k]); |
| 127 | }); |
| 128 | return; |
| 129 | } |
| 130 | throw new Error("unsupported manifest value" ); |
| 131 | }; |
| 132 | class MessagePackReader { |
| 133 | constructor(b) { |
| 134 | this.input = asBytes(b); |
| 135 | this.offset = 0; |
| 136 | } |
| 137 | need(n) { |
| 138 | if (this.offset + n > this.input.length) throw new Error("truncated MessagePack" ); |
| 139 | } |
| 140 | u8() { |
| 141 | this.need(1); |
| 142 | return this.input[this.offset++]; |
| 143 | } |
| 144 | n(len) { |
| 145 | this.need(len); |
| 146 | const x = this.input.slice(this.offset, this.offset + len); |
| 147 | this.offset += len; |
| 148 | return x; |
| 149 | } |
| 150 | } |
| 151 | const readMap = (r, n, depth) => { |
| 152 | const o = Object.create(null); |
| 153 | for (let i = 0; i < n; i++) { |
| 154 | const k = readValue(r, depth + 1); |
| 155 | if (typeof k !== "string" || Object.prototype.hasOwnProperty.call(o, k)) throw Error("invalid map key" ); |
| 156 | o[k] = readValue(r, depth + 1); |
| 157 | } |
| 158 | return o; |
| 159 | }; |
| 160 | const readValue = (r, depth = 0) => { |
| 161 | if (depth > MAX_DEPTH) throw Error("MessagePack recursion limit exceeded" ); |
| 162 | const t = r.u8(); |
| 163 | if (t <= 127) return t; |
| 164 | if (t >= 224) return t - 256; |
| 165 | if ((t & 224) === 160) return textDecoder.decode(r.n(t & 31)); |
| 166 | if ((t & 240) === 144) return Array.from({length: t & 15}, () => readValue(r, depth + 1)); |
| 167 | if ((t & 240) === 128) return readMap(r, t & 15, depth); |
| 168 | if (t === 192) return null; |
| 169 | if (t === 194) return false; |
| 170 | if (t === 195) return true; |
| 171 | let n; |
| 172 | if (t === 204) n = r.u8(); |
| 173 | else if (t === 205) n = (r.u8() << 8) | r.u8(); |
| 174 | else if (t === 206) n = r.u8() * 16777216 + (r.u8() << 16) + (r.u8() << 8) + r.u8(); |
| 175 | else if (t === 207) { |
| 176 | let x = 0n; |
| 177 | for (let i = 0; i < 8; i++) x = (x << 8n) | BigInt(r.u8()); |
| 178 | if (x > BigInt(Number.MAX_SAFE_INTEGER)) throw Error("integer exceeds safe range" ); |
| 179 | n = Number(x); |
| 180 | } else if (t === 208) n = (r.u8() << 24) >> 24; |
| 181 | else if (t === 209) { |
| 182 | n = (r.u8() << 8) | r.u8(); |
| 183 | if (n & 32768) n -= 65536; |
| 184 | } else if (t === 210) { |
| 185 | n = r.u8() * 16777216 + (r.u8() << 16) + (r.u8() << 8) + r.u8(); |
| 186 | if (n >= 2147483648) n -= 4294967296; |
| 187 | } else if (t === 211) { |
| 188 | let x = 0n; |
| 189 | for (let i = 0; i < 8; i++) x = (x << 8n) | BigInt(r.u8()); |
| 190 | if (x & 0x8000000000000000n) x -= 0x10000000000000000n; |
| 191 | if (x > BigInt(Number.MAX_SAFE_INTEGER) || x < BigInt(Number.MIN_SAFE_INTEGER)) |
| 192 | throw Error("integer exceeds safe range" ); |
| 193 | n = Number(x); |
| 194 | } else if (t === 202) { |
| 195 | const b = new ArrayBuffer(4), |
| 196 | d = new DataView(b); |
| 197 | for (let i = 0; i < 4; i++) d.setUint8(i, r.u8()); |
| 198 | n = d.getFloat32(0); |
| 199 | } else if (t === 203) { |
| 200 | const b = new ArrayBuffer(8), |
| 201 | d = new DataView(b); |
| 202 | for (let i = 0; i < 8; i++) d.setUint8(i, r.u8()); |
| 203 | n = d.getFloat64(0); |
| 204 | } else if (t === 217) n = textDecoder.decode(r.n(r.u8())); |
| 205 | else if (t === 218) n = textDecoder.decode(r.n((r.u8() << 8) | r.u8())); |
| 206 | else if (t === 196) n = r.n(r.u8()); |
| 207 | else if (t === 197) n = r.n((r.u8() << 8) | r.u8()); |
| 208 | else if (t === 220) { |
| 209 | n = (r.u8() << 8) | r.u8(); |
| 210 | return Array.from({length: n}, () => readValue(r, depth + 1)); |
| 211 | } else if (t === 222) { |
| 212 | n = (r.u8() << 8) | r.u8(); |
| 213 | return readMap(r, n, depth); |
| 214 | } else throw Error("unsupported MessagePack type" ); |
| 215 | return n; |
| 216 | }; |
| 217 | const validateMeta = (m, depth = 0) => { |
| 218 | if (m === undefined) return; |
| 219 | if (depth > MAX_DEPTH || !m || Array.isArray(m) || typeof m !== "object" || m instanceof Uint8Array) |
| 220 | throw Error("metadata must be a JSON map" ); |
| 221 | const keys = Object.keys(m); |
| 222 | if (keys.length > 64) throw Error("too many metadata keys" ); |
| 223 | for (const [k, v] of Object.entries(m)) { |
| 224 | if (typeof k !== "string" || textEncoder.encode(k).length > 256) throw Error("invalid metadata key" ); |
| 225 | if ( |
| 226 | v === undefined || |
| 227 | typeof v === "bigint" || |
| 228 | typeof v === "function" || |
| 229 | typeof v === "symbol" || |
| 230 | (typeof v === "number" && !Number.isFinite(v)) |
| 231 | ) |
| 232 | throw Error("metadata is not JSON-compatible" ); |
| 233 | if (v && typeof v === "object" ) { |
| 234 | if (Array.isArray(v)) { |
| 235 | if (v.length > 256) throw Error("metadata array too large" ); |
| 236 | v.forEach(x => validateMeta({x}, depth + 1)); |
| 237 | } else validateMeta(v, depth + 1); |
| 238 | } |
| 239 | } |
| 240 | }; |
| 241 | const validateLink = (x, nodeType) => { |
| 242 | if (!x || typeof x !== "object" || Array.isArray(x)) throw Error("invalid manifest link" ); |
| 243 | if (Object.keys(x).some(k => !["h" , "k" , "m" , "n" , "s" , "t" ].includes(k))) |
| 244 | throw Error("unknown manifest link field" ); |
| 245 | if (!(x.h instanceof Uint8Array) || x.h.length !== 32) throw Error("link hash must be 32 bytes" ); |
| 246 | if (x.k !== undefined) keyBytes(x.k); |
| 247 | if (x.m !== undefined) validateMeta(x.m); |
| 248 | if (!int(x.s)) throw Error("invalid link size" ); |
| 249 | if (!int(x.t) || x.t < 0 || x.t > 3) throw Error("unsupported link type" ); |
| 250 | if (nodeType === 1 && x.t !== 0 && x.t !== 1) throw Error("invalid file link type" ); |
| 251 | if (nodeType === 1 && x.n !== undefined) throw Error("file links cannot have names" ); |
| 252 | if (nodeType === 3 && x.n !== undefined) throw Error("fanout links cannot have names" ); |
| 253 | if (nodeType === 3 && x.t !== 2 && x.t !== 3) throw Error("invalid fanout child type" ); |
| 254 | if (nodeType === 2 && !nameOK(x.n)) throw Error("invalid directory entry name" ); |
| 255 | }; |
| 256 | const validateNode = (node, depth = 0) => { |
| 257 | if (depth > MAX_DEPTH) throw Error("manifest recursion limit exceeded" ); |
| 258 | if ( |
| 259 | !node || |
| 260 | typeof node !== "object" || |
| 261 | Array.isArray(node) || |
| 262 | !Array.isArray(node.l) || |
| 263 | !int(node.t) || |
| 264 | ![1, 2, 3].includes(node.t) || |
| 265 | Object.keys(node).some(k => k !== "l" && k !== "t" ) |
| 266 | ) |
| 267 | throw Error("invalid manifest node" ); |
| 268 | if (node.l.length > MAX_LINKS) throw Error("too many manifest links" ); |
| 269 | const seen = new Set(), |
| 270 | links = node.l; |
| 271 | for (const x of links) { |
| 272 | validateLink(x, node.t); |
| 273 | if (node.t === 2) { |
| 274 | if (seen.has(x.n)) throw Error("duplicate directory entry" ); |
| 275 | seen.add(x.n); |
| 276 | } |
| 277 | if (node.t === 3) { |
| 278 | if ( |
| 279 | !x.m || |
| 280 | !int(x.m.count) || |
| 281 | x.m.count < 1 || |
| 282 | !nameOK(x.m.first) || |
| 283 | !nameOK(x.m.last) || |
| 284 | utf8cmp(x.m.first, x.m.last) > 0 |
| 285 | ) |
| 286 | throw Error("invalid fanout bounds" ); |
| 287 | } |
| 288 | } |
| 289 | if (node.t === 2 && links.some((x, i) => i && utf8cmp(links[i - 1].n, x.n) >= 0)) |
| 290 | throw Error("directory links are not canonical" ); |
| 291 | return node; |
| 292 | }; |
| 293 | const encodeManifest = node => { |
| 294 | validateNode(node); |
| 295 | const w = new MessagePackWriter(); |
| 296 | w.u8(130); |
| 297 | writeValue(w, "l" ); |
| 298 | if (node.l.length < 16) w.u8(144 | node.l.length); |
| 299 | else w.push(220, node.l.length >> 8, node.l.length); |
| 300 | for (const x of node.l) { |
| 301 | const fields = [ |
| 302 | "h" , |
| 303 | ...(x.k !== undefined ? ["k" ] : []), |
| 304 | ...(x.m !== undefined ? ["m" ] : []), |
| 305 | ...(x.n !== undefined ? ["n" ] : []), |
| 306 | "s" , |
| 307 | "t" |
| 308 | ]; |
| 309 | if (fields.length < 16) w.u8(128 | fields.length); |
| 310 | else w.push(222, fields.length >> 8, fields.length); |
| 311 | for (const k of fields) { |
| 312 | writeValue(w, k); |
| 313 | writeValue(w, k === "h" || k === "k" ? asBytes(x[k]) : x[k]); |
| 314 | } |
| 315 | } |
| 316 | writeValue(w, "t" ); |
| 317 | writeValue(w, node.t); |
| 318 | const out = w.bytes(); |
| 319 | if (out.length > MAX_MANIFEST_BYTES) throw Error("manifest too large" ); |
| 320 | return out; |
| 321 | }; |
| 322 | const decodeManifest = (bytes, options = {}) => { |
| 323 | const b = asBytes(bytes); |
| 324 | if (b.length > MAX_MANIFEST_BYTES) throw Error("manifest too large" ); |
| 325 | const r = new MessagePackReader(b), |
| 326 | node = readValue(r); |
| 327 | if (r.offset !== b.length) throw Error("trailing MessagePack data" ); |
| 328 | validateNode(node); |
| 329 | if (node.t === 3) |
| 330 | for (const x of node.l) { |
| 331 | if (x.m.count > Number(options.maxEntries || 1e9)) throw Error("fanout count limit exceeded" ); |
| 332 | } |
| 333 | return node; |
| 334 | }; |
| 335 | const manifestHash = async node => hex(await sha256(encodeManifest(node))); |
| 336 | const leaf = (hash, size, key) => { |
| 337 | const x = {h: hashBytes(hash), s: size, t: 0}; |
| 338 | if (key !== undefined) x.k = keyBytes(key); |
| 339 | return x; |
| 340 | }; |
| 341 | const storeNode = async (node, options, manifests) => { |
| 342 | const bytes = encodeManifest(node), |
| 343 | computed = hex(await sha256(bytes)); |
| 344 | let result = {hash: computed}; |
| 345 | if (options.store) { |
| 346 | const stored = await options.store({hash: computed, bytes, node}); |
| 347 | if (stored) { |
| 348 | if (typeof stored === "string" ) result.hash = stored; |
| 349 | else result = {...result, ...stored}; |
| 350 | } |
| 351 | } |
| 352 | if (!/^[0-9a-f]{64}$/.test(result.hash)) throw Error("store returned invalid manifest hash" ); |
| 353 | if (result.key !== undefined) keyBytes(result.key); |
| 354 | manifests.push({hash: result.hash, bytes, node, key: result.key}); |
| 355 | return result; |
| 356 | }; |
| 357 | const buildFile = async (chunks, options = {}) => { |
| 358 | const max = options.maxLinks ?? MAX_LINKS; |
| 359 | if (!Number.isInteger(max) || max < 2 || max > MAX_LINKS) throw Error("invalid maxLinks" ); |
| 360 | const leaves = chunks.map(c => |
| 361 | typeof c === "string" ? leaf(c, options.chunkSize || 0) : leaf(c.hash, c.size, c.key) |
| 362 | ); |
| 363 | if (leaves.some(x => x.s < 0 || x.s > CHUNK_SIZE)) throw Error("invalid chunk size" ); |
| 364 | const manifests = []; |
| 365 | let level = leaves; |
| 366 | while (level.length > max) { |
| 367 | const next = []; |
| 368 | for (let i = 0; i < level.length; i += max) { |
| 369 | const group = level.slice(i, i + max), |
| 370 | child = {l: group, t: 1}, |
| 371 | stored = await storeNode(child, options, manifests); |
| 372 | next.push({ |
| 373 | h: hashBytes(stored.hash), |
| 374 | ...(stored.key ? {k: keyBytes(stored.key)} : {}), |
| 375 | s: group.reduce((n, x) => n + x.s, 0), |
| 376 | t: 1 |
| 377 | }); |
| 378 | } |
| 379 | level = next; |
| 380 | } |
| 381 | const root = {l: level, t: 1}; |
| 382 | await storeNode(root, options, manifests); |
| 383 | return options.returnDetails ? {root, manifests} : root; |
| 384 | }; |
| 385 | const buildDirectory = async (entries, options = {}) => { |
| 386 | if (!Array.isArray(entries)) throw Error("entries must be an array" ); |
| 387 | const max = options.maxLinks ?? MAX_LINKS; |
| 388 | if (!Number.isInteger(max) || max < 2 || max > MAX_LINKS) throw Error("invalid maxLinks" ); |
| 389 | const sorted = entries |
| 390 | .map(e => { |
| 391 | const x = { |
| 392 | h: hashBytes(e.hash), |
| 393 | n: e.name, |
| 394 | s: e.size || 0, |
| 395 | t: e.type || 0 |
| 396 | }; |
| 397 | if (e.key !== undefined) x.k = keyBytes(e.key); |
| 398 | if (e.metadata !== undefined) x.m = e.metadata; |
| 399 | return x; |
| 400 | }) |
| 401 | .sort((a, b) => utf8cmp(a.n, b.n)); |
| 402 | for (let i = 1; i < sorted.length; i++) |
| 403 | if (sorted[i - 1].n === sorted[i].n) throw Error("duplicate directory entry" ); |
| 404 | const manifests = []; |
| 405 | if (sorted.length <= max) { |
| 406 | const directory = {l: sorted, t: 2}; |
| 407 | await storeNode(directory, options, manifests); |
| 408 | return options.returnDetails ? {root: directory, manifests} : directory; |
| 409 | } |
| 410 | let level = []; |
| 411 | for (let i = 0; i < sorted.length; i += max) { |
| 412 | const l = sorted.slice(i, i + max), |
| 413 | child = {l, t: 2}, |
| 414 | stored = await storeNode(child, options, manifests); |
| 415 | level.push({ |
| 416 | h: hashBytes(stored.hash), |
| 417 | ...(stored.key ? {k: keyBytes(stored.key)} : {}), |
| 418 | m: {count: l.length, first: l[0].n, last: l[l.length - 1].n}, |
| 419 | s: l.reduce((n, x) => n + x.s, 0), |
| 420 | t: 2 |
| 421 | }); |
| 422 | } |
| 423 | while (level.length > max) { |
| 424 | const next = []; |
| 425 | for (let i = 0; i < level.length; i += max) { |
| 426 | const l = level.slice(i, i + max), |
| 427 | child = {l, t: 3}, |
| 428 | stored = await storeNode(child, options, manifests); |
| 429 | next.push({ |
| 430 | h: hashBytes(stored.hash), |
| 431 | ...(stored.key ? {k: keyBytes(stored.key)} : {}), |
| 432 | m: { |
| 433 | count: l.reduce((n, x) => n + x.m.count, 0), |
| 434 | first: l[0].m.first, |
| 435 | last: l[l.length - 1].m.last |
| 436 | }, |
| 437 | s: l.reduce((n, x) => n + x.s, 0), |
| 438 | t: 3 |
| 439 | }); |
| 440 | } |
| 441 | level = next; |
| 442 | } |
| 443 | const root = {l: level, t: 3}; |
| 444 | await storeNode(root, options, manifests); |
| 445 | return options.returnDetails ? {root, manifests} : root; |
| 446 | }; |
| 447 | const flattenDirectory = (node, out = []) => { |
| 448 | validateNode(node); |
| 449 | if (node.t === 2) { |
| 450 | out.push(...node.l); |
| 451 | return out; |
| 452 | } |
| 453 | if (node.t !== 3) throw Error("not a directory" ); |
| 454 | for (const x of node.l) if (x.child) flattenDirectory(x.child, out); |
| 455 | return out; |
| 456 | }; |
| 457 | const resolveDirectory = async (root, fetchNode, options = {}) => { |
| 458 | if (typeof fetchNode !== "function" ) throw Error("fetchNode is required" ); |
| 459 | const maxDepth = options.maxDepth ?? MAX_DEPTH, |
| 460 | maxEntries = options.maxEntries ?? 1e5, |
| 461 | maxManifests = options.maxManifests ?? 1e4, |
| 462 | maxBytes = options.maxBytes ?? 64 * 1024 * 1024; |
| 463 | if ([maxDepth, maxEntries, maxManifests, maxBytes].some(value => !Number.isSafeInteger(value) || value < 0)) |
| 464 | throw Error("invalid directory limits" ); |
| 465 | let count = 0, |
| 466 | manifestCount = 0, |
| 467 | fetched = 0; |
| 468 | const active = new Set(); |
| 469 | const walk = async (node, depth) => { |
| 470 | if (depth > maxDepth) throw Error("directory recursion limit exceeded" ); |
| 471 | if (++manifestCount > maxManifests) throw Error("manifest count limit exceeded" ); |
| 472 | validateNode(node); |
| 473 | if (node.t === 2) { |
| 474 | count += node.l.length; |
| 475 | if (count > maxEntries) throw Error("directory entry limit exceeded" ); |
| 476 | return node.l.slice(); |
| 477 | } |
| 478 | if (node.t !== 3) throw Error("not a directory" ); |
| 479 | const result = []; |
| 480 | let previous = "" ; |
| 481 | for (const link of node.l) { |
| 482 | if (previous && utf8cmp(previous, link.m.first) >= 0) throw Error("fanout bounds are not ordered" ); |
| 483 | previous = link.m.last; |
| 484 | const linkHash = hex(link.h); |
| 485 | if (active.has(linkHash)) throw Error("manifest cycle detected" ); |
| 486 | active.add(linkHash); |
| 487 | let raw = asBytes(await fetchNode(linkHash, link)); |
| 488 | fetched += raw.length; |
| 489 | if (fetched > maxBytes) throw Error("directory byte limit exceeded" ); |
| 490 | if (hex(await sha256(raw)) !== linkHash) throw Error("manifest hash mismatch" ); |
| 491 | if (link.k) { |
| 492 | if (typeof options.decrypt !== "function" ) throw Error("encrypted manifest requires decrypt callback" ); |
| 493 | raw = asBytes(await options.decrypt(raw, link)); |
| 494 | } |
| 495 | const child = decodeManifest(raw); |
| 496 | if (child.t !== link.t) throw Error("fanout child type mismatch" ); |
| 497 | const links = await walk(child, depth + 1); |
| 498 | active.delete(linkHash); |
| 499 | if (links.length !== link.m.count || links[0]?.n !== link.m.first || links.at(-1)?.n !== link.m.last) |
| 500 | throw Error("fanout bounds mismatch" ); |
| 501 | result.push(...links); |
| 502 | } |
| 503 | for (let i = 1; i < result.length; i++) |
| 504 | if (utf8cmp(result[i - 1].n, result[i].n) >= 0) throw Error("duplicate or unordered directory entry" ); |
| 505 | return result; |
| 506 | }; |
| 507 | return walk(root, 0); |
| 508 | }; |
| 509 | const readFile = async (root, fetchBlob, options = {}) => { |
| 510 | if (typeof fetchBlob !== "function" ) throw Error("fetchBlob is required" ); |
| 511 | const maxDepth = options.maxDepth ?? MAX_DEPTH, |
| 512 | maxBytes = options.maxBytes ?? 512 * 1024 * 1024, |
| 513 | maxManifests = options.maxManifests ?? 1e4; |
| 514 | const maxFetchedBytes = options.maxFetchedBytes ?? maxBytes + 16 * 1024 * 1024; |
| 515 | if ([maxDepth, maxBytes, maxManifests, maxFetchedBytes].some(value => !Number.isSafeInteger(value) || value < 0)) |
| 516 | throw Error("invalid file limits" ); |
| 517 | let used = 0, |
| 518 | fetched = 0, |
| 519 | manifests = 0; |
| 520 | const active = new Set(), |
| 521 | parts = []; |
| 522 | const readNode = async (node, depth) => { |
| 523 | if (depth > maxDepth) throw Error("file recursion limit exceeded" ); |
| 524 | validateNode(node); |
| 525 | if (node.t !== 1) throw Error("not a file manifest" ); |
| 526 | if (++manifests > maxManifests) throw Error("manifest count limit exceeded" ); |
| 527 | for (const link of node.l) { |
| 528 | const hash = hex(link.h); |
| 529 | if (active.has(hash)) throw Error("manifest cycle detected" ); |
| 530 | active.add(hash); |
| 531 | let raw = asBytes(await fetchBlob(hash, link)); |
| 532 | fetched += raw.length; |
| 533 | if (fetched > maxFetchedBytes) throw Error("file fetched byte limit exceeded" ); |
| 534 | if (hex(await sha256(raw)) !== hash) throw Error("blob hash mismatch" ); |
| 535 | if (link.t === 1) { |
| 536 | if (typeof options.decrypt !== "function" && link.k) |
| 537 | throw Error("encrypted manifest requires decrypt callback" ); |
| 538 | if (link.k) raw = asBytes(await options.decrypt(raw, link)); |
| 539 | const child = decodeManifest(raw); |
| 540 | if (child.t !== 1) throw Error("file child type mismatch" ); |
| 541 | const before = used; |
| 542 | await readNode(child, depth + 1); |
| 543 | if (used - before !== link.s) throw Error("file manifest size mismatch" ); |
| 544 | } else { |
| 545 | if (link.k && typeof options.decrypt !== "function" ) throw Error("encrypted chunk requires decrypt callback" ); |
| 546 | const data = link.k ? asBytes(await options.decrypt(raw, link)) : raw; |
| 547 | if (data.length !== link.s) throw Error("file chunk size mismatch" ); |
| 548 | used += data.length; |
| 549 | if (used > maxBytes) throw Error("file byte limit exceeded" ); |
| 550 | parts.push(data); |
| 551 | } |
| 552 | active.delete(hash); |
| 553 | } |
| 554 | }; |
| 555 | await readNode(root, 0); |
| 556 | const out = new Uint8Array(used); |
| 557 | let at = 0; |
| 558 | for (const p of parts) { |
| 559 | out.set(p, at); |
| 560 | at += p.length; |
| 561 | } |
| 562 | return out; |
| 563 | }; |
| 564 | const api = Object.freeze({ |
| 565 | CHUNK_SIZE, |
| 566 | MAX_LINKS, |
| 567 | encodeManifest, |
| 568 | decodeManifest, |
| 569 | encode: encodeManifest, |
| 570 | decode: decodeManifest, |
| 571 | manifestHash, |
| 572 | buildFile, |
| 573 | buildDirectory, |
| 574 | flattenDirectory, |
| 575 | resolveDirectory, |
| 576 | readFile, |
| 577 | sha256, |
| 578 | hex |
| 579 | }); |
| 580 | globalThis.tiny = globalThis.tiny || {}; |
| 581 | globalThis.tiny.blossom = {...globalThis.tiny.blossom, manifests: api}; |
| 582 | })(); |