* 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>
707 lines
21 KiB
TypeScript
707 lines
21 KiB
TypeScript
import {
|
|
Il2cppMetadataFormatFailure,
|
|
type Il2cppMetadataFailureReason,
|
|
} from "../../../src/il2cpp/Il2cppMetadataFormat.js";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
il2cppIndexWidth,
|
|
il2cppMetadataRecordBytes,
|
|
readIl2cppMetadata,
|
|
readIl2cppMetadataSteps,
|
|
} from "../../../src/il2cpp/Il2cppMetadataFile.js";
|
|
import {
|
|
createIl2cppMetadataFixture,
|
|
FIXTURE_IMAGES,
|
|
fixtureImages,
|
|
FIXTURE_METHODS,
|
|
FIXTURE_FIELDS,
|
|
FIXTURE_PROPERTIES,
|
|
FIXTURE_EVENTS,
|
|
FIXTURE_PARAMETERS,
|
|
FIXTURE_STRING_LITERALS,
|
|
FIXTURE_TYPES,
|
|
fixtureRows,
|
|
type Il2cppMetadataFixture,
|
|
} from "../../fixtures/il2cpp/metadata.js";
|
|
|
|
const failure = (bytes: Buffer) => {
|
|
try {
|
|
readIl2cppMetadata(bytes);
|
|
} catch (cause: unknown) {
|
|
if (cause instanceof Il2cppMetadataFormatFailure) return cause;
|
|
throw cause;
|
|
}
|
|
throw new Error("Expected the metadata to be rejected");
|
|
};
|
|
|
|
const rejects = (
|
|
bytes: Buffer,
|
|
reason: Il2cppMetadataFailureReason,
|
|
kind: "unsupported" | "corrupt" = "corrupt",
|
|
) => {
|
|
const rejected = failure(bytes);
|
|
expect(rejected.reason).toBe(reason);
|
|
expect(rejected.kind).toBe(kind);
|
|
expect(rejected.message.startsWith(`${reason}: `)).toBe(true);
|
|
};
|
|
|
|
const mutated = (
|
|
edit: (fixture: Il2cppMetadataFixture, bytes: Buffer) => void,
|
|
fixture = createIl2cppMetadataFixture(),
|
|
) => {
|
|
const bytes = Buffer.from(fixture.bytes);
|
|
edit(fixture, bytes);
|
|
return bytes;
|
|
};
|
|
|
|
describe("IL2CPP global-metadata version 39", () => {
|
|
it("yields through a large owner range before emitting its first parameter fact", () => {
|
|
const fixture = createIl2cppMetadataFixture({
|
|
wide: { fillerTypes: 0, genericContainers: 0, parameters: 65_000 },
|
|
});
|
|
const bytes = Buffer.from(fixture.bytes),
|
|
total = fixture.section("parameters").count!;
|
|
bytes.writeUInt16LE(
|
|
total,
|
|
fixture.record("methods", 2) + fixture.recordBytes.methods - 2,
|
|
);
|
|
let firstParameter = false,
|
|
yieldsBeforeParameter = 0;
|
|
const steps = readIl2cppMetadataSteps(bytes, {
|
|
onRow: (row) => {
|
|
if ("method" in row) firstParameter = true;
|
|
},
|
|
});
|
|
for (;;) {
|
|
const step = steps.next();
|
|
if (step.done) break;
|
|
if (!firstParameter) yieldsBeforeParameter++;
|
|
}
|
|
expect(firstParameter).toBe(true);
|
|
expect(yieldsBeforeParameter).toBeGreaterThan(200);
|
|
});
|
|
it.each([-1, 0x7fffffff])(
|
|
"rejects an invalid empty-literal sentinel %i",
|
|
(sentinel) => {
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
bytes.writeInt32LE(4, fixture.header("stringLiterals", "size"));
|
|
bytes.writeInt32LE(1, fixture.header("stringLiterals", "count"));
|
|
bytes.writeInt32LE(
|
|
sentinel,
|
|
fixture.section("stringLiterals").offset,
|
|
);
|
|
}),
|
|
"index-out-of-range",
|
|
);
|
|
},
|
|
);
|
|
it.each([1, 2, 4] as const)(
|
|
"decodes names, ranges and nesting with a %i-byte TypeIndex in about 1 KiB",
|
|
(typeIndexWidth) => {
|
|
const fixture = createIl2cppMetadataFixture({ typeIndexWidth });
|
|
expect(fixture.bytes.length).toBeLessThan(1100);
|
|
const decoded = readIl2cppMetadata(fixture.bytes);
|
|
expect(decoded.magic).toBe(fixture.bytes.readUInt32LE(0));
|
|
expect(decoded.metadata_version).toBe(39);
|
|
expect(decoded.index_widths).toEqual({
|
|
type_definition: 1,
|
|
generic_container: 1,
|
|
parameter: 1,
|
|
type: typeIndexWidth,
|
|
type_width_source: "interface-offsets-record",
|
|
});
|
|
expect(decoded.images).toEqual(FIXTURE_IMAGES);
|
|
expect(decoded.types).toEqual(FIXTURE_TYPES);
|
|
expect(decoded.methods).toEqual(FIXTURE_METHODS);
|
|
expect(decoded.sections).toHaveLength(31);
|
|
expect(decoded.sections[0]).toEqual({
|
|
name: "stringLiterals",
|
|
offset: 380,
|
|
size: 12,
|
|
count: 3,
|
|
});
|
|
},
|
|
);
|
|
|
|
it("recovers the TypeIndex width from type records without interfaceOffsets", () => {
|
|
const fixture = createIl2cppMetadataFixture({
|
|
typeIndexWidth: 2,
|
|
interfaceOffsets: false,
|
|
});
|
|
const decoded = readIl2cppMetadata(fixture.bytes);
|
|
expect(decoded.index_widths.type).toBe(2);
|
|
expect(decoded.index_widths.type_width_source).toBe(
|
|
"type-definition-record",
|
|
);
|
|
expect(decoded.methods).toEqual(FIXTURE_METHODS);
|
|
});
|
|
|
|
it("accepts empty sections that share an offset with occupied sections and padding gaps", () => {
|
|
const bytes = mutated((fixture, target) => {
|
|
const methods = fixture.section("methods");
|
|
target.writeInt32LE(
|
|
methods.offset + 3,
|
|
fixture.header("windowsRuntimeStrings", "offset"),
|
|
);
|
|
// Grow strings into trailing padding that no other section occupies.
|
|
target.writeInt32LE(
|
|
fixture.section("strings").size,
|
|
fixture.header("strings", "size"),
|
|
);
|
|
});
|
|
expect(readIl2cppMetadata(bytes).types).toEqual(FIXTURE_TYPES);
|
|
});
|
|
});
|
|
|
|
describe("rejected IL2CPP global-metadata", () => {
|
|
it("rejects an unrecognized magic and other versions as unsupported", () => {
|
|
rejects(
|
|
mutated((_, bytes) => bytes.writeUInt32LE(0x12345678, 0)),
|
|
"unrecognized-magic",
|
|
"unsupported",
|
|
);
|
|
rejects(Buffer.alloc(4), "unrecognized-magic", "unsupported");
|
|
rejects(
|
|
mutated((_, bytes) => bytes.writeInt32LE(30, 4)),
|
|
"unsupported-version",
|
|
"unsupported",
|
|
);
|
|
});
|
|
|
|
it("rejects a truncated header and an unexpected header size", () => {
|
|
rejects(
|
|
createIl2cppMetadataFixture().bytes.subarray(0, 200),
|
|
"truncated-header",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(384, fixture.header("stringLiterals", "offset")),
|
|
),
|
|
"unexpected-header-size",
|
|
);
|
|
});
|
|
|
|
it("rejects sections outside the file or with negative fields", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(bytes.length, fixture.header("methods", "size")),
|
|
),
|
|
"section-out-of-bounds",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(-1, fixture.header("events", "count")),
|
|
),
|
|
"section-out-of-bounds",
|
|
);
|
|
});
|
|
|
|
it("rejects overlapping occupied sections", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.section("typeDefinitions").offset + 4,
|
|
fixture.header("images", "offset"),
|
|
),
|
|
),
|
|
"section-overlap",
|
|
);
|
|
});
|
|
|
|
it("rejects record sizes that disagree with counts or index widths", () => {
|
|
// Not a whole multiple of the record count.
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.section("methods").count! + 1,
|
|
fixture.header("methods", "count"),
|
|
),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
// 300 generic containers widen GenericContainerIndex to two bytes.
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(300, fixture.header("genericContainers", "count")),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(2, fixture.header("nestedTypes", "count")),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(0, fixture.header("methods", "count")),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
});
|
|
|
|
it("rejects an interfaceOffsets record that implies no valid TypeIndex width", () => {
|
|
rejects(
|
|
createIl2cppMetadataFixture({ interfaceOffsetRecordBytes: 7 }).bytes,
|
|
"type-index-width",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("rejected IL2CPP metadata references", () => {
|
|
it("rejects image, type, method and nested-type indices outside their tables", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeUInt32LE(9, fixture.record("images", 1) + 9),
|
|
),
|
|
"index-out-of-range",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
// methodStart follows fieldStart in the int32 start block.
|
|
const at = fixture.record("typeDefinitions", 0) + 8 + 3 + 1 + 4 + 4;
|
|
bytes.writeInt32LE(4, at);
|
|
}),
|
|
"index-out-of-range",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeUInt8(4, fixture.record("methods", 2) + 4),
|
|
),
|
|
"index-out-of-range",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(4, fixture.section("nestedTypes").offset),
|
|
),
|
|
"index-out-of-range",
|
|
);
|
|
});
|
|
|
|
it("rejects a type nested in two declaring types", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
// Two string indices, three 1-byte type indices, generic container, flags.
|
|
const starts = fixture.record("typeDefinitions", 3) + 8 + 3 + 1 + 4;
|
|
bytes.writeInt32LE(0, starts + 16);
|
|
bytes.writeUInt16LE(1, starts + 32 + 8);
|
|
}),
|
|
"nested-type-conflict",
|
|
);
|
|
});
|
|
|
|
it("rejects unterminated, out-of-range and non-UTF-8 strings", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.section("strings").size,
|
|
fixture.record("methods", 0),
|
|
),
|
|
),
|
|
"invalid-string",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.section("strings").size - 1,
|
|
fixture.header("strings", "size"),
|
|
),
|
|
),
|
|
"invalid-string",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeUInt8(0xff, fixture.stringOffset("Update")),
|
|
),
|
|
"invalid-string",
|
|
);
|
|
});
|
|
|
|
it("rejects a shifted string section and names that start inside another string", () => {
|
|
// Pulled back, the fixture's strings would overlap the literal data.
|
|
for (const [delta, reason] of [
|
|
[-1, "section-overlap"],
|
|
[1, "invalid-string"],
|
|
] as const)
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
const strings = fixture.section("strings");
|
|
bytes.writeInt32LE(
|
|
strings.offset + delta,
|
|
fixture.header("strings", "offset"),
|
|
);
|
|
// Shrink a forward shift so it stays clear of the next section.
|
|
bytes.writeInt32LE(
|
|
strings.size - Math.max(delta, 0),
|
|
fixture.header("strings", "size"),
|
|
);
|
|
}),
|
|
reason,
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.stringOffset("Update") +
|
|
1 -
|
|
fixture.section("strings").offset,
|
|
fixture.record("methods", 0),
|
|
),
|
|
),
|
|
"invalid-string",
|
|
);
|
|
});
|
|
|
|
it("rejects nesting cycles and self-nesting", () => {
|
|
// Type 0 owns nestedTypes entry 0; listing itself there is a self-cycle.
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(0, fixture.section("nestedTypes").offset),
|
|
),
|
|
"nested-type-conflict",
|
|
);
|
|
rejects(
|
|
createIl2cppMetadataFixture({ nestingCycle: true }).bytes,
|
|
"nested-type-conflict",
|
|
);
|
|
});
|
|
|
|
it("rejects methods whose declaring type disagrees with the owning type range", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeUInt8(3, fixture.record("methods", 1) + 4),
|
|
),
|
|
"declaring-type-mismatch",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP index widths", () => {
|
|
it("switches widths at the runtime's 255 and 65535 entry boundaries", () => {
|
|
expect(
|
|
[0, 255, 256, 65535, 65536].map((entries) => il2cppIndexWidth(entries)),
|
|
).toEqual([1, 1, 2, 2, 4]);
|
|
expect(
|
|
il2cppMetadataRecordBytes(
|
|
{
|
|
type_definition: 2,
|
|
generic_container: 2,
|
|
parameter: 4,
|
|
type: 2,
|
|
},
|
|
39,
|
|
),
|
|
).toEqual({ typeDefinitions: 76, images: 36, methods: 30 });
|
|
const fixed = {
|
|
type_definition: 4,
|
|
generic_container: 4,
|
|
parameter: 4,
|
|
type: 4,
|
|
} as const;
|
|
expect(il2cppMetadataRecordBytes(fixed, 29)).toEqual({
|
|
typeDefinitions: 88,
|
|
images: 40,
|
|
methods: 32,
|
|
});
|
|
expect(il2cppMetadataRecordBytes(fixed, 31).methods).toBe(36);
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP global-metadata versions 29 and 31", () => {
|
|
it.each([29, 31] as const)(
|
|
"decodes version %i with fixed 4-byte indices and size-only headers",
|
|
(version) => {
|
|
const fixture = createIl2cppMetadataFixture({ version });
|
|
const decoded = readIl2cppMetadata(fixture.bytes);
|
|
expect(decoded.metadata_version).toBe(version);
|
|
expect(decoded.magic).toBe(fixture.bytes.readUInt32LE(0));
|
|
expect(decoded.index_widths).toEqual({
|
|
type_definition: 4,
|
|
generic_container: 4,
|
|
parameter: 4,
|
|
type: 4,
|
|
type_width_source: "fixed-width-version",
|
|
});
|
|
expect(decoded.sections[0]).toEqual({
|
|
name: "stringLiterals",
|
|
offset: 256,
|
|
size: 16,
|
|
count: null,
|
|
});
|
|
expect(decoded.sections.every((section) => section.count === null)).toBe(
|
|
true,
|
|
);
|
|
expect(decoded.images).toEqual(FIXTURE_IMAGES);
|
|
expect(decoded.types).toEqual(FIXTURE_TYPES);
|
|
expect(decoded.methods).toEqual(FIXTURE_METHODS);
|
|
},
|
|
);
|
|
|
|
it("rejects method records sized for the other version", () => {
|
|
for (const [built, labeled] of [
|
|
[29, 31],
|
|
[31, 29],
|
|
] as const)
|
|
rejects(
|
|
mutated(
|
|
(_, bytes) => bytes.writeInt32LE(labeled, 4),
|
|
createIl2cppMetadataFixture({ version: built }),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
});
|
|
|
|
it("rejects a version 29 header that does not end at the first section", () => {
|
|
rejects(
|
|
createIl2cppMetadataFixture({ version: 29 }).bytes.subarray(0, 200),
|
|
"truncated-header",
|
|
);
|
|
rejects(
|
|
mutated(
|
|
(fixture, bytes) =>
|
|
bytes.writeInt32LE(380, fixture.header("stringLiterals", "offset")),
|
|
createIl2cppMetadataFixture({ version: 29 }),
|
|
),
|
|
"unexpected-header-size",
|
|
);
|
|
});
|
|
|
|
it("rejects size-only sections that do not hold whole records", () => {
|
|
rejects(
|
|
mutated(
|
|
(fixture, bytes) =>
|
|
bytes.writeInt32LE(
|
|
fixture.section("typeDefinitions").size - 1,
|
|
fixture.header("typeDefinitions", "size"),
|
|
),
|
|
createIl2cppMetadataFixture({ version: 31 }),
|
|
),
|
|
"record-size-mismatch",
|
|
);
|
|
});
|
|
|
|
it("applies the shared string and ownership checks", () => {
|
|
const fixture = createIl2cppMetadataFixture({ version: 31 });
|
|
rejects(
|
|
mutated(
|
|
(target, bytes) =>
|
|
bytes.writeInt32LE(
|
|
target.stringOffset("Update") +
|
|
1 -
|
|
target.section("strings").offset,
|
|
target.record("methods", 0),
|
|
),
|
|
fixture,
|
|
),
|
|
"invalid-string",
|
|
);
|
|
rejects(
|
|
mutated(
|
|
(target, bytes) =>
|
|
bytes.writeInt32LE(3, target.record("methods", 1) + 4),
|
|
fixture,
|
|
),
|
|
"declaring-type-mismatch",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP metadata coverage checks", () => {
|
|
it("rejects occupied sections that overlap the header", () => {
|
|
rejects(
|
|
mutated(
|
|
(fixture, bytes) =>
|
|
bytes.writeInt32LE(27, fixture.header("strings", "offset")),
|
|
createIl2cppMetadataFixture({ version: 29 }),
|
|
),
|
|
"section-out-of-bounds",
|
|
);
|
|
});
|
|
|
|
it("rejects a method outside its declaring type's range", () => {
|
|
// Type 3 owns methods [3, 5); shrinking it strands method 4.
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
const starts = fixture.record("typeDefinitions", 3) + 8 + 3 + 1 + 4;
|
|
bytes.writeUInt16LE(1, starts + 32);
|
|
}),
|
|
"declaring-type-mismatch",
|
|
);
|
|
});
|
|
|
|
it("rejects images whose type ranges overlap", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeUInt8(2, fixture.record("images", 1) + 8),
|
|
),
|
|
"image-range-overlap",
|
|
);
|
|
});
|
|
|
|
it("preserves a leading byte order mark in names", () => {
|
|
const fixture = createIl2cppMetadataFixture();
|
|
const bytes = Buffer.from(fixture.bytes);
|
|
// Replace the three bytes of "Add" with a lone UTF-8 byte order mark.
|
|
const at = fixture.stringOffset("Add");
|
|
bytes.set([0xef, 0xbb, 0xbf], at);
|
|
expect(readIl2cppMetadata(bytes).methods[2]?.name).toBe("\ufeff");
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP version 39 wide indices", () => {
|
|
it.each([
|
|
[2, { fillerTypes: 300, genericContainers: 300, parameters: 300 }],
|
|
[4, { fillerTypes: 65_540, genericContainers: 65_540, parameters: 65_540 }],
|
|
] as const)(
|
|
"decodes %i-byte type-definition, generic-container and parameter indices",
|
|
(width, wide) => {
|
|
const decoded = readIl2cppMetadata(
|
|
createIl2cppMetadataFixture({ wide }).bytes,
|
|
);
|
|
expect(decoded.index_widths).toMatchObject({
|
|
type_definition: width,
|
|
generic_container: width,
|
|
parameter: width,
|
|
});
|
|
const rows = fixtureRows({ wide });
|
|
expect(decoded.types).toEqual(rows.types);
|
|
expect(decoded.methods).toEqual(rows.methods);
|
|
// The last method declares a type index beyond the narrower widths.
|
|
expect(decoded.methods.at(-1)?.declaring_type).toBe(
|
|
rows.types.length - 1,
|
|
);
|
|
expect(decoded.images).toEqual(FIXTURE_IMAGES);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("IL2CPP narrow index sentinels", () => {
|
|
it.each([
|
|
[1, undefined],
|
|
[2, { fillerTypes: 300, genericContainers: 0, parameters: 0 }],
|
|
] as const)(
|
|
"reads an empty image's all-ones %i-byte type start as -1",
|
|
(width, wide) => {
|
|
const options = {
|
|
emptyImage: true,
|
|
...(wide === undefined ? {} : { wide }),
|
|
};
|
|
const decoded = readIl2cppMetadata(
|
|
createIl2cppMetadataFixture(options).bytes,
|
|
);
|
|
expect(decoded.index_widths.type_definition).toBe(width);
|
|
expect(decoded.images).toEqual(fixtureImages(options));
|
|
},
|
|
);
|
|
});
|
|
|
|
describe("IL2CPP metadata members and string literals", () => {
|
|
it.each([
|
|
{ version: 24, typeIndexWidth: 4 },
|
|
{ version: 27, typeIndexWidth: 4 },
|
|
{ version: 29, typeIndexWidth: 4 },
|
|
{ version: 31, typeIndexWidth: 4 },
|
|
{ version: 39, typeIndexWidth: 1 },
|
|
{ version: 39, typeIndexWidth: 2 },
|
|
{ version: 39, typeIndexWidth: 4 },
|
|
] as const)(
|
|
"decodes nonempty properties and events with distinct owners: %j",
|
|
(options) => {
|
|
const fixture = createIl2cppMetadataFixture({
|
|
...options,
|
|
propertiesAndEvents: true,
|
|
});
|
|
const decoded = readIl2cppMetadata(fixture.bytes);
|
|
expect(decoded.properties).toEqual(FIXTURE_PROPERTIES);
|
|
expect(decoded.events).toEqual(FIXTURE_EVENTS);
|
|
expect(fixture.section("properties").size).toBe(3 * 20);
|
|
expect(fixture.section("events").size).toBe(
|
|
2 * (20 + options.typeIndexWidth),
|
|
);
|
|
},
|
|
);
|
|
it.each([29, 31, 39] as const)(
|
|
"pairs version %i members with their owners and keeps undecodable literals as bytes",
|
|
(version) => {
|
|
const decoded = readIl2cppMetadata(
|
|
createIl2cppMetadataFixture({ version }).bytes,
|
|
);
|
|
expect(decoded.fields).toEqual(FIXTURE_FIELDS);
|
|
expect(decoded.parameters).toEqual(FIXTURE_PARAMETERS);
|
|
expect(decoded.properties).toEqual([]);
|
|
expect(decoded.events).toEqual([]);
|
|
expect(decoded.string_literals).toEqual(FIXTURE_STRING_LITERALS);
|
|
},
|
|
);
|
|
|
|
it("rejects member ranges claimed twice or outside their table", () => {
|
|
// Type 1 claims field 0 as well, which type 0 already owns.
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
const starts = fixture.record("typeDefinitions", 1) + 8 + 3 + 1 + 4;
|
|
bytes.writeInt32LE(0, starts);
|
|
}),
|
|
"declaring-type-mismatch",
|
|
);
|
|
rejects(
|
|
mutated((fixture, bytes) => {
|
|
const starts = fixture.record("typeDefinitions", 1) + 8 + 3 + 1 + 4;
|
|
bytes.writeInt32LE(2, starts);
|
|
}),
|
|
"index-out-of-range",
|
|
);
|
|
});
|
|
|
|
it("rejects string literals outside the literal data", () => {
|
|
rejects(
|
|
mutated((fixture, bytes) =>
|
|
bytes.writeInt32LE(99, fixture.section("stringLiterals").offset + 4),
|
|
),
|
|
"index-out-of-range",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP global-metadata version 27", () => {
|
|
it("decodes the version 29 record layout with version 27 section names", () => {
|
|
const decoded = readIl2cppMetadata(
|
|
createIl2cppMetadataFixture({ version: 27 }).bytes,
|
|
);
|
|
expect(decoded.metadata_version).toBe(27);
|
|
expect(decoded.index_widths.type_width_source).toBe("fixed-width-version");
|
|
expect(decoded.sections.slice(24, 28).map(({ name }) => name)).toEqual([
|
|
"attributesInfo",
|
|
"attributeTypes",
|
|
"unresolvedVirtualCallParameterTypes",
|
|
"unresolvedVirtualCallParameterRanges",
|
|
]);
|
|
expect(decoded.images).toEqual(FIXTURE_IMAGES);
|
|
expect(decoded.types).toEqual(FIXTURE_TYPES);
|
|
expect(decoded.methods).toEqual(FIXTURE_METHODS);
|
|
});
|
|
});
|
|
|
|
describe("IL2CPP global-metadata version 24", () => {
|
|
it("decodes the 32-section header and five-type-index records", () => {
|
|
const fixture = createIl2cppMetadataFixture({ version: 24 });
|
|
const decoded = readIl2cppMetadata(fixture.bytes);
|
|
expect(decoded.metadata_version).toBe(24);
|
|
expect(decoded.sections).toHaveLength(32);
|
|
expect(decoded.sections[0]?.offset).toBe(264);
|
|
expect(decoded.sections[22]?.name).toBe("metadataUsageLists");
|
|
expect(decoded.images).toEqual(FIXTURE_IMAGES);
|
|
expect(decoded.types).toEqual(FIXTURE_TYPES);
|
|
expect(decoded.methods).toEqual(FIXTURE_METHODS);
|
|
});
|
|
|
|
it("rejects version 24 records labeled as version 27", () => {
|
|
rejects(
|
|
mutated(
|
|
(_, bytes) => bytes.writeInt32LE(27, 4),
|
|
createIl2cppMetadataFixture({ version: 24 }),
|
|
),
|
|
"unexpected-header-size",
|
|
);
|
|
});
|
|
});
|