Release Archive Tools 0.1.0

This commit is contained in:
2026-09-01 02:42:42 +02:00
commit a49ec6b17a
77 changed files with 11528 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
// @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<typeof NodeFile>[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);
});
});