// @vitest-environment node import { File as NodeFile } from "node:buffer"; import { BlobReader, BlobWriter, TextReader, ZipWriter } from "@zip.js/zip.js"; import { describe, expect, it } from "vitest"; import { compareArchives } from "../../src/archive/compare"; import { previewEntry } from "../../src/archive/preview"; import { createArchive, createSafeSelectionZip, inspectArchive, readEntryBytes, } from "../../src/archive/service"; const asFile = ( parts: ConstructorParameters[0], name: string, type = "application/octet-stream", ) => new NodeFile(parts, name, { type }) as unknown as globalThis.File; describe("ZIP inspection and safe workflows", () => { it("creates byte-deterministic ZIPs, reads CRC-checked content and previews text", async () => { const files = [ asFile(["hello"], "hello.txt"), asFile(["world"], "world.txt"), ]; const first = await createArchive(files, "zip"); const second = await createArchive(files, "zip"); expect(new Uint8Array(await first.arrayBuffer())).toEqual( new Uint8Array(await second.arrayBuffer()), ); const document = await inspectArchive( asFile([await first.arrayBuffer()], "sample.zip"), ); expect(document.entries).toHaveLength(2); expect(document.entries.every((entry) => entry.extractable)).toBe(true); const bytes = await readEntryBytes(document, document.entries[0]!, 1024); expect(new TextDecoder().decode(bytes)).toBe("hello"); await expect( previewEntry(document, document.entries[0]!), ).resolves.toMatchObject({ kind: "text", text: "hello" }); }); it("blocks traversal, case-colliding and encrypted entries at inspection", async () => { const blobWriter = new BlobWriter("application/zip"); const writer = new ZipWriter(blobWriter, { useWebWorkers: false }); await writer.add("../escape.txt", new TextReader("bad"), { useWebWorkers: false, }); await writer.add("Report.txt", new TextReader("one"), { useWebWorkers: false, }); await writer.add("report.TXT", new TextReader("two"), { useWebWorkers: false, }); await writer.add("secret.txt", new TextReader("secret"), { password: "test-password", useWebWorkers: false, }); const blob = await writer.close(); const document = await inspectArchive( asFile([await blob.arrayBuffer()], "unsafe.zip"), ); expect(document.entries[0]?.issues.map((issue) => issue.code)).toContain( "PATH_TRAVERSAL", ); expect(document.entries[2]?.issues.map((issue) => issue.code)).toContain( "DUPLICATE_PATH", ); expect(document.entries[3]?.issues.map((issue) => issue.code)).toContain( "ENCRYPTED_ENTRY", ); expect(document.entries.filter((entry) => entry.extractable)).toEqual([]); }); it("enforces compression-ratio policy before decompression", async () => { const blob = await createArchive( [asFile([new Uint8Array(1024 * 1024)], "zeros.bin")], "zip", ); const document = await inspectArchive( asFile([await blob.arrayBuffer()], "ratio.zip"), ); expect(document.entries[0]?.extractable).toBe(false); expect(document.entries[0]?.issues.map((issue) => issue.code)).toContain( "COMPRESSION_RATIO", ); }); it("detects stored-entry corruption when content is requested", async () => { const writer = new ZipWriter(new BlobWriter("application/zip"), { level: 0, useWebWorkers: false, }); await writer.add("hello.txt", new TextReader("hello"), { level: 0, useWebWorkers: false, }); const blob = await writer.close(); const bytes = new Uint8Array(await blob.arrayBuffer()); const nameLength = bytes[26]! | (bytes[27]! << 8); const extraLength = bytes[28]! | (bytes[29]! << 8); bytes[30 + nameLength + extraLength] = bytes[30 + nameLength + extraLength]! ^ 1; const document = await inspectArchive(asFile([bytes], "corrupt.zip")); await expect( readEntryBytes(document, document.entries[0]!, 1024), ).rejects.toThrow(); }); it("repackages only a verified selection into a fresh safe ZIP", async () => { const original = await createArchive( [asFile(["one"], "one.txt"), asFile(["two"], "two.txt")], "zip", ); const document = await inspectArchive( asFile([await original.arrayBuffer()], "original.zip"), ); const safe = await createSafeSelectionZip(document, [ document.entries[1]!.id, ]); const repackaged = await inspectArchive( asFile([await safe.arrayBuffer()], "safe.zip"), ); expect(repackaged.entries.map((entry) => entry.path)).toEqual(["two.txt"]); expect( new TextDecoder().decode( await readEntryBytes(repackaged, repackaged.entries[0]!, 1024), ), ).toBe("two"); }); it("compares archive content independent of container format", async () => { const leftBlob = await createArchive( [asFile(["same"], "same.txt"), asFile(["old"], "changed.txt")], "zip", ); const rightBlob = await createArchive( [asFile(["same"], "same.txt"), asFile(["new"], "changed.txt")], "tar", ); const left = await inspectArchive( asFile([await leftBlob.arrayBuffer()], "left.zip"), ); const right = await inspectArchive( asFile([await rightBlob.arrayBuffer()], "right.tar"), ); expect( compareArchives(left, right).map(({ path, status }) => ({ path, status, })), ).toEqual([ { path: "changed.txt", status: "changed" }, { path: "same.txt", status: "same" }, ]); }); it("reads a ZIP produced externally through a BlobReader", async () => { const writer = new ZipWriter(new BlobWriter("application/zip"), { useWebWorkers: false, }); await writer.add( "folder/data.bin", new BlobReader(new Blob([new Uint8Array([0, 1, 2, 3])])), { useWebWorkers: false }, ); const document = await inspectArchive( asFile([await (await writer.close()).arrayBuffer()], "external.zip"), ); expect(document.entries[0]).toMatchObject({ path: "folder/data.bin", size: 4, compression: "deflate", }); }); it("recognizes ZIP64 metadata without requiring a multi-gigabyte fixture", async () => { const writer = new ZipWriter(new BlobWriter("application/zip"), { useWebWorkers: false, zip64: true, }); await writer.add("zip64.txt", new TextReader("small fixture"), { useWebWorkers: false, zip64: true, }); const document = await inspectArchive( asFile([await (await writer.close()).arrayBuffer()], "forced-zip64.zip"), ); expect(document.zip64).toBe(true); expect(document.entries[0]?.zip64).toBe(true); }); });