import { expect, test } from "bun:test"; import { closeSync, mkdirSync, mkdtempSync, openSync, writeFileSync, writeSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { CHUNK, SMALL_FILE, crc32Update, toStream, zipStream } from "../src/server/zip"; import { forEachDb } from "./helpers"; import { FIXTURES, makeApp } from "./app"; const rnd = (n: number) => { const b = new Uint8Array(n); for (let i = 0; i < n; i += 65536) crypto.getRandomValues(b.subarray(i, Math.min(n, i + 65536))); return b; }; test("incremental CRC-32 matches Bun.hash.crc32", () => { const data = rnd(300_000); let c = 0; for (let i = 0; i < data.length; i += 7777) c = crc32Update(c, data.subarray(i, i + 7777)); expect(c).toBe(Bun.hash.crc32(data) >>> 0); }); test("the stream holds at most one chunk of a large file; unzip verifies every CRC", async () => { const dir = mkdtempSync(join(tmpdir(), "stc-zs-")); mkdirSync(join(dir, "Ünï sub")); writeFileSync(join(dir, "big.bin"), rnd(SMALL_FILE * 3 + 12345)); // streamed, stored, data descriptor writeFileSync(join(dir, "Ünï sub/small.txt"), "hello ".repeat(10_000)); // deflated writeFileSync(join(dir, "empty.txt"), ""); const entries = ["big.bin", "Ünï sub/small.txt", "empty.txt"].map((p) => ({ name: `pkg/${p}`, path: join(dir, p), size: Bun.file(join(dir, p)).size })); const out = join(dir, "out.zip"); const fd = openSync(out, "w"); let largest = 0; for await (const chunk of zipStream(entries)) { largest = Math.max(largest, chunk.length); writeSync(fd, chunk); } closeSync(fd); expect(largest).toBeLessThanOrEqual(Math.max(CHUNK, 60_000)); // never the whole 12 MB file const t = await Bun.$`unzip -t ${out}`.nothrow().text(); expect(t).toContain("No errors detected"); const x = join(dir, "x"); await Bun.$`unzip -q ${out} -d ${x}`; expect(Bun.hash.crc32(new Uint8Array(await Bun.file(join(x, "pkg/big.bin")).arrayBuffer()))).toBe(Bun.hash.crc32(new Uint8Array(await Bun.file(join(dir, "big.bin")).arrayBuffer()))); expect(await Bun.file(join(x, "pkg/Ünï sub/small.txt")).text()).toBe("hello ".repeat(10_000)); }); test("toStream pulls one chunk at a time", async () => { let produced = 0; async function* gen() { for (let i = 0; i < 100; i++) { produced++; yield new Uint8Array(10); } } const reader = toStream(gen()).getReader(); await reader.read(); await Bun.sleep(20); expect(produced).toBeLessThanOrEqual(3); // not run ahead to 100 await reader.cancel(); }); forEachDb("a 300 MB code folder downloads as a valid streamed zip without buffering it", async (db) => { const root = mkdtempSync(join(tmpdir(), "stc-bigcode-")); mkdirSync(join(root, "code"), { recursive: true }); // sparse file: no disk or memory cost to create, 300 MB to read const f = openSync(join(root, "code/huge.bin"), "w"); writeSync(f, new Uint8Array([1]), 0, 1, 300 * 1024 * 1024 - 1); closeSync(f); writeFileSync(join(root, "code/readme.md"), "# hi\n"); writeFileSync(join(root, "stcourse.yaml"), `schema: 1\nid: bigcode\ntitle: Big\nvisibility: public\nmodules:\n - id: m1\n title: M\n code: { dir: code }\n lessons: [{ id: l1, title: L, type: text, file: code/readme.md }]\n`); const t = await makeApp(db, [FIXTURES, root]); Bun.gc(true); const before = process.memoryUsage().rss; let peak = before; const r = await t.call("GET", "/api/courses/bigcode/code/m1/zip"); expect(r.status).toBe(200); expect(r.headers.get("content-length")).toBeNull(); // streamed const out = join(root, "out.zip"); const w = Bun.file(out).writer(); const reader = r.body!.getReader(); let n = 0; for (;;) { const { done, value } = await reader.read(); if (done) break; w.write(value); if (++n % 64 === 0) { await w.flush(); peak = Math.max(peak, process.memoryUsage().rss); } } await w.end(); expect(Bun.file(out).size).toBeGreaterThan(300 * 1024 * 1024); expect(peak - before).toBeLessThan(150 * 1024 * 1024); // a buffered zip would need > 300 MB expect(await Bun.$`unzip -t ${out}`.nothrow().text()).toContain("No errors detected"); }, 120_000);