Files
rea/scripts/verify-il2cpp-metadata.mjs
T
JANG BYUNGKUNandgensokyo-dev 21e0c580e7 feat(il2cpp): read member names, string literals and metadata versions 24 and 27 (#5326)
* feat(il2cpp): read member names, string literals and metadata versions 24 and 27

Adds field, property, event and parameter names with their owners, managed string literals, and the version 24 and 27 header layouts to inspect_il2cpp_metadata, with filtered pages for the new collections. Member decoding yields between row batches like the other row readers.

* fix(il2cpp): bound metadata expansion and query parameter owners

* test(il2cpp): cover nonempty property and event tables

---------

Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
2026-10-20 19:21:42 +08:00

188 lines
7.2 KiB
JavaScript

import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { resolve } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { absoluteCallerPath } from "../dist/filesystem/callerPath.js";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { analysisViewResultSchema } from "../dist/domain/analysisView/analysisView.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
import { closeVerifierResources } from "./lib/verifier-cleanup.mjs";
import { hashVerifierFile } from "./lib/verifier-file-resources.mjs";
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
console.log(
"Usage: npm run verify:il2cpp:metadata -- [--metadata PATH]\nPATH is a caller-owned, unencrypted global-metadata.dat (version 24, 27, 29, 31 or 39). Without --metadata, only the advertised contract is verified.",
);
process.exit(0);
}
if (!(args.length === 0 || (args.length === 2 && args[0] === "--metadata")))
throw new Error("Expected --metadata PATH");
const path = args[1] === undefined ? null : absoluteCallerPath(args[1]);
const operation = "inspect_il2cpp_metadata";
const contract = toolContract(operation);
const entrypoint = resolve("scripts/rea.mjs");
const execute = promisify(execFile);
const client = new Client({ name: "il2cpp-metadata-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: Object.fromEntries(
Object.entries(process.env).filter(
([, value]) => typeof value === "string",
),
),
stderr: "pipe",
});
transport.stderr?.resume();
const call = async (name, input) => {
const response = await client.callTool(
{ name, arguments: input },
{ timeout: 120_000 },
);
assert.notEqual(response.isError, true, mcpTextValue(response));
return JSON.parse(mcpTextValue(response));
};
const resources = new ArtifactResourceScope();
let primaryError;
try {
await client.connect(transport);
const { tools } = await client.listTools();
assert(tools.some((tool) => tool.name === operation));
if (path === null)
console.log("[unverified] Metadata inspection: no --metadata was supplied");
else {
const identity = await hashVerifierFile(path, resources);
const started = performance.now();
const { stdout } = await execute(
process.execPath,
[entrypoint, "inspect-il2cpp-metadata", path, "--json"],
{ timeout: 120_000, maxBuffer: 128 * 1024 * 1024 },
);
const cliMs = Math.round(performance.now() - started);
const cli = contract.outputSchema.parse(JSON.parse(stdout));
const report = cli.normalized_result;
assert.equal(report.artifact.path, path);
assert.equal(report.artifact.sha256, identity.sha256);
assert.equal(report.magic, 0xfab11baf);
const complete = await client.callTool(
{ name: operation, arguments: { path, detail: "complete" } },
{ timeout: 120_000 },
);
if (complete.isError === true) {
// Large inventories exceed the stdio budget; the result must stay
// retained and addressable rather than truncated.
const { error } = JSON.parse(mcpTextValue(complete));
assert.equal(error.code, "resource_constraint");
assert.equal(error.details.reported_limits.boundary, "mcp-response");
assert.match(error.details.reported_limits.evidence_id, /^ev_/u);
console.log(
`[ok] MCP complete delivery refused at the stdio budget (${error.details.reported_limits.response_bytes_at_least} bytes) with retained Evidence`,
);
} else {
const mcp = contract.outputSchema.parse(
JSON.parse(mcpTextValue(complete)),
);
assert.deepEqual(mcp.normalized_result, report);
console.log("[ok] CLI/MCP complete Evidence parity");
}
// Cross-check independently reported ranges against the row tables.
assert.equal(
report.images.reduce((total, image) => total + image.type_count, 0),
report.types.length,
);
assert.equal(
report.types.reduce((total, type) => total + type.method_count, 0),
report.methods.length,
);
report.types.forEach((type, index) => {
for (let at = 0; at < type.method_count; at += 1)
assert.equal(
report.methods[type.method_start + at].declaring_type,
index,
`method ${type.method_start + at} is outside its declaring type range`,
);
});
console.log("[ok] image type ranges and type method ranges agree");
const summary = contract.outputSchema.parse(
await call(operation, { path }),
).normalized_result;
assert.equal(summary.summary.magic, report.magic);
assert.deepEqual(summary.summary.counts, {
sections: report.sections.length,
images: report.images.length,
types: report.types.length,
methods: report.methods.length,
nested_types: report.types.filter((type) => type.declaring_type !== -1)
.length,
fields: report.fields.length,
properties: report.properties.length,
events: report.events.length,
parameters: report.parameters.length,
string_literals: report.string_literals.length,
});
const page = analysisViewResultSchema.parse(
(
await call("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: { kind: "il2cpp-metadata", collection: "methods", limit: 16 },
})
).normalized_result,
);
assert.deepEqual(
page.items,
report.methods.slice(0, 16).map((row, index) => ({ ...row, index })),
);
const last = analysisViewResultSchema.parse(
(
await call("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: {
kind: "il2cpp-metadata",
collection: "types",
offset: Math.max(0, report.types.length - 4),
limit: 16,
},
})
).normalized_result,
);
const lastOffset = Math.max(0, report.types.length - 4);
assert.deepEqual(
last.items,
report.types.slice(lastOffset).map((row, index) => ({
...row,
index: lastOffset + index,
})),
);
assert.equal(last.coverage.next_offset ?? null, null);
assert.deepEqual(
await hashVerifierFile(path, resources),
identity,
"Input metadata must remain unchanged",
);
console.log(
`[ok] summary and page parity; ${report.images.length} images, ${report.types.length} types, ${report.methods.length} methods; complete Evidence ${Buffer.byteLength(stdout)} bytes, summary ${Buffer.byteLength(JSON.stringify(summary))} bytes; CLI ${cliMs} ms`,
);
}
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await closeVerifierResources(
[() => client.close(), () => transport.close(), () => resources.close()],
primaryError,
);
}
console.log("All asserted IL2CPP metadata CLI/MCP checks passed");