@@ -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")],
|
||||
|
||||
Reference in New Issue
Block a user