mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 02:45:58 +00:00
feat(il2cpp): link native methods and export Ghidra seeds
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.
This commit is contained in:
1 parent
e50a271c72
commit
52d1a72af0
43 files changed
+5060
-368
No files matched your search
@@ -0,0 +1,175 @@
|
||||
import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js";
|
||||
import {
|
||||
readPointer,
|
||||
readUInt32,
|
||||
isMapped,
|
||||
type NativeImage,
|
||||
} from "./Il2cppBinaryImage.js";
|
||||
import type {
|
||||
Il2cppMetadataDecoded,
|
||||
Il2cppMetadataLinkFacts,
|
||||
} from "./Il2cppMetadataFile.js";
|
||||
const PRIMITIVES = new Map([
|
||||
[0x01, "void"],
|
||||
[0x02, "bool"],
|
||||
[0x03, "char"],
|
||||
[0x04, "sbyte"],
|
||||
[0x05, "byte"],
|
||||
[0x06, "short"],
|
||||
[0x07, "ushort"],
|
||||
[0x08, "int"],
|
||||
[0x09, "uint"],
|
||||
[0x0a, "long"],
|
||||
[0x0b, "ulong"],
|
||||
[0x0c, "float"],
|
||||
[0x0d, "double"],
|
||||
[0x0e, "string"],
|
||||
[0x16, "System.TypedReference"],
|
||||
[0x18, "nint"],
|
||||
[0x19, "nuint"],
|
||||
[0x1c, "object"],
|
||||
]);
|
||||
|
||||
// Cached rendered text has its own workspace bound; the report owner separately
|
||||
// charges each returned occurrence and JSON expansion. No live-state cache.
|
||||
const TYPE_TEXT_BYTES = 64 * 1024 * 1024;
|
||||
const TYPE_DEPTH = 64;
|
||||
|
||||
/** Render one snapshot's type graph cooperatively without allocating unchecked expansions. */
|
||||
export const createTypeNamer = (
|
||||
image: NativeImage,
|
||||
metadata: Il2cppMetadataDecoded,
|
||||
facts: Il2cppMetadataLinkFacts,
|
||||
version: number,
|
||||
beforeText?: (bytes: number) => void,
|
||||
) => {
|
||||
const fail = (message: string): never => {
|
||||
throw new Il2cppBinaryFormatFailure("invalid-type", message);
|
||||
};
|
||||
const pointer = (address: bigint, label: string) =>
|
||||
readPointer(image, address) ??
|
||||
fail(`${label} at 0x${address.toString(16)} is not file mapped`);
|
||||
const u32 = (address: bigint, label: string) =>
|
||||
readUInt32(image, address) ??
|
||||
fail(`${label} at 0x${address.toString(16)} is not file mapped`);
|
||||
let cachedBytes = 0;
|
||||
let work = 0;
|
||||
const guard = (bytes: number) => {
|
||||
if (bytes > TYPE_TEXT_BYTES - cachedBytes)
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"native-type-text-capacity",
|
||||
`rendered type text workspace exceeds ${TYPE_TEXT_BYTES} UTF-8 bytes`,
|
||||
);
|
||||
beforeText?.(bytes);
|
||||
};
|
||||
const join = (parts: readonly string[], separator = "") => {
|
||||
let bytes = Math.max(parts.length - 1, 0) * Buffer.byteLength(separator);
|
||||
for (const part of parts) {
|
||||
bytes += Buffer.byteLength(part);
|
||||
guard(bytes);
|
||||
}
|
||||
return parts.join(separator);
|
||||
};
|
||||
function* definitionName(index: number): Generator<void, string> {
|
||||
const seen = new Set<number>();
|
||||
const parts: string[] = [];
|
||||
let namespace = "";
|
||||
for (let at = index; at !== -1;) {
|
||||
if (seen.has(at)) fail(`cyclic declaring type at ${at}`);
|
||||
if (++work % 256 === 0) yield;
|
||||
seen.add(at);
|
||||
const type =
|
||||
metadata.types[at] ?? fail(`type definition ${at} is out of range`);
|
||||
parts.push(type.name);
|
||||
namespace = type.namespace;
|
||||
at = type.declaring_type;
|
||||
}
|
||||
const name = join(parts.reverse(), "+");
|
||||
return namespace === "" ? name : join([namespace, name], ".");
|
||||
}
|
||||
const byAddress = new Map<bigint, string>();
|
||||
const active = new Set<bigint>();
|
||||
const byrefBit = version >= 29 ? 29 : 30;
|
||||
function* name(address: bigint, depth: number): Generator<void, string> {
|
||||
const cached = byAddress.get(address);
|
||||
if (cached !== undefined) return cached;
|
||||
if (active.has(address))
|
||||
fail(`cyclic Il2CppType at 0x${address.toString(16)}`);
|
||||
if (depth > TYPE_DEPTH)
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"type-nesting-capacity",
|
||||
`type rendering exceeds ${TYPE_DEPTH} nested frames`,
|
||||
);
|
||||
active.add(address);
|
||||
try {
|
||||
if (++work % 256 === 0) yield;
|
||||
const data = pointer(address, "Il2CppType data");
|
||||
const bits = u32(address + 8n, "Il2CppType attributes");
|
||||
const kind = (bits >>> 16) & 0xff;
|
||||
const index = Number(BigInt.asIntN(32, data & 0xffffffffn));
|
||||
let result: string;
|
||||
const primitive = PRIMITIVES.get(kind);
|
||||
if (primitive !== undefined) result = primitive;
|
||||
else if (kind === 0x11 || kind === 0x12)
|
||||
result = yield* definitionName(index);
|
||||
else if (kind === 0x0f || kind === 0x1d)
|
||||
result = join([
|
||||
yield* name(data, depth + 1),
|
||||
kind === 0x0f ? "*" : "[]",
|
||||
]);
|
||||
else if (kind === 0x14) {
|
||||
const rank = u32(data + 8n, "Il2CppArrayType rank") & 0xff;
|
||||
if (rank === 0) fail("array rank is zero");
|
||||
result = join([
|
||||
yield* name(pointer(data, "array element"), depth + 1),
|
||||
"[",
|
||||
",".repeat(rank - 1),
|
||||
"]",
|
||||
]);
|
||||
} else if (kind === 0x13 || kind === 0x1e)
|
||||
result =
|
||||
facts.genericParameterName(index) ??
|
||||
fail(`generic parameter ${index} is out of range`);
|
||||
else if (kind === 0x15) result = yield* generic(data, depth);
|
||||
else
|
||||
throw new Il2cppBinaryFormatFailure(
|
||||
"unsupported-type-kind",
|
||||
`type kind 0x${kind.toString(16)} is not rendered`,
|
||||
);
|
||||
if (((bits >>> byrefBit) & 1) === 1) result = join([result, "&"]);
|
||||
const bytes = Buffer.byteLength(result);
|
||||
guard(bytes);
|
||||
cachedBytes += bytes;
|
||||
byAddress.set(address, result);
|
||||
return result;
|
||||
} finally {
|
||||
active.delete(address);
|
||||
}
|
||||
}
|
||||
function* generic(data: bigint, depth: number): Generator<void, string> {
|
||||
const definition =
|
||||
version >= 27
|
||||
? yield* name(pointer(data, "generic class type"), depth + 1)
|
||||
: yield* definitionName(u32(data, "generic class definition"));
|
||||
const instance = pointer(data + 8n, "generic class instance");
|
||||
const count = u32(instance, "generic argument count");
|
||||
const argv = pointer(instance + 8n, "generic arguments");
|
||||
guard(Math.max(count - 1, 0) * 2);
|
||||
if (!isMapped(image, argv, count * 8))
|
||||
fail("generic argument array is not file mapped");
|
||||
const args: string[] = [];
|
||||
let bytes = Math.max(count - 1, 0) * 2;
|
||||
for (let slot = 0; slot < count; slot++) {
|
||||
if (++work % 256 === 0) yield;
|
||||
const argument = yield* name(
|
||||
pointer(argv + BigInt(slot * 8), "generic argument"),
|
||||
depth + 1,
|
||||
);
|
||||
bytes += Buffer.byteLength(argument);
|
||||
guard(bytes);
|
||||
args.push(argument);
|
||||
}
|
||||
return join([definition.replace(/`\d+$/u, ""), "<", join(args, ", "), ">"]);
|
||||
}
|
||||
return name;
|
||||
};
|
||||
Reference in new issue
Block a user