diff --git a/src/native/AppleDispatchMetadata.test.ts b/src/native/AppleDispatchMetadata.test.ts index f1af48e436..9dc99812f1 100644 --- a/src/native/AppleDispatchMetadata.test.ts +++ b/src/native/AppleDispatchMetadata.test.ts @@ -851,4 +851,50 @@ describe("Apple dispatch list scan budget", () => { ); expect(coverage?.reason).not.toContain("max_records_reached"); }); + + it.each([ + ["RetainedProtocol", ["RetainedProtocol"], "complete"], + ["", [], "partial"], + ])( + "keeps the class when the protocol name is %j", + (protocolName, protocols, status) => { + const bytes = unmappedProtocolImage("neither"); + bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x700n, 0x300); + bytes.writeBigUInt64LE(unmappedProtocolSectionBase + 0x800n, 0x708); + bytes.writeUInt32LE(72, 0x740); + bytes.write(`${protocolName}\0`, 0x800); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]); + expect(result.objc_protocols.map(({ name }) => name)).toEqual(protocols); + const coverage = unmappedProtocolClassCoverage(result); + expect(coverage).toMatchObject({ status }); + if (status === "partial") + expect(coverage?.reason).toBe( + "Protocol 0x100000700: Objective-C protocol has an empty name", + ); + }, + ); + + it("keeps a protocol adopted only by an empty-named protocol", () => { + const bytes = unmappedProtocolImage("neither"); + const at = (offset: bigint) => unmappedProtocolSectionBase + offset; + bytes.writeBigUInt64LE(at(0x700n), 0x300); + bytes.writeBigUInt64LE(at(0x800n), 0x708); // empty parent name + bytes.writeBigUInt64LE(at(0x900n), 0x710); // adopted protocol list + bytes.writeUInt32LE(72, 0x740); + bytes.writeBigUInt64LE(1n, 0x900); + bytes.writeBigUInt64LE(at(0xa00n), 0x908); + bytes.writeBigUInt64LE(at(0xb00n), 0xa08); + bytes.writeUInt32LE(72, 0xa40); + bytes.write("ChildProtocol\0", 0xb00); + const result = decodeAppleDispatchMetadata(bytes, 100, provenance); + expect(result.objc_classes).toMatchObject([{ name: "RetainedClass" }]); + expect(result.objc_protocols.map(({ name }) => name)).toEqual([ + "ChildProtocol", + ]); + expect(unmappedProtocolClassCoverage(result)).toMatchObject({ + status: "partial", + reason: "Protocol 0x100000700: Objective-C protocol has an empty name", + }); + }); }); diff --git a/src/native/AppleObjcProtocols.ts b/src/native/AppleObjcProtocols.ts index 20066ca5c7..19d35872be 100644 --- a/src/native/AppleObjcProtocols.ts +++ b/src/native/AppleObjcProtocols.ts @@ -67,7 +67,11 @@ export const createObjcProtocolReader = (input: { if (read.u32(address + 64n) < 72) throw new RangeError("Truncated Objective-C protocol declaration"); const protocolName = name(address); + // Adopted protocols are independent declarations; decode them before + // rejecting a parent whose own name is empty. const adopted = list(read.pointer(address + 16n)); + if (protocolName.length === 0) + throw new TypeError("Objective-C protocol has an empty name"); const required = [ ...methods(read.pointer(address + 24n), true, true), ...methods(read.pointer(address + 32n), true, false),