mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
748 lines
23 KiB
TypeScript
748 lines
23 KiB
TypeScript
import {
|
|
Il2cppMetadataFormatFailure,
|
|
type Il2cppMetadataFailureReason,
|
|
} from "../../../src/il2cpp/Il2cppMetadataFormat.js";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
il2cppIndexWidth,
|
|
il2cppMetadataRecordBytes,
|
|
readIl2cppMetadata,
|
|
readIl2cppMetadataForLinking,
|
|
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,
|
|
reader:
|
|
| typeof readIl2cppMetadata
|
|
| typeof readIl2cppMetadataForLinking = readIl2cppMetadata,
|
|
) => {
|
|
try {
|
|
reader(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",
|
|
);
|
|
for (const length of [4, 5, 6, 7])
|
|
rejects(Buffer.alloc(length), "unrecognized-magic", "unsupported");
|
|
rejects(
|
|
mutated((_, bytes) => bytes.writeInt32LE(30, 4)),
|
|
"unsupported-version",
|
|
"unsupported",
|
|
);
|
|
});
|
|
|
|
it("retains the decoder's signed interpretation of a complete high-bit version", () => {
|
|
const bytes = Buffer.alloc(8);
|
|
bytes.writeUInt32LE(0xfab11baf, 0);
|
|
bytes.writeUInt32LE(0xffffffff, 4);
|
|
const rejected = failure(bytes);
|
|
expect(rejected.reason).toBe("unsupported-version");
|
|
expect(rejected.kind).toBe("unsupported");
|
|
expect(rejected.message).toContain("metadata version -1 is not supported");
|
|
});
|
|
|
|
it.each([4, 5, 6, 7])(
|
|
"retains recognized magic and reports a %i-byte version prefix as truncated",
|
|
(length) => {
|
|
const bytes = Buffer.alloc(length);
|
|
bytes.writeUInt32LE(0xfab11baf, 0);
|
|
const rejected = failure(bytes);
|
|
expect(rejected.reason).toBe("truncated-header");
|
|
expect(rejected.kind).toBe("corrupt");
|
|
expect(rejected.message).toContain(
|
|
"fewer than the 8-byte magic/version prefix",
|
|
);
|
|
expect(rejected.message).toContain("metadata version is unknown");
|
|
},
|
|
);
|
|
|
|
it("uses the same truncated-prefix classification before native linking", () => {
|
|
const bytes = Buffer.alloc(7);
|
|
bytes.writeUInt32LE(0xfab11baf, 0);
|
|
const rejected = failure(bytes, readIl2cppMetadataForLinking);
|
|
expect(rejected.reason).toBe("truncated-header");
|
|
expect(rejected.kind).toBe("corrupt");
|
|
expect(rejected.message).toContain("metadata version is unknown");
|
|
});
|
|
|
|
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",
|
|
);
|
|
});
|
|
});
|