g1t/apps/web/app/lib/zip.ts

106 lines3,697 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths1/**
2 * A zip archive of files, made in memory: each file deflated when that makes
3 * it smaller, stored otherwise, with UTF-8 names. Enough for downloading a
4 * commit; no zip64, so the archive and each file stay under 4 GB (the caller
5 * caps them far below that).
6 */
7
8const CRC_TABLE = (() => {
9 const table = new Uint32Array(256);
10 for (let n = 0; n < 256; n++) {
11 let c = n;
12 for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
13 table[n] = c >>> 0;
14 }
15 return table;
16})();
17
18export function crc32(data: Uint8Array): number {
19 let crc = 0xffffffff;
20 for (let i = 0; i < data.length; i++) crc = CRC_TABLE[(crc ^ data[i]!) & 0xff]! ^ (crc >>> 8);
21 return (crc ^ 0xffffffff) >>> 0;
22}
23
24async function deflate(data: Uint8Array): Promise<Uint8Array> {
25 const stream = new Blob([data as Uint8Array<ArrayBuffer>]).stream().pipeThrough(new CompressionStream("deflate-raw"));
26 return new Uint8Array(await new Response(stream).arrayBuffer());
27}
28
29/** The archive of `files`, in the order given. */
30export async function zip(files: { path: string; data: Uint8Array }[]): Promise<Uint8Array<ArrayBuffer>> {
31 const encoder = new TextEncoder();
32 const parts: Uint8Array[] = [];
33 const central: Uint8Array[] = [];
34 let offset = 0;
Download ZIP deflates sixteen files at a time, within a Worker's memory35 // Sixteen files deflating at a time: quicker than one by one, without
36 // every file's stream held at once.
37 const deflatedAll: Uint8Array[] = new Array(files.length);
38 let next = 0;
39 const worker = async () => {
40 for (let index = next++; index < files.length; index = next++) {
41 const data = files[index]!.data;
42 deflatedAll[index] = data.length > 0 ? await deflate(data) : data;
43 }
44 };
45 await Promise.all(Array.from({ length: Math.min(16, files.length) }, worker));
Download ZIP reads blobs eight batches at a time and deflates files together46 for (const [index, file] of files.entries()) {
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths47 const name = encoder.encode(file.path);
48 const crc = crc32(file.data);
Download ZIP reads blobs eight batches at a time and deflates files together49 const deflated = deflatedAll[index]!;
Download ZIP from the Code button; a slimmer lifecycle panel; agent steps say what was done, not sandbox paths50 const [method, body] = deflated.length < file.data.length ? [8, deflated] : [0, file.data];
51 const header = (central: boolean) => {
52 const out = new Uint8Array((central ? 46 : 30) + name.length);
53 const view = new DataView(out.buffer);
54 let at = 0;
55 const u16 = (v: number) => {
56 view.setUint16(at, v, true);
57 at += 2;
58 };
59 const u32 = (v: number) => {
60 view.setUint32(at, v >>> 0, true);
61 at += 4;
62 };
63 u32(central ? 0x02014b50 : 0x04034b50);
64 if (central) u16(0x031e); // made by: Unix, 3.0
65 u16(20); // version needed
66 u16(0x0800); // UTF-8 names
67 u16(method);
68 u16(0); // time
69 u16(0x0021); // date: 1980-01-01
70 u32(crc);
71 u32(body.length);
72 u32(file.data.length);
73 u16(name.length);
74 u16(0); // extra
75 if (central) {
76 u16(0); // comment
77 u16(0); // disk
78 u16(0); // internal attributes
79 u32(0o100644 << 16);
80 u32(offset);
81 }
82 out.set(name, at);
83 return out;
84 };
85 const local = header(false);
86 parts.push(local, body);
87 central.push(header(true));
88 offset += local.length + body.length;
89 }
90 const centralSize = central.reduce((sum, part) => sum + part.length, 0);
91 const end = new Uint8Array(22);
92 const view = new DataView(end.buffer);
93 view.setUint32(0, 0x06054b50, true);
94 view.setUint16(8, files.length, true);
95 view.setUint16(10, files.length, true);
96 view.setUint32(12, centralSize, true);
97 view.setUint32(16, offset, true);
98 const all = [...parts, ...central, end];
99 const out = new Uint8Array(all.reduce((sum, part) => sum + part.length, 0));
100 let at = 0;
101 for (const part of all) {
102 out.set(part, at);
103 at += part.length;
104 }
105 return out;
106}