Files
rea/src/il2cpp/Il2cppMetadataFile.ts
T

972 lines
34 KiB
TypeScript

import {
checkRange,
readIndex,
Il2cppMetadataFormatFailure,
} from "./Il2cppMetadataFormat.js";
import {
IL2CPP_METADATA_VERSIONS,
il2cppMetadataSectionNames,
type Il2cppMetadata,
type Il2cppMetadataSection,
type IL2cppMetadataSectionName,
} from "../domain/il2cpp/il2cppMetadata.js";
import { observeIl2cppMetadataHeaderPrefix } from "../domain/il2cpp/il2cppMetadataHeaderPrefix.js";
import {
readIl2cppMembers,
readIl2cppMemberTables,
type MetadataOwnerRange,
} from "./Il2cppMetadataMembers.js";
const NESTED_TYPE_ENTRY_BYTES = 4;
type SectionName = IL2cppMetadataSectionName;
type IndexWidth = 1 | 2 | 4;
export type Il2cppMetadataVersion = (typeof IL2CPP_METADATA_VERSIONS)[number];
type RecordSection = "typeDefinitions" | "images" | "methods";
export type Il2cppMetadataDecoded = Omit<
Il2cppMetadata,
| "artifact"
| "limitations"
| "binary"
| "method_addresses"
| "return_types"
| "field_types"
| "parameter_types"
| "generic_method_instances"
>;
/**
* Version-specific header and record shape. "fixed" headers hold
* (offset, size) pairs and every index is a 4-byte int32; "counted" headers
* hold (offset, size, count) triples whose counts select variable index widths.
*/
interface VersionLayout {
readonly indexing: "fixed" | "counted";
/** Type index fields: version 24 adds byref, 24 to 31 add elementTypeIndex. */
readonly typeDefinitionTypeIndices: 3 | 4 | 5;
/** Version 31 added returnParameterToken to method records. */
readonly methodReturnParameterToken: boolean;
}
const VERSION_LAYOUTS: Readonly<Record<Il2cppMetadataVersion, VersionLayout>> =
{
27: {
indexing: "fixed",
typeDefinitionTypeIndices: 4,
methodReturnParameterToken: false,
},
24: {
indexing: "fixed",
typeDefinitionTypeIndices: 5,
methodReturnParameterToken: false,
},
29: {
indexing: "fixed",
typeDefinitionTypeIndices: 4,
methodReturnParameterToken: false,
},
31: {
indexing: "fixed",
typeDefinitionTypeIndices: 4,
methodReturnParameterToken: true,
},
39: {
indexing: "counted",
typeDefinitionTypeIndices: 3,
methodReturnParameterToken: true,
},
};
const sectionEntryBytes = (layout: VersionLayout) =>
layout.indexing === "counted" ? 12 : 8;
/** Magic, version and one entry per section. */
export const il2cppMetadataHeaderBytes = (version: Il2cppMetadataVersion) =>
8 +
il2cppMetadataSectionNames(version).length *
sectionEntryBytes(VERSION_LAYOUTS[version]);
const isSupportedVersion = (value: number): value is Il2cppMetadataVersion =>
(IL2CPP_METADATA_VERSIONS as readonly number[]).includes(value);
/** Width the runtime selects for an index into a collection of this many entries. */
export const il2cppIndexWidth = (entries: number): IndexWidth =>
entries <= 0xff ? 1 : entries <= 0xffff ? 2 : 4;
/** Record sizes implied by the index widths of one metadata version. */
export const il2cppMetadataRecordBytes = (
widths: {
readonly type_definition: IndexWidth;
readonly generic_container: IndexWidth;
readonly parameter: IndexWidth;
readonly type: IndexWidth;
},
version: Il2cppMetadataVersion,
): Readonly<Record<RecordSection, number>> => {
const layout = VERSION_LAYOUTS[version];
return {
// Two string indices, the type indices, a generic container index,
// flags, eight int32 starts, eight uint16 counts, bitfield and token.
typeDefinitions:
68 +
layout.typeDefinitionTypeIndices * widths.type +
widths.generic_container,
// Name, assembly, two type-definition starts with uint32 counts, entry
// point, token, custom-attribute start and count.
images: 32 + 2 * widths.type_definition,
// Name, declaring type, return type, optional return token, parameter
// start, generic container, token and four uint16 fields.
methods:
16 +
(layout.methodReturnParameterToken ? 4 : 0) +
widths.type_definition +
widths.type +
widths.parameter +
widths.generic_container,
};
};
const isIndexWidth = (value: number): value is IndexWidth =>
value === 1 || value === 2 || value === 4;
const readHeader = (bytes: Buffer) => {
const prefix = observeIl2cppMetadataHeaderPrefix(bytes);
if (prefix.kind !== "il2cpp")
throw new Il2cppMetadataFormatFailure(
"unrecognized-magic",
"the file does not start with the IL2CPP global-metadata magic 0xFAB11BAF; encrypted or non-metadata inputs are not decoded",
);
if (prefix.version === null)
throw new Il2cppMetadataFormatFailure(
"truncated-header",
`the file has ${bytes.length} bytes, fewer than the 8-byte magic/version prefix; the metadata version is unknown`,
);
const { magic, version } = prefix;
if (!isSupportedVersion(version))
throw new Il2cppMetadataFormatFailure(
"unsupported-version",
`metadata version ${version} is not supported; versions ${IL2CPP_METADATA_VERSIONS.join(", ")} are decoded`,
);
const headerBytes = il2cppMetadataHeaderBytes(version);
if (bytes.length < headerBytes)
throw new Il2cppMetadataFormatFailure(
"truncated-header",
`the file has ${bytes.length} bytes, fewer than the ${headerBytes}-byte version ${version} header`,
);
const entryBytes = sectionEntryBytes(VERSION_LAYOUTS[version]);
const sections: Il2cppMetadataSection[] = il2cppMetadataSectionNames(
version,
).map((name, index) => {
const at = 8 + index * entryBytes;
return {
name,
offset: bytes.readInt32LE(at),
size: bytes.readInt32LE(at + 4),
count: entryBytes === 12 ? bytes.readInt32LE(at + 8) : null,
};
});
return { magic, version, headerBytes, sections } as const;
};
const checkSections = (
bytes: Buffer,
headerBytes: number,
sections: readonly Il2cppMetadataSection[],
) => {
const first = sections[0];
if (first?.offset !== headerBytes)
throw new Il2cppMetadataFormatFailure(
"unexpected-header-size",
`stringLiterals starts at ${String(first?.offset)}, not immediately after the ${headerBytes}-byte header`,
);
for (const section of sections)
if (
section.offset < 0 ||
section.size < 0 ||
(section.count ?? 0) < 0 ||
section.offset + section.size > bytes.length
)
throw new Il2cppMetadataFormatFailure(
"section-out-of-bounds",
`section ${section.name} (offset ${section.offset}, size ${section.size}, count ${String(section.count)}) is negative or extends past the ${bytes.length}-byte file`,
);
else if (section.size > 0 && section.offset < headerBytes)
throw new Il2cppMetadataFormatFailure(
"section-out-of-bounds",
`section ${section.name} starts at ${section.offset}, inside the ${headerBytes}-byte header`,
);
// Empty sections may share an offset with any neighbor; gaps are padding.
const occupied = sections
.filter((section) => section.size > 0)
.sort((left, right) => left.offset - right.offset);
for (let index = 1; index < occupied.length; index += 1) {
const previous = occupied[index - 1]!;
const current = occupied[index]!;
if (previous.offset + previous.size > current.offset)
throw new Il2cppMetadataFormatFailure(
"section-overlap",
`section ${previous.name} [${previous.offset}, ${previous.offset + previous.size}) overlaps ${current.name} starting at ${current.offset}`,
);
}
};
/** A counted header's entry count; fixed headers record none. */
const headerCount = (section: Il2cppMetadataSection) => {
if (section.count === null)
throw new TypeError(`section ${section.name} has no header count`);
return section.count;
};
/** Record size from a counted header; null for an empty section. */
const recordBytes = (section: Il2cppMetadataSection): number | null => {
const count = headerCount(section);
if (count === 0) {
if (section.size !== 0)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section ${section.name} declares ${section.size} bytes for zero records`,
);
return null;
}
if (section.size % count !== 0)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section ${section.name} size ${section.size} is not a whole multiple of its ${count} records`,
);
return section.size / count;
};
/** Entry count of a section whose header records only its size. */
const entriesIn = (section: Il2cppMetadataSection, entryBytes: number) => {
if (section.size % entryBytes !== 0)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section ${section.name} size ${section.size} is not a whole multiple of its ${entryBytes}-byte records`,
);
return section.size / entryBytes;
};
const FIXED_WIDTHS: Il2cppMetadataDecoded["index_widths"] = {
type_definition: 4,
generic_container: 4,
parameter: 4,
type: 4,
type_width_source: "fixed-width-version",
};
const GENERIC_CONTAINER_RECORD_BYTES = 16;
const resolveWidths = (
section: (name: SectionName) => Il2cppMetadataSection,
): Il2cppMetadataDecoded["index_widths"] => {
const genericContainers = section("genericContainers");
const genericContainerBytes = recordBytes(genericContainers);
if (
genericContainerBytes !== null &&
genericContainerBytes !== GENERIC_CONTAINER_RECORD_BYTES
)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section genericContainers records are ${genericContainerBytes} bytes, but each record is ${GENERIC_CONTAINER_RECORD_BYTES}`,
);
const type_definition = il2cppIndexWidth(
headerCount(section("typeDefinitions")),
);
const generic_container = il2cppIndexWidth(headerCount(genericContainers));
const parameter = il2cppIndexWidth(headerCount(section("parameters")));
// TypeIndex counts native types, so its width is recovered from records.
const interfaceOffset = recordBytes(section("interfaceOffsets"));
let type: number;
let type_width_source: Il2cppMetadataDecoded["index_widths"]["type_width_source"];
if (interfaceOffset !== null) {
type = interfaceOffset - 4;
type_width_source = "interface-offsets-record";
} else {
const typeDefinition = recordBytes(section("typeDefinitions"));
type =
typeDefinition === null
? Number.NaN
: (typeDefinition - 68 - generic_container) / 3;
type_width_source = "type-definition-record";
}
if (!isIndexWidth(type))
throw new Il2cppMetadataFormatFailure(
"type-index-width",
`the ${type_width_source} does not imply a 1-, 2- or 4-byte TypeIndex`,
);
return {
type_definition,
generic_container,
parameter,
type,
type_width_source,
};
};
/** Record counts checked against the record sizes the widths imply. */
const recordCounts = (
version: Il2cppMetadataVersion,
section: (name: SectionName) => Il2cppMetadataSection,
expected: Readonly<Record<RecordSection, number>>,
): Readonly<Record<RecordSection | "nestedTypes", number>> => {
const names = ["typeDefinitions", "images", "methods"] as const;
if (VERSION_LAYOUTS[version].indexing === "fixed") {
const sized = Object.fromEntries(
names.map((name) => [name, entriesIn(section(name), expected[name])]),
) as Record<RecordSection, number>;
return {
...sized,
nestedTypes: entriesIn(section("nestedTypes"), NESTED_TYPE_ENTRY_BYTES),
};
}
for (const name of names) {
const actual = recordBytes(section(name));
if (actual !== null && actual !== expected[name])
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section ${name} records are ${actual} bytes, but the index widths imply ${expected[name]}`,
);
}
const nestedTypes = section("nestedTypes");
if (nestedTypes.size !== headerCount(nestedTypes) * NESTED_TYPE_ENTRY_BYTES)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
`section nestedTypes size ${nestedTypes.size} does not hold ${String(nestedTypes.count)} 4-byte entries`,
);
return {
typeDefinitions: headerCount(section("typeDefinitions")),
images: headerCount(section("images")),
methods: headerCount(section("methods")),
nestedTypes: headerCount(nestedTypes),
};
};
/**
* Decode NUL-terminated UTF-8 names once each from the strings section. Indices
* are byte offsets and may refer to a suffix of another stored name. Bounds,
* termination, UTF-8 validity, and the total decoding budget remain enforced.
*/
const createStringReader = (
bytes: Buffer,
strings: Il2cppMetadataSection,
beforeText?: (bytes: number) => void,
) => {
const stringEnd = strings.offset + strings.size;
const pool = bytes.subarray(strings.offset, stringEnd);
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
const cache = new Map<number, string>();
return (index: number, label: string) => {
const cached = cache.get(index);
if (cached !== undefined) return cached;
const end = index < 0 || index >= pool.length ? -1 : pool.indexOf(0, index);
if (end === -1)
throw new Il2cppMetadataFormatFailure(
"invalid-string",
`${label} string index ${index} has no terminated entry inside the ${strings.size}-byte string section`,
);
beforeText?.(end - index);
let value: string;
try {
value = decoder.decode(pool.subarray(index, end));
} catch (cause: unknown) {
throw new Il2cppMetadataFormatFailure(
"invalid-string",
`${label} string index ${index} is not valid UTF-8`,
{ cause },
);
}
cache.set(index, value);
return value;
};
};
/** Checked header facts shared by the record readers. */
export interface MetadataTables {
readonly bytes: Buffer;
readonly section: (name: SectionName) => Il2cppMetadataSection;
readonly widths: Il2cppMetadataDecoded["index_widths"];
readonly recordBytes: ReturnType<typeof il2cppMetadataRecordBytes>;
readonly counts: ReturnType<typeof recordCounts>;
readonly typeDefinitionTypeIndices: number;
readonly indexing: "fixed" | "counted";
readonly methodReturnParameterToken: boolean;
readonly readString: ReturnType<typeof createStringReader>;
readonly members: ReturnType<typeof readIl2cppMemberTables>;
readonly beforeText: Il2cppMetadataReadOptions["beforeText"];
readonly onRow: Il2cppMetadataReadOptions["onRow"];
readonly linkFacts?: Il2cppMetadataLinkFacts;
}
function* readImages({
bytes,
section,
widths,
recordBytes: sizes,
counts,
readString,
onRow,
}: MetadataTables): Generator<void, Il2cppMetadataDecoded["images"]> {
const images = section("images");
const typeTotal = counts.typeDefinitions;
const width = widths.type_definition;
const result: Il2cppMetadataDecoded["images"] = [];
for (let index = 0; index < counts.images; index++) {
if (index % 256 === 0) yield;
const at = images.offset + index * sizes.images;
const image = {
name: readString(bytes.readInt32LE(at), `image ${index} name`),
type_start: readIndex(bytes, at + 8, width),
type_count: bytes.readUInt32LE(at + 8 + width),
};
checkRange(
`image ${index} types`,
image.type_start,
image.type_count,
typeTotal,
);
onRow?.(image);
result.push(image);
}
return result;
}
/** Record each type's enclosing type from its declaring type's nestedTypes range. */
function* assignNesting(
{ bytes, section, counts }: MetadataTables,
types: Il2cppMetadataDecoded["types"],
) {
let work = 0;
const nestedTypes = section("nestedTypes");
for (const [index, type] of types.entries()) {
if (work++ % 256 === 0) yield;
checkRange(
`type ${index} nested types`,
type.nested_type_start,
type.nested_type_count,
counts.nestedTypes,
);
for (let offset = 0; offset < type.nested_type_count; offset += 1) {
if (work++ % 256 === 0) yield;
const nested = bytes.readInt32LE(
nestedTypes.offset +
(type.nested_type_start + offset) * NESTED_TYPE_ENTRY_BYTES,
);
const target = types[nested];
if (target === undefined)
throw new Il2cppMetadataFormatFailure(
"index-out-of-range",
`type ${index} lists nested type ${nested} outside the ${types.length} type definitions`,
);
if (nested === index)
throw new Il2cppMetadataFormatFailure(
"nested-type-conflict",
`type ${index} lists itself as a nested type`,
);
if (target.declaring_type !== -1)
throw new Il2cppMetadataFormatFailure(
"nested-type-conflict",
`type ${nested} is listed as nested in type ${index} and type ${target.declaring_type}`,
);
target.declaring_type = index;
}
}
yield* rejectNestingCycles(types);
}
/** Each enclosing chain must reach a top-level type. */
function* rejectNestingCycles(types: Il2cppMetadataDecoded["types"]) {
// 0 unvisited, 1 on the current chain, 2 reaches a top-level type.
let work = 0;
const state = new Uint8Array(types.length);
for (let first = 0; first < types.length; first += 1) {
if (work++ % 256 === 0) yield;
const chain: number[] = [];
let current = first;
while (current !== -1 && state[current] === 0) {
if (work++ % 256 === 0) yield;
state[current] = 1;
chain.push(current);
current = types[current]!.declaring_type;
}
if (current !== -1 && state[current] === 1)
throw new Il2cppMetadataFormatFailure(
"nested-type-conflict",
`type ${current} is nested inside itself through its declaring types`,
);
for (const index of chain) {
if (work++ % 256 === 0) yield;
state[index] = 2;
}
}
}
function* readTypes(tables: MetadataTables): Generator<
void,
{
types: Il2cppMetadataDecoded["types"];
memberRanges: {
fields: MetadataOwnerRange[];
properties: MetadataOwnerRange[];
events: MetadataOwnerRange[];
};
}
> {
const { bytes, section, widths, recordBytes: sizes, counts } = tables;
const { readString, typeDefinitionTypeIndices } = tables;
const typeDefinitions = section("typeDefinitions");
const methodTotal = counts.methods;
const types: Il2cppMetadataDecoded["types"] = [];
const memberRanges = {
fields: [] as MetadataOwnerRange[],
properties: [] as MetadataOwnerRange[],
events: [] as MetadataOwnerRange[],
};
const genericContainerCount =
tables.linkFacts === undefined
? 0
: entriesIn(section("genericContainers"), GENERIC_CONTAINER_RECORD_BYTES);
for (let index = 0; index < counts.typeDefinitions; index++) {
if (index % 256 === 0) yield;
const at = typeDefinitions.offset + index * sizes.typeDefinitions;
// int32 starts follow both string indices, the type indices, the
// generic container index and flags.
const starts =
at +
12 +
typeDefinitionTypeIndices * widths.type +
widths.generic_container;
if (tables.linkFacts !== undefined) {
const genericContainerIndex = readIndex(
bytes,
at + 8 + typeDefinitionTypeIndices * widths.type,
widths.generic_container,
);
if (
genericContainerIndex < -1 ||
genericContainerIndex >= genericContainerCount
)
throw new Il2cppMetadataFormatFailure(
"index-out-of-range",
`type ${index} generic container ${genericContainerIndex} is outside the ${genericContainerCount} available rows`,
);
if (genericContainerIndex !== -1) {
const genericContainerAt =
section("genericContainers").offset +
genericContainerIndex * GENERIC_CONTAINER_RECORD_BYTES;
const argumentCount = bytes.readInt32LE(genericContainerAt + 4);
if (argumentCount < 0)
throw new Il2cppMetadataFormatFailure(
"index-out-of-range",
`type ${index} generic container ${genericContainerIndex} has negative argument count ${argumentCount}`,
);
tables.linkFacts.genericTypeArgumentCounts[index] = argumentCount;
}
}
const countsAt = starts + 32;
const type = {
name: readString(bytes.readInt32LE(at), `type ${index} name`),
namespace: readString(
bytes.readInt32LE(at + 4),
`type ${index} namespace`,
),
method_start: bytes.readInt32LE(starts + 4),
method_count: bytes.readUInt16LE(countsAt),
nested_type_start: bytes.readInt32LE(starts + 16),
nested_type_count: bytes.readUInt16LE(countsAt + 8),
declaring_type: -1,
};
checkRange(
`type ${index} methods`,
type.method_start,
type.method_count,
methodTotal,
);
memberRanges.fields.push({
start: bytes.readInt32LE(starts),
count: bytes.readUInt16LE(countsAt + 4),
});
memberRanges.events.push({
start: bytes.readInt32LE(starts + 8),
count: bytes.readUInt16LE(countsAt + 6),
});
memberRanges.properties.push({
start: bytes.readInt32LE(starts + 12),
count: bytes.readUInt16LE(countsAt + 2),
});
tables.onRow?.(type);
types.push(type);
}
yield* assignNesting(tables, types);
return { types, memberRanges };
}
function* readMethods({
bytes,
section,
widths,
recordBytes: sizes,
counts,
readString,
methodReturnParameterToken,
linkFacts,
onRow,
}: MetadataTables): Generator<
void,
{
methods: Il2cppMetadataDecoded["methods"];
parameterRanges: MetadataOwnerRange[];
}
> {
const methods = section("methods");
const typeTotal = counts.typeDefinitions;
const result: Il2cppMetadataDecoded["methods"] = [];
const parameterRanges: MetadataOwnerRange[] = [];
for (let index = 0; index < counts.methods; index++) {
if (index % 256 === 0) yield;
const at = methods.offset + index * sizes.methods;
const method = {
name: readString(bytes.readInt32LE(at), `method ${index} name`),
declaring_type: readIndex(bytes, at + 4, widths.type_definition),
};
if (method.declaring_type < -1 || method.declaring_type >= typeTotal)
throw new Il2cppMetadataFormatFailure(
"index-out-of-range",
`method ${index} declares type ${method.declaring_type} outside the ${typeTotal} type definitions`,
);
if (linkFacts !== undefined) {
const returnAt = at + 4 + widths.type_definition;
linkFacts.returnTypes[index] = readIndex(bytes, returnAt, widths.type);
linkFacts.methodTokens[index] = bytes.readUInt32LE(
at +
methodContainerAt({ widths, methodReturnParameterToken }) +
widths.generic_container,
);
}
parameterRanges.push({
start: readIndex(
bytes,
at +
4 +
widths.type_definition +
widths.type +
(methodReturnParameterToken ? 4 : 0),
widths.parameter,
),
count: bytes.readUInt16LE(at + sizes.methods - 2),
});
onRow?.(method);
result.push(method);
}
return { methods: result, parameterRanges };
}
/**
* Type method ranges and method declaring types must agree in both
* directions, so a damaged range cannot strand or misattribute a method.
*/
function* checkMethodOwners(
types: Il2cppMetadataDecoded["types"],
methods: Il2cppMetadataDecoded["methods"],
) {
let work = 0;
for (const [index, type] of types.entries()) {
if (work++ % 256 === 0) yield;
for (let offset = 0; offset < type.method_count; offset += 1) {
if (work++ % 256 === 0) yield;
const method = type.method_start + offset;
const declaring = methods[method]!.declaring_type;
if (declaring !== index)
throw new Il2cppMetadataFormatFailure(
"declaring-type-mismatch",
`method ${method} is in type ${index}'s range but declares type ${declaring}`,
);
}
}
for (const [index, method] of methods.entries()) {
if (work++ % 256 === 0) yield;
const owner = types[method.declaring_type];
if (
owner !== undefined &&
(index < owner.method_start ||
index >= owner.method_start + owner.method_count)
)
throw new Il2cppMetadataFormatFailure(
"declaring-type-mismatch",
`method ${index} declares type ${method.declaring_type} but is outside its method range`,
);
}
}
/** Each type belongs to at most one image. */
function* checkImageRanges(images: Il2cppMetadataDecoded["images"]) {
const ranges: { start: number; end: number; index: number }[] = [];
for (let index = 0; index < images.length; index++) {
if (index % 256 === 0) yield;
const image = images[index]!;
if (image.type_count > 0)
ranges.push({
start: image.type_start,
end: image.type_start + image.type_count,
index,
});
}
ranges.sort((left, right) => left.start - right.start);
for (let at = 1; at < ranges.length; at++) {
if (at % 256 === 0) yield;
const previous = ranges[at - 1]!;
const current = ranges[at]!;
if (previous.end > current.start)
throw new Il2cppMetadataFormatFailure(
"image-range-overlap",
`image ${previous.index} types [${previous.start}, ${previous.end}) overlap image ${current.index} starting at type ${current.start}`,
);
}
}
/** Row counts known from the header and record sizes, before rows are built. */
export interface Il2cppMetadataRowCounts {
readonly images: number;
readonly types: number;
readonly methods: number;
readonly fields: number;
readonly properties: number;
readonly events: number;
readonly parameters: number;
readonly string_literals: number;
}
export interface Il2cppMetadataReadOptions {
/** Called before any row is decoded; may throw to refuse the file. */
readonly beforeRows?: (counts: Il2cppMetadataRowCounts) => void;
/** Guard raw text before allocating its decoded or base64 representation. */
readonly beforeText?: (bytes: number) => void;
/** Admit each fact before retention, including repeated strings. */
readonly onRow?: (
row: Readonly<Record<string, string | number | null>>,
) => void;
}
/** Per-row typed fields the public inventory omits but native linking needs. */
export interface Il2cppMetadataLinkFacts {
/** Method definition token per method index. */
readonly methodTokens: Uint32Array;
/** Il2CppType table index of each method's return type. */
readonly returnTypes: Int32Array;
readonly fieldTypes: Int32Array;
readonly parameterTypes: Int32Array;
/** Total class generic argument count per TypeDefinition, including enclosing types. */
readonly genericTypeArgumentCounts: Int32Array;
/** Demand-read names from this stable metadata snapshot, by GenericParameterIndex. */
readonly genericParameterName: (index: number) => string | undefined;
/**
* Demand-read GenericContainerIndex of a type definition: -1 for a
* non-generic type, undefined when the index or layout is not decoded.
*/
readonly typeGenericContainer: (index: number) => number | undefined;
/**
* Demand-read GenericContainerIndex of a method definition: -1 for a
* non-generic method, undefined when the index is out of range.
*/
readonly methodGenericContainer: (index: number) => number | undefined;
/**
* Demand-read argument count and first GenericParameterIndex of a generic
* container; undefined out of range and for the counted (version 39)
* layout, whose container records are not decoded.
*/
readonly genericContainer: (
index: number,
) => { readonly count: number; readonly parameterStart: number } | undefined;
}
/** Byte offset of GenericContainerIndex in a method record; the token follows. */
const methodContainerAt = ({
widths,
methodReturnParameterToken,
}: Pick<MetadataTables, "widths" | "methodReturnParameterToken">) =>
4 +
widths.type_definition +
widths.type +
(methodReturnParameterToken ? 4 : 0) +
widths.parameter;
/** Type and method container indices and container records, read on demand. */
const createGenericContainerReaders = (tables: MetadataTables) => {
const types = tables.section("typeDefinitions"),
typeCount = tables.counts.typeDefinitions,
typeBytes = tables.recordBytes.typeDefinitions;
// Both string indices and the type indices precede the container index.
const containerAt = 8 + tables.typeDefinitionTypeIndices * tables.widths.type;
const containers = tables.section("genericContainers");
// ownerIndex, type_argc, is_method and genericParameterStart, all int32.
const containerBytes = 16;
const containerCount =
tables.indexing === "counted"
? 0
: Math.floor(containers.size / containerBytes);
const inRange = (index: number, count: number) =>
Number.isInteger(index) && index >= 0 && index < count;
const methods = tables.section("methods"),
methodCount = tables.counts.methods,
methodBytes = tables.recordBytes.methods,
methodAt = methodContainerAt(tables);
return {
methodGenericContainer: (index: number): number | undefined =>
!inRange(index, methodCount)
? undefined
: readIndex(
tables.bytes,
methods.offset + index * methodBytes + methodAt,
tables.widths.generic_container,
),
typeGenericContainer: (index: number): number | undefined =>
tables.indexing === "counted" || !inRange(index, typeCount)
? undefined
: readIndex(
tables.bytes,
types.offset + index * typeBytes + containerAt,
tables.widths.generic_container,
),
genericContainer: (index: number) => {
if (!inRange(index, containerCount)) return undefined;
const at = containers.offset + index * containerBytes;
return {
count: tables.bytes.readInt32LE(at + 4),
parameterStart: tables.bytes.readInt32LE(at + 12),
};
},
};
};
/** Own the row layout once; unused generic names allocate no arrays or strings. */
const createGenericParameterReader = (tables: MetadataTables) => {
const parameters = tables.section("genericParameters"),
width = tables.widths.generic_container + 12;
const count = entriesIn(parameters, width);
if (parameters.count !== null && count !== parameters.count)
throw new Il2cppMetadataFormatFailure(
"record-size-mismatch",
"generic parameter count disagrees with its extent",
);
return (index: number): string | undefined => {
if (!Number.isInteger(index) || index < 0 || index >= count)
return undefined;
return tables.readString(
tables.bytes.readInt32LE(
parameters.offset + index * width + tables.widths.generic_container,
),
`generic parameter ${index} name`,
);
};
};
/** Decoded metadata plus the typed row facts that link it to a native binary. */
export interface Il2cppMetadataForLinking {
readonly metadata: Il2cppMetadataDecoded;
readonly link: Il2cppMetadataLinkFacts;
}
/** Decode image, type, method and member names and string literals without native binaries or decryption. */
export function* readIl2cppMetadataSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Generator<void, Il2cppMetadataDecoded> {
return (yield* readMetadataCoreSteps(bytes, options, false)).metadata;
}
/** Decode metadata and retain the typed row facts used for native linking. */
export function* readIl2cppMetadataForLinkingSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions = {},
): Generator<void, Il2cppMetadataForLinking> {
const parsed = yield* readMetadataCoreSteps(bytes, options, true);
if (parsed.link === undefined)
throw new Error(
"Native metadata reader did not retain requested linking facts",
);
return { metadata: parsed.metadata, link: parsed.link };
}
function* readMetadataCoreSteps(
bytes: Buffer,
options: Il2cppMetadataReadOptions,
linking: boolean,
): Generator<
void,
{ metadata: Il2cppMetadataDecoded; link: Il2cppMetadataLinkFacts | undefined }
> {
const { magic, version, headerBytes, sections } = readHeader(bytes);
checkSections(bytes, headerBytes, sections);
const byName = new Map(sections.map((entry) => [entry.name, entry]));
const section = (name: SectionName) => byName.get(name)!;
const layout = VERSION_LAYOUTS[version];
const widths =
layout.indexing === "counted" ? resolveWidths(section) : FIXED_WIDTHS;
const expected = il2cppMetadataRecordBytes(widths, version);
let tables: MetadataTables = {
bytes,
section,
widths,
recordBytes: expected,
counts: recordCounts(version, section, expected),
typeDefinitionTypeIndices: layout.typeDefinitionTypeIndices,
indexing: layout.indexing,
methodReturnParameterToken: layout.methodReturnParameterToken,
readString: createStringReader(
bytes,
section("strings"),
options.beforeText,
),
members: readIl2cppMemberTables({
widths,
indexing: layout.indexing,
section,
}),
beforeText: options.beforeText,
onRow: options.onRow,
};
options.beforeRows?.({
images: tables.counts.images,
types: tables.counts.typeDefinitions,
methods: tables.counts.methods,
...tables.members.counts,
});
if (linking)
tables = {
...tables,
linkFacts: {
methodTokens: new Uint32Array(tables.counts.methods),
returnTypes: new Int32Array(tables.counts.methods),
fieldTypes: new Int32Array(tables.members.counts.fields),
parameterTypes: new Int32Array(tables.members.counts.parameters),
genericTypeArgumentCounts: new Int32Array(
tables.counts.typeDefinitions,
),
genericParameterName: createGenericParameterReader(tables),
...createGenericContainerReaders(tables),
},
};
yield;
const images = yield* readImages(tables);
yield* checkImageRanges(images);
const { types, memberRanges } = yield* readTypes(tables);
const { methods, parameterRanges } = yield* readMethods(tables);
yield* checkMethodOwners(types, methods);
const members = yield* readIl2cppMembers(tables, {
...memberRanges,
parameters: parameterRanges,
});
return {
metadata: {
magic,
metadata_version: version,
index_widths: widths,
sections,
images,
types,
methods,
...members,
},
link: tables.linkFacts,
};
}