mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
@@ -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-");
|
||||
|
||||
Reference in New Issue
Block a user