* feat(artifacts): read Godot PCK packs Add a Godot Engine PCK artifact reader for standalone packs and packs embedded behind an executable trailer (formats 2 and 3). Members are uncompressed byte ranges verified against their declared MD5 on read. Encrypted directories, sparse bundles, patch removal entries, unknown flags and format 1 fail closed as unavailable; encrypted members are reported without being read. inspect-artifact and extract-artifact recognize standalone packs by their GDPC magic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(artifacts): bound Godot PCK metadata and own member streams Cap member paths at 4096 bytes and the retained directory at 64 MiB before allocation. Read members with positional chunk reads through pipeline so cancellation is typed, abandoned consumers release their source, and the shared descriptor stays reader-owned. Retain the root descriptor for godot-pck inventory, as for ZIP and universal Mach-O. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(artifacts): complete Godot PCK facts, integrity and selected views * fix(artifacts): preserve atomic-root inventory Evidence * fix(artifacts): bound aggregate PCK member expansion --------- Co-authored-by: Samucahub <151155051+Samucahub@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
340 lines
11 KiB
TypeScript
340 lines
11 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import {
|
|
buildAliasedGodotPck,
|
|
buildGodotPck,
|
|
pckU64,
|
|
} from "../fixtures/godotPck.js";
|
|
import { elf } from "../../src/domain/binaryTarget.fixture.js";
|
|
import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js";
|
|
import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js";
|
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
|
import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js";
|
|
import {
|
|
artifactExtractionResultSchema,
|
|
artifactInventoryResultSchema,
|
|
} from "../../src/domain/artifactGraph.js";
|
|
|
|
const members = [
|
|
{ path: "res://scripts/main.gd", data: "extends Node\n" },
|
|
{ path: "res://data/玩家.txt", data: "玩家数据\n" },
|
|
];
|
|
const graphFrom = (reply: unknown) => {
|
|
const evidence = parseEvidence(reply);
|
|
const inspection = artifactInspectionResultSchema.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
return artifactInventoryResultSchema.parse(
|
|
inspection.substeps[0]?.evidence.normalized_result,
|
|
);
|
|
};
|
|
|
|
cliTest.for([2, 3, 4] as const)(
|
|
"inspects and safely extracts format %s PCK through the compiled CLI",
|
|
async (version, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-cli-");
|
|
const source = join(root, "game.pck");
|
|
await writeFile(source, buildGodotPck(version, members));
|
|
const inspected = await cli.run({
|
|
arguments: ["inspect-artifact", source, "--json"],
|
|
});
|
|
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
|
|
const graph = graphFrom(inspected.json);
|
|
expect(graph.coverage).toEqual({
|
|
scope: "container-entries",
|
|
status: "complete",
|
|
});
|
|
const rootRow = graph.occurrences.find((row) => row.logical_path === ".");
|
|
expect(rootRow?.format_metadata).toMatchObject({
|
|
kind: "godot-pck",
|
|
format_version: version,
|
|
declared_file_count: 2,
|
|
});
|
|
for (const [index, member] of members.entries()) {
|
|
const row = graph.occurrences.find(
|
|
(row) => row.logical_path === member.path.slice(6),
|
|
);
|
|
expect(row?.declared_integrity).toEqual({
|
|
algorithm: "md5",
|
|
digest: createHash("md5").update(member.data).digest("hex"),
|
|
});
|
|
expect(row?.format_metadata).toMatchObject({
|
|
index,
|
|
source_path: member.path,
|
|
flags: 0,
|
|
});
|
|
expect(row?.hash_status).toBe("verified");
|
|
}
|
|
const extracted = await cli.run({
|
|
arguments: ["extract-artifact", source, "--json"],
|
|
environment: { TMPDIR: root, TMP: root, TEMP: root },
|
|
});
|
|
expect(extracted.exitCode, extracted.stdout + extracted.stderr).toBe(0);
|
|
const result = artifactExtractionResultSchema.parse(
|
|
parseEvidence(extracted.json).normalized_result,
|
|
);
|
|
expect(result.containment_verified).toBe(true);
|
|
for (const member of members)
|
|
expect(
|
|
await readFile(join(result.output_root, member.path.slice(6)), "utf8"),
|
|
).toBe(member.data);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"reads an embedded pack while preserving the native carrier's content classification",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-embedded-cli-");
|
|
const source = join(root, "game.x86_64");
|
|
const pack = buildGodotPck(4, members);
|
|
const carrier = elf(2, 1, 62);
|
|
const bytes = Buffer.concat([
|
|
carrier,
|
|
pack,
|
|
pckU64(pack.length),
|
|
Buffer.from("GDPC"),
|
|
]);
|
|
await writeFile(source, bytes);
|
|
const reply = await cli.run({
|
|
arguments: ["inspect-artifact", source, "--json"],
|
|
});
|
|
expect(reply.exitCode, reply.stdout + reply.stderr).toBe(0);
|
|
const graph = graphFrom(reply.json);
|
|
expect(graph.manifest.root_sha256).toBe(
|
|
createHash("sha256").update(bytes).digest("hex"),
|
|
);
|
|
expect(
|
|
graph.nodes.find(
|
|
(row) => row.artifact_id === graph.manifest.root_artifact_id,
|
|
)?.format,
|
|
).toBe("elf");
|
|
expect(
|
|
graph.occurrences.find((row) => row.logical_path === ".")
|
|
?.format_metadata,
|
|
).toMatchObject({
|
|
embedded: true,
|
|
pack_start: carrier.length,
|
|
pack_end: carrier.length + pack.length,
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"applies strict and record-and-continue MD5 integrity to inspection and extraction",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-integrity-cli-");
|
|
const source = join(root, "contradiction.pck");
|
|
await writeFile(
|
|
source,
|
|
buildGodotPck(4, [
|
|
{ path: "res://main.gd", data: "real", md5: Buffer.alloc(16, 1) },
|
|
]),
|
|
);
|
|
const rejected = await cli.run({
|
|
arguments: ["inspect-artifact", source, "--json"],
|
|
});
|
|
expect(rejected.exitCode).toBe(1);
|
|
expect(rejected.json).toMatchObject({
|
|
code: "artifact_integrity_mismatch",
|
|
details: {
|
|
algorithm: "md5",
|
|
logical_path: "main.gd",
|
|
declared_md5: "01".repeat(16),
|
|
calculated_md5: createHash("md5").update("real").digest("hex"),
|
|
},
|
|
});
|
|
const inspected = await cli.run({
|
|
arguments: [
|
|
"inspect-artifact",
|
|
source,
|
|
"--integrity-policy",
|
|
"record-and-continue",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
|
|
const graph = graphFrom(inspected.json);
|
|
expect(graph.integrity_contradictions).toEqual([
|
|
expect.objectContaining({
|
|
algorithm: "md5",
|
|
logical_path: "main.gd",
|
|
declared_md5: "01".repeat(16),
|
|
observed_md5: createHash("md5").update("real").digest("hex"),
|
|
trust: "observed-untrusted",
|
|
}),
|
|
]);
|
|
expect(
|
|
graph.occurrences.find((row) => row.logical_path === "main.gd")
|
|
?.hash_status,
|
|
).toBe("mismatched");
|
|
const extracted = await cli.run({
|
|
arguments: [
|
|
"extract-artifact",
|
|
source,
|
|
"--integrity-policy",
|
|
"record-and-continue",
|
|
"--json",
|
|
],
|
|
environment: { TMPDIR: root, TMP: root, TEMP: root },
|
|
});
|
|
expect(extracted.exitCode, extracted.stdout + extracted.stderr).toBe(0);
|
|
const result = artifactExtractionResultSchema.parse(
|
|
parseEvidence(extracted.json).normalized_result,
|
|
);
|
|
expect(result.integrity_contradictions).toHaveLength(1);
|
|
expect(await readFile(join(result.output_root, "main.gd"), "utf8")).toBe(
|
|
"real",
|
|
);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"retains encrypted-directory declarations without claiming an empty package",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-encrypted-cli-");
|
|
const source = join(root, "encrypted.pck");
|
|
const bytes = buildGodotPck(4, members, { packFlags: 3 });
|
|
const directory = Number(bytes.readBigUInt64LE(32));
|
|
bytes.fill(0xff, directory + 4);
|
|
await writeFile(source, bytes);
|
|
const inspected = await cli.run({
|
|
arguments: ["inspect-artifact", source, "--json"],
|
|
});
|
|
expect(inspected.exitCode, inspected.stdout + inspected.stderr).toBe(0);
|
|
const graph = graphFrom(inspected.json);
|
|
expect(graph.coverage).toEqual({
|
|
scope: "container-entries",
|
|
status: "unknown",
|
|
});
|
|
expect(graph.occurrences).toHaveLength(1);
|
|
expect(graph.occurrences[0]).toMatchObject({
|
|
encrypted: true,
|
|
format_metadata: {
|
|
directory_status: "encrypted",
|
|
declared_file_count: 2,
|
|
},
|
|
});
|
|
const extracted = await cli.run({
|
|
arguments: ["extract-artifact", source, "--json"],
|
|
environment: { TMPDIR: root, TMP: root, TEMP: root },
|
|
});
|
|
expect(extracted.exitCode).toBe(1);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"exports compact CLI delivery and queries its retained complete Evidence after unlink",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-export-");
|
|
const source = join(root, "game.pck");
|
|
const bundle = join(root, "evidence.json");
|
|
const refused = await cli.run({
|
|
arguments: ["inspect-artifact", source, "--detail", "summary", "--json"],
|
|
});
|
|
expect(refused.exitCode).toBe(1);
|
|
expect(refused.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { issues: [{ path: ["evidence_output"] }] },
|
|
});
|
|
await writeFile(source, buildGodotPck(4, members));
|
|
const args = [
|
|
"inspect-artifact",
|
|
source,
|
|
"--detail",
|
|
"summary",
|
|
"--evidence_output",
|
|
bundle,
|
|
"--json",
|
|
];
|
|
const reply = await cli.run({ arguments: args });
|
|
expect(reply.exitCode, reply.stdout + reply.stderr).toBe(0);
|
|
expect(reply.json).toMatchObject({
|
|
evidence_bundle: { path: bundle, bytes: expect.any(Number) },
|
|
summary: {
|
|
normalized_result: {
|
|
kind: "summary",
|
|
summary: { root_format_metadata: { format_version: 4 } },
|
|
},
|
|
},
|
|
});
|
|
const saved = await readEvidenceBundle(bundle);
|
|
if (!saved.ok) throw saved.error;
|
|
const evidence = saved.value.records[0];
|
|
if (evidence === undefined) throw new Error("Missing complete inspection");
|
|
expect(graphFrom(evidence).occurrences).toHaveLength(3);
|
|
const before = await readFile(bundle);
|
|
expect((await cli.run({ arguments: args })).exitCode).toBe(1);
|
|
expect(await readFile(bundle)).toEqual(before);
|
|
await unlink(source);
|
|
const query = join(root, "query.json");
|
|
await writeFile(
|
|
query,
|
|
JSON.stringify({
|
|
source: { kind: "inline", evidence },
|
|
view: {
|
|
kind: "inventory",
|
|
collection: "occurrences",
|
|
logical_paths: ["scripts/main.gd"],
|
|
fields: ["logical_path"],
|
|
},
|
|
}),
|
|
);
|
|
const selected = await cli.run({
|
|
arguments: ["inspect-analysis-view", query, "--json"],
|
|
});
|
|
expect(selected.exitCode, selected.stdout + selected.stderr).toBe(0);
|
|
const view = analysisViewResultSchema.parse(
|
|
parseEvidence(selected.json).normalized_result,
|
|
);
|
|
if (view.kind !== "inventory") throw new Error("Expected inventory view");
|
|
expect(view.parent_evidence_id).toBe(evidence.evidence_id);
|
|
expect(view.items).toEqual([
|
|
expect.objectContaining({
|
|
logical_path: "scripts/main.gd",
|
|
occurrence_id: expect.any(String),
|
|
artifact_id: expect.any(String),
|
|
parent_occurrence_id: expect.any(String),
|
|
}),
|
|
]);
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"rejects repeated-range expansion before hashing members or creating extraction output",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-pck-expansion-");
|
|
const source = join(root, "aliased.pck");
|
|
await writeFile(source, buildAliasedGodotPck(4, 1100));
|
|
for (const operation of ["inspect-artifact", "extract-artifact"]) {
|
|
for (const policy of ["fail", "record-and-continue"]) {
|
|
const reply = await cli.run({
|
|
arguments: [
|
|
operation,
|
|
source,
|
|
"--integrity-policy",
|
|
policy,
|
|
"--json",
|
|
],
|
|
environment: { TMPDIR: root, TMP: root, TEMP: root },
|
|
});
|
|
expect(reply.exitCode, reply.stdout + reply.stderr).toBe(1);
|
|
expect(reply.json).toMatchObject({
|
|
code: "truncated",
|
|
details: {
|
|
reason: "limit",
|
|
truncated: true,
|
|
detail: expect.stringContaining("expansion budget"),
|
|
},
|
|
});
|
|
expect(await readdir(root)).toEqual(["aliased.pck"]);
|
|
}
|
|
}
|
|
},
|
|
30000,
|
|
);
|