Release Archive Tools 0.2.0
Verify / verify (push) Canceled after 0s

This commit is contained in:
2026-09-02 09:34:59 +02:00
parent a49ec6b17a
commit fe578f46bd
47 changed files with 3337 additions and 253 deletions
+76 -5
View File
@@ -9,6 +9,7 @@ import {
createSafeSelectionZip,
inspectArchive,
readEntryBytes,
detectArchiveFormat,
} from "../../src/archive/service";
const asFile = (
@@ -18,6 +19,26 @@ const asFile = (
) => new NodeFile(parts, name, { type }) as unknown as globalThis.File;
describe("ZIP inspection and safe workflows", () => {
it("identifies structural-only 7z, RAR4 and RAR5 inputs", () => {
expect(
detectArchiveFormat(
"archive.bin",
new Uint8Array([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]),
),
).toBe("7z");
expect(
detectArchiveFormat(
"archive.bin",
new Uint8Array([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00]),
),
).toBe("rar4");
expect(
detectArchiveFormat(
"archive.bin",
new Uint8Array([0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00]),
),
).toBe("rar5");
});
it("creates byte-deterministic ZIPs, reads CRC-checked content and previews text", async () => {
const files = [
asFile(["hello"], "hello.txt"),
@@ -41,7 +62,7 @@ describe("ZIP inspection and safe workflows", () => {
).resolves.toMatchObject({ kind: "text", text: "hello" });
});
it("blocks traversal, case-colliding and encrypted entries at inspection", async () => {
it("blocks unsafe paths while opening AES entries only with a password", async () => {
const blobWriter = new BlobWriter("application/zip");
const writer = new ZipWriter(blobWriter, { useWebWorkers: false });
await writer.add("../escape.txt", new TextReader("bad"), {
@@ -67,12 +88,62 @@ describe("ZIP inspection and safe workflows", () => {
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([]);
expect(document.entries[3]).toMatchObject({
encrypted: true,
encryption: "AES-256",
extractable: true,
});
await expect(
readEntryBytes(document, document.entries[3]!, 1024),
).rejects.toThrow(/password is required/iu);
await expect(
readEntryBytes(document, document.entries[3]!, 1024, undefined, "wrong"),
).rejects.toThrow(/incorrect|damaged/iu);
expect(
new TextDecoder().decode(
await readEntryBytes(
document,
document.entries[3]!,
1024,
undefined,
"test-password",
),
),
).toBe("secret");
});
it.each([
["aes-256", "AES-256"],
["zipcrypto", "ZipCrypto"],
] as const)(
"creates and reads %s encrypted ZIPs locally",
async (method, label) => {
const encrypted = await createArchive(
[asFile(["classified"], "secret.txt")],
"zip",
undefined,
undefined,
{ password: "correct horse battery staple", method },
);
const document = await inspectArchive(
asFile([await encrypted.arrayBuffer()], `${method}.zip`),
);
expect(document.entries[0]).toMatchObject({
encrypted: true,
encryption: label,
extractable: true,
});
const bytes = await readEntryBytes(
document,
document.entries[0]!,
1024,
undefined,
"correct horse battery staple",
);
expect(new TextDecoder().decode(bytes)).toBe("classified");
},
);
it("enforces compression-ratio policy before decompression", async () => {
const blob = await createArchive(
[asFile([new Uint8Array(1024 * 1024)], "zeros.bin")],