| 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 | |
| 8 | const 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 | |
| 18 | export 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 | |
| 24 | async 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. */ |
| 30 | export 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 | // 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)); |
| 46 | for (const [index, file] of files.entries()) { |
| 47 | const name = encoder.encode(file.path); |
| 48 | const crc = crc32(file.data); |
| 49 | const deflated = deflatedAll[index]!; |
| 50 | 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 | } |