diff --git a/src/il2cpp/Il2cppMetadataFile.ts b/src/il2cpp/Il2cppMetadataFile.ts index b5a2c0c6d9..730b4adf00 100644 --- a/src/il2cpp/Il2cppMetadataFile.ts +++ b/src/il2cpp/Il2cppMetadataFile.ts @@ -253,15 +253,25 @@ const FIXED_WIDTHS: Il2cppMetadataDecoded["index_widths"] = { 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(section("genericContainers")), - ); + 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")); diff --git a/tests/acceptance/il2cppMetadataCli.test.ts b/tests/acceptance/il2cppMetadataCli.test.ts index ab69cca9dd..506a4471c6 100644 --- a/tests/acceptance/il2cppMetadataCli.test.ts +++ b/tests/acceptance/il2cppMetadataCli.test.ts @@ -52,6 +52,80 @@ const parseMetadataSummary = (value: unknown) => parseEvidence(value).normalized_result, ); +cliTest( + "validates generic-container extents before returning CLI or MCP metadata", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-il2cpp-container-extents-"); + const { call } = await connectLocalToolsMcp(); + for (const count of [0, 1, 300]) { + const { bytes } = createIl2cppMetadataFixture({ + wide: { fillerTypes: 0, genericContainers: count, parameters: 0 }, + }); + const path = join(root, `valid-${count}.dat`); + await writeFile(path, bytes); + const output = await cli.run({ + arguments: ["inspect-il2cpp-metadata", path, "--json"], + }); + expect(output.exitCode, output.stderr).toBe(0); + const report = parseCliMetadata(output.json); + expect(report.index_widths.generic_container).toBe(count > 255 ? 2 : 1); + const response = await call("inspect_il2cpp_metadata", { + path, + detail: "complete", + }); + expect(response.isError).not.toBe(true); + expect(parseCliMetadata(response.structuredContent)).toEqual(report); + expect(await readFile(path)).toEqual(bytes); + } + for (const [records, declaredCount, declaredBytes] of [ + [0, 1, 0], + [0, 300, 0], + [1, 1, 1], + [1, 0, 16], + ] as const) { + const fixture = createIl2cppMetadataFixture({ + wide: { fillerTypes: 0, genericContainers: records, parameters: 0 }, + }); + fixture.bytes.writeInt32LE( + declaredCount, + fixture.header("genericContainers", "count"), + ); + fixture.bytes.writeInt32LE( + declaredBytes, + fixture.header("genericContainers", "size"), + ); + const path = join(root, `invalid-${declaredCount}-${declaredBytes}.dat`); + await writeFile(path, fixture.bytes); + const output = await cli.run({ + arguments: ["inspect-il2cpp-metadata", path, "--json"], + }); + expect(output.exitCode).toBe(1); + const expected = { + code: "invalid_request", + details: { + operation: "inspect_il2cpp_metadata", + issues: [ + { + path: ["path"], + reason: "invalid_format", + message: expect.stringContaining("record-size-mismatch"), + }, + ], + }, + }; + expect(output.json).toMatchObject(expected); + const response = await call("inspect_il2cpp_metadata", { + path, + detail: "complete", + }); + expect(response.isError).toBe(true); + expect(parseMcpToolError(response).error).toMatchObject(expected); + expect(await readFile(path)).toEqual(fixture.bytes); + } + }, + 30000, +); + cliTest( "returns identical complete inventories through CLI and MCP and pages a retained summary after removal", async ({ cli }) => { diff --git a/tests/boundary/il2cpp/il2cppMetadataFile.test.ts b/tests/boundary/il2cpp/il2cppMetadataFile.test.ts index 1402a86d3b..8418e3313a 100644 --- a/tests/boundary/il2cpp/il2cppMetadataFile.test.ts +++ b/tests/boundary/il2cpp/il2cppMetadataFile.test.ts @@ -261,13 +261,6 @@ describe("rejected IL2CPP global-metadata", () => { ), "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")), @@ -290,6 +283,62 @@ describe("rejected IL2CPP global-metadata", () => { }); }); +describe("IL2CPP generic-container extents", () => { + it("checks generic-container count against its fixed 16-byte records before resolving widths", () => { + // A positive count with no record bytes. + for (const count of [1, 300]) + rejects( + mutated((fixture, bytes) => + bytes.writeInt32LE( + count, + fixture.header("genericContainers", "count"), + ), + ), + "record-size-mismatch", + ); + const oneRecord = createIl2cppMetadataFixture({ + wide: { fillerTypes: 0, genericContainers: 1, parameters: 0 }, + }); + // A positive count with a partial record. + rejects( + mutated( + (fixture, bytes) => + bytes.writeInt32LE(1, fixture.header("genericContainers", "size")), + oneRecord, + ), + "record-size-mismatch", + ); + // Bytes without a corresponding record. + rejects( + mutated( + (fixture, bytes) => + bytes.writeInt32LE(0, fixture.header("genericContainers", "count")), + oneRecord, + ), + "record-size-mismatch", + ); + }); + + it("retains width selection for empty and complete generic-container records", () => { + const empty = readIl2cppMetadata(createIl2cppMetadataFixture().bytes); + expect(empty.index_widths.generic_container).toBe(1); + + const one = readIl2cppMetadata( + createIl2cppMetadataFixture({ + wide: { fillerTypes: 0, genericContainers: 1, parameters: 0 }, + }).bytes, + ); + expect(one.index_widths.generic_container).toBe(1); + + const wide = readIl2cppMetadata( + createIl2cppMetadataFixture({ + wide: { fillerTypes: 0, genericContainers: 300, parameters: 0 }, + }).bytes, + ); + expect(wide.index_widths.generic_container).toBe(2); + }); +}); + describe("rejected IL2CPP metadata references", () => { it("rejects image, type, method and nested-type indices outside their tables", () => { rejects(