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 | |