import { test } from "node:test"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { execa } from "execa"; import { makeZip } from "../testing/makeZip"; import { ZipFormatError, copyFileHashed, copyZipEntry, readZipEntries, readZipEntryText, zipEntryProblem, } from "./zipReader"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/zipReader.test.ts const sha = (b: Buffer) => createHash("sha256").update(b).digest("hex"); const VIDEO = Buffer.from(Array.from({ length: 200_000 }, (_, i) => (i * 7) & 0xff)); async function withTmp(fn: (dir: string) => Promise): Promise { const dir = await mkdtemp(path.join(tmpdir(), "ttb-zip-")); try { await fn(dir); } finally { await rm(dir, { recursive: true, force: true }); } } for (const zip64 of [false, true]) { test(`reads the central directory${zip64 ? " (ZIP64)" : ""}: names, sizes, methods, directories`, async () => { await withTmp(async (dir) => { const file = path.join(dir, "a.zip"); await writeFile( file, makeZip( [ { name: "YouTube/" }, { name: "YouTube/001 - Ünïcode | name - (abcdefghijk)/v-abcdefghijk.mp4", data: VIDEO }, { name: "YouTube/001 - Ünïcode | name - (abcdefghijk)/description.txt", data: "hello", deflate: true }, ], { zip64 }, ), ); const entries = await readZipEntries(file); assert.equal(entries.length, 3); assert.equal(entries[0].isDirectory, true); assert.equal(entries[1].name, "YouTube/001 - Ünïcode | name - (abcdefghijk)/v-abcdefghijk.mp4"); assert.equal(entries[1].method, 0); assert.equal(entries[1].size, VIDEO.length); assert.equal(entries[2].method, 8); assert.equal(entries[2].size, 5); }); }); test(`copies a stored and a deflated entry out, hashed and CRC-checked${zip64 ? " (ZIP64)" : ""}`, async () => { await withTmp(async (dir) => { const file = path.join(dir, "a.zip"); await writeFile( file, makeZip( [ { name: "a/stored.mp4", data: VIDEO }, { name: "a/deflated.mkv", data: VIDEO, deflate: true }, { name: "a/empty.txt", data: "" }, ], { zip64 }, ), ); const entries = await readZipEntries(file); for (const [i, name] of [[0, "s"], [1, "d"]] as const) { const dest = path.join(dir, name); const got = await copyZipEntry(file, entries[i], dest); assert.equal(got.bytes, VIDEO.length); assert.equal(got.sha256, sha(VIDEO)); assert.deepEqual(await readFile(dest), VIDEO); } const empty = await copyZipEntry(file, entries[2], path.join(dir, "e")); assert.equal(empty.bytes, 0); }); }); } test("a damaged entry is refused by its CRC, and the archive is never written", async () => { await withTmp(async (dir) => { const file = path.join(dir, "a.zip"); const zip = makeZip([{ name: "x.mp4", data: VIDEO }]); // Flip one byte of the stored data (the local header is 30 + name bytes). zip[30 + "x.mp4".length + 100] ^= 0xff; await writeFile(file, zip); const before = sha(await readFile(file)); const [entry] = await readZipEntries(file); await assert.rejects(copyZipEntry(file, entry, path.join(dir, "out")), /CRC32 mismatch/); assert.equal(sha(await readFile(file)), before); }); }); test("an existing destination is never overwritten (staging files are exclusive)", async () => { await withTmp(async (dir) => { const file = path.join(dir, "a.zip"); await writeFile(file, makeZip([{ name: "x.mp4", data: VIDEO }])); const [entry] = await readZipEntries(file); await writeFile(path.join(dir, "out"), "keep"); await assert.rejects(copyZipEntry(file, entry, path.join(dir, "out"))); assert.equal(await readFile(path.join(dir, "out"), "utf8"), "keep"); }); }); test("text entries read, stored or deflated; a big one is refused", async () => { await withTmp(async (dir) => { const file = path.join(dir, "a.zip"); await writeFile( file, makeZip([ { name: "a.info.json", data: '{"id":"x"}' }, { name: "description.txt", data: "Published on July 13th 2018", deflate: true }, { name: "big.mp4", data: VIDEO }, ]), ); const [info, desc, big] = await readZipEntries(file); assert.equal(await readZipEntryText(file, info), '{"id":"x"}'); assert.equal(await readZipEntryText(file, desc), "Published on July 13th 2018"); await assert.rejects(readZipEntryText(file, big, 1000), /too large/); }); }); test("unsupported methods and encryption are named, not half-read", () => { const base = { name: "x", isDirectory: false, compressedSize: 1, size: 1, crc32: 0, localHeaderOffset: 0, encrypted: false }; assert.equal(zipEntryProblem({ ...base, method: 0 }), null); assert.equal(zipEntryProblem({ ...base, method: 8 }), null); assert.match(zipEntryProblem({ ...base, method: 14 }) ?? "", /method 14/); assert.equal(zipEntryProblem({ ...base, method: 0, encrypted: true }), "encrypted"); }); test("not a zip: a sentence, not a crash", async () => { await withTmp(async (dir) => { const file = path.join(dir, "not.zip"); await writeFile(file, "plain text, no end record"); await assert.rejects(readZipEntries(file), ZipFormatError); }); }); test("copyFileHashed hashes a plain file the same way", async () => { await withTmp(async (dir) => { await writeFile(path.join(dir, "src"), VIDEO); const got = await copyFileHashed(path.join(dir, "src"), path.join(dir, "dst")); assert.equal(got.sha256, sha(VIDEO)); assert.deepEqual(await readFile(path.join(dir, "dst")), VIDEO); }); }); // Interop with a real writer: Info-ZIP's `zip`, stored (-0) and deflated, and // forced ZIP64 (-fz). Skipped where `zip` is not installed. test("reads what Info-ZIP's zip writes, ZIP64 included", async (t) => { const probe = await execa("zip", ["-v"], { reject: false }).catch(() => null); if (!probe || probe.exitCode !== 0) { t.skip("zip is not installed"); return; } await withTmp(async (dir) => { await writeFile(path.join(dir, "v.mp4"), VIDEO); await writeFile(path.join(dir, "d.txt"), "some text ".repeat(100)); for (const args of [["-0"], ["-9"], ["-0", "-fz"]]) { const file = path.join(dir, `out${args.join("")}.zip`); await execa("zip", ["-q", "-X", ...args, file, "v.mp4", "d.txt"], { cwd: dir }); const entries = await readZipEntries(file); const v = entries.find((e) => e.name === "v.mp4")!; const got = await copyZipEntry(file, v, path.join(dir, `copy${args.join("")}`)); assert.equal(got.sha256, sha(VIDEO), args.join(" ")); const d = entries.find((e) => e.name === "d.txt")!; assert.equal(await readZipEntryText(file, d), "some text ".repeat(100)); } }); });