mirror of
https://github.com/morluto/rea.git
synced 2026-10-21 21:32:26 -05:00
907 lines
29 KiB
TypeScript
907 lines
29 KiB
TypeScript
import { desktopSummarySchema } from "../../src/domain/analysisView/desktopApplicationView.js";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
mkdir,
|
|
readFile,
|
|
realpath,
|
|
symlink,
|
|
unlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it } from "vitest";
|
|
import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js";
|
|
import { toolContract } from "../../src/contracts/toolContracts.js";
|
|
import {
|
|
analysisViewResultSchema,
|
|
il2cppMetadataSummaryViewResultSchema,
|
|
} from "../../src/domain/analysisView/analysisView.js";
|
|
import { createEvidence, parseEvidence } from "../../src/domain/evidence.js";
|
|
import {
|
|
createEvidenceBundle,
|
|
parseEvidenceBundle,
|
|
} from "../../src/domain/evidenceBundle.js";
|
|
import { jsonObjectSchema } from "../../src/domain/jsonValue.js";
|
|
import { il2cppMetadataSchema } from "../../src/domain/il2cpp/il2cppMetadata.js";
|
|
import {
|
|
createIl2cppMetadataFixture,
|
|
FIXTURE_IMAGES,
|
|
FIXTURE_METHODS,
|
|
FIXTURE_TYPES,
|
|
} from "../fixtures/il2cpp/metadata.js";
|
|
import {
|
|
createElfFixture,
|
|
createIl2cppBinaryFixture,
|
|
createUniversalFixture,
|
|
FIXTURE_BINARY_BASE,
|
|
FIXTURE_METHOD_ADDRESSES,
|
|
FIXTURE_RETURN_TYPE_NAME,
|
|
} from "../fixtures/il2cpp/binary.js";
|
|
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
|
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
|
|
const parseCliMetadata = (value: unknown) =>
|
|
il2cppMetadataSchema.parse(
|
|
toolContract("inspect_il2cpp_metadata").outputSchema.parse(value)
|
|
.normalized_result,
|
|
);
|
|
const parseMetadataSummary = (value: unknown) =>
|
|
il2cppMetadataSummaryViewResultSchema.parse(
|
|
parseEvidence(value).normalized_result,
|
|
);
|
|
|
|
cliTest(
|
|
"returns identical complete inventories through CLI and MCP and pages a retained summary after removal",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-metadata-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const { bytes } = createIl2cppMetadataFixture({
|
|
typeIndexWidth: 2,
|
|
propertiesAndEvents: true,
|
|
});
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const evidence = toolContract("inspect_il2cpp_metadata").outputSchema.parse(
|
|
result.json,
|
|
);
|
|
const report = il2cppMetadataSchema.parse(evidence.normalized_result);
|
|
expect(report.magic).toBe(bytes.readUInt32LE(0));
|
|
expect(report.artifact).toEqual({
|
|
path,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes: bytes.length,
|
|
});
|
|
expect(report.images).toEqual(FIXTURE_IMAGES);
|
|
expect(report.types).toEqual(FIXTURE_TYPES);
|
|
expect(report.methods).toEqual(FIXTURE_METHODS);
|
|
expect(evidence.parameters).toEqual({ path });
|
|
|
|
const { client, call } = await connectLocalToolsMcp();
|
|
const complete = await call("inspect_il2cpp_metadata", {
|
|
path,
|
|
detail: "complete",
|
|
});
|
|
expect(complete.isError, JSON.stringify(complete)).not.toBe(true);
|
|
expect(parseEvidence(complete.structuredContent).normalized_result).toEqual(
|
|
report,
|
|
);
|
|
|
|
// The default keeps complete Evidence while returning a compact receipt.
|
|
const omitted = await call("inspect_il2cpp_metadata", { path });
|
|
expect(omitted.isError, JSON.stringify(omitted)).not.toBe(true);
|
|
expect(
|
|
parseMetadataSummary(omitted.structuredContent).summary.counts.methods,
|
|
).toBe(FIXTURE_METHODS.length);
|
|
const summarized = await call("inspect_il2cpp_metadata", {
|
|
path,
|
|
detail: "summary",
|
|
});
|
|
expect(summarized.isError, JSON.stringify(summarized)).not.toBe(true);
|
|
const view = parseMetadataSummary(summarized.structuredContent);
|
|
expect(view.summary.magic).toBe(report.magic);
|
|
expect(view.summary.counts).toEqual({
|
|
sections: 31,
|
|
images: 2,
|
|
types: 4,
|
|
methods: 5,
|
|
nested_types: 1,
|
|
fields: 2,
|
|
properties: 3,
|
|
events: 2,
|
|
parameters: 1,
|
|
string_literals: 2,
|
|
code_gen_modules: null,
|
|
method_addresses: null,
|
|
method_address_states: null,
|
|
return_types: null,
|
|
field_types: null,
|
|
parameter_types: null,
|
|
});
|
|
expect(view.summary).not.toHaveProperty("sections");
|
|
expect(view.summary.binary).toBeNull();
|
|
expect(view.summary.index_widths).toEqual(report.index_widths);
|
|
|
|
// Pages come from retained Evidence, not the file.
|
|
await unlink(path);
|
|
for (const collection of [
|
|
"sections",
|
|
"images",
|
|
"types",
|
|
"methods",
|
|
"fields",
|
|
"properties",
|
|
"events",
|
|
"parameters",
|
|
"string_literals",
|
|
] as const) {
|
|
const rows: unknown[] = [];
|
|
let offset = 0;
|
|
do {
|
|
const page = await call("inspect_analysis_view", {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: view.parent_evidence_id,
|
|
},
|
|
view: { kind: "il2cpp-metadata", collection, offset, limit: 2 },
|
|
});
|
|
expect(page.isError, JSON.stringify(page)).not.toBe(true);
|
|
const projected = analysisViewResultSchema.parse(
|
|
parseEvidence(page.structuredContent).normalized_result,
|
|
);
|
|
if (projected.kind !== "il2cpp-metadata")
|
|
throw new Error("Wrong IL2CPP page kind");
|
|
expect(projected.counts).toEqual(view.summary.counts);
|
|
rows.push(...projected.items);
|
|
offset = projected.coverage.next_offset ?? report[collection].length;
|
|
} while (offset < report[collection].length);
|
|
expect(rows).toEqual(
|
|
report[collection].map((row, index) => ({ index, ...row })),
|
|
);
|
|
}
|
|
await client.ping();
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"distinguishes unsupported versions from corrupt metadata through CLI and MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-metadata-fail-");
|
|
const { client, call } = await connectLocalToolsMcp();
|
|
const fixture = createIl2cppMetadataFixture();
|
|
const unsupported = Buffer.from(fixture.bytes);
|
|
unsupported.writeInt32LE(26, 4);
|
|
const corrupt = Buffer.from(fixture.bytes);
|
|
corrupt.writeInt32LE(
|
|
fixture.section("typeDefinitions").offset + 4,
|
|
fixture.header("images", "offset"),
|
|
);
|
|
for (const [name, bytes, expected] of [
|
|
[
|
|
"v26.dat",
|
|
unsupported,
|
|
{
|
|
code: "unsupported_target",
|
|
details: { operation: "inspect_il2cpp_metadata" },
|
|
message: expect.stringContaining("unsupported-version"),
|
|
},
|
|
],
|
|
[
|
|
"overlap.dat",
|
|
corrupt,
|
|
{
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "inspect_il2cpp_metadata",
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: expect.stringContaining("section-overlap"),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
] as const) {
|
|
const path = join(root, name);
|
|
await writeFile(path, bytes);
|
|
const result = await cli.run({
|
|
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject(expected);
|
|
const response = await call("inspect_il2cpp_metadata", { path });
|
|
expect(parseMcpToolError(response).error).toMatchObject(expected);
|
|
}
|
|
await client.ping();
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"reports a recognized IL2CPP magic with an incomplete version as corrupt truncation through CLI and MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-prefix-fail-");
|
|
const { client, call } = await connectLocalToolsMcp();
|
|
|
|
for (const length of [4, 5, 6, 7]) {
|
|
const path = join(root, `magic-${length}.dat`);
|
|
const bytes = Buffer.alloc(length);
|
|
bytes.writeUInt32LE(0xfab11baf, 0);
|
|
await writeFile(path, bytes);
|
|
|
|
const expected = {
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "inspect_il2cpp_metadata",
|
|
issues: [
|
|
{
|
|
path: ["path"],
|
|
reason: "invalid_format",
|
|
message: expect.stringContaining(
|
|
"truncated-header: the file has",
|
|
),
|
|
},
|
|
],
|
|
partial_observation: {
|
|
normalized_result: {
|
|
artifact: {
|
|
path,
|
|
bytes: length,
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
},
|
|
inventory_status: "not-returned-as-complete",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const result = await cli.run({
|
|
arguments: ["inspect-il2cpp-metadata", path, "--json"],
|
|
});
|
|
expect(result.exitCode).toBe(1);
|
|
expect(result.json).toMatchObject(expected);
|
|
|
|
const response = await call("inspect_il2cpp_metadata", { path });
|
|
const projected = parseMcpToolError(response).error;
|
|
expect(projected).toMatchObject({
|
|
...expected,
|
|
details: {
|
|
...expected.details,
|
|
partial_observation: {
|
|
kind: "retained-evidence",
|
|
evidence_id: expect.any(String),
|
|
},
|
|
},
|
|
});
|
|
const reference = projected.details?.partial_observation as {
|
|
evidence_id: string;
|
|
};
|
|
const bundle = parseEvidenceBundle(
|
|
toolContract("get_evidence_bundle").outputSchema.parse(
|
|
(await call("get_evidence_bundle", { detail: "complete" }))
|
|
.structuredContent,
|
|
).result,
|
|
);
|
|
expect(
|
|
bundle.records.find(
|
|
({ evidence_id }) => evidence_id === reference.evidence_id,
|
|
),
|
|
).toMatchObject(expected.details.partial_observation);
|
|
}
|
|
await client.ping();
|
|
},
|
|
30000,
|
|
);
|
|
|
|
const metadataPageReader =
|
|
(
|
|
call: Awaited<ReturnType<typeof connectLocalToolsMcp>>["call"],
|
|
context: { parentId: string; sha256: string },
|
|
) =>
|
|
async (view: Record<string, unknown>) => {
|
|
const result = await call("inspect_analysis_view", {
|
|
source: { kind: "retained-evidence", evidence_id: context.parentId },
|
|
view: { kind: "il2cpp-metadata", ...view },
|
|
});
|
|
expect(result.isError).not.toBe(true);
|
|
const evidence = parseEvidence(result.structuredContent);
|
|
expect(evidence.evidence_links).toContain(context.parentId);
|
|
const payload = analysisViewResultSchema.parse(evidence.normalized_result);
|
|
if (payload.kind !== "il2cpp-metadata")
|
|
throw new Error("Wrong collection response");
|
|
expect(payload.artifact.sha256).toBe(context.sha256);
|
|
return payload;
|
|
};
|
|
|
|
const assertRetainedNativeSelections = async (
|
|
selected: ReturnType<typeof metadataPageReader>,
|
|
binaryPath: string,
|
|
binaryBytes: Buffer,
|
|
) => {
|
|
expect(
|
|
await selected({
|
|
collection: "code_gen_modules",
|
|
image_indices: [0],
|
|
fields: ["image"],
|
|
limit: 1,
|
|
}),
|
|
).toMatchObject({
|
|
items: [{ index: 0, image: 0 }],
|
|
coverage: { total: 1, exhausted: true },
|
|
});
|
|
const native = await selected({
|
|
collection: "method_addresses",
|
|
addresses: ["0x0000000100000200", "0x100000220"],
|
|
states: ["code"],
|
|
fields: ["rva"],
|
|
offset: 1,
|
|
limit: 1,
|
|
});
|
|
expect(native).toMatchObject({
|
|
items: [{ index: 2, rva: "0x220" }],
|
|
binary_source: {
|
|
artifact: {
|
|
path: binaryPath,
|
|
sha256: createHash("sha256").update(binaryBytes).digest("hex"),
|
|
},
|
|
},
|
|
coverage: { total: 2, exhausted: true },
|
|
});
|
|
};
|
|
|
|
it("queries compact retained metadata through the real SDK with exact row identities and fields", async () => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-sdk-selection-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const fixture = createIl2cppMetadataFixture({ version: 31 });
|
|
await writeFile(path, fixture.bytes);
|
|
const binaryPath = join(root, "GameAssembly.dylib");
|
|
const binaryBytes = createIl2cppBinaryFixture();
|
|
await writeFile(binaryPath, binaryBytes);
|
|
const { client, call } = await connectLocalToolsMcp();
|
|
const directory = await call("inspect_desktop_application", { path: root });
|
|
const directoryView = analysisViewResultSchema.parse(
|
|
parseEvidence(directory.structuredContent).normalized_result,
|
|
);
|
|
if (directoryView.kind !== "summary")
|
|
throw new Error("Expected a directory summary");
|
|
const probe = desktopSummarySchema
|
|
.parse(directoryView.summary)
|
|
.next_probe_groups.find(
|
|
(group) => group.operation === "inspect_il2cpp_metadata",
|
|
)?.example;
|
|
if (probe === undefined)
|
|
throw new Error("Missing IL2CPP metadata next operation");
|
|
expect(probe.path).toBe(await realpath(path));
|
|
const response = await call(probe.operation, {
|
|
path: probe.path,
|
|
binary_path: binaryPath,
|
|
});
|
|
expect(response.isError).not.toBe(true);
|
|
const overview = parseMetadataSummary(response.structuredContent);
|
|
expect(overview.summary.counts).toMatchObject({
|
|
sections: 31,
|
|
images: 2,
|
|
types: 4,
|
|
methods: 5,
|
|
nested_types: 1,
|
|
});
|
|
expect(overview.summary).not.toHaveProperty("sections");
|
|
expect(overview.summary).not.toHaveProperty("methods");
|
|
await Promise.all([unlink(path), unlink(binaryPath)]);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: overview.parent_evidence_id,
|
|
};
|
|
const selected = metadataPageReader(call, {
|
|
parentId: overview.parent_evidence_id,
|
|
sha256: createHash("sha256").update(fixture.bytes).digest("hex"),
|
|
});
|
|
expect(
|
|
(
|
|
await selected({
|
|
collection: "sections",
|
|
names: ["methods"],
|
|
fields: ["count"],
|
|
})
|
|
).items,
|
|
).toEqual([{ index: 5, count: null }]);
|
|
expect(
|
|
(
|
|
await selected({
|
|
collection: "types",
|
|
indices: [3, 0, 3, 999],
|
|
fields: ["namespace"],
|
|
})
|
|
).items,
|
|
).toEqual([
|
|
{ index: 0, namespace: "Game" },
|
|
{ index: 3, namespace: "System" },
|
|
]);
|
|
expect(
|
|
(
|
|
await selected({
|
|
collection: "types",
|
|
image_indices: [0],
|
|
namespaces: ["Game"],
|
|
names: ["Player"],
|
|
fields: ["name"],
|
|
})
|
|
).items,
|
|
).toEqual([{ index: 0, name: "Player" }]);
|
|
const methods = {
|
|
collection: "methods",
|
|
declaring_types: [0, 1],
|
|
names: [".ctor", "Add"],
|
|
fields: ["name", "declaring_type"],
|
|
};
|
|
expect((await selected(methods)).items).toEqual([
|
|
{ index: 0, name: ".ctor", declaring_type: 0 },
|
|
{ index: 2, name: "Add", declaring_type: 1 },
|
|
]);
|
|
expect(await selected({ ...methods, offset: 1, limit: 1 })).toMatchObject({
|
|
items: [{ index: 2, name: "Add", declaring_type: 1 }],
|
|
coverage: { total: 2, exhausted: true, next_offset: null },
|
|
});
|
|
expect(
|
|
await selected({
|
|
collection: "parameters",
|
|
method_indices: [2, 2, 99],
|
|
fields: ["name", "method"],
|
|
}),
|
|
).toMatchObject({
|
|
items: [{ index: 0, name: "amount", method: 2 }],
|
|
coverage: { total: 1, exhausted: true },
|
|
});
|
|
expect(
|
|
await selected({
|
|
collection: "parameters",
|
|
method_indices: [],
|
|
fields: [],
|
|
}),
|
|
).toMatchObject({ items: [], coverage: { total: 0, exhausted: true } });
|
|
expect(
|
|
(await selected({ collection: "methods", indices: [2, 0], fields: [] }))
|
|
.items,
|
|
).toEqual([{ index: 0 }, { index: 2 }]);
|
|
expect(await selected({ collection: "types", indices: [] })).toMatchObject({
|
|
items: [],
|
|
coverage: { total: 0, exhausted: true },
|
|
});
|
|
await assertRetainedNativeSelections(selected, binaryPath, binaryBytes);
|
|
const invalid = await call("inspect_analysis_view", {
|
|
source,
|
|
view: {
|
|
kind: "il2cpp-metadata",
|
|
collection: "methods",
|
|
fields: ["namespace"],
|
|
},
|
|
});
|
|
expect(parseMcpToolError(invalid).error.code).toBe("invalid_request");
|
|
await client.ping();
|
|
}, 30000);
|
|
|
|
it("imports historical metadata Evidence with unavailable member collections without inventing empty results", async () => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-history-"),
|
|
path = join(root, "global-metadata.dat");
|
|
await writeFile(path, createIl2cppMetadataFixture({ version: 29 }).bytes);
|
|
const { call } = await connectLocalToolsMcp();
|
|
const fresh = await call("inspect_il2cpp_metadata", {
|
|
path,
|
|
detail: "complete",
|
|
});
|
|
expect(fresh.isError, JSON.stringify(fresh)).not.toBe(true);
|
|
const original = parseEvidence(fresh.structuredContent),
|
|
legacy = jsonObjectSchema.parse(original.normalized_result);
|
|
for (const key of [
|
|
"fields",
|
|
"properties",
|
|
"events",
|
|
"parameters",
|
|
"string_literals",
|
|
])
|
|
delete legacy[key];
|
|
const historical = createEvidence(
|
|
{
|
|
path,
|
|
sha256: original.subject!.digest.sha256,
|
|
format: "file",
|
|
},
|
|
original.provider,
|
|
{
|
|
operation: "inspect_il2cpp_metadata",
|
|
parameters: { path },
|
|
result: legacy,
|
|
limitations: original.limitations,
|
|
locations: original.locations,
|
|
},
|
|
);
|
|
const bundle = join(root, "historical.json");
|
|
await writeFile(bundle, JSON.stringify(createEvidenceBundle([historical])));
|
|
await unlink(path);
|
|
const imported = await call("import_evidence_bundle", { path: bundle });
|
|
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: historical.evidence_id,
|
|
};
|
|
const summary = await call("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "summary" },
|
|
});
|
|
expect(summary.isError, JSON.stringify(summary)).not.toBe(true);
|
|
expect(
|
|
parseEvidence(summary.structuredContent).normalized_result,
|
|
).toMatchObject({
|
|
summary: { counts: { methods: 5, fields: null, parameters: null } },
|
|
unknowns: expect.arrayContaining([
|
|
expect.stringContaining("did not collect parameters"),
|
|
]),
|
|
});
|
|
const types = await call("inspect_analysis_view", {
|
|
source,
|
|
view: {
|
|
kind: "il2cpp-metadata",
|
|
collection: "types",
|
|
indices: [0],
|
|
fields: ["name"],
|
|
},
|
|
});
|
|
expect(types.isError).not.toBe(true);
|
|
expect(
|
|
parseEvidence(types.structuredContent).normalized_result,
|
|
).toMatchObject({ items: [{ index: 0, name: "Player" }] });
|
|
const unavailable = await call("inspect_analysis_view", {
|
|
source,
|
|
view: { kind: "il2cpp-metadata", collection: "parameters" },
|
|
});
|
|
expect(parseMcpToolError(unavailable).error.code).toBe("invalid_request");
|
|
}, 30000);
|
|
|
|
cliTest(
|
|
"exports complete metadata Evidence for compact CLI delivery and portable selected reads",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-cli-export-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const bundle = join(root, "evidence.json");
|
|
const refused = await cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
join(root, "must-not-be-read"),
|
|
"--detail",
|
|
"summary",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(refused.exitCode).toBe(1);
|
|
expect(refused.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { issues: [{ path: ["evidence_output"] }] },
|
|
});
|
|
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
|
const args = [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--detail",
|
|
"summary",
|
|
"--evidence_output",
|
|
bundle,
|
|
"--json",
|
|
];
|
|
const result = await cli.run({ arguments: args });
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({
|
|
evidence_bundle: { path: bundle, bytes: expect.any(Number) },
|
|
summary: { normalized_result: { kind: "summary" } },
|
|
});
|
|
const before = await readFile(bundle);
|
|
const overwrite = await cli.run({ arguments: args });
|
|
expect(overwrite.exitCode).toBe(1);
|
|
expect(await readFile(bundle)).toEqual(before);
|
|
const saved = await readEvidenceBundle(bundle);
|
|
if (!saved.ok) throw saved.error;
|
|
const evidence = saved.value.records[0];
|
|
if (evidence === undefined)
|
|
throw new Error("Missing exported source Evidence");
|
|
expect(JSON.stringify(overwrite.json)).toContain(evidence.evidence_id);
|
|
expect(
|
|
il2cppMetadataSchema.parse(evidence.normalized_result).methods,
|
|
).toEqual(FIXTURE_METHODS);
|
|
await unlink(path);
|
|
const query = join(root, "query.json");
|
|
await writeFile(
|
|
query,
|
|
JSON.stringify({
|
|
source: { kind: "inline", evidence },
|
|
view: {
|
|
kind: "il2cpp-metadata",
|
|
collection: "methods",
|
|
indices: [2, 0],
|
|
fields: ["name"],
|
|
},
|
|
}),
|
|
);
|
|
const selected = await cli.run({
|
|
arguments: ["inspect-analysis-view", query, "--json"],
|
|
});
|
|
expect(selected.exitCode, selected.stderr).toBe(0);
|
|
expect(parseEvidence(selected.json).normalized_result).toMatchObject({
|
|
items: [
|
|
{ index: 0, name: ".ctor" },
|
|
{ index: 2, name: "Add" },
|
|
],
|
|
parent_evidence_id: evidence.evidence_id,
|
|
});
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest.skipIf(process.platform === "win32")(
|
|
"exports and reads Evidence at the filesystem-selected symlink-parent path",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-export-path-");
|
|
const path = join(root, "global-metadata.dat");
|
|
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
|
const actual = join(root, "actual");
|
|
await mkdir(join(actual, "nested"), { recursive: true });
|
|
await symlink(join(actual, "nested"), join(root, "link"));
|
|
const decoy = join(root, "evidence.json");
|
|
await writeFile(decoy, "unrelated caller data");
|
|
const selected = `${root}/link/../evidence.json`;
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--detail",
|
|
"summary",
|
|
"--evidence_output",
|
|
selected,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.json).toMatchObject({ evidence_bundle: { path: selected } });
|
|
expect(await readFile(decoy, "utf8")).toBe("unrelated caller data");
|
|
const saved = await readEvidenceBundle(selected);
|
|
if (!saved.ok) throw saved.error;
|
|
expect(
|
|
JSON.parse(await readFile(join(actual, "evidence.json"), "utf8")),
|
|
).toEqual(saved.value);
|
|
const read = await cli.run({
|
|
arguments: [
|
|
"inspect-evidence-bundle",
|
|
JSON.stringify({ path: selected, detail: "summary" }),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(read.exitCode, read.stderr).toBe(0);
|
|
expect(read.stdout).toContain(saved.value.records[0]?.evidence_id);
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"links method addresses from a selected binary slice through CLI and MCP pages",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-binary-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const binaryPath = join(root, "GameAssembly.dylib");
|
|
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
|
await writeFile(
|
|
binaryPath,
|
|
createUniversalFixture([
|
|
{ cpu: 0x0100000c, bytes: createIl2cppBinaryFixture() },
|
|
]),
|
|
);
|
|
const expected = FIXTURE_METHOD_ADDRESSES.map((address) =>
|
|
typeof address !== "bigint"
|
|
? { state: address, address: null, rva: null }
|
|
: {
|
|
state: "code",
|
|
address: `0x${address.toString(16)}`,
|
|
rva: `0x${(address - FIXTURE_BINARY_BASE).toString(16)}`,
|
|
},
|
|
);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--binary",
|
|
binaryPath,
|
|
"--architecture",
|
|
"arm64",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const report = parseCliMetadata(result.json);
|
|
expect(report.method_addresses).toEqual(expected);
|
|
expect(report.return_types.map((row) => row.name)).toEqual(
|
|
Array.from({ length: 5 }, () => FIXTURE_RETURN_TYPE_NAME),
|
|
);
|
|
expect(report.binary).toMatchObject({
|
|
format: "macho",
|
|
architecture: "arm64",
|
|
image_base: "0x100000000",
|
|
});
|
|
|
|
const { client, call } = await connectLocalToolsMcp();
|
|
const missing = await call("inspect_il2cpp_metadata", {
|
|
path,
|
|
binary_path: binaryPath,
|
|
});
|
|
expect(parseMcpToolError(missing).error).toMatchObject({
|
|
code: "unsupported_target",
|
|
message: expect.stringContaining("slice-selection-required"),
|
|
});
|
|
const summarized = await call("inspect_il2cpp_metadata", {
|
|
path,
|
|
binary_path: binaryPath,
|
|
binary_architecture: "arm64",
|
|
detail: "summary",
|
|
});
|
|
expect(summarized.isError, JSON.stringify(summarized)).not.toBe(true);
|
|
const view = parseMetadataSummary(summarized.structuredContent);
|
|
expect(view.summary.counts.method_addresses).toBe(5);
|
|
expect(view.summary.counts.method_address_states).toEqual({
|
|
code: 4,
|
|
no_body: 1,
|
|
unknown: 0,
|
|
});
|
|
const page = await call("inspect_analysis_view", {
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: view.parent_evidence_id,
|
|
},
|
|
view: {
|
|
kind: "il2cpp-metadata",
|
|
collection: "method_addresses",
|
|
offset: 2,
|
|
},
|
|
});
|
|
const projected = analysisViewResultSchema.parse(
|
|
parseEvidence(page.structuredContent).normalized_result,
|
|
);
|
|
if (projected.kind !== "il2cpp-metadata")
|
|
throw new Error("Wrong IL2CPP page kind");
|
|
expect(projected.items).toEqual(
|
|
expected.slice(2).map((row, offset) => ({ index: offset + 2, ...row })),
|
|
);
|
|
await client.ping();
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"marks a module array found through an x86-64 instruction as derived",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-inlined-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const binaryPath = join(root, "GameAssembly.so");
|
|
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
|
await writeFile(
|
|
binaryPath,
|
|
createElfFixture(
|
|
createIl2cppBinaryFixture({
|
|
codeRegistration: false,
|
|
codeReference: true,
|
|
}),
|
|
{ machine: 62 },
|
|
),
|
|
);
|
|
const result = await cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--binary",
|
|
binaryPath,
|
|
"--json",
|
|
],
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
const report = parseCliMetadata(result.json);
|
|
expect(report.binary?.code_gen_modules_field).toBeNull();
|
|
expect(
|
|
report.limitations.some((line) =>
|
|
line.includes("static CodeRegistration pair was not resolved"),
|
|
),
|
|
).toBe(true);
|
|
},
|
|
30000,
|
|
);
|
|
|
|
cliTest(
|
|
"writes Ghidra function seeds for a linked ELF and refuses to overwrite them",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-il2cpp-seeds-");
|
|
const path = join(root, "global-metadata.dat");
|
|
const binaryPath = join(root, "libil2cpp.so");
|
|
const seeds = join(root, "seeds.tsv");
|
|
const shifted = join(root, "shifted.tsv");
|
|
await writeFile(path, createIl2cppMetadataFixture().bytes);
|
|
await writeFile(binaryPath, createElfFixture(createIl2cppBinaryFixture()));
|
|
const run = (output: string, extra: readonly string[] = []) =>
|
|
cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--binary",
|
|
binaryPath,
|
|
"--ghidra-seeds",
|
|
output,
|
|
...extra,
|
|
"--json",
|
|
],
|
|
});
|
|
|
|
const linked = await run(seeds);
|
|
expect(linked.exitCode, linked.stderr).toBe(0);
|
|
const linkedFacts = il2cppMetadataSchema.parse(
|
|
parseEvidence(linked.json).normalized_result,
|
|
);
|
|
// This ELF is linked at the fixture base, so Ghidra keeps that base.
|
|
expect(await readFile(seeds, "utf8")).toBe(
|
|
[
|
|
"0x100000200\tfunction\tGame.Player$$.ctor",
|
|
"0x100000210\tfunction\tGame.Player$$Update",
|
|
"0x100000220\tfunction\tGame.Player.Slot$$Add",
|
|
"0x100000230\tfunction\tSystem.Größe$$.ctor",
|
|
`# metadata_sha256 ${linkedFacts.artifact.sha256}`,
|
|
`# binary_sha256 ${linkedFacts.binary!.artifact.sha256}`,
|
|
"# link_base 0x100000000",
|
|
"# seed_base 0x100000000",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
|
|
const moved = await run(shifted, ["--seed-base", "0x100000"]);
|
|
expect(moved.exitCode, moved.stderr).toBe(0);
|
|
expect((await readFile(shifted, "utf8")).split("\n")[0]).toBe(
|
|
"0x100200\tfunction\tGame.Player$$.ctor",
|
|
);
|
|
|
|
await writeFile(seeds, "kept\n");
|
|
const again = await run(seeds);
|
|
expect(again.exitCode).not.toBe(0);
|
|
expect(again.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["ghidra_seeds"],
|
|
message: expect.stringContaining("already exists"),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(await readFile(seeds, "utf8")).toBe("kept\n");
|
|
|
|
const unlinked = await cli.run({
|
|
arguments: [
|
|
"inspect-il2cpp-metadata",
|
|
path,
|
|
"--ghidra-seeds",
|
|
join(root, "unlinked.tsv"),
|
|
"--json",
|
|
],
|
|
});
|
|
expect(unlinked.exitCode).not.toBe(0);
|
|
expect(unlinked.json).toMatchObject({
|
|
code: "invalid_request",
|
|
details: {
|
|
issues: [
|
|
{
|
|
path: ["ghidra_seeds"],
|
|
message: expect.stringContaining("needs --binary"),
|
|
},
|
|
],
|
|
},
|
|
});
|
|
},
|
|
);
|