Files
rea/src/application/il2cpp/Il2cppMetadataService.ts
T
JANG BYUNGKUNandgensokyo-dev 3ce311757d feat(il2cpp): link generic method instances behind --generic-instances (#5785)
* feat(il2cpp): read generic containers on demand and render method spec names

Generic container indices and records are demand readers on the same metadata snapshot, so links that do not ask for generic instances allocate nothing. Type rendering keeps one graph walk with its workspace, depth and cycle guards; the C#-like names and the Il2CppDumper GetMethodSpecName names are two styles over it.

* feat(il2cpp): link generic method instances with admitted rows and kept issues

generic_instances links every genericMethodTable row through methodSpecs, genericInsts and genericMethodPointers, choosing the one CodeRegistration layout whose counts bound every row. The table is reserved against the report budget before any row is resolved; rows and issues are admitted before retention, and a contradicting row stays unknown with an address-backed issue instead of failing the link. Views page the rows by definition, state, address and name, unrequested and historical collections stay unknown, and Ghidra seeds fold instances after the definitions.

* feat(il2cpp): accept --generic-instances on the CLI and describe it in tool contracts

* docs(il2cpp): describe generic method instances and their limits

* fix(il2cpp): keep issued generic rows unknown and name their row

A method spec that contradicted the metadata still reported its pointer slot as code, while the schema and provider limitation promised unknown. Such rows now keep no address, every issue carries its genericMethodTable row, the domain validator requires unknown exactly on issued rows, and shared specs name one other row instead of all of them. The row-size lower bound moves to the metadata admission owner, the hex helper is shared, and budget wording says checked rather than reserved.

* test(il2cpp): reject ambiguous generic layouts and pin budget, yield and limit paths

* fix(il2cpp): keep unmapped generic slots per row and reject undeclared arguments

Layout selection no longer requires every genericMethodPointers slot to be
mapped; each row reads its own slot, so one unmapped slot leaves only that
row unknown with an issue. Method specs whose class or method argument count
differs from the owner's declared generic container are issued and unnamed.
Fixtures now declare the containers their generic rows fill.

* fix(il2cpp): page generic method issues instead of retaining them in summaries

Summaries keep generic_methods without its issues and count them in
counts.generic_method_issues; the generic_method_issues collection pages
them by rows, indices and fields, as code_gen_modules already does. Docs
also name the method_indices filter the request schema accepts.

* fix(il2cpp): read split generic arguments per slot and method containers on demand

Generic argument arrays may span adjacent loader ranges: admit them with
hasPointerArrayCoverage and read each slot through readPointer, instead of
requiring the whole array in one mapping.

Method generic container indices are read on demand from the method record,
like type containers, so no link path allocates a per-method column. An owner
whose container index is out of range now leaves the method spec unnamed with
an issue instead of naming it unchecked.

* fix(il2cpp): report generic entries past the address space at their row

A damaged methodSpecs or genericMethodPointers pointer could make a spec or
slot address exceed 64 bits. The issue kept that address, which the address
schema rejects, so inspection failed instead of leaving the row unknown.
Check the entry range first and attach the issue to the genericMethodTable
row (or its slot field).

* chore(il2cpp): remove unrelated formatting changes

* fix: restore adb float pattern and mobile fixture from main

The merge commit's lint-staged run rewrote these two main files: oxlint --fix
dropped the escape in [+\-], which is invalid under the RegExp v flag.

* fix(il2cpp): preserve unreadable generic method identities

* docs(il2cpp): record generic-instance workflow verification

* docs(il2cpp): qualify final integration checks

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 16:19:05 +08:00

283 lines
9.1 KiB
TypeScript

import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import type {
Il2cppSeedOutput,
Il2cppSeedWriterPort,
Il2cppSeedReceipt,
} from "./Il2cppSeedWriterPort.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
type AnalysisViewRetention,
} from "../analysisView/AnalysisViewService.js";
import type { Il2cppMetadataPort } from "./Il2cppMetadataPort.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
il2cppMetadataSchema,
inspectIl2cppMetadataInputSchema,
type Il2cppMetadata,
type Il2cppMetadataProviderInput,
} from "../../domain/il2cpp/il2cppMetadata.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
const OPERATION = "inspect_il2cpp_metadata";
const NO_RETENTION: AnalysisViewRetention = {
evidenceById: undefined,
recordEvidence: undefined,
};
/** Shared exact input validation, artifact-bound Evidence and summary delivery for CLI and MCP. */
export class Il2cppMetadataService {
constructor(
readonly provider: Il2cppMetadataPort,
private readonly seeds?: Il2cppSeedWriterPort,
) {}
async close(): Promise<void> {
let failure: AnalysisError | undefined;
for (const close of [
() => this.provider.close?.(),
() => this.seeds?.close(),
]) {
try {
await close();
} catch (cause) {
const next =
cause instanceof AnalysisError
? cause
: new ProviderCleanupError(
this.provider.identity.id,
[],
{
reason:
cause instanceof Error ? cause.message : String(cause),
},
{ cause, operation: OPERATION },
);
failure =
failure === undefined
? next
: analysisErrorWithCleanupFailure(
failure,
next,
OPERATION,
).bindCleanupDependencies([failure, next]);
}
}
if (failure !== undefined) throw failure;
}
inspect(
rawInput: unknown,
options?: ExecutionOptions,
retention: AnalysisViewRetention = NO_RETENTION,
): Promise<Result<Evidence, AnalysisError>> {
return this.#inspect(rawInput, options, retention);
}
/** Explicit output workflow for CLI/SDK callers; ordinary inspection stays read-only. */
inspectWithGhidraSeeds(
rawInput: unknown,
output: Il2cppSeedOutput,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
return this.#inspect(rawInput, options, NO_RETENTION, output);
}
/** Inspect one explicitly selected file; detail changes delivery, not decoded coverage. */
async #inspect(
rawInput: unknown,
options: ExecutionOptions | undefined,
retention: AnalysisViewRetention,
output?: Il2cppSeedOutput,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectIl2cppMetadataInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
if (
input.data.binary_architecture !== undefined &&
input.data.binary_path === undefined
)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["binary_architecture"],
reason: "invalid_value",
message: "binary_architecture selects a slice of binary_path.",
},
]),
);
if (
input.data.generic_instances === true &&
input.data.binary_path === undefined
)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["generic_instances"],
reason: "invalid_value",
message: "generic_instances links generic methods in binary_path.",
},
]),
);
if (output !== undefined) {
if (input.data.binary_path === undefined)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["ghidra_seeds"],
reason: "invalid_value",
message:
"--ghidra-seeds needs --binary to link method addresses.",
},
]),
);
if (this.seeds === undefined)
return err(
new AnalysisOutputError(
OPERATION,
"No seed output adapter is configured.",
),
);
const ready = await this.seeds.check(output, options);
if (!ready.ok) return ready;
}
const { detail, ...providerInput } = input.data;
if (detail === "summary") {
const ready = requireAnalysisViewRetention(OPERATION, retention);
if (!ready.ok) return ready;
}
const inspected = await this.provider.inspect(providerInput, options);
if (!inspected.ok) return inspected;
const report = il2cppMetadataSchema.safeParse(inspected.value);
if (
!report.success ||
report.data.artifact.path !== providerInput.path ||
(report.data.binary?.artifact.path ?? undefined) !==
providerInput.binary_path ||
(providerInput.binary_architecture !== undefined &&
report.data.binary?.architecture !== providerInput.binary_architecture)
)
return err(
new AnalysisOutputError(
OPERATION,
"IL2CPP metadata reader returned malformed metadata or changed the selected artifact identity.",
),
);
const value = report.data;
const evidence = this.#evidence(value, providerInput, output);
let seedReceipt: Il2cppSeedReceipt | undefined;
const cancelled = () =>
err(
new AnalysisCancelledError(OPERATION, {
cause: options?.signal?.reason,
partialObservation: evidence,
...(seedReceipt === undefined
? {}
: { diagnostics: { published_seed_output: { ...seedReceipt } } }),
}),
);
if (options?.signal?.aborted) return cancelled();
if (output !== undefined && this.seeds !== undefined) {
const written = await this.seeds.write(value, output, options);
if (!written.ok) {
written.error.retainPartialObservation(evidence);
return written;
}
seedReceipt = written.value;
}
if (options?.signal?.aborted) return cancelled();
return detail === "summary"
? summarizeRetainedAnalysis(evidence, retention)
: ok(evidence);
}
#evidence(
value: Il2cppMetadata,
providerInput: Il2cppMetadataProviderInput,
output: Il2cppSeedOutput | undefined,
): Evidence {
return createEvidence(
{
path: value.artifact.path,
sha256: value.artifact.sha256,
format: "file",
},
this.provider.identity,
{
operation: OPERATION,
parameters: {
...(Object.fromEntries(
Object.entries(providerInput).filter(
([, value]) => value !== undefined,
),
) as Record<string, string | boolean>),
...(output === undefined
? {}
: {
ghidra_seeds: output.path,
...(output.base === undefined
? {}
: { seed_base: output.base }),
}),
},
result: value,
confidence:
value.binary === null
? "observed"
: value.binary.code_gen_modules_field === null
? "inferred"
: "derived",
limitations: value.limitations,
locations: [
{ kind: "artifact-path", path: value.artifact.path },
...(value.binary === null
? []
: [
{
kind: "artifact-path" as const,
path: value.binary.artifact.path,
},
]),
...value.sections
.filter(
({ name, size }) =>
size > 0 &&
(name === "images" ||
name === "typeDefinitions" ||
name === "methods" ||
name === "nestedTypes" ||
name === "strings" ||
name === "fields" ||
name === "properties" ||
name === "events" ||
name === "parameters" ||
name === "stringLiterals" ||
name === "stringLiteralData"),
)
.map(({ offset, size }) => ({
kind: "file-offset-range" as const,
start: offset,
end: offset + size,
})),
],
},
);
}
}