fix(artifacts): recognize empty Zip64 containers

Fixes #6540
This commit is contained in:
morluto
2026-10-23 16:30:34 +08:00
parent bcd1f95e68
commit e44ff4666a
2 changed files with 82 additions and 33 deletions
+2 -1
View File
@@ -17,13 +17,14 @@ export const isZipPackageFormat = (
): format is ZipPackageFormat =>
ZIP_PACKAGE_FORMATS.some((candidate) => candidate === format);
/** Recognize local-header, empty-archive, or split-archive ZIP signatures, not archive validity. */
/** Recognize local-header, classic/Zip64 empty-archive, or split ZIP signatures, not validity. */
export const hasZipSignature = (bytes: Uint8Array): boolean =>
bytes.length >= 4 &&
bytes[0] === 0x50 &&
bytes[1] === 0x4b &&
((bytes[2] === 0x03 && bytes[3] === 0x04) ||
(bytes[2] === 0x05 && bytes[3] === 0x06) ||
(bytes[2] === 0x06 && bytes[3] === 0x06) ||
(bytes[2] === 0x07 && bytes[3] === 0x08));
const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
@@ -1,8 +1,9 @@
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, rm, 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 { expect, it, onTestFinished } from "vitest";
import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js";
import { inventoryArtifact } from "../../fixtures/artifactInventory.js";
@@ -23,27 +24,69 @@ import {
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
it("completes an empty ZIP extraction with an empty identity", async () => {
const root = await createTestTempDirectory("rea-empty-artifact-extraction-");
const archive = join(root, "empty.zip");
await writeOrderedZip(archive, []);
const archiveTarget = await parseBinaryTarget(archive);
if (!archiveTarget.ok) throw archiveTarget.error;
const output = join(root, "output");
const result = await withArtifactClient(archiveTarget.value, (client) =>
client.execute("extract_artifact", { output_root: output }),
for (const zip64 of [false, true]) {
cliTest(
`inspects and extracts an empty ${zip64 ? "Zip64" : "classic ZIP"} without changing its identity`,
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-empty-artifact-extraction-",
);
const archive = join(root, "empty.zip");
const writer = new ZipWriter(new Uint8ArrayWriter(), {
zip64,
supportZip64SplitFile: false,
});
const bytes = Buffer.from(await writer.close());
await writeFile(archive, bytes);
const digest = createHash("sha256").update(bytes).digest("hex");
const inventory = await inventoryArtifact(archive);
expect(inventory.manifest).toMatchObject({
root_sha256: digest,
root_format: "zip",
});
expect(inventory.coverage).toEqual({
scope: "container-entries",
status: "complete",
});
const inspected = await cli.run({
arguments: ["inspect-artifact", archive, "--json"],
});
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
const inspection = artifactInspectionResultSchema.parse(
parseEvidence(inspected.json).normalized_result,
);
const retained = artifactInventoryResultSchema.parse(
inspection.substeps[0]?.evidence?.normalized_result,
);
expect(retained.manifest).toMatchObject({
root_sha256: digest,
root_format: "zip",
});
const extracted = await cli.run({
arguments: ["extract-artifact", archive, "--json"],
});
expect(extracted.exitCode, extracted.stdout + extracted.stderr).toBe(0);
const extraction = artifactExtractionResultSchema.parse(
parseEvidence(extracted.json).normalized_result,
);
onTestFinished(() =>
rm(extraction.output_root, { recursive: true, force: true }),
);
expect(extraction.manifest).toMatchObject({
root_sha256: digest,
root_format: "zip",
});
expect(extraction.extraction_manifest).toMatchObject({
source_manifest_id: extraction.manifest.manifest_id,
selected_occurrence_ids: [],
});
expect(extraction.artifacts).toEqual([]);
expect(extraction.containment_verified).toBe(true);
expect(await readdir(extraction.output_root)).toEqual([]);
expect(await readFile(archive)).toEqual(bytes);
},
);
if (!result.ok) throw result.error;
const extraction = artifactExtractionResultSchema.parse(result.value.result);
expect(extraction.extraction_manifest).toMatchObject({
source_manifest_id: extraction.manifest.manifest_id,
selected_occurrence_ids: [],
});
expect(extraction.artifacts).toEqual([]);
expect(extraction.containment_verified).toBe(true);
expect(await readdir(output)).toEqual([]);
});
}
const legacyControlNames = [
["unflagged UTF-8-looking legacy bytes", Buffer.from("café"), false, "caf├⌐"],
@@ -573,17 +616,22 @@ it("preserves encrypted-member and corrupt-CRC observations", async () => {
);
});
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]));
it.each([0x04034b50, 0x06064b50])(
"rejects a truncated ZIP signature %i through artifact inventory",
async (signature) => {
const root = await createTestTempDirectory("rea-zip-malformed-");
const path = join(root, "truncated.zip");
const bytes = Buffer.alloc(4);
bytes.writeUInt32LE(signature);
await writeFile(path, bytes);
await expectInventoryFailure(
path,
"format",
"ZIP archive is malformed: File format is not recognized",
);
});
await expectInventoryFailure(
path,
"format",
"ZIP archive is malformed: File format is not recognized",
);
},
);
it("reports an unsupported ZIP compression method as unavailable", async () => {
const root = await createTestTempDirectory("rea-zip-method-");