diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index 3074606422..1b7668a905 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -70,6 +70,7 @@ const failureForPrimary = ( path: string, field: "path" | "binary_path" = "path", maximum = INPUT_BYTES, + metadataPath = path, ): AnalysisError => { if (cause instanceof AnalysisError) return cause; if (cause instanceof Il2cppBinaryFormatFailure && cause.kind === "capacity") @@ -91,8 +92,11 @@ const failureForPrimary = ( cause instanceof Il2cppMetadataFormatFailure ) { const binary = cause instanceof Il2cppBinaryFormatFailure; + // Metadata names are read on demand while linking, so a metadata failure + // can surface while path still names the binary. + const target = binary ? path : metadataPath; return cause.kind === "unsupported" - ? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, { + ? new AnalysisUnsupportedTargetError(OPERATION, target, cause.message, { cause, }) : new AnalysisInputError(OPERATION, { cause }, [ @@ -100,8 +104,8 @@ const failureForPrimary = ( path: [binary ? field : "path"], reason: "invalid_format", message: binary - ? `Selected binary ${path} does not link to the metadata: ${cause.message}` - : `Selected artifact ${path} is corrupt: ${cause.message}`, + ? `Selected binary ${target} does not link to the metadata: ${cause.message}` + : `Selected artifact ${target} is corrupt: ${cause.message}`, }, ]); } @@ -166,12 +170,14 @@ const failureFor = ( path: string, field: "path" | "binary_path" = "path", maximum = INPUT_BYTES, + metadataPath = path, ): AnalysisError => { const primary = failureForPrimary( artifactFilePrimaryFailure(cause), path, field, maximum, + metadataPath, ); if ( cause instanceof ArtifactReaderFailure && @@ -505,6 +511,7 @@ export class Il2cppMetadataProvider implements Il2cppMetadataPort { binaryPath, "binary_path", BINARY_BYTES, + input.path, ); failure.partialObservation = metadataSourceEvidence( artifact, diff --git a/tests/boundary/il2cpp/il2cppMetadataProvider.test.ts b/tests/boundary/il2cpp/il2cppMetadataProvider.test.ts index c944b04355..4b9aac1041 100644 --- a/tests/boundary/il2cpp/il2cppMetadataProvider.test.ts +++ b/tests/boundary/il2cpp/il2cppMetadataProvider.test.ts @@ -575,3 +575,54 @@ it("retains both observed file identities and the selected architecture when nat await provider.close(); } }); + +it("names the metadata, not the binary, when linking reads a malformed metadata name", async () => { + const root = await createTestTempDirectory("rea-il2cpp-link-metadata-name-"); + const path = join(root, "global-metadata.dat"), + binaryPath = join(root, "GameAssembly.dylib"); + const fixture = createIl2cppMetadataFixture({ version: 39 }); + // One generic parameter whose name index points past the string table; + // only native linking reads it, through a field typed as that parameter. + const parameterAt = fixture.bytes.length; + const metadata = Buffer.concat([fixture.bytes, Buffer.alloc(13)]); + metadata.writeInt32LE(fixture.section("strings").size, parameterAt + 1); + metadata.writeInt32LE(parameterAt, 8 + 12 * 12); + metadata.writeInt32LE(13, 8 + 12 * 12 + 4); + metadata.writeInt32LE(1, 8 + 12 * 12 + 8); + metadata.writeUInt8(2, fixture.section("fields").offset + 4); + const binary = Buffer.from(createIl2cppBinaryFixture()); + binary.writeBigUInt64LE(0n, 0x980 + 32); + binary.writeUInt32LE(0x13 << 16, 0x980 + 32 + 8); + await writeFile(path, metadata); + await writeFile(binaryPath, binary); + const provider = new Il2cppMetadataProvider(); + try { + const result = await provider.inspect({ path, binary_path: binaryPath }); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected the metadata name to be refused"); + expect(result.error).toMatchObject({ + _tag: "AnalysisInputError", + issues: [ + { + path: ["path"], + reason: "invalid_format", + message: expect.stringContaining( + `Selected artifact ${path} is corrupt: invalid-string:`, + ), + }, + ], + }); + + const corrected = Buffer.from(metadata); + corrected.writeInt32LE( + fixture.stringOffset("Player") - fixture.section("strings").offset, + parameterAt + 1, + ); + await writeFile(path, corrected); + expect((await provider.inspect({ path, binary_path: binaryPath })).ok).toBe( + true, + ); + } finally { + await provider.close(); + } +});