1"""GDB Pretty-printers for MongoDB
2"""
3from __future__ import print_function
4
5import gdb.printing
6import struct
7import sys
8
9try:
10    import bson
11    import bson.json_util
12    import collections
13    from bson.codec_options import CodecOptions
14except ImportError as e:
15    print("Warning: Could not load bson library for Python '" + str(sys.version) + "'.")
16    print("Check with the pip command if pymongo 3.x is installed.")
17    bson = None
18
19
20def get_unique_ptr(obj):
21    """Read the value of a libstdc++ std::unique_ptr"""
22    return obj["_M_t"]['_M_head_impl']
23
24
25
26###################################################################################################
27#
28# Pretty-Printers
29#
30###################################################################################################
31
32
33class StatusPrinter(object):
34    """Pretty-printer for mongo::Status"""
35    OK = 0  # ErrorCodes::OK
36
37    def __init__(self, val):
38        self.val = val
39
40    def to_string(self):
41        if not self.val['_error']:
42            return 'Status::OK()'
43
44        code = self.val['_error']['code']
45        # Remove the mongo::ErrorCodes:: prefix. Does nothing if not a real ErrorCode.
46        code = str(code).split('::')[-1]
47
48        info = self.val['_error'].dereference()
49        location = info['location']
50        reason = info['reason']
51        if location:
52            return 'Status(%s, %s, %s)' % (code, reason, location)
53        else:
54            return 'Status(%s, %s)' % (code, reason)
55
56
57class StatusWithPrinter:
58    """Pretty-printer for mongo::StatusWith<>"""
59    def __init__(self, val):
60        self.val = val
61
62    def to_string(self):
63        if not self.val['_status']['_error']:
64            return 'StatusWith(OK, %s)' % (self.val['_t'])
65
66        code = self.val['_status']['_error']['code']
67
68        # Remove the mongo::ErrorCodes:: prefix. Does nothing if not a real ErrorCode.
69        code = str(code).split('::')[-1]
70
71        info = self.val['_status']['_error'].dereference()
72        location = info['location']
73        reason = info['reason']
74        if location:
75            return 'StatusWith(%s, %s, %s)' % (code, reason, location)
76        else:
77            return 'StatusWith(%s, %s)' % (code, reason)
78
79
80class StringDataPrinter:
81    """Pretty-printer for mongo::StringData"""
82
83    def __init__(self, val):
84        self.val = val
85
86    def display_hint(self):
87        return 'string'
88
89    def to_string(self):
90        size = self.val["_size"]
91        if size == -1:
92            return self.val['_data'].lazy_string()
93        else:
94            return self.val['_data'].lazy_string(length=size)
95
96
97class BSONObjPrinter:
98    """Pretty-printer for mongo::BSONObj"""
99
100    def __init__(self, val):
101        self.val = val
102        self.ptr = self.val['_objdata'].cast(gdb.lookup_type('void').pointer())
103        # Handle the endianness of the BSON object size, which is represented as a 32-bit integer
104        # in little-endian format.
105        inferior = gdb.selected_inferior()
106        if self.ptr.is_optimized_out:
107            # If the value has been optimized out, we cannot decode it.
108            self.size = -1
109        else:
110            self.size = struct.unpack('<I', inferior.read_memory(self.ptr, 4))[0]
111
112    def display_hint(self):
113        return 'map'
114
115    def children(self):
116        # Do not decode a BSONObj with an invalid size.
117        if not bson or self.size < 5 or self.size > 17 * 1024 * 1024:
118            return
119
120        inferior = gdb.selected_inferior()
121        buf = bytes(inferior.read_memory(self.ptr, self.size))
122        options = CodecOptions(document_class=collections.OrderedDict)
123        bsondoc = bson.BSON.decode(buf, codec_options=options)
124
125        for k, v in bsondoc.items():
126            yield 'key', k
127            yield 'value', bson.json_util.dumps(v)
128
129    def to_string(self):
130        # The value has been optimized out.
131        if self.size == -1:
132            return "BSONObj @ %s" % (self.ptr)
133
134        ownership = "owned" if self.val['_ownedBuffer']['_buffer']['_holder']['px'] else "unowned"
135
136        size = self.size
137        # Print an invalid BSONObj size in hex.
138        if size < 5 or size > 17 * 1024 * 1024:
139            size = hex(size)
140
141        if size == 5:
142            return "%s empty BSONObj @ %s" % (ownership, self.ptr)
143        else:
144            return "%s BSONObj %s bytes @ %s" % (ownership, size, self.ptr)
145
146
147class UnorderedFastKeyTablePrinter:
148    """Pretty-printer for mongo::UnorderedFastKeyTable<>"""
149
150    def __init__(self, val):
151        self.val = val
152
153        # Get the value_type by doing a type lookup
154        valueTypeName = val.type.strip_typedefs().name + "::value_type"
155        valueType = gdb.lookup_type(valueTypeName).target()
156        self.valueTypePtr = valueType.pointer()
157
158    def display_hint(self):
159        return 'map'
160
161    def to_string(self):
162        return "UnorderedFastKeyTablePrinter<%s> with %s elems " % (
163            self.val.type.template_argument(0), self.val["_size"])
164
165    def children(self):
166        cap = self.val["_area"]["_hashMask"] + 1
167        it = get_unique_ptr(self.val["_area"]["_entries"])
168        end = it + cap
169
170        if it == 0:
171            return
172
173        while it != end:
174            elt = it.dereference()
175            it += 1
176            if not elt['_used']:
177                continue
178
179            value = elt['_data']["__data"].cast(self.valueTypePtr).dereference()
180
181            yield ('key', value['first'])
182            yield ('value', value['second'])
183
184
185class DecorablePrinter:
186    """Pretty-printer for mongo::Decorable<>"""
187
188    def __init__(self, val):
189        self.val = val
190
191        decl_vector = val["_decorations"]["_registry"]["_decorationInfo"]
192        # TODO: abstract out navigating a std::vector
193        self.start = decl_vector["_M_impl"]["_M_start"]
194        finish = decl_vector["_M_impl"]["_M_finish"]
195        decinfo_t = gdb.lookup_type('mongo::DecorationRegistry::DecorationInfo')
196        self.count = int((int(finish) - int(self.start)) / decinfo_t.sizeof)
197
198    def display_hint(self):
199        return 'map'
200
201    def to_string(self):
202        return "Decorable<%s> with %s elems " % (self.val.type.template_argument(0),
203                                                 self.count)
204
205    def children(self):
206        decorationData = get_unique_ptr(self.val["_decorations"]["_decorationData"])
207
208        for index in range(self.count):
209            descriptor = self.start[index]
210            dindex = int(descriptor["descriptor"]["_index"])
211
212            # In order to get the type stored in the decorable, we examine the type of its
213            # constructor, and do some string manipulations.
214            # TODO: abstract out navigating a std::function
215            type_name = str(descriptor["constructor"]["_M_functor"]["_M_unused"]["_M_object"])
216            type_name = type_name[0:len(type_name) - 1]
217            type_name = type_name[0: type_name.rindex(">")]
218            type_name = type_name[type_name.index("constructAt<"):].replace("constructAt<", "")
219
220            # If the type is a pointer type, strip the * at the end.
221            if type_name.endswith('*'):
222                type_name = type_name[0:len(type_name) - 1]
223            type_name = type_name.rstrip()
224
225            # Cast the raw char[] into the actual object that is stored there.
226            type_t = gdb.lookup_type(type_name)
227            obj = decorationData[dindex].cast(type_t)
228
229            yield ('key', "%d:%s:%s" % (index, obj.address, type_name))
230            yield ('value', obj)
231
232
233def find_match_brackets(search, opening='<', closing='>'):
234    """Returns the index of the closing bracket that matches the first opening bracket.
235       Returns -1 if no last matching bracket is found, i.e. not a template.
236
237       Example:
238         'Foo<T>::iterator<U>''
239         returns 5
240    """
241    index = search.find(opening)
242    if index == -1:
243        return -1
244
245    start = index + 1
246    count = 1
247    str_len = len(search)
248    for index in range(start, str_len):
249        c = search[index]
250
251        if c == opening:
252            count += 1
253        elif c == closing:
254            count -= 1
255
256        if count == 0:
257            return index
258
259    return -1
260
261
262class MongoSubPrettyPrinter(gdb.printing.SubPrettyPrinter):
263    """Sub pretty printer managed by the pretty-printer collection"""
264
265    def __init__(self, name, prefix, is_template, printer):
266        super(MongoSubPrettyPrinter, self).__init__(name)
267        self.prefix = prefix
268        self.printer = printer
269        self.is_template = is_template
270
271
272class MongoPrettyPrinterCollection(gdb.printing.PrettyPrinter):
273    """MongoDB-specific printer printer collection that ignores subtypes.
274    It will match 'HashTable<T> but not 'HashTable<T>::iterator' when asked for 'HashTable'.
275    """
276
277    def __init__(self):
278        super(MongoPrettyPrinterCollection, self).__init__("mongo", [])
279
280    def add(self, name, prefix, is_template, printer):
281        self.subprinters.append(MongoSubPrettyPrinter(name, prefix, is_template, printer))
282
283    def __call__(self, val):
284
285        # Get the type name.
286        lookup_tag = gdb.types.get_basic_type(val.type).tag
287        if not lookup_tag:
288            lookup_tag = val.type.name
289        if not lookup_tag:
290            return None
291
292        index = find_match_brackets(lookup_tag)
293
294        # Ignore subtypes of classes
295        # We do not want HashTable<T>::iterator as an example, just HashTable<T>
296        if index == -1 or index + 1 == len(lookup_tag):
297            for printer in self.subprinters:
298                if printer.enabled and (
299                   (printer.is_template and lookup_tag.find(printer.prefix) == 0) or
300                   (not printer.is_template and lookup_tag == printer.prefix)):
301                    return printer.printer(val)
302
303        return None
304
305
306def build_pretty_printer():
307    pp = MongoPrettyPrinterCollection()
308    pp.add('BSONObj', 'mongo::BSONObj', False, BSONObjPrinter)
309    pp.add('Decorable', 'mongo::Decorable', True, DecorablePrinter)
310    pp.add('Status', 'mongo::Status', False, StatusPrinter)
311    pp.add('StatusWith', 'mongo::StatusWith', True, StatusWithPrinter)
312    pp.add('StringData', 'mongo::StringData', False, StringDataPrinter)
313    pp.add('UnorderedFastKeyTable', 'mongo::UnorderedFastKeyTable', True, UnorderedFastKeyTablePrinter)
314    return pp
315
316###################################################################################################
317#
318# Setup
319#
320###################################################################################################
321
322# Register pretty-printers, replace existing mongo printers
323gdb.printing.register_pretty_printer(
324    gdb.current_objfile(),
325    build_pretty_printer(),
326    True)
327
328print("MongoDB GDB pretty-printers loaded")
329