Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

98 lines · 4.0 KBRaw History
1import { deflateSync } from "node:zlib";
2import { describe, expect, it } from "vitest";
3import { applyDelta, parsePack, sha1 } from "../src/index.js";
4
5/** A pack from raw entries: [type code, declared size, body (already deflated, with any delta base prefix)]. */
6async function pack(entries: Array<[number, number, Uint8Array]>): Promise<Uint8Array> {
7 const parts: number[] = [..."PACK"].map((c) => c.charCodeAt(0));
8 const u32 = (n: number) => parts.push((n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255);
9 u32(2);
10 u32(entries.length);
11 for (const [type, size, body] of entries) {
12 let s = size;
13 let c = (type << 4) | (s & 15);
14 s = Math.floor(s / 16);
15 while (s > 0) {
16 parts.push(c | 0x80);
17 c = s & 0x7f;
18 s = Math.floor(s / 128);
19 }
20 parts.push(c);
21 parts.push(...body);
22 }
23 const body = new Uint8Array(parts);
24 const out = new Uint8Array(body.length + 20);
25 out.set(body);
26 out.set(await sha1(body), body.length);
27 return out;
28}
29const varint = (n: number) => {
30 const out: number[] = [];
31 do {
32 let b = n % 128;
33 n = Math.floor(n / 128);
34 if (n > 0) b |= 0x80;
35 out.push(b);
36 } while (n > 0);
37 return out;
38};
39
40describe("untrusted packs can't make the server inflate or allocate without bound", () => {
41 it("stops inflating an object at its declared size", async () => {
42 // Declares 10 bytes, inflates to 64 MiB.
43 const bomb = deflateSync(Buffer.alloc(64 * 1024 * 1024));
44 await expect(parsePack(await pack([[3, 10, bomb]]))).rejects.toThrow(/larger than its declared size/);
45 });
46
47 it("refuses a pack whose objects unpack past the budget", async () => {
48 const blob = Buffer.alloc(600 * 1024, 120);
49 const p = await pack([
50 [3, blob.length, deflateSync(blob)],
51 [3, blob.length, deflateSync(Buffer.concat([blob.subarray(1), Buffer.from("y")]))],
52 ]);
53 await expect(parsePack(p, undefined, { maxBytes: 1024 * 1024 })).rejects.toThrow(/unpacks to more than/);
54 expect((await parsePack(p, undefined, { maxBytes: 2 * 1024 * 1024 })).length).toBe(2);
55 });
56
57 it("inflates a large blob that tiny deltas use as their base once for all of them, within the memory budget", async () => {
58 // One 2 MiB blob, kept compressed, then 20 one-byte deltas against it by offset.
59 const blob = Buffer.alloc(2 * 1024 * 1024);
60 const header = (type: number, size: number) => {
61 const out: number[] = [];
62 let c = (type << 4) | (size & 15);
63 size = Math.floor(size / 16);
64 while (size > 0) {
65 out.push(c | 0x80);
66 c = size & 0x7f;
67 size = Math.floor(size / 128);
68 }
69 out.push(c);
70 return out;
71 };
72 const ofs = (n: number) => {
73 const out = [n & 127];
74 while ((n = Math.floor(n / 128))) out.unshift(0x80 | (--n & 127));
75 return out;
76 };
77 const parts: number[] = [..."PACK"].map((c) => c.charCodeAt(0)), u32 = (n: number) => parts.push((n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255);
78 u32(2);
79 u32(21);
80 const base = parts.length;
81 parts.push(...header(3, blob.length), ...deflateSync(blob));
82 const delta = Uint8Array.from([...varint(blob.length), ...varint(1), 0x91, 0, 1]);
83 for (let i = 0; i < 20; i++) parts.push(...header(6, delta.length), ...ofs(parts.length - base), ...deflateSync(delta));
84 const body = new Uint8Array(parts);
85 const p = new Uint8Array(body.length + 20);
86 p.set(body);
87 p.set(await sha1(body), body.length);
88 // Kept compressed, the blob is inflated once to hash it and once, held, for all 20 deltas: not once for each.
89 const opts = { maxBytes: 5 * 1024 * 1024, largeObjectBytes: 1024 * 1024 };
90 expect((await parsePack(p, undefined, opts)).length).toBe(21);
91 await expect(parsePack(p, undefined, { ...opts, maxHeldBytes: 1024 * 1024 })).rejects.toThrow(/memory/);
92 });
93
94 it("checks a delta's result size before allocating it", () => {
95 const delta = Uint8Array.from([...varint(3), ...varint(2 ** 40), 0x91, 0, 3]);
96 expect(() => applyDelta(Uint8Array.from([1, 2, 3]), delta, 1024 * 1024)).toThrow(/larger than/);
97 });
98});