perf(artifacts): replace quadratic ZIP overlap checks (#1013)
This commit is contained in:
parent
966cca8efe
commit
76e7e510bb
|
|
@ -19,7 +19,7 @@
|
|||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"@zip.js/zip.js": "2.15.0",
|
||||
"canonicalize": "3.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
|
|
@ -3945,9 +3945,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.8.26",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.26.tgz",
|
||||
"integrity": "sha512-RQ4h9F6DOiHxpdocUDrOl6xBM+yOtz+LkUol47AVWcfebGBDpZ7w7Xvz9PS24JgXvLGiXXzSAfdCdVy1tPlaFA==",
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.15.0.tgz",
|
||||
"integrity": "sha512-hYAuHAaWjt0axbofaDL5XUlmrQPsBDcK3f45ApZuh6N+8UGVk26GoGdtGDvcvzahsHytTQ6DtBVXwG9IcCMjjQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"bun": ">=0.7.0",
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@
|
|||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"@zip.js/zip.js": "2.15.0",
|
||||
"canonicalize": "3.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ export class ZipArtifactReader implements ArtifactReader {
|
|||
this.#reader = new ZipReader(this.#source, {
|
||||
checkSignature: true,
|
||||
checkOverlappingEntry: true,
|
||||
// REA applies its own provider-neutral path validation in scanReader and
|
||||
// must preserve that typed failure instead of zip.js rejecting first.
|
||||
filenameValidation: "tolerant",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
|
||||
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
|
||||
|
||||
it("rejects overlapping ZIP member data ranges through artifact inventory", async () => {
|
||||
const root = await createTestTempDirectory("rea-zip-overlap-");
|
||||
const path = join(root, "overlap.zip");
|
||||
await writeOrderedZip(path, ["a.txt", "b.txt"]);
|
||||
const bytes = await readFile(path);
|
||||
const centralDirectorySignature = Buffer.from([0x50, 0x4b, 0x01, 0x02]);
|
||||
const first = bytes.indexOf(centralDirectorySignature);
|
||||
const second = bytes.indexOf(centralDirectorySignature, first + 4);
|
||||
if (first < 0 || second <= first)
|
||||
throw new Error("Expected two ZIP central-directory entries");
|
||||
|
||||
const firstLocalOffset = bytes.readUInt32LE(first + 42);
|
||||
const secondLocalOffset = bytes.readUInt32LE(second + 42);
|
||||
expect(secondLocalOffset).not.toBe(firstLocalOffset);
|
||||
bytes.writeUInt32LE(firstLocalOffset, second + 42);
|
||||
await writeFile(path, bytes);
|
||||
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const result = await new ArtifactProvider()
|
||||
.createClient(target.value)
|
||||
.execute("inventory_artifact", {});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "ArtifactOperationError", reason: "io" },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves encrypted-member and corrupt-CRC observations", async () => {
|
||||
const root = await createTestTempDirectory("rea-zip-integrity-");
|
||||
const encryptedPath = join(root, "encrypted.zip");
|
||||
const encryptedWriter = new ZipWriter(new Uint8ArrayWriter());
|
||||
await encryptedWriter.add("secret.txt", new TextReader("secret bytes"), {
|
||||
password: "fixture-password",
|
||||
});
|
||||
await writeFile(encryptedPath, await encryptedWriter.close());
|
||||
|
||||
const encryptedTarget = await parseBinaryTarget(encryptedPath);
|
||||
if (!encryptedTarget.ok) throw encryptedTarget.error;
|
||||
const encryptedResult = await new ArtifactProvider()
|
||||
.createClient(encryptedTarget.value)
|
||||
.execute("inventory_artifact", {});
|
||||
if (!encryptedResult.ok) throw encryptedResult.error;
|
||||
const encryptedInventory = artifactInventoryResultSchema.parse(
|
||||
encryptedResult.value.result,
|
||||
);
|
||||
expect(encryptedInventory.occurrences).toContainEqual(
|
||||
expect.objectContaining({
|
||||
logical_path: "secret.txt",
|
||||
encrypted: true,
|
||||
hash_status: "unavailable",
|
||||
}),
|
||||
);
|
||||
|
||||
const corruptPath = join(root, "corrupt-crc.zip");
|
||||
const corruptWriter = new ZipWriter(new Uint8ArrayWriter());
|
||||
await corruptWriter.add("data.txt", new TextReader("evidence\n"));
|
||||
await writeFile(corruptPath, await corruptWriter.close());
|
||||
const bytes = await readFile(corruptPath);
|
||||
const localFileHeaderSignature = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
||||
const localHeader = bytes.indexOf(localFileHeaderSignature);
|
||||
if (localHeader < 0) throw new Error("Expected a ZIP local-file header");
|
||||
const dataOffset =
|
||||
localHeader +
|
||||
30 +
|
||||
bytes.readUInt16LE(localHeader + 26) +
|
||||
bytes.readUInt16LE(localHeader + 28);
|
||||
const firstByte = bytes[dataOffset];
|
||||
if (firstByte === undefined) throw new Error("Expected ZIP member bytes");
|
||||
bytes[dataOffset] = firstByte ^ 1;
|
||||
await writeFile(corruptPath, bytes);
|
||||
|
||||
const corruptTarget = await parseBinaryTarget(corruptPath);
|
||||
if (!corruptTarget.ok) throw corruptTarget.error;
|
||||
const corruptResult = await new ArtifactProvider()
|
||||
.createClient(corruptTarget.value)
|
||||
.execute("inventory_artifact", {});
|
||||
expect(corruptResult).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "ArtifactOperationError", reason: "io" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a truncated ZIP through artifact inventory", async () => {
|
||||
const root = await createTestTempDirectory("rea-zip-malformed-");
|
||||
const path = join(root, "truncated.zip");
|
||||
await writeFile(path, Buffer.from([0x50, 0x4b, 0x03, 0x04]));
|
||||
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
const result = await new ArtifactProvider()
|
||||
.createClient(target.value)
|
||||
.execute("inventory_artifact", {});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "ArtifactOperationError", reason: "io" },
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue