Files
reverse-engineering-anything/tests/acceptance/artifactComparisonFit.test.ts
T
d39b3000ec feat(artifacts): inventory and extract U-Boot FIT images (#7771)
* feat(artifacts): inventory and extract U-Boot FIT images

open_binary refused U-Boot Flattened Image Tree files as an unsupported
format, so inspect_artifact and extract_artifact could not list or
recover a FIT's kernel, devicetree, ramdisk and loadable images.

Recognize an FDT v16/v17 with a top-level /images node and parse it with
the existing parseDevicetree. Each /images child becomes an inventory
entry named images/<unit-name>.bin that extracts its stored bytes from
embedded data, data-offset (past the 4-aligned devicetree end) or
data-position (from the file start). Image format_metadata records the
declared type, arch, os, compression, load and entry; root metadata
records the description, timestamp and configurations.

crc32, md5, sha1, sha256, sha384 and sha512 hash nodes are verified in
one streamed pass per image; other algorithms are reported unverified.
Signatures are reported, not verified, and nothing is decompressed,
decrypted or executed. Data outside the file, overlapping or duplicate
image data, a missing or repeated data source and hash mismatches fail
closed. The devicetree is bounded by the fixed 16 MiB allowance;
external image data is streamed from disk.

Refs #7129

* fix(artifacts): bound FIT configuration references and suggest analysis only for plain images

* fix(artifacts): retain buffered FIT stream cancellation

* test(artifacts): gate FIT faults on the observed read phase

* fix(artifacts): verify FIT links and summary identities

* fix(artifacts): bound FIT diagnostics and retain unknown signatures

* fix(artifacts): compare FIT declarations and resolved data links

* fix(artifacts): admit FIT raw probes through the shared loader contract

* fix(artifacts): preserve FIT field availability independently

* fix(artifacts): isolate nested entry metadata from reader facts

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+users.noreply.github.com>
2026-10-29 13:33:26 +08:00

160 lines
5.5 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { fitFixture } from "../../src/artifacts/fit/FitImage.fixture.js";
import {
cells,
dtString,
} from "../../src/firmware/devicetree/Devicetree.fixture.js";
import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js";
import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js";
import { artifactInventoryResultSchema } from "../../src/domain/artifactGraph.js";
import { parseEvidence, type Evidence } from "../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
it("compares FIT declarations while preserving layout independence and unknowns", async () => {
const root = await createTestTempDirectory("rea-fit-comparison-");
const { call } = await connectLocalToolsMcp({
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_TOOL_FAMILIES: "artifact,application,session",
});
const properties = {
type: dtString("kernel"),
arch: dtString("arm64"),
compression: dtString("none"),
load: cells(0x400000),
entry: cells(0x400004),
};
const image = {
name: "kernel",
data: Buffer.from("payload"),
hashes: ["sha256"],
};
const inspect = async (
name: string,
overrides: Record<string, Buffer> = {},
options: {
mode?: "embedded" | "position";
prefix?: boolean;
omitLoad?: boolean;
unsupportedHash?: boolean;
} = {},
): Promise<Evidence> => {
const path = join(root, name);
const { load: _load, ...withoutLoad } = properties;
await writeFile(
path,
fitFixture({
images: [
...(options.prefix
? [{ name: "prefix", data: Buffer.from("prefix") }]
: []),
{
...image,
properties: {
...(options.omitLoad ? withoutLoad : properties),
...overrides,
},
...(options.unsupportedHash
? {
children: [
{
name: "hash-unsupported",
properties: {
algo: dtString("crc16-ccitt"),
value: Buffer.alloc(2),
},
},
],
}
: {}),
},
],
mode: options.mode ?? "embedded",
}),
);
const opened = await call("open_binary", { path });
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const response = await call("inspect_artifact", {});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
return parseEvidence(response.structuredContent);
};
const compare = async (left: Evidence, right: Evidence) => {
const response = await call("compare_artifacts", { left, right });
expect(response.isError, JSON.stringify(response)).not.toBe(true);
return artifactComparisonResultSchema.parse(
parseEvidence(response.structuredContent).normalized_result,
);
};
const occurrence = (evidence: Evidence) => {
const inspection = artifactInspectionResultSchema.parse(
evidence.normalized_result,
);
const row = artifactInventoryResultSchema
.parse(inspection.substeps[0]?.evidence.normalized_result)
.occurrences.find((row) => row.logical_path === "images/kernel.bin");
if (row === undefined) throw new Error("Missing kernel occurrence");
return row;
};
const left = await inspect("base.itb");
for (const field of ["load", "entry"] as const) {
const right = await inspect(field + ".itb", { [field]: cells(0x500000) });
expect(occurrence(right).artifact_id).toBe(occurrence(left).artifact_id);
const result = await compare(left, right);
expect(
result.changes.filter((row) => row.logical_path === "images/kernel.bin"),
).toMatchObject([{ classification: "changed", dimensions: ["metadata"] }]);
}
const relocated = await inspect(
"relocated.itb",
{},
{ mode: "position", prefix: true },
);
expect(occurrence(relocated).artifact_id).toBe(occurrence(left).artifact_id);
expect(
(await compare(left, relocated)).changes.filter(
(row) => row.logical_path === "images/kernel.bin",
),
).toEqual([]);
const unknownLeft = await inspect("unknown-left.itb", {
load: cells(1, 2, 3),
});
const unknownRight = await inspect("unknown-right.itb", {
load: cells(4, 5, 6),
});
expect(
(await compare(unknownLeft, unknownRight)).changes.filter(
(row) => row.logical_path === "images/kernel.bin",
),
).toMatchObject([
{ classification: "unknown", dimensions: ["availability"] },
]);
const partialChange = await inspect("partial-entry.itb", {
load: cells(4, 5, 6),
entry: cells(0x600000),
});
expect(
(await compare(unknownLeft, partialChange)).changes.filter(
(row) => row.logical_path === "images/kernel.bin",
),
).toMatchObject([
{ classification: "changed", dimensions: ["metadata", "availability"] },
]);
const absent = await inspect(
"warn-absent.itb",
{},
{ omitLoad: true, unsupportedHash: true },
);
const present = await inspect(
"warn-present.itb",
{},
{ unsupportedHash: true },
);
expect(
(await compare(absent, present)).changes.filter(
(row) => row.logical_path === "images/kernel.bin",
),
).toMatchObject([{ classification: "changed", dimensions: ["metadata"] }]);
});