| 1 | import assert from "node:assert/strict" ; |
| 2 | import {readFile} from "node:fs/promises" ; |
| 3 | import test from "node:test" ; |
| 4 | import vm from "node:vm" ; |
| 5 | |
| 6 | const source = await readFile(new URL("../tinyclient/blossom-manifests.js" , import.meta.url), "utf8" ); |
| 7 | const shared = await readFile(new URL("../tinyclient/tiny.js" , import.meta.url), "utf8" ); |
| 8 | const sandbox = {TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, crypto: (await import("node:crypto" )).webcrypto}; |
| 9 | sandbox.globalThis = sandbox; |
| 10 | vm.runInNewContext(shared + source, sandbox); |
| 11 | const api = sandbox.tiny.blossom.manifests; |
| 12 | const bytes = hex => Uint8Array.from(hex.match(/../g), x => parseInt(x, 16)); |
| 13 | const hash = c => c.repeat(64); |
| 14 | |
| 15 | test("matches the BUD-16 empty and single-entry vectors" , async () => { |
| 16 | const empty = {l: [], t: 2}; |
| 17 | assert.equal(Buffer.from(api.encodeManifest(empty)).toString("hex" ), "82a16c90a17402" ); |
| 18 | assert.equal(await api.manifestHash(empty), "0218ed9a4fbb0993757f17e5d08d089cb0c6ac851928ba1ba82d337d76c41c0c" ); |
| 19 | const one = {l: [{h: bytes("ab" .repeat(32)), n: "test.txt" , s: 100, t: 0}], t: 2}; |
| 20 | assert.equal(await api.manifestHash(one), "16121fa792b3afc72ec8bfc1dc85060518b6adba1429973ecc12891165cbe67e" ); |
| 21 | const decoded = api.decodeManifest(api.encodeManifest(one)); |
| 22 | assert.equal(decoded.t, 2); |
| 23 | assert.equal(decoded.l[0].n, "test.txt" ); |
| 24 | assert.equal(Buffer.from(decoded.l[0].h).toString("hex" ), "ab" .repeat(32)); |
| 25 | }); |
| 26 | |
| 27 | test("matches the BUD-17 two-chunk vector and preserves chunk order" , async () => { |
| 28 | const node = {l: [{h: bytes("aa" .repeat(32)), s: 100, t: 0}, {h: bytes("bb" .repeat(32)), s: 50, t: 0}], t: 1}; |
| 29 | assert.equal(await api.manifestHash(node), "559b726c38295aa0ecbbaef43d438cc86dd63324a0c3e9426dc5f1d0285f483f" ); |
| 30 | const decoded = api.decodeManifest(api.encodeManifest(node)); |
| 31 | assert.equal(decoded.t, 1); |
| 32 | assert.equal(JSON.stringify(decoded.l.map(x => [Buffer.from(x.h).toString("hex" ), x.s, x.t])), JSON.stringify([["aa" .repeat(32), 100, 0], ["bb" .repeat(32), 50, 0]])); |
| 33 | }); |
| 34 | |
| 35 | test("canonicalizes directory names by UTF-8 bytes and builds file manifests" , async () => { |
| 36 | const directory = await api.buildDirectory([ |
| 37 | {hash: hash("a" ), name: "z" , size: 2}, {hash: hash("b" ), name: "a" , size: 1} |
| 38 | ]); |
| 39 | assert.deepEqual(directory.l.map(x => x.n), ["a" , "z" ]); |
| 40 | const details = await api.buildFile(Array.from({length: 175}, (_, i) => ({hash: hash((i % 10).toString()), size: 1})), {returnDetails: true}); |
| 41 | assert.equal(details.root.t, 1); |
| 42 | assert.equal(details.root.l.length, 2); |
| 43 | assert.equal(details.manifests.length, 3); |
| 44 | assert.equal(details.root.l.reduce((n, x) => n + x.s, 0), 175); |
| 45 | }); |
| 46 | |
| 47 | test("builds typed directory fanout nodes with bounded child metadata" , async () => { |
| 48 | const details = await api.buildDirectory(Array.from({length: 175}, (_, i) => ({hash: hash("a" ), name: `file-${String(i).padStart(3, "0")}` , size: 1})), {returnDetails: true}); |
| 49 | assert.equal(details.root.t, 3); |
| 50 | assert.equal(details.root.l.length, 2); |
| 51 | assert.equal(JSON.stringify(details.root.l[0].m), JSON.stringify({count: 174, first: "file-000" , last: "file-173" })); |
| 52 | assert.equal(details.root.l[0].t, 2); |
| 53 | }); |
| 54 | |
| 55 | test("resolves fanout children only after hash and bound verification" , async () => { |
| 56 | const child = {l: [{h: bytes("aa" .repeat(32)), n: "a.txt" , s: 1, t: 0}], t: 2}; |
| 57 | const childHash = await api.manifestHash(child); |
| 58 | const root = {l: [{h: bytes(childHash), m: {count: 1, first: "a.txt" , last: "a.txt" }, s: 1, t: 2}], t: 3}; |
| 59 | const resolved = await api.resolveDirectory(root, async hash => hash === childHash ? api.encodeManifest(child) : new Uint8Array()); |
| 60 | assert.equal(resolved.length, 1); |
| 61 | await assert.rejects(api.resolveDirectory(root, async () => api.encodeManifest({...child, l: []})), /manifest hash mismatch/); |
| 62 | }); |
| 63 | |
| 64 | test("rejects traversal, duplicate names, unknown types and malformed bytes" , () => { |
| 65 | const link = {h: bytes("aa" .repeat(32)), n: "ok" , s: 1, t: 0}; |
| 66 | assert.throws(() => api.encodeManifest({l: [{...link, n: "../secret" }], t: 2}), /invalid directory entry name/); |
| 67 | assert.throws(() => api.encodeManifest({l: [link, link], t: 2}), /duplicate directory entry/); |
| 68 | assert.throws(() => api.encodeManifest({l: [{...link, t: 9}], t: 2}), /unsupported link type/); |
| 69 | assert.throws(() => api.decodeManifest(Uint8Array.from([0x82, 0xa1, 0x6c])), /truncated/); |
| 70 | assert.throws(() => api.encodeManifest({l: [{...link, n: "b" }, {...link, n: "a" }], t: 2}), /canonical/); |
| 71 | }); |