mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 19:35:56 +00:00
feat(il2cpp): read member names, string literals and metadata versions 24 and 27 (#5326)
* feat(il2cpp): read member names, string literals and metadata versions 24 and 27 Adds field, property, event and parameter names with their owners, managed string literals, and the version 24 and 27 header layouts to inspect_il2cpp_metadata, with filtered pages for the new collections. Member decoding yields between row batches like the other row readers. * fix(il2cpp): bound metadata expansion and query parameter owners * test(il2cpp): cover nonempty property and event tables --------- Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
052956af34
commit
21e0c580e7
24 files changed
+1707
-315
No files matched your search
@@ -0,0 +1,226 @@
|
||||
import type {
|
||||
Il2cppMetadata,
|
||||
Il2cppMetadataSection,
|
||||
} from "../domain/il2cpp/il2cppMetadata.js";
|
||||
import {
|
||||
checkRange,
|
||||
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]!,
|
||||
);
|
||||
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),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user