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