profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
zip.ts 3,317 bytes
1 const CRC_TABLE = buildCrcTable()
2
3 function buildCrcTable(): Uint32Array {
4 const table = new Uint32Array(256)
5 for (let n = 0; n < 256; n++) {
6 let c = n
7 for (let k = 0; k < 8; k++) {
8 c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
9 }
10 table[n] = c >>> 0
11 }
12 return table
13 }
14
15 function crc32(buf: Buffer): number {
16 let crc = 0xffffffff
17 for (const byte of buf) {
18 crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
19 }
20 return (crc ^ 0xffffffff) >>> 0
21 }
22
23 function dosDateTime(date: Date): { time: number; date: number } {
24 const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2)
25 const dosDate = ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate()
26 return { time, date: dosDate }
27 }
28
29 export interface ZipEntryInput {
30 name: string
31 content: Buffer
32 }
33
34 /**
35 * Minimal ZIP writer (STORE method, no compression) so the project doesn't need a
36 * zip dependency for one contained feature. See spec/PLAN.md "Decisions already made".
37 */
38 export function createZip(entries: ZipEntryInput[], now: Date = new Date()): Buffer {
39 const { time, date } = dosDateTime(now)
40 const localParts: Buffer[] = []
41 const centralParts: Buffer[] = []
42 let offset = 0
43
44 for (const entry of entries) {
45 const nameBuf = Buffer.from(entry.name, 'utf8')
46 const crc = crc32(entry.content)
47 const size = entry.content.length
48
49 const localHeader = Buffer.alloc(30)
50 localHeader.writeUInt32LE(0x04034b50, 0)
51 localHeader.writeUInt16LE(20, 4)
52 localHeader.writeUInt16LE(0, 6)
53 localHeader.writeUInt16LE(0, 8)
54 localHeader.writeUInt16LE(time, 10)
55 localHeader.writeUInt16LE(date, 12)
56 localHeader.writeUInt32LE(crc, 14)
57 localHeader.writeUInt32LE(size, 18)
58 localHeader.writeUInt32LE(size, 22)
59 localHeader.writeUInt16LE(nameBuf.length, 26)
60 localHeader.writeUInt16LE(0, 28)
61 localParts.push(localHeader, nameBuf, entry.content)
62
63 const centralHeader = Buffer.alloc(46)
64 centralHeader.writeUInt32LE(0x02014b50, 0)
65 centralHeader.writeUInt16LE(20, 4)
66 centralHeader.writeUInt16LE(20, 6)
67 centralHeader.writeUInt16LE(0, 8)
68 centralHeader.writeUInt16LE(0, 10)
69 centralHeader.writeUInt16LE(time, 12)
70 centralHeader.writeUInt16LE(date, 14)
71 centralHeader.writeUInt32LE(crc, 16)
72 centralHeader.writeUInt32LE(size, 20)
73 centralHeader.writeUInt32LE(size, 24)
74 centralHeader.writeUInt16LE(nameBuf.length, 28)
75 centralHeader.writeUInt16LE(0, 30)
76 centralHeader.writeUInt16LE(0, 32)
77 centralHeader.writeUInt16LE(0, 34)
78 centralHeader.writeUInt16LE(0, 36)
79 centralHeader.writeUInt32LE(0, 38)
80 centralHeader.writeUInt32LE(offset, 42)
81 centralParts.push(centralHeader, nameBuf)
82
83 offset += localHeader.length + nameBuf.length + entry.content.length
84 }
85
86 const centralDirOffset = offset
87 const centralDirSize = centralParts.reduce((sum, buf) => sum + buf.length, 0)
88
89 const eocd = Buffer.alloc(22)
90 eocd.writeUInt32LE(0x06054b50, 0)
91 eocd.writeUInt16LE(0, 4)
92 eocd.writeUInt16LE(0, 6)
93 eocd.writeUInt16LE(entries.length, 8)
94 eocd.writeUInt16LE(entries.length, 10)
95 eocd.writeUInt32LE(centralDirSize, 12)
96 eocd.writeUInt32LE(centralDirOffset, 16)
97 eocd.writeUInt16LE(0, 20)
98
99 return Buffer.concat([...localParts, ...centralParts, eocd])
100 }
101