diff --git a/docs/il2cpp-metadata.md b/docs/il2cpp-metadata.md index db05d0e28c..06d67a500e 100644 --- a/docs/il2cpp-metadata.md +++ b/docs/il2cpp-metadata.md @@ -248,7 +248,8 @@ The result adds: field and parameter index with `type`, the index into the binary's `Il2CppType` table, and `name`, rendered from that table: C# keywords for primitives (`int`, `string`, `object`), `Namespace.Outer+Inner` for type - definitions, `T[]`, `T[,]` and `T*`, generic instances as + definitions, `T[]` for vectors, `T[*]` for rank-one multidimensional arrays, + `T[,]` for rank-two arrays and `T*`, generic instances as `System.Collections.Generic.List` and generic parameters by their metadata name (`TKey`). A trailing `&` marks a by-reference type. `name` is `null` only for type index `-1`. diff --git a/src/il2cpp/Il2cppBinaryTypes.ts b/src/il2cpp/Il2cppBinaryTypes.ts index f6921bc3c6..3d8b56166b 100644 --- a/src/il2cpp/Il2cppBinaryTypes.ts +++ b/src/il2cpp/Il2cppBinaryTypes.ts @@ -122,9 +122,7 @@ export const createTypeNamer = ( if (rank === 0) fail("array rank is zero"); result = join([ yield* name(pointer(data, "array element"), depth + 1), - "[", - ",".repeat(rank - 1), - "]", + rank === 1 ? "[*]" : `[${",".repeat(rank - 1)}]`, ]); } else if (kind === 0x13 || kind === 0x1e) result = diff --git a/tests/acceptance/il2cppMetadataCli.test.ts b/tests/acceptance/il2cppMetadataCli.test.ts index 2d7bb5517d..9abd9d86c8 100644 --- a/tests/acceptance/il2cppMetadataCli.test.ts +++ b/tests/acceptance/il2cppMetadataCli.test.ts @@ -979,6 +979,70 @@ cliTest( }, ); +cliTest( + "keeps IL2CPP rank-one arrays distinct from vectors in CLI type rows", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-il2cpp-array-kind-"); + const path = join(root, "global-metadata.dat"); + const binaryPath = join(root, "GameAssembly.dylib"); + await writeFile(path, createIl2cppMetadataFixture({ version: 31 }).bytes); + + const makeBinary = (kind: "vector" | "array", rank = 1) => { + const bytes = createIl2cppBinaryFixture(); + // Type record 0 is the returned type; slot 3 is the fixture's `int`. + const typeRecord = 0x980; + const intType = FIXTURE_BINARY_BASE + 0x9b0n; + if (kind === "vector") { + bytes.writeBigUInt64LE(intType, typeRecord); + return bytes; + } + // ARRAY points to Il2CppArrayType: element pointer, then rank byte. + const arrayType = 0xb00; + bytes.writeBigUInt64LE( + FIXTURE_BINARY_BASE + BigInt(arrayType), + typeRecord, + ); + bytes.writeUInt32LE(0x14 << 16, typeRecord + 8); + bytes.writeBigUInt64LE(intType, arrayType); + bytes.writeUInt8(rank, arrayType + 8); + return bytes; + }; + + const inspect = async (binary: Buffer) => { + await writeFile(binaryPath, binary); + const result = await runFileCli(cli, { + arguments: [ + "inspect-il2cpp-metadata", + path, + "--binary", + binaryPath, + "--json", + ], + }); + return result; + }; + + const vector = await inspect(makeBinary("vector")); + expect(vector.exitCode, vector.stderr).toBe(0); + const vectorType = parseCliMetadata(vector.json).return_types[0]; + expect(vectorType?.name).toBe("int[]"); + + const rankOne = await inspect(makeBinary("array", 1)); + expect(rankOne.exitCode, rankOne.stderr).toBe(0); + const rankOneType = parseCliMetadata(rankOne.json).return_types[0]; + expect(rankOneType?.name).toBe("int[*]"); + expect(rankOneType?.type).toBe(vectorType?.type); + + const rankTwo = await inspect(makeBinary("array", 2)); + expect(rankTwo.exitCode, rankTwo.stderr).toBe(0); + expect(parseCliMetadata(rankTwo.json).return_types[0]?.name).toBe("int[,]"); + + const malformed = await inspect(makeBinary("array", 0)); + expect(malformed.exitCode).not.toBe(0); + expect(JSON.stringify(malformed.json)).toContain("array rank is zero"); + }, +); + cliTest.skipIf(process.platform === "win32")( "refuses a Ghidra seed path below a regular file as caller input", async ({ cli }) => {