// build-zip.mjs — Minimal synthetic ZIP builder for tests. // Supports STORE method only. Lets tests construct adversarial archives that // real zip tools refuse to emit (zip-slip names, symlink mode bits, oversized // uncompressed sizes for bomb tests). import { crc32 } from 'node:zlib'; const SIG_LFH = 0x04034b50; const SIG_CD = 0x02014b50; const SIG_EOCD = 0x06054b50; function crc(buf) { return crc32(buf) >>> 0; } /** * Build a ZIP buffer from a list of entries. * @param {Array<{ name: string, data: Buffer|string, externalAttr?: number, versionMadeBy?: number, declaredUncompSize?: number, declaredCompSize?: number }>} entries * @returns {Buffer} */ export function buildZip(entries) { const lfhParts = []; const cdParts = []; let offset = 0; for (const entry of entries) { const nameBuf = Buffer.from(entry.name, 'utf8'); const data = Buffer.isBuffer(entry.data) ? entry.data : Buffer.from(entry.data || '', 'utf8'); const compSize = entry.declaredCompSize ?? data.length; const uncompSize = entry.declaredUncompSize ?? data.length; const c = crc(data); // Local file header (30 bytes) const lfh = Buffer.alloc(30); lfh.writeUInt32LE(SIG_LFH, 0); lfh.writeUInt16LE(20, 4); // version needed lfh.writeUInt16LE(0, 6); // flags lfh.writeUInt16LE(0, 8); // method = STORE lfh.writeUInt16LE(0, 10); // time lfh.writeUInt16LE(0, 12); // date lfh.writeUInt32LE(c, 14); // crc32 lfh.writeUInt32LE(compSize, 18); // compressed size lfh.writeUInt32LE(uncompSize, 22); // uncompressed size lfh.writeUInt16LE(nameBuf.length, 26); lfh.writeUInt16LE(0, 28); // extra len lfhParts.push(lfh, nameBuf, data); const thisLfhOffset = offset; offset += lfh.length + nameBuf.length + data.length; // Central directory header (46 bytes) const cd = Buffer.alloc(46); cd.writeUInt32LE(SIG_CD, 0); cd.writeUInt16LE(entry.versionMadeBy ?? (3 << 8) | 20, 4); // OS=Unix(3), version=20 cd.writeUInt16LE(20, 6); cd.writeUInt16LE(0, 8); cd.writeUInt16LE(0, 10); cd.writeUInt16LE(0, 12); cd.writeUInt16LE(0, 14); cd.writeUInt32LE(c, 16); cd.writeUInt32LE(compSize, 20); cd.writeUInt32LE(uncompSize, 24); cd.writeUInt16LE(nameBuf.length, 28); cd.writeUInt16LE(0, 30); cd.writeUInt16LE(0, 32); // comment len cd.writeUInt16LE(0, 34); // disk start cd.writeUInt16LE(0, 36); // internal attrs cd.writeUInt32LE((entry.externalAttr ?? 0) >>> 0, 38); // external attrs (unsigned) cd.writeUInt32LE(thisLfhOffset, 42); cdParts.push(cd, nameBuf); } const lfhSection = Buffer.concat(lfhParts); const cdSection = Buffer.concat(cdParts); const cdOffset = lfhSection.length; const cdSize = cdSection.length; const eocd = Buffer.alloc(22); eocd.writeUInt32LE(SIG_EOCD, 0); eocd.writeUInt16LE(0, 4); eocd.writeUInt16LE(0, 6); eocd.writeUInt16LE(entries.length, 8); eocd.writeUInt16LE(entries.length, 10); eocd.writeUInt32LE(cdSize, 12); eocd.writeUInt32LE(cdOffset, 16); eocd.writeUInt16LE(0, 20); return Buffer.concat([lfhSection, cdSection, eocd]); } /** Convenience: produce a unix mode in the upper 16 bits of externalAttr. */ export function unixModeAttr(mode) { return (mode & 0xFFFF) << 16; } export const MODE_SYMLINK = 0xA1FF; // S_IFLNK | rwxrwxrwx