// Minimal, dependency-free ZIP (STORE, no compression) for serving the // browser extension as a single downloadable file. The files are tiny, so // skipping DEFLATE keeps the code small and needs no native tooling in the // container (the slim runtime image ships no `zip` binary). // // Returns a Buffer containing a valid .zip whose entries sit at the archive // root (flat — matching "load unpacked extension" on an extracted folder). const fs = require('fs'); const path = require('path'); 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; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } // DOS time/date for a fixed deterministic stamp (1980-01-01 00:00:00). // We avoid per-build timestamps so the archive is byte-identical for a given // source state (cache-friendly, reproducible). const DOS_TIME = 0x0000; const DOS_DATE = 0x0021; // 1980-01-01 function u16(n) { return Buffer.from([n & 0xff, (n >>> 8) & 0xff]); } function u32(n) { return Buffer.from([n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff]); } // Files: array of { name, data: Buffer } (root-relative, no leading slash). function zipFiles(files) { const localParts = []; const centralParts = []; let offset = 0; for (const f of files) { const nameBuf = Buffer.from(f.name, 'utf8'); const crc = crc32(f.data); const size = f.data.length; const local = Buffer.concat([ u32(0x04034b50), // local file header signature u16(20), // version needed to extract u16(0), // general purpose bit flag u16(0), // compression method (0 = store) u16(DOS_TIME), u16(DOS_DATE), u32(crc), u32(size), u32(size), u16(nameBuf.length), u16(0), nameBuf, f.data, ]); const central = Buffer.concat([ u32(0x02014b50), // central directory header signature u16(20), // version made by u16(20), // version needed to extract u16(0), u16(0), u16(DOS_TIME), u16(DOS_DATE), u32(crc), u32(size), u32(size), u16(nameBuf.length), u16(0), u16(0), // name len, extra len, comment len u16(0), // disk number start u16(0), // internal attributes u32(0), // external attributes u32(offset), // local header offset nameBuf, ]); localParts.push(local); centralParts.push(central); offset += local.length; } const centralBuf = Buffer.concat(centralParts); const end = Buffer.concat([ u32(0x06054b50), // end of central directory signature u16(0), u16(0), // disk number, disk with CD u16(files.length), u16(files.length), // entries on this disk, total u32(centralBuf.length), u32(offset), u16(0), // comment length ]); return Buffer.concat([...localParts, centralBuf, end]); } // Read every file in a directory (non-recursive, flat) and return a zip Buffer. function zipDir(dir) { const names = fs.readdirSync(dir).sort(); const files = []; for (const name of names) { const full = path.join(dir, name); if (!fs.statSync(full).isFile()) continue; files.push({ name, data: fs.readFileSync(full) }); } return zipFiles(files); } module.exports = { zipDir, zipFiles, crc32 };