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

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