import { describe, it, expect } from "vitest"; import { createZipBytes, __testing } from "./zip"; const { crc32, dosDateTime } = __testing; const bytes = (s: string) => new TextEncoder().encode(s); /** Reads the archive back through a DataView — the format is the contract. */ function parse(buf: Uint8Array) { const view = new DataView(buf.buffer); const dec = new TextDecoder(); // End-of-central-directory sits at the very end for archives without comments. const endAt = buf.length - 22; expect(view.getUint32(endAt, true)).toBe(0x06054b50); const count = view.getUint16(endAt + 8, true); const centralAt = view.getUint32(endAt + 16, true); const entries: { name: string; content: string; crc: number; size: number }[] = []; let p = centralAt; for (let i = 0; i < count; i++) { expect(view.getUint32(p, true)).toBe(0x02014b50); const crc = view.getUint32(p + 16, true); const size = view.getUint32(p + 24, true); const nameLen = view.getUint16(p + 28, true); const localAt = view.getUint32(p + 42, true); const name = dec.decode(buf.slice(p + 46, p + 46 + nameLen)); expect(view.getUint32(localAt, true)).toBe(0x04034b50); const localNameLen = view.getUint16(localAt + 26, true); const dataAt = localAt + 30 + localNameLen + view.getUint16(localAt + 28, true); const content = dec.decode(buf.slice(dataAt, dataAt + size)); entries.push({ name, content, crc, size }); p += 46 + nameLen + view.getUint16(p + 30, true) + view.getUint16(p + 32, true); } return entries; } describe("crc32", () => { /* Against the values every reference implementation agrees on — if this drifts, * archives still open but every entry reports as corrupt. */ it("matches the known checksum of a plain string", () => { expect(crc32(bytes("hello world"))).toBe(0x0d4a1185); }); it("is zero for no bytes", () => { expect(crc32(new Uint8Array())).toBe(0); }); it("changes when a single byte changes", () => { expect(crc32(bytes("hello worlD"))).not.toBe(crc32(bytes("hello world"))); }); }); describe("dosDateTime", () => { it("packs a date into the MS-DOS words", () => { const { date, time } = dosDateTime(new Date(2026, 7, 8, 13, 30, 20)); expect((date >> 9) + 1980).toBe(2026); expect((date >> 5) & 0xf).toBe(8); expect(date & 0x1f).toBe(8); expect(time >> 11).toBe(13); expect((time >> 5) & 0x3f).toBe(30); }); }); describe("createZip", () => { it("round-trips a single text file", () => { const zip = createZipBytes([{ name: "notiz.md", data: "# Hallo" }]); const [entry] = parse(zip); expect(entry!.name).toBe("notiz.md"); expect(entry!.content).toBe("# Hallo"); }); it("keeps several entries with their paths", () => { const zip = createZipBytes([ { name: "Buch/eins.md", data: "eins" }, { name: "Buch/zwei.md", data: "zwei" }, { name: "Buch/images/a.png", data: new Uint8Array([1, 2, 3]) }, ]); const entries = parse(zip); expect(entries.map((e) => e.name)).toEqual(["Buch/eins.md", "Buch/zwei.md", "Buch/images/a.png"]); expect(entries[1]!.content).toBe("zwei"); }); it("records a checksum matching the data, so archivers do not report corruption", () => { const zip = createZipBytes([{ name: "a.txt", data: "some content" }]); const [entry] = parse(zip); expect(entry!.crc).toBe(crc32(bytes("some content"))); expect(entry!.size).toBe(12); }); it("stores binary data unchanged", async () => { const raw = new Uint8Array([0, 255, 128, 10, 13, 0]); const zip = createZipBytes([{ name: "b.bin", data: raw }]); // Data begins right after the local header and the filename. const start = 30 + "b.bin".length; expect(Array.from(zip.slice(start, start + raw.length))).toEqual(Array.from(raw)); }); it("writes umlauts in filenames as UTF-8", () => { const zip = createZipBytes([{ name: "Übersicht.md", data: "x" }]); const [entry] = parse(zip); expect(entry!.name).toBe("Übersicht.md"); }); it("produces a valid empty archive", () => { const zip = createZipBytes([]); expect(parse(zip)).toEqual([]); expect(zip.length).toBe(22); }); });