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.

146 lines · 6.7 KBRaw History
1import { inflateSync } from "node:zlib";
2import pako from "pako";
3
4// Reading an object up to NATIVE_MAX inflates it with the runtime's own zlib (node:zlib, native in
5// Workers with nodejs_compat): about four times faster than pako on the small trees and commits that
6// every page, diff and clone reads. Larger objects, pack entries (whose length is only known once
7// inflated) and all compression stay on pako: compressed sizes are measured ahead of storing
8// (pack-stream.ts), so what deflate and deflateChunks write must stay the same bytes.
9const NATIVE_MAX = 1024 * 1024;
10
11/** zlib-deflate (RFC 1950), the format git uses for loose objects and pack entries. Byte for byte what deflateChunks writes. */
12export function deflate(data: Uint8Array): Uint8Array {
13 return pako.deflate(data);
14}
15
16/**
17 * The same zlib data as `deflate`, piece by piece as it is produced, so compressing a large object
18 * holds it and a few small chunks, not a second copy of it compressed. Each chunk is new.
19 */
20export function* deflateChunks(data: Uint8Array): Generator<Uint8Array> {
21 const d = new pako.Deflate();
22 let out: Uint8Array[] = [];
23 d.onData = (chunk) => void out.push(chunk as Uint8Array);
24 const step = 256 * 1024;
25 for (let at = 0; ; at += step) {
26 const last = at + step >= data.length;
27 d.push(data.subarray(at, Math.min(data.length, at + step)), last);
28 if (d.err) throw new Error(`zlib error: ${d.msg}`);
29 yield* out;
30 out = [];
31 if (last) return;
32 }
33}
34
35// zlib only: pako also takes gzip unless `windowBits` is given, and git can't read gzip.
36const ZLIB = { windowBits: 15 };
37
38export function inflate(data: Uint8Array): Uint8Array {
39 return pako.inflate(data, ZLIB);
40}
41
42/**
43 * Inflate zlib data known to hold exactly `size` bytes. Past NATIVE_MAX it inflates into one buffer of
44 * that size, no chunks joined at the end, so a large object never needs twice its memory.
45 */
46export function inflateSized(data: Uint8Array, size: number): Uint8Array {
47 if (size > NATIVE_MAX) return inflateSizedPako(data, size);
48 let out: Uint8Array;
49 try {
50 out = inflateSync(data, { maxOutputLength: nativeLimit(size) });
51 } catch (e) {
52 // Past the cap: pako, which stops at exactly `size`, tells an object that is too large apart from the runtime's rounding.
53 if (e instanceof RangeError) return inflateSizedPako(data, size);
54 throw new Error(`zlib error: ${(e as Error).message}`);
55 }
56 if (out.length > size) throw new Error(`object is larger than its recorded size (${size} bytes)`);
57 if (out.length !== size) throw new Error(`object inflated to ${out.length} bytes, not its recorded ${size}`);
58 return new Uint8Array(out.buffer, out.byteOffset, out.byteLength);
59}
60
61/**
62 * The output cap for inflating `size` bytes natively. In Workers, node:zlib checks maxOutputLength
63 * against the buffer it grows while inflating, not the bytes it wrote, so a cap of `size + 1` refused
64 * many objects between 32 KiB and 200 KiB that fit exactly (Node's own zlib doesn't). Twice the size
65 * plus 64 KiB leaves room for that growth and still bounds what a corrupt object can take.
66 */
67export function nativeLimit(size: number): number {
68 return 2 * size + 64 * 1024;
69}
70
71function inflateSizedPako(data: Uint8Array, size: number): Uint8Array {
72 const inflator = new pako.Inflate(ZLIB) as pako.Inflate & { ended: boolean };
73 const out = new Uint8Array(size);
74 let produced = 0;
75 inflator.onData = (chunk) => {
76 const c = chunk as Uint8Array;
77 if (produced + c.length > size) throw new Error(`object is larger than its recorded size (${size} bytes)`);
78 out.set(c, produced);
79 produced += c.length;
80 };
81 inflator.push(data, true);
82 if (inflator.err) throw new Error(`zlib error: ${inflator.msg}`);
83 if (!inflator.ended || produced !== size) throw new Error(`object inflated to ${produced} bytes, not its recorded ${size}`);
84 return out;
85}
86
87/**
88 * Inflate one zlib stream that starts at `offset` inside `data`, and report how
89 * many input bytes it consumed. Pack files concatenate zlib streams without
90 * lengths, so the consumed count is how we find the next entry.
91 */
92export function inflateAt(data: Uint8Array, offset: number, expectedSize?: number): { out: Uint8Array; consumed: number } {
93 const inflator = new pako.Inflate(ZLIB) as pako.Inflate & { ended: boolean };
94 if (expectedSize !== undefined) {
95 // Stop as soon as the output passes the size the pack declared, so a small zlib stream
96 // can't be made to inflate to gigabytes.
97 const keep = inflator.onData.bind(inflator);
98 let produced = 0;
99 inflator.onData = (chunk) => {
100 produced += (chunk as Uint8Array).length;
101 if (produced > expectedSize) throw new Error(`object at ${offset} is larger than its declared size (${expectedSize} bytes)`);
102 keep(chunk);
103 };
104 }
105 // Feed in bounded windows so a huge trailing buffer is not copied repeatedly.
106 const window = Math.max(64 * 1024, expectedSize ? expectedSize + 1024 : 0);
107 let pos = offset;
108 while (!inflator.ended && pos < data.length) {
109 const end = Math.min(data.length, pos + window);
110 inflator.push(data.subarray(pos, end), false);
111 if (inflator.err) throw new Error(`zlib error at ${offset}: ${inflator.msg}`);
112 pos = end;
113 }
114 if (!inflator.ended) throw new Error(`truncated zlib stream at offset ${offset}`);
115 const strm = (inflator as unknown as { strm: { avail_in: number } }).strm;
116 const consumed = pos - offset - strm.avail_in;
117 const out = inflator.result as Uint8Array;
118 return { out: out ?? new Uint8Array(0), consumed };
119}
120
121/**
122 * Like `inflateAt`, but hands each inflated chunk to `onChunk` instead of keeping it, so an object
123 * much larger than memory allows can be hashed or scanned. Returns the input bytes consumed and the
124 * bytes produced.
125 */
126export function inflateEach(data: Uint8Array, offset: number, onChunk: (chunk: Uint8Array) => void, expectedSize?: number): { consumed: number; produced: number } {
127 const inflator = new pako.Inflate({ ...ZLIB, chunkSize: 256 * 1024 }) as pako.Inflate & { ended: boolean };
128 let produced = 0;
129 inflator.onData = (chunk) => {
130 const c = chunk as Uint8Array;
131 produced += c.length;
132 if (expectedSize !== undefined && produced > expectedSize) throw new Error(`object at ${offset} is larger than its declared size (${expectedSize} bytes)`);
133 onChunk(c);
134 };
135 const window = 1024 * 1024;
136 let pos = offset;
137 while (!inflator.ended && pos < data.length) {
138 const end = Math.min(data.length, pos + window);
139 inflator.push(data.subarray(pos, end), false);
140 if (inflator.err) throw new Error(`zlib error at ${offset}: ${inflator.msg}`);
141 pos = end;
142 }
143 if (!inflator.ended) throw new Error(`truncated zlib stream at offset ${offset}`);
144 const strm = (inflator as unknown as { strm: { avail_in: number } }).strm;
145 return { consumed: pos - offset - strm.avail_in, produced };
146}