diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 20f5beba05..8587d0c36c 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -506,6 +506,12 @@ complete observation. comments do not change that handling. XML key names combine their text and CDATA segments without trimming or expanding literal CDATA entities. Other values and the original archive digest are preserved. + Symbol fallback accepts complete Objective-C class/metaclass symbol leaves, + including qualified names. Pointer/string labels that merely contain a class + marker do not establish classes. Provider namespace method labels are not + interpreted as Objective-C declarations; class definition versus import origin + remains unsupported coverage rather than an inferred classification. + - `decode_interface_builder` / `rea decode-interface-builder ` reads compiled storyboard and nib resources into objects, outlets, actions, connections and controller/class names. A full app graph is often several diff --git a/src/domain/native/objcSwiftMetadata.test.ts b/src/domain/native/objcSwiftMetadata.test.ts index 5a1cc28570..15327c15f7 100644 --- a/src/domain/native/objcSwiftMetadata.test.ts +++ b/src/domain/native/objcSwiftMetadata.test.ts @@ -214,3 +214,34 @@ describe("ObjC/Swift metadata", () => { ); }); }); + +it("derives classes only from complete class symbol leaves", () => { + const result = inspectNativeDispatchMetadata( + [ + "PTR__OBJC_CLASS_$_NSAlert_100028668", + "s__OBJC_CLASS_$_NSAlert_10002ed0d", + "s__OBJC_CLASS_$_NSAppleEventManage_10002ed23", + "_OBJC_CLASS_$_Local", + "/System/Library/Frameworks/AppKit::_OBJC_CLASS_$_NSAlert", + "_OBJC_METACLASS_$_Local", + "OwnController::viewDidLoad", + ].map((name, index) => ({ address: `0x${String(index + 1)}`, name })), + ); + expect(result.objc_classes.map(({ name }) => name)).toEqual([ + "Local", + "NSAlert", + ]); + expect(result.coverage).toContainEqual( + expect.objectContaining({ + facet: "objc_class_definitions", + status: "unsupported", + reason: expect.stringContaining("namespace_declarations"), + }), + ); + expect(result.coverage).toContainEqual( + expect.objectContaining({ + facet: "objc_dispatch_implementations", + reason: "provider_name_inventory_has_no_supported_objc_method_symbols", + }), + ); +}); diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index f2ce8b7bb1..fc61c26cdf 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -1,3 +1,4 @@ +import { objcClassSymbol } from "../objcClassSymbol.js"; import { z } from "zod"; import { BINARY_ARCHITECTURES, @@ -500,10 +501,8 @@ export const inspectNativeDispatchMetadata = ( const parsedAddress = nativeAddressInputSchema.safeParse(entry.address); const address = parsedAddress.success ? parsedAddress.data : null; const sourceAddress = parsedAddress.success ? entry.address : null; - const classMatch = /(?:OBJC_CLASS_|OBJC_METACLASS_)\$_([^\s]+)/u.exec( - entry.name, - ); - if (classMatch?.[1] !== undefined) classNames.add(classMatch[1]); + const classSymbol = objcClassSymbol(entry.name); + if (classSymbol !== undefined) classNames.add(classSymbol.name); const methodMatch = /^([+-])\[([^\s\]]+)\s+([^\]]+)\]$/u.exec(entry.name); if (methodMatch?.[1] !== undefined && methodMatch[2] && methodMatch[3]) { @@ -605,10 +604,18 @@ export const inspectNativeDispatchMetadata = ( ? "record_limit_reached_and_only_symbolized_methods_are_resolved" : decodedImplementations.length > 0 ? "only_symbolized_methods_are_resolved" - : "provider_name_inventory_has_no_objc_method_symbols", + : "provider_name_inventory_has_no_supported_objc_method_symbols", examined: examinedNames.length, decoded: decodedImplementations.length, }, + { + facet: "objc_class_definitions", + status: "unsupported", + reason: + "symbol_inventory_does_not_establish_class_definitions_import_origins_or_namespace_declarations", + examined: examinedNames.length, + decoded: 0, + }, { facet: "objc_ivar_layouts", status: "unsupported", diff --git a/src/domain/objcClassSymbol.ts b/src/domain/objcClassSymbol.ts new file mode 100644 index 0000000000..07dca28743 --- /dev/null +++ b/src/domain/objcClassSymbol.ts @@ -0,0 +1,20 @@ +/** An Objective-C class symbol leaf, optionally qualified by a provider namespace. */ +export const objcClassSymbol = ( + name: string, +): + | { + readonly name: string; + readonly symbol: string; + readonly metaclass: boolean; + } + | undefined => { + const namespace = name.lastIndexOf("::"); + const leaf = namespace < 0 ? name : name.slice(namespace + 2); + const match = /^_?(OBJC_(META)?CLASS_\$_([^\s]+))$/u.exec(leaf); + if (match?.[1] === undefined || match[3] === undefined) return undefined; + return { + name: match[3], + symbol: match[1], + metaclass: match[2] !== undefined, + }; +}; diff --git a/src/domain/symbolAnalysis.objcClasses.test.ts b/src/domain/symbolAnalysis.objcClasses.test.ts index 1b0aa13745..28ba29bf89 100644 --- a/src/domain/symbolAnalysis.objcClasses.test.ts +++ b/src/domain/symbolAnalysis.objcClasses.test.ts @@ -224,3 +224,18 @@ it("keeps regular self-scope and weak rebinding records separate", () => { ], }); }); + +it("excludes pointer and string labels that only contain a class marker", () => { + const result = discoverObjcClasses( + [ + { address: "0x1", name: "PTR__OBJC_CLASS_$_NSAlert_100028668" }, + { address: "0x2", name: "s__OBJC_CLASS_$_NSAlert_10002ed0d" }, + { address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" }, + ], + "", + ); + expect(result).toMatchObject({ + count: 1, + classes: [{ address: "0x3", name: "AppKit::_OBJC_CLASS_$_NSAlert" }], + }); +}); diff --git a/src/domain/symbolAnalysis.ts b/src/domain/symbolAnalysis.ts index 7bddd2ee79..852150646f 100644 --- a/src/domain/symbolAnalysis.ts +++ b/src/domain/symbolAnalysis.ts @@ -1,3 +1,4 @@ +import { objcClassSymbol } from "./objcClassSymbol.js"; import type { JsonValue } from "./jsonValue.js"; import type { AddressedName } from "./hopperValues.js"; import type { @@ -10,9 +11,6 @@ import { type SwiftTypeCategory, } from "./swiftSymbolClassification.js"; -/** Objective-C class definition markers emitted by modern Apple clang. */ -const OBJC_CLASS_MARKER = "OBJC_CLASS_$_" as const; - /** Local class records emitted by Apple's fragile Objective-C ABI. */ const LEGACY_OBJC_CLASS_PREFIX = "l_OBJC_CLASS_" as const; @@ -44,7 +42,7 @@ export const selectObjcClassCandidates = ( names .filter( ({ name, objc_class_label }) => - name.includes(OBJC_CLASS_MARKER) || + objcClassSymbol(name)?.metaclass === false || objc_class_label === "hopper_legacy_class_object" || (name.startsWith(LEGACY_OBJC_CLASS_PREFIX) && name.length > LEGACY_OBJC_CLASS_PREFIX.length && @@ -333,20 +331,11 @@ const coalesceBoundClassAliases = ( }; const classNameFromImport = (symbol: string): string | undefined => { - const canonical = symbol.startsWith("_") ? symbol.slice(1) : symbol; - return canonical.startsWith(OBJC_CLASS_MARKER) && - canonical.length > OBJC_CLASS_MARKER.length - ? canonical - : undefined; + const facts = objcClassSymbol(symbol); + return facts?.metaclass === false ? facts.symbol : undefined; }; -const classNameFromDisplayName = (name: string): string | undefined => - name.startsWith(OBJC_CLASS_MARKER) && name.length > OBJC_CLASS_MARKER.length - ? name - : name.startsWith(`_${OBJC_CLASS_MARKER}`) && - name.length > OBJC_CLASS_MARKER.length + 1 - ? name.slice(1) - : undefined; +const classNameFromDisplayName = classNameFromImport; const toObjcClassEntry = (entry: ObjcClassOutputEntry): JsonValue => ({ address: entry.address,