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;
}