From 204f379f6b28fafaf9d1f768dd21194cac639ce5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 21 Feb 2026 23:35:21 +0000 Subject: [PATCH] test(archive): share zip/tar fixture generation --- src/infra/archive.test.ts | 123 ++++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/src/infra/archive.test.ts b/src/infra/archive.test.ts index 9877fef895f..6b25d430c6a 100644 --- a/src/infra/archive.test.ts +++ b/src/infra/archive.test.ts @@ -27,6 +27,26 @@ async function withArchiveCase( await run({ workDir, archivePath, extractDir }); } +async function writePackageArchive(params: { + ext: "zip" | "tar"; + workDir: string; + archivePath: string; + fileName: string; + content: string; +}) { + if (params.ext === "zip") { + const zip = new JSZip(); + zip.file(`package/${params.fileName}`, params.content); + await fs.writeFile(params.archivePath, await zip.generateAsync({ type: "nodebuffer" })); + return; + } + + const packageDir = path.join(params.workDir, "package"); + await fs.mkdir(packageDir, { recursive: true }); + await fs.writeFile(path.join(packageDir, params.fileName), params.content); + await tar.c({ cwd: params.workDir, file: params.archivePath }, ["package"]); +} + async function expectExtractedSizeBudgetExceeded(params: { archivePath: string; destDir: string; @@ -53,25 +73,36 @@ afterAll(async () => { describe("archive utils", () => { it("detects archive kinds", () => { - expect(resolveArchiveKind("/tmp/file.zip")).toBe("zip"); - expect(resolveArchiveKind("/tmp/file.tgz")).toBe("tar"); - expect(resolveArchiveKind("/tmp/file.tar.gz")).toBe("tar"); - expect(resolveArchiveKind("/tmp/file.tar")).toBe("tar"); - expect(resolveArchiveKind("/tmp/file.txt")).toBeNull(); + const cases = [ + { input: "/tmp/file.zip", expected: "zip" }, + { input: "/tmp/file.tgz", expected: "tar" }, + { input: "/tmp/file.tar.gz", expected: "tar" }, + { input: "/tmp/file.tar", expected: "tar" }, + { input: "/tmp/file.txt", expected: null }, + ] as const; + for (const testCase of cases) { + expect(resolveArchiveKind(testCase.input), testCase.input).toBe(testCase.expected); + } }); - it("extracts zip archives", async () => { - await withArchiveCase("zip", async ({ archivePath, extractDir }) => { - const zip = new JSZip(); - zip.file("package/hello.txt", "hi"); - await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); - - await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }); - const rootDir = await resolvePackedRootDir(extractDir); - const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8"); - expect(content).toBe("hi"); - }); - }); + it.each([{ ext: "zip" as const }, { ext: "tar" as const }])( + "extracts $ext archives", + async ({ ext }) => { + await withArchiveCase(ext, async ({ workDir, archivePath, extractDir }) => { + await writePackageArchive({ + ext, + workDir, + archivePath, + fileName: "hello.txt", + content: "hi", + }); + await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }); + const rootDir = await resolvePackedRootDir(extractDir); + const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8"); + expect(content).toBe("hi"); + }); + }, + ); it("rejects zip path traversal (zip slip)", async () => { await withArchiveCase("zip", async ({ archivePath, extractDir }) => { @@ -110,20 +141,6 @@ describe("archive utils", () => { }); }); - it("extracts tar archives", async () => { - await withArchiveCase("tar", async ({ workDir, archivePath, extractDir }) => { - const packageDir = path.join(workDir, "package"); - await fs.mkdir(packageDir, { recursive: true }); - await fs.writeFile(path.join(packageDir, "hello.txt"), "yo"); - await tar.c({ cwd: workDir, file: archivePath }, ["package"]); - - await extractArchive({ archivePath, destDir: extractDir, timeoutMs: 5_000 }); - const rootDir = await resolvePackedRootDir(extractDir); - const content = await fs.readFile(path.join(rootDir, "hello.txt"), "utf-8"); - expect(content).toBe("yo"); - }); - }); - it("rejects tar path traversal (zip slip)", async () => { await withArchiveCase("tar", async ({ workDir, archivePath, extractDir }) => { const insideDir = path.join(workDir, "inside"); @@ -138,19 +155,26 @@ describe("archive utils", () => { }); }); - it("rejects zip archives that exceed extracted size budget", async () => { - await withArchiveCase("zip", async ({ archivePath, extractDir }) => { - const zip = new JSZip(); - zip.file("package/big.txt", "x".repeat(64)); - await fs.writeFile(archivePath, await zip.generateAsync({ type: "nodebuffer" })); + it.each([{ ext: "zip" as const }, { ext: "tar" as const }])( + "rejects $ext archives that exceed extracted size budget", + async ({ ext }) => { + await withArchiveCase(ext, async ({ workDir, archivePath, extractDir }) => { + await writePackageArchive({ + ext, + workDir, + archivePath, + fileName: "big.txt", + content: "x".repeat(64), + }); - await expectExtractedSizeBudgetExceeded({ - archivePath, - destDir: extractDir, - maxExtractedBytes: 32, + await expectExtractedSizeBudgetExceeded({ + archivePath, + destDir: extractDir, + maxExtractedBytes: 32, + }); }); - }); - }); + }, + ); it("rejects archives that exceed archive size budget", async () => { await withArchiveCase("zip", async ({ archivePath, extractDir }) => { @@ -178,21 +202,6 @@ describe("archive utils", () => { await expect(resolvePackedRootDir(extractDir)).rejects.toThrow(/unexpected archive layout/i); }); - it("rejects tar archives that exceed extracted size budget", async () => { - await withArchiveCase("tar", async ({ workDir, archivePath, extractDir }) => { - const packageDir = path.join(workDir, "package"); - await fs.mkdir(packageDir, { recursive: true }); - await fs.writeFile(path.join(packageDir, "big.txt"), "x".repeat(64)); - await tar.c({ cwd: workDir, file: archivePath }, ["package"]); - - await expectExtractedSizeBudgetExceeded({ - archivePath, - destDir: extractDir, - maxExtractedBytes: 32, - }); - }); - }); - it("rejects tar entries with absolute extraction paths", async () => { await withArchiveCase("tar", async ({ workDir, archivePath, extractDir }) => { const inputDir = path.join(workDir, "input");