mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
52d1a72af0
Link IL2CPP metadata names to native addresses and types through shared CLI/MCP workflows, with bounded queries and atomic Ghidra seed export. Preserve the original implementation by 8rulerstar and follow-up correctness, resource, provenance, and CI dependency repairs. Verified against real Windows/Android artifacts, independent references, actual Ghidra, and focused CLI/MCP tests. Fixes #5112.
244 lines
7.4 KiB
TypeScript
244 lines
7.4 KiB
TypeScript
import type {
|
|
Il2cppMetadata,
|
|
Il2cppMetadataSection,
|
|
} from "../domain/il2cpp/il2cppMetadata.js";
|
|
import {
|
|
checkRange,
|
|
readIndex,
|
|
Il2cppMetadataFormatFailure,
|
|
} from "./Il2cppMetadataFormat.js";
|
|
import type { MetadataTables } from "./Il2cppMetadataFile.js";
|
|
|
|
type Members = Pick<
|
|
Il2cppMetadata,
|
|
"fields" | "properties" | "events" | "parameters" | "string_literals"
|
|
>;
|
|
export interface MetadataOwnerRange {
|
|
readonly start: number;
|
|
readonly count: number;
|
|
}
|
|
export interface MetadataMemberOwners {
|
|
readonly fields: readonly MetadataOwnerRange[];
|
|
readonly properties: readonly MetadataOwnerRange[];
|
|
readonly events: readonly MetadataOwnerRange[];
|
|
readonly parameters: readonly MetadataOwnerRange[];
|
|
}
|
|
|
|
const entries = (
|
|
indexing: MetadataTables["indexing"],
|
|
section: Il2cppMetadataSection,
|
|
recordBytes: number,
|
|
) => {
|
|
const count =
|
|
indexing === "counted" ? (section.count ?? 0) : section.size / recordBytes;
|
|
if (!Number.isInteger(count) || section.size !== count * recordBytes)
|
|
throw new Il2cppMetadataFormatFailure(
|
|
"record-size-mismatch",
|
|
`section ${section.name} size ${section.size} does not hold whole ${recordBytes}-byte records`,
|
|
);
|
|
return count;
|
|
};
|
|
|
|
/** Own member record widths and validate all counts before any row allocation. */
|
|
export const readIl2cppMemberTables = ({
|
|
widths,
|
|
indexing,
|
|
section,
|
|
}: Pick<MetadataTables, "widths" | "indexing" | "section">) => {
|
|
const recordBytes = {
|
|
fields: 8 + widths.type,
|
|
properties: 20,
|
|
events: 20 + widths.type,
|
|
parameters: 8 + widths.type,
|
|
stringLiterals: indexing === "counted" ? 4 : 8,
|
|
};
|
|
const literalRecords = entries(
|
|
indexing,
|
|
section("stringLiterals"),
|
|
recordBytes.stringLiterals,
|
|
);
|
|
return {
|
|
recordBytes,
|
|
literalRecords,
|
|
counts: {
|
|
fields: entries(indexing, section("fields"), recordBytes.fields),
|
|
properties: entries(
|
|
indexing,
|
|
section("properties"),
|
|
recordBytes.properties,
|
|
),
|
|
events: entries(indexing, section("events"), recordBytes.events),
|
|
parameters: entries(
|
|
indexing,
|
|
section("parameters"),
|
|
recordBytes.parameters,
|
|
),
|
|
string_literals:
|
|
indexing === "counted"
|
|
? Math.max(literalRecords - 1, 0)
|
|
: literalRecords,
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Owners come from already parsed type/method facts, never a second raw-table interpretation. */
|
|
function* claim(
|
|
ranges: readonly MetadataOwnerRange[],
|
|
total: number,
|
|
label: string,
|
|
): Generator<void, Int32Array> {
|
|
const owners = new Int32Array(total).fill(-1);
|
|
let work = 0;
|
|
for (const [owner, range] of ranges.entries()) {
|
|
if (++work % 256 === 0) yield;
|
|
checkRange(
|
|
`${label} range of owner ${owner}`,
|
|
range.start,
|
|
range.count,
|
|
total,
|
|
);
|
|
for (let row = range.start; row < range.start + range.count; row++) {
|
|
if (++work % 256 === 0) yield;
|
|
if (owners[row] !== -1)
|
|
throw new Il2cppMetadataFormatFailure(
|
|
"declaring-type-mismatch",
|
|
`${label} ${row} is claimed by owners ${owners[row]} and ${owner}`,
|
|
);
|
|
owners[row] = owner;
|
|
}
|
|
}
|
|
return owners;
|
|
}
|
|
|
|
function* readNamed<
|
|
T extends { name: string } & Record<string, string | number>,
|
|
>(
|
|
tables: MetadataTables,
|
|
name: "fields" | "properties" | "events" | "parameters",
|
|
ranges: readonly MetadataOwnerRange[],
|
|
makeRow: (name: string, owner: number) => T,
|
|
): Generator<void, T[]> {
|
|
const section = tables.section(name),
|
|
total = tables.members.counts[name];
|
|
const owners = yield* claim(ranges, total, name),
|
|
rows: T[] = [];
|
|
for (let index = 0; index < total; index++) {
|
|
if (index % 256 === 0) yield;
|
|
const row = makeRow(
|
|
tables.readString(
|
|
tables.bytes.readInt32LE(
|
|
section.offset + index * tables.members.recordBytes[name],
|
|
),
|
|
`${name} ${index} name`,
|
|
),
|
|
owners[index]!,
|
|
);
|
|
if (
|
|
tables.linkFacts !== undefined &&
|
|
(name === "fields" || name === "parameters")
|
|
) {
|
|
const column =
|
|
name === "fields"
|
|
? tables.linkFacts.fieldTypes
|
|
: tables.linkFacts.parameterTypes;
|
|
column[index] = readIndex(
|
|
tables.bytes,
|
|
section.offset +
|
|
index * tables.members.recordBytes[name] +
|
|
(name === "fields" ? 4 : 8),
|
|
tables.widths.type,
|
|
);
|
|
}
|
|
tables.onRow?.(row);
|
|
rows.push(row);
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function* readStringLiterals(
|
|
tables: MetadataTables,
|
|
): Generator<void, Members["string_literals"]> {
|
|
const { bytes, section, indexing, members } = tables;
|
|
const literals = section("stringLiterals"),
|
|
data = section("stringLiteralData");
|
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
// Repeated spans reuse their decoded text for this immutable file snapshot.
|
|
const decoded = new Map<string, Members["string_literals"][number]>();
|
|
const rows: Members["string_literals"] = [];
|
|
if (indexing === "counted" && members.literalRecords === 1) {
|
|
const sentinel = bytes.readInt32LE(literals.offset);
|
|
if (sentinel < 0 || sentinel > data.size)
|
|
throw new Il2cppMetadataFormatFailure(
|
|
"index-out-of-range",
|
|
`empty string literal sentinel ${sentinel} is outside the ${data.size}-byte literal data`,
|
|
);
|
|
}
|
|
for (let index = 0; index < members.counts.string_literals; index++) {
|
|
if (index % 256 === 0) yield;
|
|
const at = literals.offset + index * members.recordBytes.stringLiterals;
|
|
const start = bytes.readInt32LE(at + (indexing === "counted" ? 0 : 4));
|
|
const length =
|
|
indexing === "counted"
|
|
? bytes.readInt32LE(at + 4) - start
|
|
: bytes.readUInt32LE(at);
|
|
if (start < 0 || length < 0 || start + length > data.size)
|
|
throw new Il2cppMetadataFormatFailure(
|
|
"index-out-of-range",
|
|
`string literal ${index} [${start}, ${start + length}) is outside the ${data.size}-byte literal data`,
|
|
);
|
|
const key = `${start}:${length}`;
|
|
let literal = decoded.get(key);
|
|
if (literal === undefined) {
|
|
// Strict UTF-8 round trips preserve at least these bytes; base64 is larger.
|
|
// Refuse impossible output before allocating either representation.
|
|
tables.beforeText?.(length);
|
|
const value = bytes.subarray(
|
|
data.offset + start,
|
|
data.offset + start + length,
|
|
);
|
|
try {
|
|
literal = { value: decoder.decode(value), value_base64: null };
|
|
} catch {
|
|
tables.beforeText?.(4 * Math.ceil(length / 3));
|
|
literal = { value: null, value_base64: value.toString("base64") };
|
|
}
|
|
decoded.set(key, literal);
|
|
}
|
|
tables.onRow?.(literal);
|
|
rows.push({ ...literal });
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
/** Decode each fact directly into its final row shape, yielding through ownership and content. */
|
|
export function* readIl2cppMembers(
|
|
tables: MetadataTables,
|
|
owners: MetadataMemberOwners,
|
|
): Generator<void, Members> {
|
|
const member = (name: string, declaring_type: number) => ({
|
|
name,
|
|
declaring_type,
|
|
});
|
|
const fields = yield* readNamed(tables, "fields", owners.fields, member);
|
|
const properties = yield* readNamed(
|
|
tables,
|
|
"properties",
|
|
owners.properties,
|
|
member,
|
|
);
|
|
const events = yield* readNamed(tables, "events", owners.events, member);
|
|
const parameters = yield* readNamed(
|
|
tables,
|
|
"parameters",
|
|
owners.parameters,
|
|
(name, method) => ({ name, method }),
|
|
);
|
|
return {
|
|
fields,
|
|
properties,
|
|
events,
|
|
parameters,
|
|
string_literals: yield* readStringLiterals(tables),
|
|
};
|
|
}
|