Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
279918da1This 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])
}