import { expect, test } from "bun:test"; import { mkdtempSync, symlinkSync, writeFileSync, mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { zip } from "../src/server/zip"; import { forEachDb } from "./helpers"; import { FIXTURES, makeApp } from "./app"; async function unzipList(bytes: Uint8Array): Promise { const dir = mkdtempSync(join(tmpdir(), "stc-zip-")); await Bun.write(join(dir, "a.zip"), bytes); const out = await Bun.$`unzip -l ${join(dir, "a.zip")}`.text(); await Bun.$`unzip -o -q ${join(dir, "a.zip")} -d ${join(dir, "x")}`; return out + "\n" + (await Bun.$`cd ${join(dir, "x")} && find . -type f -exec cat {} +`.text()); } test("zip round-trips through unzip with non-ASCII names", async () => { const big = new TextEncoder().encode("abc".repeat(1000)); const listing = await unzipList(zip([ { name: "S08 - Containerizing Applications/Æble notes.txt", data: new TextEncoder().encode("hej") }, { name: "S08 - Containerizing Applications/big.txt", data: big }, ])); expect(listing).toContain("S08 - Containerizing Applications/Æble notes.txt"); expect(listing).toContain("hej"); expect(listing).toContain("abcabc"); }); forEachDb("code listing and zip for a module, skipping escaping symlinks", async (db) => { const root = mkdtempSync(join(tmpdir(), "stc-code-")); const outside = mkdtempSync(join(tmpdir(), "stc-outside-")); writeFileSync(join(outside, "secret.txt"), "TOP SECRET"); mkdirSync(join(root, "c/S01 - Ünïcode/sub"), { recursive: true }); mkdirSync(join(root, "c/S01 - Ünïcode/node_modules"), { recursive: true }); writeFileSync(join(root, "c/S01 - Ünïcode/run.sh"), "echo hi"); writeFileSync(join(root, "c/S01 - Ünïcode/sub/app.py"), "print(1)"); writeFileSync(join(root, "c/S01 - Ünïcode/node_modules/x.js"), "x"); symlinkSync(join(outside, "secret.txt"), join(root, "c/S01 - Ünïcode/leak.txt")); symlinkSync(join(root, "c/S01 - Ünïcode/run.sh"), join(root, "c/S01 - Ünïcode/alias.sh")); writeFileSync(join(root, "stcourse.yaml"), `schema: 1\nid: codey\ntitle: Codey\nvisibility: public\nmodules:\n - id: m1\n title: M\n code: { dir: "c/S01 - Ünïcode", command: "sh run.sh" }\n lessons: [{ id: l1, title: L, type: exercise }]\n`); const t = await makeApp(db, [FIXTURES, root]); const list = await (await t.call("GET", "/api/courses/codey/code/m1")).json(); expect(list.files.map((f: { path: string }) => f.path)).toEqual(["alias.sh", "run.sh", "sub/app.py"]); expect(list.command).toBe("sh run.sh"); const r = await t.call("GET", "/api/courses/codey/code/m1/zip"); expect(r.status).toBe(200); expect(r.headers.get("content-type")).toBe("application/zip"); expect(r.headers.get("content-disposition")).toContain("filename*=UTF-8''S01%20-%20%C3%9Cn%C3%AFcode.zip"); const listing = await unzipList(new Uint8Array(await r.arrayBuffer())); expect(listing).toContain("S01 - Ünïcode/sub/app.py"); expect(listing).not.toContain("TOP SECRET"); expect(listing).not.toContain("node_modules"); expect((await t.call("GET", "/api/courses/codey/code/nope")).status).toBe(404); expect((await t.call("GET", "/api/courses/sample/code/m1")).status).toBe(200); expect((await t.call("GET", "/api/courses/sample/code/m2")).status).toBe(404); }, 30_000);