Files
reverse-engineering-anything/src/domain/analysisView/il2cppMetadataView.ts
T
JANG BYUNGKUNandmorluto 149b43ee61 fix(il2cpp): link arm64 builds whose registrations were inlined (#6989)
* fix(il2cpp): accept arm64 module arrays loaded through adrp/add pairs

A Unity 2022.3 macOS Master (LTO) arm64 build keeps no static CodeRegistration, so the module-array fallback that only decoded x86-64 RIP-relative references reported code-registration-not-found.
The registration owner now also decodes aligned adrp + add pairs in executable ranges and keeps the same exact-module-set and single-reference rules.
A minimal Mach-O fixture failed on the previous reader (47 passed / 1 failed) and passes here with four fail-closed variants (48/48 binary tests).

* fix(il2cpp): name the matched instruction form in inferred module-array limits

The inferred-array limitation always described RIP-relative LEA/MOV, which is wrong for arm64 results.
The provider now names the adrp/add pair on arm64, and the tool contract and docs cover both forms and state that a located array alone does not finish linking a Master build.
The new arm64 provider case failed before the wording change (1 passed / 1 failed) and both cases pass now.

* fix(il2cpp): accept type tables loaded without a static MetadataRegistration

A Unity 2022.3.62f3 macOS Master build inlines MetadataRegistration as well as CodeRegistration, so linking stopped at metadata-registration-not-found after the module array was found. Registration now infers the only field linking reads, the Il2CppType table, from the one run of type-shaped pointers that covers every referenced type index and whose start is loaded by the existing RIP-relative or adrp/add forms; types_field is null and the Evidence is inferred. The new arm64 and x86-64 fixtures failed on the previous reader (52 passed / 2 failed). Four run-selection variants (interior reference, run extended before the loaded address, two loaded runs, run shorter than the referenced indices) keep metadata-registration-not-found; instruction decoding and record-shape checks are not covered by these fixtures.

* test(il2cpp): pin adrp page deltas, register pairing and type-record shape

Every adrp fixture targeted its own 4 KiB page, so dropping the page delta or its sign extension, the add register check, the type-kind bound or the type-definition bound left all binary tests passing. Fixtures now reach a later and an earlier page, pair an add with a different destination or source register, break the type run with an out-of-range kind or type-definition index, and bound type-table candidates when the metadata references few type indices. Each of those five source mutations now fails 1 to 3 of 85 binary and provider tests.

* refactor(il2cpp): share Il2CppType kind decoding and document type-table bounds

The type-table fallback decoded the Il2CppType kind and class/value-type kinds separately from the type namer; both now use the Types owner. Zero and unaligned words are rejected before mapping lookup (Master sample metadata-registration search 92 to 68 ms). Docs and the limitation state that types_count is an upper bound, that a run spanning mappings or a small referenced type range can fail, and credit Cpp2IL StrippedCodeRegSupport and the data-side MetadataRegistration searches of Il2CppDumper and Cpp2IL. Master sample output is unchanged (11,031 code addresses, 37,025 type rows).

* fix(il2cpp): refuse generic instances when the type table was inferred

An inferred type table has no static MetadataRegistration, so the
genericMethodTable it would anchor cannot be located. Report
generic-methods-unsupported instead of reading from a null field.

* fix(il2cpp): fail closed when an inferred type table is not unique

Adversarial review found that the type-table fallback could pick another
type-shaped pointer table when the real one was unloaded or excluded, could
replace a static MetadataRegistration whose counts matched, and accepted a
run cut short by a mapping end. Require exactly one long-enough run in the
image, ending inside its mapping and loaded by code; skip the fallback when a
record's two counts matched; accept only assigned Il2CppTypeEnum kinds; and
look records up through the indexed file mapping so many mappings cannot
multiply the scan. Docs and limitations now describe the run length as at
least every referenced type index rather than an upper bound.

* fix(il2cpp): keep the type-table fallback cheap and its refusals exact

Review follow-ups: check that an address can lie in a mapping before the
throwing indexed lookup, so words pointing into gaps or zero-fill no longer
build an exception each (64 MiB: 74 s to under 2 s); treat a run starting at
its mapping start like one reaching its end; stop at the second long-enough
run instead of a 64-candidate capacity error; ignore ADRP/ADD pairs on
register 31; and say whether a counted MetadataRegistration was unusable or
no code-loaded run was found. Docs no longer generalize from one build or
state tool history they do not source.

* fix(il2cpp): judge type-table slots by their loaded value

The run scan read stored words, so a dyld-bound slot's placeholder could
decide whether a run qualified. Confirm each slot through readSlot and give
a run bounded by a bound or unresolved slot an unknown extent, so the
placeholder no longer changes the choice.

* docs(il2cpp): keep the failure-reason table layout

Shorten the two changed meanings so the table keeps its column widths and the
diff shows only those rows.

* docs(il2cpp): keep the reason table at its formatted width

* fix(il2cpp): read type-table record fields through the loader too

The previous change confirmed table slots through readSlot but still read
each record's kind and definition index from stored bytes. Read them with
readUInt32 and readSlot, and treat a record field the loader binds or leaves
unresolved like an unresolved slot: the run it bounds has an unknown extent.

* perf(il2cpp): share image mappings across loader-aware reads

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-25 04:21:18 +08:00

756 lines
26 KiB
TypeScript

import { z } from "zod";
import { analysisViewInputError } from "./analysisViewErrors.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import type { JsonValue } from "../jsonValue.js";
import { isImmutableJsonSnapshot } from "../immutableJson.js";
import {
il2cppMetadataImageSchema,
il2cppMetadataMemberSchema,
il2cppMetadataMethodSchema,
il2cppMetadataSectionSchema,
il2cppMetadataParameterSchema,
il2cppMetadataStringLiteralSchema,
il2cppMethodAddressSchema,
il2cppGenericMethodInstanceSchema,
il2cppGenericMethodIssueSchema,
il2cppGenericMethodLinkSchema,
il2cppBinaryLinkSchema,
il2cppCodeGenModuleSchema,
il2cppTypeReferenceSchema,
il2cppMetadataObjectSchema,
il2cppMetadataParentSchema,
il2cppMetadataMemberCollections,
il2cppMetadataBinaryCollections,
il2cppMetadataTypeSchema,
} from "../il2cpp/il2cppMetadata.js";
import { err, ok, type Result } from "../result.js";
import { parseAnalysisViewResult } from "./analysisViewParent.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage,
pageViewCoverage,
} from "./analysisViewCoverage.js";
const countsSchema = z.strictObject({
sections: z.number().int().nonnegative(),
images: z.number().int().nonnegative(),
types: z.number().int().nonnegative(),
methods: z.number().int().nonnegative(),
nested_types: z.number().int().nonnegative(),
fields: z.number().int().nonnegative().nullable(),
properties: z.number().int().nonnegative().nullable(),
events: z.number().int().nonnegative().nullable(),
parameters: z.number().int().nonnegative().nullable(),
string_literals: z.number().int().nonnegative().nullable(),
code_gen_modules: z.number().int().nonnegative().nullable(),
method_addresses: z.number().int().nonnegative().nullable(),
/** method_addresses rows per state; null when native observations were not collected. */
method_address_states: z
.strictObject({
code: z.number().int().nonnegative(),
no_body: z.number().int().nonnegative(),
unknown: z.number().int().nonnegative(),
})
.nullable(),
return_types: z.number().int().nonnegative().nullable(),
field_types: z.number().int().nonnegative().nullable(),
parameter_types: z.number().int().nonnegative().nullable(),
/** Null unless generic_instances was requested, or before it was collected. */
generic_method_instances: z.number().int().nonnegative().nullable(),
/** generic_method_instances rows per state; null as above. */
generic_method_instance_states: z
.strictObject({
code: z.number().int().nonnegative(),
no_body: z.number().int().nonnegative(),
unknown: z.number().int().nonnegative(),
})
.nullable(),
/** generic_methods.issues rows; null as above. */
generic_method_issues: z.number().int().nonnegative().nullable(),
});
export const il2cppMetadataSummarySchema = il2cppMetadataObjectSchema
.omit({
sections: true,
images: true,
types: true,
methods: true,
fields: true,
properties: true,
events: true,
parameters: true,
string_literals: true,
method_addresses: true,
return_types: true,
field_types: true,
parameter_types: true,
generic_method_instances: true,
binary: true,
})
.extend({
// Links retained before generic instances existed omit generic_methods.
// Issues page through generic_method_issues; the summary keeps a count.
binary: il2cppBinaryLinkSchema
.omit({ code_gen_modules: true })
.extend({
generic_methods: il2cppGenericMethodLinkSchema
.omit({ issues: true })
.nullable(),
})
.partial({ generic_methods: true })
.nullable()
.optional(),
counts: countsSchema,
});
const indexSchema = z.number().int().nonnegative();
const sectionRowSchema = il2cppMetadataSectionSchema.extend({
index: indexSchema,
});
const imageRowSchema = il2cppMetadataImageSchema.extend({ index: indexSchema });
const typeRowSchema = il2cppMetadataTypeSchema.extend({ index: indexSchema });
const methodRowSchema = il2cppMetadataMethodSchema.extend({
index: indexSchema,
});
const memberRowSchema = il2cppMetadataMemberSchema.extend({
index: indexSchema,
});
const parameterRowSchema = il2cppMetadataParameterSchema.extend({
index: indexSchema,
});
const stringLiteralRowSchema = il2cppMetadataStringLiteralSchema.extend({
index: indexSchema,
});
const methodAddressRowSchema = il2cppMethodAddressSchema.extend({
index: indexSchema,
});
const typeReferenceRowSchema = il2cppTypeReferenceSchema.extend({
index: indexSchema,
});
const genericInstanceRowSchema = il2cppGenericMethodInstanceSchema.extend({
index: indexSchema,
});
const codeGenModuleRowSchema = il2cppCodeGenModuleSchema.extend({
index: indexSchema,
});
const genericIssueRowSchema = il2cppGenericMethodIssueSchema.extend({
index: indexSchema,
});
const indexPage = {
kind: z.literal("il2cpp-metadata"),
indices: z.array(indexSchema).optional(),
offset: indexSchema.default(0),
limit: z.number().int().positive().default(64),
};
const page = { ...indexPage, names: z.array(z.string()).optional() };
const declaringTypes = z.array(z.number().int().min(-1)).optional();
/** Filters intersect; pages use metadata order and always retain the source row index. */
export const il2cppMetadataViewRequestSchema = z.discriminatedUnion(
"collection",
[
z.strictObject({
...page,
collection: z.literal("sections"),
fields: z.array(sectionRowSchema.keyof()).optional(),
}),
z.strictObject({
...page,
collection: z.literal("images"),
fields: z.array(imageRowSchema.keyof()).optional(),
}),
z.strictObject({
...page,
collection: z.literal("types"),
image_indices: z.array(indexSchema).optional(),
namespaces: z.array(z.string()).optional(),
declaring_types: declaringTypes,
fields: z.array(typeRowSchema.keyof()).optional(),
}),
z.strictObject({
...page,
collection: z.literal("methods"),
declaring_types: declaringTypes,
fields: z.array(methodRowSchema.keyof()).optional(),
}),
...(["fields", "properties", "events"] as const).map((collection) =>
z.strictObject({
...page,
collection: z.literal(collection),
declaring_types: declaringTypes,
fields: z.array(memberRowSchema.keyof()).optional(),
}),
),
z.strictObject({
...page,
collection: z.literal("parameters"),
method_indices: z.array(indexSchema).optional(),
fields: z.array(parameterRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("string_literals"),
fields: z.array(stringLiteralRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("method_addresses"),
addresses: z.array(z.string().regex(/^0x[0-9a-fA-F]{1,16}$/u)).optional(),
states: z.array(il2cppMethodAddressSchema.shape.state).optional(),
fields: z.array(methodAddressRowSchema.keyof()).optional(),
}),
z.strictObject({
...page,
collection: z.literal("generic_method_instances"),
/** Metadata method indices of the generic definitions. */
method_indices: z.array(indexSchema).optional(),
addresses: z.array(z.string().regex(/^0x[0-9a-fA-F]{1,16}$/u)).optional(),
states: z.array(il2cppGenericMethodInstanceSchema.shape.state).optional(),
fields: z.array(genericInstanceRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("generic_method_issues"),
/** genericMethodTable rows the issues leave unknown. */
rows: z.array(indexSchema).optional(),
fields: z.array(genericIssueRowSchema.keyof()).optional(),
}),
z.strictObject({
...indexPage,
collection: z.literal("code_gen_modules"),
image_indices: z.array(indexSchema).optional(),
fields: z.array(codeGenModuleRowSchema.keyof()).optional(),
}),
...(["return_types", "field_types"] as const).map((collection) =>
z.strictObject({
...page,
collection: z.literal(collection),
fields: z.array(typeReferenceRowSchema.keyof()).optional(),
}),
),
z.strictObject({
...page,
collection: z.literal("parameter_types"),
// Owners come from the retained parameters collection.
method_indices: z.array(indexSchema).optional(),
fields: z.array(typeReferenceRowSchema.keyof()).optional(),
}),
],
);
export const il2cppMetadataViewItemsSchema = z.union([
z.array(sectionRowSchema.partial().required({ index: true })),
z.array(imageRowSchema.partial().required({ index: true })),
z.array(typeRowSchema.partial().required({ index: true })),
z.array(methodRowSchema.partial().required({ index: true })),
z.array(memberRowSchema.partial().required({ index: true })),
z.array(parameterRowSchema.partial().required({ index: true })),
z.array(stringLiteralRowSchema.partial().required({ index: true })),
z.array(methodAddressRowSchema.partial().required({ index: true })),
z.array(typeReferenceRowSchema.partial().required({ index: true })),
z.array(codeGenModuleRowSchema.partial().required({ index: true })),
z.array(genericInstanceRowSchema.partial().required({ index: true })),
z.array(genericIssueRowSchema.partial().required({ index: true })),
]);
export const il2cppMetadataViewContextSchema = z.strictObject({
metadata_version: il2cppMetadataObjectSchema.shape.metadata_version,
counts: countsSchema,
binary_source: il2cppBinaryLinkSchema
.pick({
artifact: true,
format: true,
architecture: true,
image_base: true,
})
.optional(),
});
type Report = z.infer<typeof il2cppMetadataParentSchema>;
type Request = z.infer<typeof il2cppMetadataViewRequestSchema>;
type BinaryLink = NonNullable<Report["binary"]>;
type CollectionRows = Omit<Report, "binary"> & {
code_gen_modules: BinaryLink["code_gen_modules"];
generic_method_issues: NonNullable<BinaryLink["generic_methods"]>["issues"];
};
type Row = NonNullable<CollectionRows[Request["collection"]]>[number];
interface CheckedReport {
readonly report: Report;
readonly counts: z.infer<typeof countsSchema>;
}
// Authenticated, frozen parents own these facts. Cache validation/counts for
// their lifetime without a second report copy or a live filesystem cache.
const checkedReports = new WeakMap<object, CheckedReport>();
const parseMetadataReport = (
parent: AnalysisViewParent,
): Result<CheckedReport, AnalysisError> => {
const key = parent.normalizedResult;
const reusable =
parent.sealedResult === true &&
typeof key === "object" &&
key !== null &&
isImmutableJsonSnapshot(key);
const cached = reusable ? checkedReports.get(key) : undefined;
if (cached !== undefined) return ok(cached);
const parsed = parseAnalysisViewResult(il2cppMetadataParentSchema, parent);
if (!parsed.ok) return parsed;
// This schema has no coercions/transforms; the authenticated owner value is
// equivalent to the validated clone and remains immutable across pages.
const report = reusable ? (key as Report) : parsed.value;
const checked = {
report,
counts: {
sections: report.sections.length,
images: report.images.length,
types: report.types.length,
methods: report.methods.length,
nested_types: report.types.reduce(
(count, type) => count + Number(type.declaring_type !== -1),
0,
),
fields: report.fields?.length ?? null,
properties: report.properties?.length ?? null,
events: report.events?.length ?? null,
parameters: report.parameters?.length ?? null,
string_literals: report.string_literals?.length ?? null,
code_gen_modules: report.binary?.code_gen_modules.length ?? null,
method_addresses:
report.binary == null
? null
: (report.method_addresses?.length ?? null),
method_address_states:
(report.binary == null ? undefined : report.method_addresses)?.reduce(
(states, row) => {
if (row.state === "code") states.code++;
else if (row.state === "no-body") states.no_body++;
else states.unknown++;
return states;
},
{ code: 0, no_body: 0, unknown: 0 },
) ?? null,
return_types:
report.binary == null ? null : (report.return_types?.length ?? null),
field_types:
report.binary == null ? null : (report.field_types?.length ?? null),
parameter_types:
report.binary == null ? null : (report.parameter_types?.length ?? null),
generic_method_instances: report.generic_method_instances?.length ?? null,
generic_method_instance_states:
report.generic_method_instances?.reduce(
(states, row) => {
if (row.state === "code") states.code++;
else if (row.state === "no-body") states.no_body++;
else states.unknown++;
return states;
},
{ code: 0, no_body: 0, unknown: 0 },
) ?? null,
generic_method_issues:
report.binary?.generic_methods?.issues.length ?? null,
},
};
if (reusable) checkedReports.set(key, checked);
return ok(checked);
};
/** Direct batch lookup visits selected indices only; metadata order is stable. */
function* rowIndices(length: number, indices: number[] | undefined) {
if (indices === undefined) {
for (let index = 0; index < length; index++) yield index;
} else {
for (const index of [...new Set(indices)].sort((a, b) => a - b))
if (index < length) yield index;
}
}
const selectFields = (
row: Row,
index: number,
fields: Set<string> | undefined,
) => {
const item: { index: number } & Record<string, JsonValue> = { index };
const values: Readonly<Record<string, JsonValue>> = row;
for (const key of Object.keys(values)) {
const value = values[key];
if (value !== undefined && (fields === undefined || fields.has(key)))
item[key] = value;
}
return item;
};
/** Compile caller selections once; large batches do not multiply per-row work. */
const selection = (view: Request, report: Report) => {
const names =
!("names" in view) || view.names === undefined
? undefined
: new Set(view.names);
const owners =
!("declaring_types" in view) || view.declaring_types === undefined
? undefined
: new Set(view.declaring_types);
const addresses =
(view.collection === "method_addresses" ||
view.collection === "generic_method_instances") &&
view.addresses !== undefined
? new Set(view.addresses.map((value) => BigInt(value).toString(16)))
: undefined;
const states =
(view.collection === "method_addresses" ||
view.collection === "generic_method_instances") &&
view.states !== undefined
? new Set<string>(view.states)
: undefined;
const methods =
(view.collection === "parameters" ||
view.collection === "parameter_types" ||
view.collection === "generic_method_instances") &&
view.method_indices !== undefined
? new Set(view.method_indices)
: undefined;
// A parameter type row is owned by the method of the parameter at its
// index; parameter and generic instance rows name their method.
const parameterOwner = (row: Row, index: number) =>
"method" in row
? row.method
: view.collection === "parameter_types"
? report.parameters?.[index]?.method
: undefined;
const namespaces =
view.collection !== "types" || view.namespaces === undefined
? undefined
: new Set(view.namespaces);
const imageIndices =
view.collection === "types" ? view.image_indices : undefined;
const ranges: { start: number; end: number }[] = [];
if (imageIndices !== undefined) {
const selected = [...new Set(imageIndices)]
.flatMap((index) => {
const image = report.images[index];
return image === undefined || image.type_count === 0
? []
: [
{
start: image.type_start,
end: image.type_start + image.type_count,
},
];
})
.sort((a, b) => a.start - b.start);
for (const range of selected) {
const last = ranges.at(-1);
if (last !== undefined && range.start <= last.end)
last.end = Math.max(last.end, range.end);
else ranges.push(range);
}
}
const issueRows =
view.collection === "generic_method_issues" && view.rows !== undefined
? new Set(view.rows)
: undefined;
const moduleImages =
view.collection === "code_gen_modules" && view.image_indices !== undefined
? new Set(view.image_indices)
: undefined;
if (
[
moduleImages,
issueRows,
addresses,
states,
names,
owners,
namespaces,
methods,
imageIndices,
].every((filter) => filter === undefined)
)
return undefined;
return (row: Row, index: number) => {
if (
moduleImages !== undefined &&
(!("image" in row) || !moduleImages.has(row.image))
)
return false;
if (issueRows !== undefined && (!("row" in row) || !issueRows.has(row.row)))
return false;
if (
addresses !== undefined &&
(!("address" in row) ||
row.address === null ||
!addresses.has(BigInt(row.address).toString(16)))
)
return false;
if (states !== undefined && (!("state" in row) || !states.has(row.state)))
return false;
if (
names !== undefined &&
(!("name" in row) || row.name === null || !names.has(row.name))
)
return false;
if (
owners !== undefined &&
(!("declaring_type" in row) || !owners.has(row.declaring_type))
)
return false;
if (
namespaces !== undefined &&
(!("namespace" in row) || !namespaces.has(row.namespace))
)
return false;
if (methods !== undefined) {
const method = parameterOwner(row, index);
if (method === null || method === undefined || !methods.has(method))
return false;
}
if (imageIndices === undefined) return true;
let lower = 0;
let upper = ranges.length;
while (lower < upper) {
const middle = lower + Math.floor((upper - lower) / 2);
if (ranges[middle]!.start <= index) lower = middle + 1;
else upper = middle;
}
const range = ranges[lower - 1];
return range !== undefined && index < range.end;
};
};
/** Reuse retained facts; source indices stay distinct from filtered page offsets. */
export const projectIl2cppMetadataView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
): Result<UnsignedAnalysisView, AnalysisError> => {
if (view.kind !== "summary" && view.kind !== "il2cpp-metadata")
return err(
analysisViewInputError([
{
path: ["view"],
reason: "invalid_value",
message:
"IL2CPP metadata Evidence supports summary and selected sections, images, types, methods, member, parameter, string literal, method address, type reference, generic method instance and generic method issue pages.",
},
]),
);
const parsed = parseMetadataReport(parent);
if (!parsed.ok) return parsed;
const { report, counts } = parsed.value;
if (
parent.subject != null &&
(parent.subject.format !== "file" ||
parent.subject.local_path !== report.artifact.path ||
parent.subject.digest.sha256 !== report.artifact.sha256)
)
return err(
analysisViewInputError([
{
path: ["source", "subject"],
reason: "invalid_value",
message:
"IL2CPP metadata artifact identity does not match its parent subject.",
},
]),
);
const {
sections,
images,
types,
methods,
fields,
properties,
events,
parameters,
string_literals,
method_addresses,
return_types,
field_types,
parameter_types,
generic_method_instances,
...identity
} = report;
const allRows = {
code_gen_modules: report.binary?.code_gen_modules,
generic_method_issues:
report.binary?.generic_methods === null
? null
: report.binary?.generic_methods?.issues,
sections,
images,
types,
methods,
fields,
properties,
events,
parameters,
string_literals,
method_addresses,
return_types,
field_types,
parameter_types,
generic_method_instances,
};
const shared = {
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_il2cpp_metadata" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
limitations: [...parent.limitations],
unknowns: [
...(report.binary != null && report.binary.code_gen_modules_field === null
? [
"Module-array association is inferred from executable byte patterns; instruction boundaries and runtime use remain unverified.",
]
: []),
...(report.binary != null && report.binary.types_field === null
? [
"Type-table association and types_count are inferred from executable byte patterns and Il2CppType-shaped records; instruction boundaries and runtime use remain unverified.",
]
: []),
...[
...il2cppMetadataMemberCollections,
...il2cppMetadataBinaryCollections,
]
.filter((name) => report[name] === undefined)
.map(
(name) =>
`Retained Evidence did not collect ${name}; its count is unknown. Reinspect the metadata to acquire these facts.`,
),
...(report.binary != null && report.generic_method_instances === undefined
? [
"Retained Evidence did not collect generic_method_instances; its count is unknown. Reinspect with generic_instances to acquire these facts.",
]
: report.generic_method_instances === null
? [
"Generic method instances were not requested; their count is unknown.",
]
: []),
report.binary == null
? "Names come from unencrypted metadata only; native code addresses and runtime registration remain unknown."
: report.binary.generic_methods == null
? "Addresses come from static binary tables; runtime behavior and generic instance code remain unknown."
: "Addresses come from static binary tables; runtime behavior and generic instances outside genericMethodTable remain unknown.",
...(parent.subject == null
? [
"The parent has no recorded subject; its normalized artifact identity cannot be cross-checked against a subject digest.",
]
: []),
],
};
if (view.kind === "summary") {
const binary =
report.binary == null
? report.binary
: (({ code_gen_modules: _modules, generic_methods, ...facts }) => ({
...facts,
...(generic_methods === undefined
? {}
: {
generic_methods:
generic_methods === null
? null
: (({ issues: _issues, ...registration }) =>
registration)(generic_methods),
}),
}))(report.binary);
return ok({
kind: "summary",
view,
...shared,
summary: {
...identity,
...(binary === undefined ? {} : { binary }),
counts,
},
coverage: completeWithinViewCoverage(1, 1),
});
}
const nativeUnavailable =
report.binary == null &&
[
"method_addresses",
"return_types",
"field_types",
"parameter_types",
].includes(view.collection);
const rows = nativeUnavailable ? undefined : allRows[view.collection];
if (rows === null)
return err(
analysisViewInputError([
{
path: ["view", "collection"],
reason: "invalid_value",
message:
"Generic method instances were not requested for this Evidence, so they are unknown; inspect again with generic_instances and binary_path.",
},
]),
);
if (rows === undefined)
return err(
analysisViewInputError([
{
path: ["view", "collection"],
reason: "invalid_value",
message: `Retained Evidence did not collect ${view.collection}; inspect the metadata again to acquire these facts.`,
},
]),
);
if (
view.collection === "parameter_types" &&
view.method_indices !== undefined &&
report.parameters === undefined
)
return err(
analysisViewInputError([
{
path: ["view", "method_indices"],
reason: "invalid_value",
message:
"Retained Evidence did not collect parameters, so parameter owners are unknown; inspect the metadata again to select parameter types by method.",
},
]),
);
const matches = selection(view, report);
const selectedFields =
view.fields === undefined ? undefined : new Set<string>(view.fields);
const items = [];
let total = 0;
if (matches === undefined && view.indices === undefined) {
total = rows.length;
const end = Math.min(rows.length, view.offset + view.limit);
for (let index = Math.min(view.offset, rows.length); index < end; index++)
items.push(selectFields(rows[index]!, index, selectedFields));
} else {
for (const index of rowIndices(rows.length, view.indices)) {
const row = rows[index]!;
if (matches !== undefined && !matches(row, index)) continue;
if (total >= view.offset && items.length < view.limit)
items.push(selectFields(row, index, selectedFields));
total++;
}
}
return ok({
kind: "il2cpp-metadata",
view,
...shared,
items,
metadata_version: report.metadata_version,
...(report.binary == null
? {}
: {
binary_source: {
artifact: report.binary.artifact,
format: report.binary.format,
architecture: report.binary.architecture,
image_base: report.binary.image_base,
},
}),
counts,
coverage: pageViewCoverage(view.offset, items.length, total),
});
};