127 lines
4.1 KiB
JavaScript
127 lines
4.1 KiB
JavaScript
// zip-writer.mjs — Minimal stored-method (no compression) ZIP writer.
|
|
// Zero dependencies. Deterministic output: fixed DOS timestamp, sorted entry order.
|
|
//
|
|
// Writes a valid ZIP that zip-extract.mjs can parse. Uses method=0 (STORE),
|
|
// CRC-32 computed, no encryption, no ZIP64. Suitable for tiny test fixtures.
|
|
|
|
import { createHash } from 'node:crypto';
|
|
|
|
// CRC-32 table (IEEE 802.3 polynomial).
|
|
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)) >>> 0;
|
|
}
|
|
return (c ^ 0xFFFFFFFF) >>> 0;
|
|
}
|
|
|
|
const DOS_DATE = ((1980 - 1980) << 9) | (1 << 5) | 1; // 1980-01-01
|
|
const DOS_TIME = 0; // 00:00:00
|
|
|
|
/**
|
|
* Build a ZIP buffer from a list of entries.
|
|
*
|
|
* @param {Array<{name: string, data: Buffer | string}>} entries
|
|
* @returns {Buffer}
|
|
*/
|
|
export function createZip(entries) {
|
|
// Normalize + sort for determinism
|
|
const normalized = entries
|
|
.map(e => ({
|
|
name: e.name,
|
|
data: Buffer.isBuffer(e.data) ? e.data : Buffer.from(e.data, 'utf8'),
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
const chunks = [];
|
|
const centralHeaders = [];
|
|
let offset = 0;
|
|
|
|
for (const e of normalized) {
|
|
const nameBuf = Buffer.from(e.name, 'utf8');
|
|
const c = crc32(e.data);
|
|
const sz = e.data.length;
|
|
|
|
// Local File Header (30 + nameLen)
|
|
const lfh = Buffer.alloc(30);
|
|
lfh.writeUInt32LE(0x04034b50, 0); // signature
|
|
lfh.writeUInt16LE(20, 4); // version needed
|
|
lfh.writeUInt16LE(0, 6); // flags
|
|
lfh.writeUInt16LE(0, 8); // method = STORE
|
|
lfh.writeUInt16LE(DOS_TIME, 10);
|
|
lfh.writeUInt16LE(DOS_DATE, 12);
|
|
lfh.writeUInt32LE(c, 14); // crc32
|
|
lfh.writeUInt32LE(sz, 18); // compressed size
|
|
lfh.writeUInt32LE(sz, 22); // uncompressed size
|
|
lfh.writeUInt16LE(nameBuf.length, 26);
|
|
lfh.writeUInt16LE(0, 28); // extra field length
|
|
chunks.push(lfh);
|
|
chunks.push(nameBuf);
|
|
chunks.push(e.data);
|
|
|
|
const localOffset = offset;
|
|
offset += 30 + nameBuf.length + sz;
|
|
|
|
// Central Directory Header (46 + nameLen)
|
|
const cdh = Buffer.alloc(46);
|
|
cdh.writeUInt32LE(0x02014b50, 0); // signature
|
|
cdh.writeUInt16LE(20, 4); // version made by
|
|
cdh.writeUInt16LE(20, 6); // version needed
|
|
cdh.writeUInt16LE(0, 8); // flags
|
|
cdh.writeUInt16LE(0, 10); // method
|
|
cdh.writeUInt16LE(DOS_TIME, 12);
|
|
cdh.writeUInt16LE(DOS_DATE, 14);
|
|
cdh.writeUInt32LE(c, 16);
|
|
cdh.writeUInt32LE(sz, 20);
|
|
cdh.writeUInt32LE(sz, 24);
|
|
cdh.writeUInt16LE(nameBuf.length, 28);
|
|
cdh.writeUInt16LE(0, 30); // extra
|
|
cdh.writeUInt16LE(0, 32); // comment
|
|
cdh.writeUInt16LE(0, 34); // disk
|
|
cdh.writeUInt16LE(0, 36); // internal attrs
|
|
cdh.writeUInt32LE(0, 38); // external attrs
|
|
cdh.writeUInt32LE(localOffset, 42);
|
|
centralHeaders.push({ cdh, nameBuf });
|
|
}
|
|
|
|
const centralStart = offset;
|
|
for (const { cdh, nameBuf } of centralHeaders) {
|
|
chunks.push(cdh);
|
|
chunks.push(nameBuf);
|
|
offset += cdh.length + nameBuf.length;
|
|
}
|
|
const centralSize = offset - centralStart;
|
|
|
|
// End of Central Directory
|
|
const eocd = Buffer.alloc(22);
|
|
eocd.writeUInt32LE(0x06054b50, 0);
|
|
eocd.writeUInt16LE(0, 4);
|
|
eocd.writeUInt16LE(0, 6);
|
|
eocd.writeUInt16LE(normalized.length, 8);
|
|
eocd.writeUInt16LE(normalized.length, 10);
|
|
eocd.writeUInt32LE(centralSize, 12);
|
|
eocd.writeUInt32LE(centralStart, 16);
|
|
eocd.writeUInt16LE(0, 20); // comment length
|
|
chunks.push(eocd);
|
|
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
/**
|
|
* Compute SHA-256 of a buffer (hex).
|
|
*/
|
|
export function sha256Hex(buf) {
|
|
return createHash('sha256').update(buf).digest('hex');
|
|
}
|