Own it · example handover · app/
src/xlsx/zip.ts
← All files · the demo this builds
/**
* A minimal ZIP writer — just enough container for one .xlsx, with zero
* dependencies. Entries are DEFLATEd through the browser's native
* CompressionStream when available and stored uncompressed otherwise;
* both are valid ZIP, both open everywhere.
*
* Why hand-rolled: the whole download weighs a few hundred lines of code
* instead of a spreadsheet library that would be half this demo's byte
* budget — and a portfolio piece should survive being read.
*/
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(data: Uint8Array): number {
let c = 0xffffffff;
for (let i = 0; i < data.length; i++) c = CRC_TABLE[(c ^ data[i]!) & 0xff]! ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
async function deflateRaw(data: Uint8Array): Promise<Uint8Array | null> {
if (typeof CompressionStream === "undefined") return null;
const stream = new Blob([data as BlobPart]).stream().pipeThrough(new CompressionStream("deflate-raw"));
return new Uint8Array(await new Response(stream).arrayBuffer());
}
export interface ZipEntry {
name: string;
data: Uint8Array;
}
/** DOS date/time pair from a real date (zip's 1980s timestamp format). */
function dosDateTime(d: Date): { time: number; date: number } {
return {
time: (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1),
date: (Math.max(0, d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
};
}
export async function makeZip(entries: ZipEntry[], stamp: Date): Promise<Uint8Array> {
const enc = new TextEncoder();
const { time, date } = dosDateTime(stamp);
const chunks: Uint8Array[] = [];
const central: Uint8Array[] = [];
let offset = 0;
const u16 = (v: number) => [v & 0xff, (v >>> 8) & 0xff];
const u32 = (v: number) => [v & 0xff, (v >>> 8) & 0xff, (v >>> 16) & 0xff, (v >>> 24) & 0xff];
for (const e of entries) {
const name = enc.encode(e.name);
const crc = crc32(e.data);
const deflated = await deflateRaw(e.data);
const useDeflate = deflated !== null && deflated.length < e.data.length;
const payload = useDeflate ? deflated : e.data;
const method = useDeflate ? 8 : 0;
const local = new Uint8Array([
...u32(0x04034b50), ...u16(20), ...u16(0x0800 /* UTF-8 names */), ...u16(method),
...u16(time), ...u16(date), ...u32(crc), ...u32(payload.length), ...u32(e.data.length),
...u16(name.length), ...u16(0),
]);
chunks.push(local, name, payload);
central.push(
new Uint8Array([
...u32(0x02014b50), ...u16(20), ...u16(20), ...u16(0x0800), ...u16(method),
...u16(time), ...u16(date), ...u32(crc), ...u32(payload.length), ...u32(e.data.length),
...u16(name.length), ...u16(0), ...u16(0), ...u16(0), ...u16(0), ...u32(0), ...u32(offset),
]),
name,
);
offset += local.length + name.length + payload.length;
}
const centralSize = central.reduce((s, c) => s + c.length, 0);
const eocd = new Uint8Array([
...u32(0x06054b50), ...u16(0), ...u16(0), ...u16(entries.length), ...u16(entries.length),
...u32(centralSize), ...u32(offset), ...u16(0),
]);
const total = offset + centralSize + eocd.length;
const out = new Uint8Array(total);
let pos = 0;
for (const c of [...chunks, ...central, eocd]) {
out.set(c, pos);
pos += c.length;
}
return out;
}