1""" 2LLDB AppKit formatters 3 4Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5See https://llvm.org/LICENSE.txt for license information. 6SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7""" 8# summary provider for NSSet 9import lldb 10import ctypes 11import lldb.runtime.objc.objc_runtime 12import lldb.formatters.metrics 13import CFBag 14import lldb.formatters.Logger 15 16statistics = lldb.formatters.metrics.Metrics() 17statistics.add_metric("invalid_isa") 18statistics.add_metric("invalid_pointer") 19statistics.add_metric("unknown_class") 20statistics.add_metric("code_notrun") 21 22# despite the similary to synthetic children providers, these classes are not 23# trying to provide anything but the port number of an NSMachPort, so they need not 24# obey the interface specification for synthetic children providers 25 26 27class NSCFSet_SummaryProvider: 28 def adjust_for_architecture(self): 29 pass 30 31 def __init__(self, valobj, params): 32 logger = lldb.formatters.Logger.Logger() 33 self.valobj = valobj 34 self.sys_params = params 35 if not (self.sys_params.types_cache.NSUInteger): 36 if self.sys_params.is_64_bit: 37 self.sys_params.types_cache.NSUInteger = ( 38 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedLong) 39 ) 40 else: 41 self.sys_params.types_cache.NSUInteger = ( 42 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedInt) 43 ) 44 self.update() 45 46 def update(self): 47 logger = lldb.formatters.Logger.Logger() 48 self.adjust_for_architecture() 49 50 # one pointer is the ISA 51 # then we have one other internal pointer, plus 52 # 4 bytes worth of flags. hence, these values 53 def offset(self): 54 logger = lldb.formatters.Logger.Logger() 55 if self.sys_params.is_64_bit: 56 return 20 57 else: 58 return 12 59 60 def count(self): 61 logger = lldb.formatters.Logger.Logger() 62 vcount = self.valobj.CreateChildAtOffset( 63 "count", self.offset(), self.sys_params.types_cache.NSUInteger 64 ) 65 return vcount.GetValueAsUnsigned(0) 66 67 68class NSSetUnknown_SummaryProvider: 69 def adjust_for_architecture(self): 70 pass 71 72 def __init__(self, valobj, params): 73 logger = lldb.formatters.Logger.Logger() 74 self.valobj = valobj 75 self.sys_params = params 76 self.update() 77 78 def update(self): 79 logger = lldb.formatters.Logger.Logger() 80 self.adjust_for_architecture() 81 82 def count(self): 83 logger = lldb.formatters.Logger.Logger() 84 stream = lldb.SBStream() 85 self.valobj.GetExpressionPath(stream) 86 expr = "(int)[" + stream.GetData() + " count]" 87 num_children_vo = self.valobj.CreateValueFromExpression("count", expr) 88 if num_children_vo.IsValid(): 89 return num_children_vo.GetValueAsUnsigned(0) 90 return "<variable is not NSSet>" 91 92 93class NSSetI_SummaryProvider: 94 def adjust_for_architecture(self): 95 pass 96 97 def __init__(self, valobj, params): 98 logger = lldb.formatters.Logger.Logger() 99 self.valobj = valobj 100 self.sys_params = params 101 if not (self.sys_params.types_cache.NSUInteger): 102 if self.sys_params.is_64_bit: 103 self.sys_params.types_cache.NSUInteger = ( 104 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedLong) 105 ) 106 else: 107 self.sys_params.types_cache.NSUInteger = ( 108 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedInt) 109 ) 110 self.update() 111 112 def update(self): 113 logger = lldb.formatters.Logger.Logger() 114 self.adjust_for_architecture() 115 116 # we just need to skip the ISA and the count immediately follows 117 def offset(self): 118 logger = lldb.formatters.Logger.Logger() 119 return self.sys_params.pointer_size 120 121 def count(self): 122 logger = lldb.formatters.Logger.Logger() 123 num_children_vo = self.valobj.CreateChildAtOffset( 124 "count", self.offset(), self.sys_params.types_cache.NSUInteger 125 ) 126 value = num_children_vo.GetValueAsUnsigned(0) 127 if value is not None: 128 # the MSB on immutable sets seems to be taken by some other data 129 # not sure if it is a bug or some weird sort of feature, but masking it out 130 # gets the count right (unless, of course, someone's dictionaries grow 131 # too large - but I have not tested this) 132 if self.sys_params.is_64_bit: 133 value = value & ~0xFF00000000000000 134 else: 135 value = value & ~0xFF000000 136 return value 137 138 139class NSSetM_SummaryProvider: 140 def adjust_for_architecture(self): 141 pass 142 143 def __init__(self, valobj, params): 144 logger = lldb.formatters.Logger.Logger() 145 self.valobj = valobj 146 self.sys_params = params 147 if not (self.sys_params.types_cache.NSUInteger): 148 if self.sys_params.is_64_bit: 149 self.sys_params.types_cache.NSUInteger = ( 150 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedLong) 151 ) 152 else: 153 self.sys_params.types_cache.NSUInteger = ( 154 self.valobj.GetType().GetBasicType(lldb.eBasicTypeUnsignedInt) 155 ) 156 self.update() 157 158 def update(self): 159 logger = lldb.formatters.Logger.Logger() 160 self.adjust_for_architecture() 161 162 # we just need to skip the ISA and the count immediately follows 163 def offset(self): 164 logger = lldb.formatters.Logger.Logger() 165 return self.sys_params.pointer_size 166 167 def count(self): 168 logger = lldb.formatters.Logger.Logger() 169 num_children_vo = self.valobj.CreateChildAtOffset( 170 "count", self.offset(), self.sys_params.types_cache.NSUInteger 171 ) 172 return num_children_vo.GetValueAsUnsigned(0) 173 174 175class NSCountedSet_SummaryProvider: 176 def adjust_for_architecture(self): 177 pass 178 179 def __init__(self, valobj, params): 180 logger = lldb.formatters.Logger.Logger() 181 self.valobj = valobj 182 self.sys_params = params 183 if not (self.sys_params.types_cache.voidptr): 184 self.sys_params.types_cache.voidptr = ( 185 self.valobj.GetType().GetBasicType(lldb.eBasicTypeVoid).GetPointerType() 186 ) 187 self.update() 188 189 def update(self): 190 logger = lldb.formatters.Logger.Logger() 191 self.adjust_for_architecture() 192 193 # an NSCountedSet is implemented using a CFBag whose pointer just follows 194 # the ISA 195 def offset(self): 196 logger = lldb.formatters.Logger.Logger() 197 return self.sys_params.pointer_size 198 199 def count(self): 200 logger = lldb.formatters.Logger.Logger() 201 cfbag_vo = self.valobj.CreateChildAtOffset( 202 "bag_impl", self.offset(), self.sys_params.types_cache.voidptr 203 ) 204 return CFBag.CFBagRef_SummaryProvider(cfbag_vo, self.sys_params).length() 205 206 207def GetSummary_Impl(valobj): 208 logger = lldb.formatters.Logger.Logger() 209 global statistics 210 ( 211 class_data, 212 wrapper, 213 ) = lldb.runtime.objc.objc_runtime.Utilities.prepare_class_detection( 214 valobj, statistics 215 ) 216 if wrapper: 217 return wrapper 218 219 name_string = class_data.class_name() 220 logger >> "class name is: " + str(name_string) 221 222 if name_string == "__NSCFSet": 223 wrapper = NSCFSet_SummaryProvider(valobj, class_data.sys_params) 224 statistics.metric_hit("code_notrun", valobj) 225 elif name_string == "__NSSetI": 226 wrapper = NSSetI_SummaryProvider(valobj, class_data.sys_params) 227 statistics.metric_hit("code_notrun", valobj) 228 elif name_string == "__NSSetM": 229 wrapper = NSSetM_SummaryProvider(valobj, class_data.sys_params) 230 statistics.metric_hit("code_notrun", valobj) 231 elif name_string == "NSCountedSet": 232 wrapper = NSCountedSet_SummaryProvider(valobj, class_data.sys_params) 233 statistics.metric_hit("code_notrun", valobj) 234 else: 235 wrapper = NSSetUnknown_SummaryProvider(valobj, class_data.sys_params) 236 statistics.metric_hit( 237 "unknown_class", valobj.GetName() + " seen as " + name_string 238 ) 239 return wrapper 240 241 242def NSSet_SummaryProvider(valobj, dict): 243 logger = lldb.formatters.Logger.Logger() 244 provider = GetSummary_Impl(valobj) 245 if provider is not None: 246 try: 247 summary = provider.count() 248 except: 249 summary = None 250 if summary is None: 251 summary = "<variable is not NSSet>" 252 if isinstance(summary, str): 253 return summary 254 else: 255 summary = str(summary) + (" objects" if summary != 1 else " object") 256 return summary 257 return "Summary Unavailable" 258 259 260def NSSet_SummaryProvider2(valobj, dict): 261 logger = lldb.formatters.Logger.Logger() 262 provider = GetSummary_Impl(valobj) 263 if provider is not None: 264 if isinstance( 265 provider, lldb.runtime.objc.objc_runtime.SpecialSituation_Description 266 ): 267 return provider.message() 268 try: 269 summary = provider.count() 270 except: 271 summary = None 272 logger >> "got summary " + str(summary) 273 # for some reason, one needs to clear some bits for the count returned 274 # to be correct when using directly CF*SetRef as compared to NS*Set 275 # this only happens on 64bit, and the bit mask was derived through 276 # experimentation (if counts start looking weird, then most probably 277 # the mask needs to be changed) 278 if summary is None: 279 summary = "<variable is not CFSet>" 280 if isinstance(summary, str): 281 return summary 282 else: 283 if provider.sys_params.is_64_bit: 284 summary = summary & ~0x1FFF000000000000 285 summary = '@"' + str(summary) + (' values"' if summary != 1 else ' value"') 286 return summary 287 return "Summary Unavailable" 288 289 290def __lldb_init_module(debugger, dict): 291 debugger.HandleCommand("type summary add -F NSSet.NSSet_SummaryProvider NSSet") 292 debugger.HandleCommand( 293 "type summary add -F NSSet.NSSet_SummaryProvider2 CFSetRef CFMutableSetRef" 294 ) 295