mirror of
https://github.com/morluto/rea.git
synced 2026-10-19 04:19:54 -05:00
* fix(flutter): reject ambiguous mappings and bound ELF notes Resolve PT_LOAD interpretations once for symbols and dynamic tables, including partial overlaps. Preserve equivalent mappings and leave conflicting or invalid alternate definitions unresolved. Bound distinct nonempty ELF note ranges before retention and reads, deduplicate shared ranges, preserve observed build-ID candidates and report partial coverage on exhaustion. Correct shared verifier workspace ownership and structured Windows path assertions. Addresses the two post-merge review findings on PR #1582. Validation: 147 focused cases across Linux, macOS Sonoma 14.8.9 and non-administrator Windows 11 build 26300.9457; real Gallery CLI/MCP checks on all three hosts; six compiler-produced ELF32/ELF64 hash-layout cases; static, docs, discovery and duplication checks. Full string-pool projections remain explicitly bounded. * test(flutter): parse complete facts through named result schemas --------- Co-authored-by: me <me@medz.org> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
248 lines
8.6 KiB
TypeScript
248 lines
8.6 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { expect } from "vitest";
|
|
import { toolContract } from "../../src/contracts/toolContracts.js";
|
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
|
import { flutterResultSchemas } from "../../src/domain/flutter/flutterBuildAnalysis.js";
|
|
import {
|
|
aotFixtureImage,
|
|
engineFixtureImage,
|
|
REFERENCE_HASH,
|
|
appendElfRanges,
|
|
} from "../../src/flutter/FlutterBuild.fixture.js";
|
|
import {
|
|
createFlutterApk,
|
|
createDuplicateFlutterApk,
|
|
} from "../fixtures/flutterApk.js";
|
|
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
|
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
|
|
cliTest(
|
|
"CLI and stdio MCP retain Flutter identity, selected ABI and duplicate-member refusals",
|
|
async ({ cli }) => {
|
|
const image = aotFixtureImage();
|
|
Buffer.from("Login\0").copy(image, 0x2300);
|
|
const engine = engineFixtureImage({
|
|
notes: Buffer.from(
|
|
"040000000800000003000000474e55000123456789abcdef",
|
|
"hex",
|
|
),
|
|
prefix: Buffer.from(
|
|
"040000000800000003000000474e55001111111111111111",
|
|
"hex",
|
|
),
|
|
labels: ["Android (test) clang version 9.0.8"],
|
|
});
|
|
const path = await createFlutterApk("selected target.apk", [
|
|
{ path: "lib/arm64-v8a/libapp.so", bytes: image },
|
|
{
|
|
path: "lib/arm64-v8a/libflutter.so",
|
|
bytes: engine,
|
|
},
|
|
{ path: "lib/x86_64/libapp.so", bytes: image },
|
|
{
|
|
path: "lib/x86_64/libflutter.so",
|
|
bytes: engine,
|
|
},
|
|
]);
|
|
const { call } = await connectLocalToolsMcp();
|
|
for (const [name, abi] of [
|
|
["identify_flutter_build", undefined],
|
|
["inspect_dart_aot", undefined],
|
|
["inspect_dart_aot", "x86_64"],
|
|
] as const) {
|
|
const result = await cli.run({
|
|
arguments: [
|
|
name.replaceAll("_", "-"),
|
|
path,
|
|
"--json",
|
|
...(abi === undefined ? [] : ["--abi", abi]),
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract(name).outputSchema.parse(result.json);
|
|
const complete = flutterResultSchemas[name].parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const response = await call(name, {
|
|
path,
|
|
...(abi === undefined ? {} : { abi }),
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
const mcp = parseEvidence(response.structuredContent);
|
|
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
|
|
expect(mcp.subject).toEqual(evidence.subject);
|
|
expect(mcp.subject).toMatchObject({
|
|
local_path: path,
|
|
format: "apk",
|
|
digest: { sha256: complete.target.sha256 },
|
|
});
|
|
expect(mcp.evidence_id).toBe(evidence.evidence_id);
|
|
if (name === "inspect_dart_aot") {
|
|
expect(evidence.normalized_result).toMatchObject({
|
|
abi: abi ?? "arm64-v8a",
|
|
libapp: {
|
|
coverage: "complete",
|
|
snapshot_hash: REFERENCE_HASH,
|
|
sha256: createHash("sha256").update(image).digest("hex"),
|
|
},
|
|
string_pool: {
|
|
coverage: "complete",
|
|
class_like_tokens: expect.arrayContaining(["Login"]),
|
|
},
|
|
});
|
|
} else {
|
|
const identified = toolContract(
|
|
"identify_flutter_build",
|
|
).outputSchema.parse(result.json).normalized_result;
|
|
expect(identified.coverage).toBe("complete");
|
|
for (const abi of identified.abis) {
|
|
expect(abi.libflutter).toMatchObject({
|
|
build_id: "0123456789abcdef",
|
|
build_id_candidates: ["0123456789abcdef"],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const duplicate = await createDuplicateFlutterApk();
|
|
for (const name of [
|
|
"identify_flutter_build",
|
|
"inspect_dart_aot",
|
|
] as const) {
|
|
const result = await cli.run({
|
|
arguments: [name.replaceAll("_", "-"), duplicate, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
const mcp = parseMcpToolError(await call(name, { path: duplicate }));
|
|
expect(mcp.error).toEqual(result.json);
|
|
expect(mcp.error).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { issues: [{ message: expect.stringContaining(duplicate) }] },
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"CLI and MCP retain unsupported snapshot kinds as partial evidence",
|
|
async ({ cli }) => {
|
|
const image = aotFixtureImage();
|
|
for (const offset of [0x1000, 0x2000])
|
|
image.writeBigUInt64LE(4n, offset + 12);
|
|
const path = await createFlutterApk("unsupported-kind.apk", [
|
|
{ path: "lib/arm64-v8a/libapp.so", bytes: image },
|
|
]);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-dart-aot", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract("inspect_dart_aot").outputSchema.parse(
|
|
result.json,
|
|
);
|
|
const complete = flutterResultSchemas.inspect_dart_aot.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const { call } = await connectLocalToolsMcp();
|
|
const response = await call("inspect_dart_aot", { path });
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
expect(parseEvidence(response.structuredContent).normalized_result).toEqual(
|
|
evidence.normalized_result,
|
|
);
|
|
expect(evidence.normalized_result).toMatchObject({
|
|
coverage: "partial",
|
|
libapp: { coverage: "partial", snapshot_hash: REFERENCE_HASH },
|
|
string_pool: { coverage: "complete" },
|
|
});
|
|
expect(
|
|
complete.libapp.sections
|
|
.filter((section) => section.name.endsWith("Data"))
|
|
.map((section) => section.kind),
|
|
).toEqual([4, 4]);
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"CLI and MCP qualify conflicting load mappings and bounded ELF notes",
|
|
async ({ cli }) => {
|
|
const note = Buffer.from(
|
|
"040000000800000003000000474e55000123456789abcdef",
|
|
"hex",
|
|
);
|
|
const engine = engineFixtureImage({
|
|
notes: Buffer.concat([note, Buffer.alloc(2000)]),
|
|
});
|
|
const denseEngine = appendElfRanges(
|
|
engine,
|
|
"program",
|
|
Array.from({ length: 2000 }, (_, index) => ({
|
|
type: 4,
|
|
offset: 0x400 + note.length + index,
|
|
size: 1,
|
|
})),
|
|
);
|
|
const { call } = await connectLocalToolsMcp();
|
|
for (const conflict of [false, true]) {
|
|
const image = appendElfRanges(aotFixtureImage(), "program", [
|
|
{
|
|
type: 1,
|
|
address: 0x2010,
|
|
offset: conflict ? 0x1010 : 0x2010,
|
|
size: 4,
|
|
},
|
|
]);
|
|
const path = await createFlutterApk(`load-${String(conflict)}.apk`, [
|
|
{ path: "lib/x86_64/libapp.so", bytes: image },
|
|
{
|
|
path: "lib/x86_64/libflutter.so",
|
|
bytes: conflict ? denseEngine : engine,
|
|
},
|
|
]);
|
|
for (const name of [
|
|
"inspect_dart_aot",
|
|
"identify_flutter_build",
|
|
] as const) {
|
|
const result = await cli.run({
|
|
arguments: [name.replaceAll("_", "-"), path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract(name).outputSchema.parse(result.json);
|
|
const response = await call(name, { path });
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
const mcp = parseEvidence(response.structuredContent);
|
|
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
|
|
expect(mcp.evidence_id).toBe(evidence.evidence_id);
|
|
expect(mcp.subject).toEqual(evidence.subject);
|
|
expect(mcp.limitations).toEqual(evidence.limitations);
|
|
expect(evidence.normalized_result.coverage).toBe(
|
|
conflict ? "partial" : "complete",
|
|
);
|
|
if (name === "inspect_dart_aot") {
|
|
const facts = flutterResultSchemas[name].parse(
|
|
evidence.normalized_result,
|
|
);
|
|
expect(facts.libapp.sections).toHaveLength(conflict ? 3 : 4);
|
|
expect(facts.string_pool.coverage).toBe(
|
|
conflict ? "partial" : "complete",
|
|
);
|
|
if (conflict) {
|
|
expect(facts.string_pool.package_uris).toEqual([]);
|
|
expect(evidence.limitations.join("\n")).toContain(
|
|
"conflicting PT_LOAD",
|
|
);
|
|
}
|
|
} else {
|
|
const facts = flutterResultSchemas[name].parse(
|
|
evidence.normalized_result,
|
|
);
|
|
expect(facts.abis[0]?.libflutter).toMatchObject({
|
|
build_id: conflict ? null : "0123456789abcdef",
|
|
build_id_candidates: ["0123456789abcdef"],
|
|
});
|
|
if (conflict)
|
|
expect(evidence.limitations.join("\n")).toContain("note-range");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
);
|