// @vitest-environment node import { File } from "node:buffer"; import { describe, expect, it } from "vitest"; import { gzipDeterministic, gunzipBounded } from "../../src/archive/gzip"; import { createTar, parseTar } from "../../src/archive/tar"; function input(path: string, text: string) { return { path, file: new File( [text], path.split("/").at(-1)!, ) as unknown as globalThis.File, }; } describe("TAR, USTAR, PAX and gzip", () => { it("creates deterministic TAR and parses regular entries", async () => { const inputs = [input("b.txt", "second"), input("folder/a.txt", "first")]; const first = await createTar(inputs); const second = await createTar(inputs); expect(first).toEqual(second); const parsed = parseTar(first); expect(parsed.entries.map((entry) => entry.path)).toEqual([ "b.txt", "folder/a.txt", ]); expect(parsed.entries.every((entry) => entry.extractable)).toBe(true); expect(parsed.entries[1]?.crc32).toBe("9271ee57"); }); it("round-trips a long path through a bounded PAX path header", async () => { const longPath = `${"long-".repeat(24)}name.txt`; const tar = await createTar([input(longPath, "PAX")]); expect(parseTar(tar).entries[0]?.path).toBe(longPath); }); it("lists but blocks symbolic links", async () => { const tar = await createTar([input("link", "target")]); tar[156] = 0x32; rewriteChecksum(tar.subarray(0, 512)); const entry = parseTar(tar).entries[0]!; expect(entry.kind).toBe("symlink"); expect(entry.extractable).toBe(false); expect(entry.issues.map((issue) => issue.code)).toContain("SPECIAL_ENTRY"); }); it("rejects a damaged TAR header checksum", async () => { const tar = await createTar([input("file.txt", "data")]); tar[0] = tar[0]! ^ 1; expect(() => parseTar(tar)).toThrow(/checksum/iu); }); it("rejects ambiguous non-zero data after the TAR end marker", async () => { const tar = await createTar([input("file.txt", "data")]); const appended = new Uint8Array(tar.length + 1); appended.set(tar); appended[tar.length] = 1; expect(() => parseTar(appended)).toThrow(/follows the TAR end/iu); }); it("creates deterministic gzip and validates CRC and ISIZE", () => { const source = new TextEncoder().encode("local archive payload"); const first = gzipDeterministic(source); expect(first).toEqual(gzipDeterministic(source)); expect(gunzipBounded(first, 1024)).toEqual(source); first[first.length - 8] = first[first.length - 8]! ^ 1; expect(() => gunzipBounded(first, 1024)).toThrow(/CRC-32|footer/iu); }); it("stops gzip expansion at the configured operation limit", () => { const compressed = gzipDeterministic(new Uint8Array(32_768)); expect(() => gunzipBounded(compressed, 1024)).toThrow(/expanded-byte/iu); }); it("explicitly rejects concatenated gzip members", () => { const left = gzipDeterministic(new TextEncoder().encode("left")); const right = gzipDeterministic(new TextEncoder().encode("right")); const joined = new Uint8Array(left.length + right.length); joined.set(left); joined.set(right, left.length); expect(() => gunzipBounded(joined, 1024)).toThrow(/multi-member/iu); }); }); function rewriteChecksum(header: Uint8Array) { header.fill(0x20, 148, 156); const checksum = header.reduce((sum, byte) => sum + byte, 0); const text = checksum.toString(8).padStart(6, "0"); header.set(new TextEncoder().encode(text), 148); header[154] = 0; header[155] = 0x20; }