// The ZIP writer (shared/zip.ts): the format's fixed fields, the CRC, the // data descriptor, the UTF-8 flag, and the streaming contract. // // The reader below is deliberately tiny and deliberately independent of the // writer: it walks the central directory the way `unzip` does (end record → // directory → local headers), so an archive that satisfies it is one the // real tools open. A test that asserted the writer against its own // constants would prove only that the constants were typed twice. import assert from "node:assert/strict"; import { describe, it } from "node:test"; import { crc32, dosDateTime, ZipWriter, zipSync } from "../shared/zip.ts"; const enc = new TextEncoder(); const dec = new TextDecoder(); interface Entry { name: string; crc: number; size: number; flags: number; method: number; offset: number; data: Uint8Array; } /** A minimal central-directory reader: enough to prove the archive is * well-formed and to hand back each entry's bytes. */ function readZip(bytes: Uint8Array): Entry[] { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); // The end record is the last 22 bytes when there is no comment. const endAt = bytes.length - 22; assert.equal(view.getUint32(endAt, true), 0x06054b50, "end-of-central-directory signature"); const count = view.getUint16(endAt + 10, true); const dirSize = view.getUint32(endAt + 12, true); const dirAt = view.getUint32(endAt + 16, true); assert.equal(dirAt + dirSize, endAt, "the directory runs up to the end record"); const entries: Entry[] = []; let at = dirAt; for (let i = 0; i < count; i++) { assert.equal(view.getUint32(at, true), 0x02014b50, "central header signature"); const flags = view.getUint16(at + 8, true); const method = view.getUint16(at + 10, true); const crc = view.getUint32(at + 16, true); const csize = view.getUint32(at + 20, true); const size = view.getUint32(at + 24, true); const nameLen = view.getUint16(at + 28, true); const extraLen = view.getUint16(at + 30, true); const commentLen = view.getUint16(at + 32, true); const offset = view.getUint32(at + 42, true); const name = dec.decode(bytes.subarray(at + 46, at + 46 + nameLen)); assert.equal(csize, size, "STORE: compressed size equals size"); // Now the local header the offset points at. assert.equal(view.getUint32(offset, true), 0x04034b50, "local header signature"); const localNameLen = view.getUint16(offset + 26, true); const localExtraLen = view.getUint16(offset + 28, true); assert.equal(dec.decode(bytes.subarray(offset + 30, offset + 30 + localNameLen)), name); const dataAt = offset + 30 + localNameLen + localExtraLen; const data = bytes.subarray(dataAt, dataAt + size); // With bit 3 set, the local header's CRC is zero and the descriptor after // the data carries the truth; the SIZE is written up front whenever the // writer knew it (a sequential reader needs it to find the entry's end) // and is zero only for a truly streamed entry. if (flags & (1 << 3)) { assert.equal(view.getUint32(offset + 14, true), 0, "local crc deferred"); const localSize = view.getUint32(offset + 18, true); assert.ok(localSize === 0 || localSize === size, "local size is the entry's or deferred"); const descAt = dataAt + size; assert.equal(view.getUint32(descAt, true), 0x08074b50, "data descriptor signature"); assert.equal(view.getUint32(descAt + 4, true), crc, "descriptor crc equals central crc"); assert.equal(view.getUint32(descAt + 8, true), size); assert.equal(view.getUint32(descAt + 12, true), size); } entries.push({ name, crc, size, flags, method, offset, data }); at += 46 + nameLen + extraLen + commentLen; } return entries; } describe("crc32", () => { it("matches the published check value", () => { // The CRC-32 of "123456789" is the constant every implementation is // checked against. assert.equal(crc32(enc.encode("123456789")), 0xcbf43926); assert.equal(crc32(new Uint8Array(0)), 0); }); it("is the same whether fed whole or in pieces", () => { const whole = enc.encode("The quick brown fox jumps over the lazy dog"); const a = crc32(whole); let running = 0; for (let i = 0; i < whole.length; i += 7) running = crc32(whole.subarray(i, i + 7), running); assert.equal(running, a); }); }); describe("dosDateTime", () => { it("keeps a modern mtime to the (even) second", () => { const { time, date } = dosDateTime(new Date(2026, 8, 13, 14, 30, 45).getTime()); assert.equal(date >> 9, 2026 - 1980); assert.equal((date >> 5) & 0xf, 9); assert.equal(date & 0x1f, 13); assert.equal(time >> 11, 14); assert.equal((time >> 5) & 0x3f, 30); assert.equal((time & 0x1f) * 2, 44); }); it("dates a file older than the format to 1980-01-01", () => { assert.deepEqual(dosDateTime(0), { time: 0, date: (1 << 5) | 1 }); assert.deepEqual(dosDateTime(Number.NaN), { time: 0, date: (1 << 5) | 1 }); }); }); describe("ZipWriter", () => { it("writes an archive a directory walk reads back byte for byte", () => { const files = [ { name: "Ideas.md", data: enc.encode("# Ideas\n\n[[Other]]\n") }, { name: "Media/pixel.png", data: new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3]) }, { name: "empty.md", data: new Uint8Array(0) }, ]; const entries = readZip(zipSync(files)); assert.deepEqual( entries.map((e) => e.name), files.map((f) => f.name), ); for (let i = 0; i < files.length; i++) { assert.deepEqual(entries[i].data, files[i].data, files[i].name); assert.equal(entries[i].crc, crc32(files[i].data)); assert.equal(entries[i].size, files[i].data.length); assert.equal(entries[i].method, 0, "STORE"); } }); it("marks every name UTF-8 so an Arabic path survives", () => { const name = "ملاحظات/فكرة جديدة.md"; const [entry] = readZip(zipSync([{ name, data: enc.encode("مرحبا") }])); assert.equal(entry.name, name); assert.ok(entry.flags & (1 << 11), "general-purpose bit 11 set"); assert.equal(dec.decode(entry.data), "مرحبا"); }); it("streams: chunks of any size, the descriptor written after the data", () => { const chunks: Uint8Array[] = []; const writer = new ZipWriter((c) => chunks.push(c)); const big = new Uint8Array(100_000); for (let i = 0; i < big.length; i++) big[i] = (i * 31) & 0xff; writer.begin("big.bin", Date.now()); for (let i = 0; i < big.length; i += 4096) writer.write(big.subarray(i, i + 4096)); writer.end(); writer.file("small.md", enc.encode("x")); writer.finish(); const bytes = new Uint8Array(writer.written); let at = 0; for (const c of chunks) { bytes.set(c, at); at += c.length; } assert.equal(at, bytes.length, "`written` is the byte count handed to the sink"); const [a, b] = readZip(bytes); assert.equal(a.name, "big.bin"); assert.deepEqual(a.data, big); assert.equal(a.crc, crc32(big)); assert.ok(a.flags & (1 << 3), "data descriptor flag set"); assert.equal(b.name, "small.md"); }); it("refuses misuse loudly rather than writing a broken archive", () => { const writer = new ZipWriter(() => undefined); assert.throws(() => writer.write(new Uint8Array(1)), /no entry is open/); assert.throws(() => writer.end(), /no entry is open/); writer.begin("a"); assert.throws(() => writer.begin("b"), /still open/); assert.throws(() => writer.finish(), /still open/); writer.end(); writer.finish(); assert.throws(() => writer.begin("c"), /already finished/); assert.throws(() => writer.finish(), /already finished/); }); it("an empty archive is the 22-byte end record", () => { const bytes = zipSync([]); assert.equal(bytes.length, 22); assert.deepEqual(readZip(bytes), []); }); }); describe("a stored entry's local header", () => { it("carries the size when the writer knows it, so a sequential reader can find the end", async () => { const { ZipWriter } = await import("../shared/zip.ts"); const chunks: Uint8Array[] = []; const w = new ZipWriter((c) => chunks.push(c)); w.file("a.txt", new TextEncoder().encode("hello"), 0); w.finish(); const bytes = Buffer.concat(chunks.map((c) => Buffer.from(c))); // local header: sig(4) ver(2) flag(2) method(2) time(2) date(2) crc(4) csize(4) usize(4) assert.equal(bytes.readUInt32LE(18), 5); assert.equal(bytes.readUInt32LE(22), 5); }); });