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, // The verifier selects no binary, so native observations are unrequested. code_gen_modules: null, method_addresses: null, method_address_states: null, return_types: null, field_types: null, parameter_types: null, }); 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");