Spaces:
Sleeping
Sleeping
| // A minimal ZIP writer. | |
| // | |
| // Deliberately dependency-free: node's zlib already does deflate, and the | |
| // container around it is a few dozen lines of little-endian headers. Adding an | |
| // archiver dependency would mean regenerating package-lock.json for the Space's | |
| // `npm ci` (which hard-fails on any disagreement) — not worth it for this. | |
| // | |
| // Writes one local header + deflated body per entry, then the central directory. | |
| // No zip64: an export is markdown and screenshots, orders of magnitude below the | |
| // 4GB/65535-entry ceilings. | |
| import zlib from 'node:zlib' | |
| const CRC_TABLE = (() => { | |
| const table = new Int32Array(256) | |
| for (let i = 0; i < 256; i++) { | |
| let c = i | |
| for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 | |
| table[i] = c | |
| } | |
| return table | |
| })() | |
| function crc32(buf) { | |
| let c = -1 | |
| for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8) | |
| return (c ^ -1) >>> 0 | |
| } | |
| // DOS time/date: seconds have 2-second resolution, years count from 1980 | |
| function dosTime(date) { | |
| const time = ((date.getHours() & 0x1f) << 11) | ((date.getMinutes() & 0x3f) << 5) | ((date.getSeconds() / 2) & 0x1f) | |
| const day = (((date.getFullYear() - 1980) & 0x7f) << 9) | (((date.getMonth() + 1) & 0xf) << 5) | (date.getDate() & 0x1f) | |
| return { time, day } | |
| } | |
| // entries: [{ name, data: Buffer|string }] — name uses forward slashes, a | |
| // trailing slash marks a directory entry (stored empty). | |
| export function makeZip(entries, { mtime = new Date() } = {}) { | |
| const { time, day } = dosTime(mtime) | |
| const chunks = [] | |
| const central = [] | |
| let offset = 0 | |
| for (const entry of entries) { | |
| const name = Buffer.from(entry.name, 'utf8') | |
| const isDir = entry.name.endsWith('/') | |
| const raw = isDir ? Buffer.alloc(0) : Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(String(entry.data ?? ''), 'utf8') | |
| // store directories and empty files; deflate everything else | |
| const deflated = isDir || !raw.length ? raw : zlib.deflateRawSync(raw, { level: 9 }) | |
| // a "compressed" body larger than the source happens with incompressible | |
| // input (already-encoded PNGs): store it instead, which is always legal | |
| const stored = deflated.length >= raw.length | |
| const body = stored ? raw : deflated | |
| const method = stored ? 0 : 8 | |
| const crc = crc32(raw) | |
| const local = Buffer.alloc(30) | |
| local.writeUInt32LE(0x04034b50, 0) | |
| local.writeUInt16LE(20, 4) // version needed | |
| local.writeUInt16LE(0, 6) // flags | |
| local.writeUInt16LE(method, 8) | |
| local.writeUInt16LE(time, 10) | |
| local.writeUInt16LE(day, 12) | |
| local.writeUInt32LE(crc, 14) | |
| local.writeUInt32LE(body.length, 18) | |
| local.writeUInt32LE(raw.length, 22) | |
| local.writeUInt16LE(name.length, 26) | |
| local.writeUInt16LE(0, 28) // extra length | |
| chunks.push(local, name, body) | |
| const dir = Buffer.alloc(46) | |
| dir.writeUInt32LE(0x02014b50, 0) | |
| dir.writeUInt16LE(20, 4) // made by | |
| dir.writeUInt16LE(20, 6) // version needed | |
| dir.writeUInt16LE(0, 8) | |
| dir.writeUInt16LE(method, 10) | |
| dir.writeUInt16LE(time, 12) | |
| dir.writeUInt16LE(day, 14) | |
| dir.writeUInt32LE(crc, 16) | |
| dir.writeUInt32LE(body.length, 20) | |
| dir.writeUInt32LE(raw.length, 24) | |
| dir.writeUInt16LE(name.length, 28) | |
| dir.writeUInt16LE(0, 30) // extra | |
| dir.writeUInt16LE(0, 32) // comment | |
| dir.writeUInt16LE(0, 34) // disk | |
| dir.writeUInt16LE(0, 36) // internal attrs | |
| dir.writeUInt32LE(isDir ? 0x41ed0010 : 0x81a40000, 38) // unix mode + dir flag | |
| dir.writeUInt32LE(offset, 42) | |
| central.push(dir, name) | |
| offset += local.length + name.length + body.length | |
| } | |
| const centralBuf = Buffer.concat(central) | |
| const end = Buffer.alloc(22) | |
| end.writeUInt32LE(0x06054b50, 0) | |
| end.writeUInt16LE(0, 4) // this disk | |
| end.writeUInt16LE(0, 6) // disk with central dir | |
| end.writeUInt16LE(entries.length, 8) | |
| end.writeUInt16LE(entries.length, 10) | |
| end.writeUInt32LE(centralBuf.length, 12) | |
| end.writeUInt32LE(offset, 16) | |
| end.writeUInt16LE(0, 20) // comment length | |
| return Buffer.concat([...chunks, centralBuf, end]) | |
| } | |