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

97 lines3,399 bytesCodeBlame
1/**
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;
35 // Every file deflated at once: the streams interleave rather than queue.
36 const deflatedAll = await Promise.all(files.map((file) => (file.data.length > 0 ? deflate(file.data) : file.data)));
37 for (const [index, file] of files.entries()) {
38 const name = encoder.encode(file.path);
39 const crc = crc32(file.data);
40 const deflated = deflatedAll[index]!;
41 const [method, body] = deflated.length < file.data.length ? [8, deflated] : [0, file.data];
42 const header = (central: boolean) => {
43 const out = new Uint8Array((central ? 46 : 30) + name.length);
44 const view = new DataView(out.buffer);
45 let at = 0;
46 const u16 = (v: number) => {
47 view.setUint16(at, v, true);
48 at += 2;
49 };
50 const u32 = (v: number) => {
51 view.setUint32(at, v >>> 0, true);
52 at += 4;
53 };
54 u32(central ? 0x02014b50 : 0x04034b50);
55 if (central) u16(0x031e); // made by: Unix, 3.0
56 u16(20); // version needed
57 u16(0x0800); // UTF-8 names
58 u16(method);
59 u16(0); // time
60 u16(0x0021); // date: 1980-01-01
61 u32(crc);
62 u32(body.length);
63 u32(file.data.length);
64 u16(name.length);
65 u16(0); // extra
66 if (central) {
67 u16(0); // comment
68 u16(0); // disk
69 u16(0); // internal attributes
70 u32(0o100644 << 16);
71 u32(offset);
72 }
73 out.set(name, at);
74 return out;
75 };
76 const local = header(false);
77 parts.push(local, body);
78 central.push(header(true));
79 offset += local.length + body.length;
80 }
81 const centralSize = central.reduce((sum, part) => sum + part.length, 0);
82 const end = new Uint8Array(22);
83 const view = new DataView(end.buffer);
84 view.setUint32(0, 0x06054b50, true);
85 view.setUint16(8, files.length, true);
86 view.setUint16(10, files.length, true);
87 view.setUint32(12, centralSize, true);
88 view.setUint32(16, offset, true);
89 const all = [...parts, ...central, end];
90 const out = new Uint8Array(all.reduce((sum, part) => sum + part.length, 0));
91 let at = 0;
92 for (const part of all) {
93 out.set(part, at);
94 at += part.length;
95 }
96 return out;
97}