Loading files
8035964e1This file is pinned to the selected release. Download the original to open it in your design tool.
import { deflateRawSync } from 'node:zlib'
/**
* A ZIP file, written by hand.
*
* Node ships zlib and nothing that writes a container, and a log bundle has to be a `.zip` — the one
* archive a person can open by double-clicking on every desktop this app ships to. The format is small:
* a local header + deflated bytes per entry, a central directory that repeats the headers, and one
* end-of-directory record. No zip64, no encryption, no streaming: a bundle is a few dozen files under
* a hundred megabytes, built in memory.
*/
export interface ZipEntry {
/** Path inside the archive, `/`-separated. */
name: string
data: Buffer
/** Modification time for the entry. Default: now. */
mtime?: Date
}
const CRC_TABLE = (() => {
const t = new Uint32Array(256)
for (let n = 0; n < 256; n++) {
let c = n
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
t[n] = c >>> 0
}
return t
})()
export function crc32(buf: Buffer): number {
let c = 0xffffffff
for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8)
return (c ^ 0xffffffff) >>> 0
}
/** MS-DOS date and time, the only stamp the classic header carries. Local time, two-second resolution. */
function dosStamp(d: Date): { date: number; time: number } {
const year = Math.max(1980, d.getFullYear())
return {
date: ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
time: (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1),
}
}
export function writeZip(entries: ZipEntry[]): Buffer {
const locals: Buffer[] = []
const centrals: Buffer[] = []
let offset = 0
for (const entry of entries) {
const name = Buffer.from(entry.name, 'utf8')
const deflated = deflateRawSync(entry.data)
// Store rather than deflate when compression buys nothing (an already-compressed file).
const stored = deflated.length >= entry.data.length
const body = stored ? entry.data : deflated
const method = stored ? 0 : 8
const crc = crc32(entry.data)
const { date, time } = dosStamp(entry.mtime ?? new Date())
// General purpose bit 11: names are UTF-8.
const flags = 0x0800
const local = Buffer.alloc(30)
local.writeUInt32LE(0x04034b50, 0)
local.writeUInt16LE(20, 4) // version needed
local.writeUInt16LE(flags, 6)
local.writeUInt16LE(method, 8)
local.writeUInt16LE(time, 10)
local.writeUInt16LE(date, 12)
local.writeUInt32LE(crc, 14)
local.writeUInt32LE(body.length, 18)
local.writeUInt32LE(entry.data.length, 22)
local.writeUInt16LE(name.length, 26)
local.writeUInt16LE(0, 28) // extra length
locals.push(local, name, body)
const central = Buffer.alloc(46)
central.writeUInt32LE(0x02014b50, 0)
central.writeUInt16LE(20, 4) // version made by
central.writeUInt16LE(20, 6) // version needed
central.writeUInt16LE(flags, 8)
central.writeUInt16LE(method, 10)
central.writeUInt16LE(time, 12)
central.writeUInt16LE(date, 14)
central.writeUInt32LE(crc, 16)
central.writeUInt32LE(body.length, 20)
central.writeUInt32LE(entry.data.length, 24)
central.writeUInt16LE(name.length, 28)
central.writeUInt16LE(0, 30) // extra
central.writeUInt16LE(0, 32) // comment
central.writeUInt16LE(0, 34) // disk
central.writeUInt16LE(0, 36) // internal attrs
central.writeUInt32LE(0, 38) // external attrs
central.writeUInt32LE(offset, 42)
centrals.push(central, name)
offset += local.length + name.length + body.length
}
const directory = Buffer.concat(centrals)
const end = Buffer.alloc(22)
end.writeUInt32LE(0x06054b50, 0)
end.writeUInt16LE(0, 4)
end.writeUInt16LE(0, 6)
end.writeUInt16LE(entries.length, 8)
end.writeUInt16LE(entries.length, 10)
end.writeUInt32LE(directory.length, 12)
end.writeUInt32LE(offset, 16)
end.writeUInt16LE(0, 20)
return Buffer.concat([...locals, directory, end])
}