toolbox 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/bake/ruby/gdb.rb +135 -0
- data/bake/toolbox/gdb.rb +137 -0
- data/bake/toolbox/lldb.rb +137 -0
- data/context/fiber-debugging.md +171 -0
- data/context/getting-started.md +178 -0
- data/context/heap-debugging.md +351 -0
- data/context/index.yaml +28 -0
- data/context/object-inspection.md +208 -0
- data/context/stack-inspection.md +188 -0
- data/data/toolbox/command.py +254 -0
- data/data/toolbox/constants.py +200 -0
- data/data/toolbox/context.py +295 -0
- data/data/toolbox/debugger/__init__.py +99 -0
- data/data/toolbox/debugger/gdb_backend.py +595 -0
- data/data/toolbox/debugger/lldb_backend.py +885 -0
- data/data/toolbox/fiber.py +885 -0
- data/data/toolbox/format.py +200 -0
- data/data/toolbox/heap.py +669 -0
- data/data/toolbox/init.py +85 -0
- data/data/toolbox/object.py +84 -0
- data/data/toolbox/rarray.py +124 -0
- data/data/toolbox/rbasic.py +103 -0
- data/data/toolbox/rbignum.py +52 -0
- data/data/toolbox/rclass.py +136 -0
- data/data/toolbox/readme.md +214 -0
- data/data/toolbox/rexception.py +150 -0
- data/data/toolbox/rfloat.py +98 -0
- data/data/toolbox/rhash.py +159 -0
- data/data/toolbox/rstring.py +234 -0
- data/data/toolbox/rstruct.py +157 -0
- data/data/toolbox/rsymbol.py +302 -0
- data/data/toolbox/stack.py +630 -0
- data/data/toolbox/value.py +183 -0
- data/lib/toolbox/gdb.rb +21 -0
- data/lib/toolbox/lldb.rb +21 -0
- data/lib/toolbox/version.rb +7 -1
- data/lib/toolbox.rb +9 -24
- data/license.md +21 -0
- data/readme.md +64 -0
- data/releases.md +9 -0
- data.tar.gz.sig +2 -0
- metadata +95 -165
- metadata.gz.sig +0 -0
- data/Rakefile +0 -61
- data/lib/dirs.rb +0 -9
- data/lib/toolbox/config.rb +0 -211
- data/lib/toolbox/default_controller.rb +0 -393
- data/lib/toolbox/helpers.rb +0 -11
- data/lib/toolbox/rendering.rb +0 -413
- data/lib/toolbox/searching.rb +0 -85
- data/lib/toolbox/session_params.rb +0 -63
- data/lib/toolbox/sorting.rb +0 -74
- data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
- data/public/images/add.png +0 -0
- data/public/images/arrow_down.gif +0 -0
- data/public/images/arrow_up.gif +0 -0
- data/public/images/close.png +0 -0
- data/public/images/edit.gif +0 -0
- data/public/images/email.png +0 -0
- data/public/images/page.png +0 -0
- data/public/images/page_acrobat.png +0 -0
- data/public/images/page_add.png +0 -0
- data/public/images/page_copy.png +0 -0
- data/public/images/page_delete.png +0 -0
- data/public/images/page_edit.png +0 -0
- data/public/images/page_excel.png +0 -0
- data/public/images/page_list.png +0 -0
- data/public/images/page_save.png +0 -0
- data/public/images/page_word.png +0 -0
- data/public/images/remove.png +0 -0
- data/public/images/show.gif +0 -0
- data/public/images/spinner.gif +0 -0
- data/public/javascripts/popup.js +0 -498
- data/public/javascripts/toolbox.js +0 -18
- data/public/stylesheets/context_menu.css +0 -168
- data/public/stylesheets/popup.css +0 -30
- data/public/stylesheets/toolbox.css +0 -107
- data/view/toolbox/_collection.html.erb +0 -24
- data/view/toolbox/_collection_header.html.erb +0 -7
- data/view/toolbox/_context_menu.html.erb +0 -17
- data/view/toolbox/_dialogs.html.erb +0 -6
- data/view/toolbox/_form.html.erb +0 -30
- data/view/toolbox/_form_collection_row.html.erb +0 -18
- data/view/toolbox/_form_fieldset.html.erb +0 -30
- data/view/toolbox/_form_fieldset_row.html.erb +0 -19
- data/view/toolbox/_list.html.erb +0 -25
- data/view/toolbox/_list_row.html.erb +0 -10
- data/view/toolbox/_menu.html.erb +0 -7
- data/view/toolbox/_search_field.html.erb +0 -8
- data/view/toolbox/_show.html.erb +0 -12
- data/view/toolbox/_show_collection_row.html.erb +0 -6
- data/view/toolbox/_show_fieldset.html.erb +0 -21
- data/view/toolbox/edit.html.erb +0 -5
- data/view/toolbox/index.html.erb +0 -3
- data/view/toolbox/new.html.erb +0 -9
- data/view/toolbox/show.html.erb +0 -39
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import debugger
|
|
2
|
+
import rbasic
|
|
3
|
+
import constants
|
|
4
|
+
import format
|
|
5
|
+
|
|
6
|
+
# Type code for arrays (same in GDB and LLDB)
|
|
7
|
+
_TYPE_CODE_ARRAY = 1
|
|
8
|
+
|
|
9
|
+
def _char_ptr_type():
|
|
10
|
+
"""Return char* type for GDB."""
|
|
11
|
+
return constants.type_struct('char').pointer()
|
|
12
|
+
|
|
13
|
+
class RStringBase:
|
|
14
|
+
"""Base class for RString variants with common functionality."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, value):
|
|
17
|
+
"""value is a VALUE pointing to a T_STRING object."""
|
|
18
|
+
self.value = value
|
|
19
|
+
self.rstring = value.cast(constants.type_struct('struct RString').pointer())
|
|
20
|
+
self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
|
|
21
|
+
self.flags = int(self.basic.dereference()['flags'])
|
|
22
|
+
|
|
23
|
+
def _is_embedded(self):
|
|
24
|
+
"""Check if string is embedded. Must be implemented by subclasses."""
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
def length(self):
|
|
28
|
+
"""Get string length. Must be implemented by subclasses."""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def data_ptr(self):
|
|
32
|
+
"""Get pointer to string data. Must be implemented by subclasses."""
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def read_bytes(self, max_fallback_scan=256):
|
|
36
|
+
"""Return (bytes, length). If declared length is 0, will fallback to scanning for NUL up to max_fallback_scan."""
|
|
37
|
+
pointer = self.data_ptr()
|
|
38
|
+
length = self.length()
|
|
39
|
+
|
|
40
|
+
# Convert pointer to integer address
|
|
41
|
+
pointer_addr = int(pointer)
|
|
42
|
+
|
|
43
|
+
if length and length > 0:
|
|
44
|
+
# Known length - read exact amount
|
|
45
|
+
try:
|
|
46
|
+
data = debugger.read_memory(pointer_addr, length)
|
|
47
|
+
return (data, length)
|
|
48
|
+
except debugger.MemoryError:
|
|
49
|
+
return (b"", 0)
|
|
50
|
+
else:
|
|
51
|
+
# Unknown length - scan for NUL terminator
|
|
52
|
+
try:
|
|
53
|
+
return debugger.read_cstring(pointer_addr, max_fallback_scan)
|
|
54
|
+
except debugger.MemoryError:
|
|
55
|
+
return (b"", 0)
|
|
56
|
+
|
|
57
|
+
def to_str(self, encoding='utf-8'):
|
|
58
|
+
"""Convert to Python string."""
|
|
59
|
+
data, length = self.read_bytes()
|
|
60
|
+
return data.decode(encoding, errors='replace')
|
|
61
|
+
|
|
62
|
+
def __str__(self):
|
|
63
|
+
"""Return the string with type tag and metadata."""
|
|
64
|
+
addr = int(self.value)
|
|
65
|
+
storage = "embedded" if self._is_embedded() else "heap"
|
|
66
|
+
content = self.to_str()
|
|
67
|
+
return f"<T_STRING@0x{addr:x} {storage} length={self.length()}> {repr(content)}"
|
|
68
|
+
|
|
69
|
+
def print_to(self, terminal):
|
|
70
|
+
"""Print this string with formatting."""
|
|
71
|
+
addr = int(self.value)
|
|
72
|
+
storage = "embedded" if self._is_embedded() else "heap"
|
|
73
|
+
content = self.to_str()
|
|
74
|
+
tag = terminal.print(
|
|
75
|
+
format.metadata, '<',
|
|
76
|
+
format.type, 'T_STRING',
|
|
77
|
+
format.metadata, f'@0x{addr:x} {storage} length={self.length()}>',
|
|
78
|
+
format.reset
|
|
79
|
+
)
|
|
80
|
+
# Use repr() to properly escape quotes, newlines, etc.
|
|
81
|
+
string_val = terminal.print(format.string, repr(content), format.reset)
|
|
82
|
+
return f"{tag} {string_val}"
|
|
83
|
+
|
|
84
|
+
def print_recursive(self, printer, depth):
|
|
85
|
+
"""Print this string (no recursion needed for strings)."""
|
|
86
|
+
printer.print(self)
|
|
87
|
+
|
|
88
|
+
class RString34(RStringBase):
|
|
89
|
+
"""Ruby 3.4+ strings: top-level len, as.embed.ary, FL_USER1 set = heap."""
|
|
90
|
+
|
|
91
|
+
def _is_embedded(self):
|
|
92
|
+
FL_USER1 = constants.flag('RUBY_FL_USER1')
|
|
93
|
+
# FL_USER1 set => heap (not embedded)
|
|
94
|
+
return (self.flags & FL_USER1) == 0
|
|
95
|
+
|
|
96
|
+
def length(self):
|
|
97
|
+
return int(self.rstring.dereference()['len'])
|
|
98
|
+
|
|
99
|
+
def data_ptr(self):
|
|
100
|
+
if not self._is_embedded():
|
|
101
|
+
return self.rstring.dereference()['as']['heap']['ptr']
|
|
102
|
+
# Embedded: use as.embed.ary
|
|
103
|
+
ary = self.rstring.dereference()['as']['embed']['ary']
|
|
104
|
+
try:
|
|
105
|
+
if ary.type.code == _TYPE_CODE_ARRAY:
|
|
106
|
+
return ary.address.cast(_char_ptr_type())
|
|
107
|
+
return ary
|
|
108
|
+
except Exception:
|
|
109
|
+
return ary.address.cast(_char_ptr_type())
|
|
110
|
+
|
|
111
|
+
class RString33(RStringBase):
|
|
112
|
+
"""Ruby 3.3 strings: top-level len, as.embed.len exists, FL_USER1 set = embedded."""
|
|
113
|
+
|
|
114
|
+
def _is_embedded(self):
|
|
115
|
+
FL_USER1 = constants.flag('RUBY_FL_USER1')
|
|
116
|
+
# FL_USER1 set => embedded
|
|
117
|
+
return (self.flags & FL_USER1) != 0
|
|
118
|
+
|
|
119
|
+
def length(self):
|
|
120
|
+
return int(self.rstring.dereference()['len'])
|
|
121
|
+
|
|
122
|
+
def data_ptr(self):
|
|
123
|
+
if not self._is_embedded():
|
|
124
|
+
# FL_USER1 not set: in Ruby 3.3, the data may be in as.heap.ptr field
|
|
125
|
+
# For small strings, this is inline data (union overlaps with as.embed)
|
|
126
|
+
# We need the ADDRESS of this field to read the bytes, not the value
|
|
127
|
+
heap_ptr_field = self.rstring.dereference()['as']['heap']['ptr']
|
|
128
|
+
return heap_ptr_field.address.cast(_char_ptr_type())
|
|
129
|
+
# Embedded: use as.embed.ary
|
|
130
|
+
ary = self.rstring.dereference()['as']['embed']['ary']
|
|
131
|
+
try:
|
|
132
|
+
if ary.type.code == _TYPE_CODE_ARRAY:
|
|
133
|
+
return ary.address.cast(_char_ptr_type())
|
|
134
|
+
return ary
|
|
135
|
+
except Exception:
|
|
136
|
+
return ary.address.cast(_char_ptr_type())
|
|
137
|
+
|
|
138
|
+
class RString32RVARGC(RStringBase):
|
|
139
|
+
"""Ruby 3.2 with RVARGC: as.embed.len for embedded, as.embed.ary for data."""
|
|
140
|
+
|
|
141
|
+
def _is_embedded(self):
|
|
142
|
+
FL_USER1 = constants.flag('RUBY_FL_USER1')
|
|
143
|
+
# FL_USER1 set => heap (not embedded)
|
|
144
|
+
return (self.flags & FL_USER1) == 0
|
|
145
|
+
|
|
146
|
+
def length(self):
|
|
147
|
+
if self._is_embedded():
|
|
148
|
+
return int(self.rstring.dereference()['as']['embed']['len'])
|
|
149
|
+
else:
|
|
150
|
+
return int(self.rstring.dereference()['as']['heap']['len'])
|
|
151
|
+
|
|
152
|
+
def data_ptr(self):
|
|
153
|
+
if not self._is_embedded():
|
|
154
|
+
return self.rstring.dereference()['as']['heap']['ptr']
|
|
155
|
+
# Embedded: use as.embed.ary
|
|
156
|
+
ary = self.rstring.dereference()['as']['embed']['ary']
|
|
157
|
+
try:
|
|
158
|
+
if ary.type.code == _TYPE_CODE_ARRAY:
|
|
159
|
+
return ary.address.cast(_char_ptr_type())
|
|
160
|
+
return ary
|
|
161
|
+
except Exception:
|
|
162
|
+
return ary.address.cast(_char_ptr_type())
|
|
163
|
+
|
|
164
|
+
class RString32Legacy(RStringBase):
|
|
165
|
+
"""Legacy Ruby 3.2: embedded length in FL_USER2..6, data in as.ary."""
|
|
166
|
+
|
|
167
|
+
def _is_embedded(self):
|
|
168
|
+
FL_USER1 = constants.flag('RUBY_FL_USER1')
|
|
169
|
+
# FL_USER1 set => heap (not embedded)
|
|
170
|
+
return (self.flags & FL_USER1) == 0
|
|
171
|
+
|
|
172
|
+
def length(self):
|
|
173
|
+
if self._is_embedded():
|
|
174
|
+
FL2 = constants.flag('RUBY_FL_USER2')
|
|
175
|
+
FL3 = constants.flag('RUBY_FL_USER3')
|
|
176
|
+
FL4 = constants.flag('RUBY_FL_USER4')
|
|
177
|
+
FL5 = constants.flag('RUBY_FL_USER5')
|
|
178
|
+
FL6 = constants.flag('RUBY_FL_USER6')
|
|
179
|
+
USHIFT = constants.flag('RUBY_FL_USHIFT')
|
|
180
|
+
mask = FL2 | FL3 | FL4 | FL5 | FL6
|
|
181
|
+
return (self.flags & mask) >> (USHIFT + 2)
|
|
182
|
+
else:
|
|
183
|
+
return int(self.rstring.dereference()['as']['heap']['len'])
|
|
184
|
+
|
|
185
|
+
def data_ptr(self):
|
|
186
|
+
if not self._is_embedded():
|
|
187
|
+
return self.rstring.dereference()['as']['heap']['ptr']
|
|
188
|
+
# Embedded: use as.ary (not as.embed.ary)
|
|
189
|
+
ary = self.rstring.dereference()['as']['ary']
|
|
190
|
+
try:
|
|
191
|
+
if ary.type.code == _TYPE_CODE_ARRAY:
|
|
192
|
+
return ary.address.cast(_char_ptr_type())
|
|
193
|
+
return ary
|
|
194
|
+
except Exception:
|
|
195
|
+
return ary.address.cast(_char_ptr_type())
|
|
196
|
+
|
|
197
|
+
def RString(value):
|
|
198
|
+
"""Factory function that detects the RString variant and returns the appropriate instance.
|
|
199
|
+
|
|
200
|
+
Caller should ensure value is a RUBY_T_STRING before calling this function.
|
|
201
|
+
|
|
202
|
+
Detects at runtime whether the process uses:
|
|
203
|
+
- Ruby 3.4+: top-level len field, as.embed.ary (no embed.len), FL_USER1 set = heap
|
|
204
|
+
- Ruby 3.3: top-level len field, as.embed.len exists, FL_USER1 set = embedded
|
|
205
|
+
- Ruby 3.2 with RVARGC: as.embed.len for embedded, as.embed.ary for data
|
|
206
|
+
- Legacy 3.2: embedded length in FL_USER2..6, data in as.ary
|
|
207
|
+
"""
|
|
208
|
+
rstring = value.cast(constants.type_struct('struct RString').pointer())
|
|
209
|
+
|
|
210
|
+
# Try top-level len field (Ruby 3.3+/3.4+)
|
|
211
|
+
len_field = rstring.dereference()['len']
|
|
212
|
+
if len_field is not None:
|
|
213
|
+
# Now check if embed structure has len field (3.3) or just ary (3.4+)
|
|
214
|
+
as_union = rstring.dereference()['as']
|
|
215
|
+
if as_union is not None:
|
|
216
|
+
embed_struct = as_union['embed']
|
|
217
|
+
if embed_struct is not None:
|
|
218
|
+
embed_len = embed_struct['len']
|
|
219
|
+
if embed_len is not None:
|
|
220
|
+
return RString33(value)
|
|
221
|
+
else:
|
|
222
|
+
return RString34(value)
|
|
223
|
+
|
|
224
|
+
# Try RVARGC embedded len field (3.2 RVARGC)
|
|
225
|
+
as_union = rstring.dereference()['as']
|
|
226
|
+
if as_union is not None:
|
|
227
|
+
embed_struct = as_union['embed']
|
|
228
|
+
if embed_struct is not None:
|
|
229
|
+
embed_len = embed_struct['len']
|
|
230
|
+
if embed_len is not None:
|
|
231
|
+
return RString32RVARGC(value)
|
|
232
|
+
|
|
233
|
+
# Fallback to legacy 3.2
|
|
234
|
+
return RString32Legacy(value)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import debugger
|
|
2
|
+
import rbasic
|
|
3
|
+
import constants
|
|
4
|
+
import format
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RStructBase:
|
|
8
|
+
"""Base class for Ruby struct variants."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, value):
|
|
11
|
+
"""value is a VALUE pointing to a T_STRUCT object."""
|
|
12
|
+
self.value = value
|
|
13
|
+
self.rstruct = value.cast(constants.type_struct('struct RStruct').pointer())
|
|
14
|
+
|
|
15
|
+
def length(self):
|
|
16
|
+
"""Get the number of fields in the struct."""
|
|
17
|
+
raise NotImplementedError("Subclass must implement length()")
|
|
18
|
+
|
|
19
|
+
def items_ptr(self):
|
|
20
|
+
"""Get pointer to the struct's data array."""
|
|
21
|
+
raise NotImplementedError("Subclass must implement items_ptr()")
|
|
22
|
+
|
|
23
|
+
def __len__(self):
|
|
24
|
+
"""Support len(struct) syntax."""
|
|
25
|
+
return self.length()
|
|
26
|
+
|
|
27
|
+
def __getitem__(self, index):
|
|
28
|
+
"""Support struct[i] syntax to access fields."""
|
|
29
|
+
length = self.length()
|
|
30
|
+
if index < 0 or index >= length:
|
|
31
|
+
raise IndexError(f"struct index out of range: {index} (length: {length})")
|
|
32
|
+
|
|
33
|
+
ptr = self.items_ptr()
|
|
34
|
+
return ptr[index]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RStructEmbedded(RStructBase):
|
|
38
|
+
"""Ruby struct with embedded storage (small structs)."""
|
|
39
|
+
|
|
40
|
+
def length(self):
|
|
41
|
+
"""Get length from flags for embedded struct."""
|
|
42
|
+
flags = int(self.rstruct.dereference()['basic']['flags'])
|
|
43
|
+
|
|
44
|
+
# Extract length from FL_USER1 and FL_USER2 flags
|
|
45
|
+
RUBY_FL_USER1 = constants.flag("RUBY_FL_USER1")
|
|
46
|
+
RUBY_FL_USER2 = constants.flag("RUBY_FL_USER2")
|
|
47
|
+
RUBY_FL_USHIFT = constants.flag("RUBY_FL_USHIFT")
|
|
48
|
+
|
|
49
|
+
mask = RUBY_FL_USER1 | RUBY_FL_USER2
|
|
50
|
+
shift = RUBY_FL_USHIFT + 1
|
|
51
|
+
return (flags & mask) >> shift
|
|
52
|
+
|
|
53
|
+
def items_ptr(self):
|
|
54
|
+
"""Get pointer to embedded data array."""
|
|
55
|
+
return self.rstruct.dereference()['as']['ary']
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
"""Return string representation of struct."""
|
|
59
|
+
addr = int(self.value)
|
|
60
|
+
return f"<T_STRUCT@0x{addr:x} embedded length={len(self)}>"
|
|
61
|
+
|
|
62
|
+
def print_to(self, terminal):
|
|
63
|
+
"""Return formatted struct representation."""
|
|
64
|
+
addr = int(self.value)
|
|
65
|
+
return terminal.print(
|
|
66
|
+
format.metadata, '<',
|
|
67
|
+
format.type, 'T_STRUCT',
|
|
68
|
+
format.metadata, f'@0x{addr:x} embedded length={len(self)}>',
|
|
69
|
+
format.reset
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def print_recursive(self, printer, depth):
|
|
73
|
+
"""Print this struct recursively."""
|
|
74
|
+
printer.print(self)
|
|
75
|
+
|
|
76
|
+
if depth <= 0:
|
|
77
|
+
if len(self) > 0:
|
|
78
|
+
printer.print_with_indent(printer.max_depth - depth, " ...")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Print each field
|
|
82
|
+
for i in range(len(self)):
|
|
83
|
+
printer.print_item_label(printer.max_depth - depth, i)
|
|
84
|
+
printer.print_value(self[i], depth - 1)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RStructHeap(RStructBase):
|
|
88
|
+
"""Ruby struct with heap-allocated storage (large structs)."""
|
|
89
|
+
|
|
90
|
+
def length(self):
|
|
91
|
+
"""Get length from heap structure."""
|
|
92
|
+
return int(self.rstruct.dereference()['as']['heap']['len'])
|
|
93
|
+
|
|
94
|
+
def items_ptr(self):
|
|
95
|
+
"""Get pointer to heap-allocated data array."""
|
|
96
|
+
return self.rstruct.dereference()['as']['heap']['ptr']
|
|
97
|
+
|
|
98
|
+
def __str__(self):
|
|
99
|
+
"""Return string representation of struct."""
|
|
100
|
+
addr = int(self.value)
|
|
101
|
+
return f"<T_STRUCT@0x{addr:x} heap length={len(self)}>"
|
|
102
|
+
|
|
103
|
+
def print_to(self, terminal):
|
|
104
|
+
"""Return formatted struct representation."""
|
|
105
|
+
addr = int(self.value)
|
|
106
|
+
return terminal.print(
|
|
107
|
+
format.metadata, '<',
|
|
108
|
+
format.type, 'T_STRUCT',
|
|
109
|
+
format.metadata, f'@0x{addr:x} heap length={len(self)}>',
|
|
110
|
+
format.reset
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def print_recursive(self, printer, depth):
|
|
114
|
+
"""Print this struct recursively."""
|
|
115
|
+
printer.print(self)
|
|
116
|
+
|
|
117
|
+
if depth <= 0:
|
|
118
|
+
if len(self) > 0:
|
|
119
|
+
printer.print_with_indent(printer.max_depth - depth, " ...")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Print each field
|
|
123
|
+
for i in range(len(self)):
|
|
124
|
+
printer.print_item_label(printer.max_depth - depth, i)
|
|
125
|
+
printer.print_value(self[i], depth - 1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def RStruct(value):
|
|
129
|
+
"""
|
|
130
|
+
Factory function to create the appropriate RStruct variant.
|
|
131
|
+
|
|
132
|
+
Caller should ensure value is a RUBY_T_STRUCT before calling this function.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
RStructEmbedded or RStructHeap instance
|
|
136
|
+
"""
|
|
137
|
+
# Cast to RStruct pointer to read flags
|
|
138
|
+
rstruct_type = constants.type_struct("struct RStruct").pointer()
|
|
139
|
+
rstruct = value.cast(rstruct_type)
|
|
140
|
+
flags = int(rstruct.dereference()['basic']['flags'])
|
|
141
|
+
|
|
142
|
+
# Feature detection: check for RSTRUCT_EMBED_LEN_MASK flag
|
|
143
|
+
# If struct uses embedded storage, the length is encoded in flags
|
|
144
|
+
RSTRUCT_EMBED_LEN_MASK = constants.get("RSTRUCT_EMBED_LEN_MASK")
|
|
145
|
+
if RSTRUCT_EMBED_LEN_MASK is None:
|
|
146
|
+
# Fallback: try to detect by checking if as.heap exists
|
|
147
|
+
try:
|
|
148
|
+
_ = rstruct.dereference()['as']['heap']
|
|
149
|
+
return RStructHeap(value)
|
|
150
|
+
except Exception:
|
|
151
|
+
return RStructEmbedded(value)
|
|
152
|
+
|
|
153
|
+
# Check if embedded flag is set
|
|
154
|
+
if flags & RSTRUCT_EMBED_LEN_MASK:
|
|
155
|
+
return RStructEmbedded(value)
|
|
156
|
+
else:
|
|
157
|
+
return RStructHeap(value)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import debugger
|
|
2
|
+
import rbasic
|
|
3
|
+
import constants
|
|
4
|
+
import format
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RubySymbol:
|
|
8
|
+
"""Ruby symbol from ID (integer identifier)."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, symbol_id):
|
|
11
|
+
"""Initialize from a symbol ID.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
symbol_id: Integer symbol ID
|
|
15
|
+
"""
|
|
16
|
+
self.symbol_id = int(symbol_id)
|
|
17
|
+
self._name = None
|
|
18
|
+
|
|
19
|
+
def to_str(self):
|
|
20
|
+
"""Get symbol name from global symbol table, or None if not found."""
|
|
21
|
+
import sys
|
|
22
|
+
if self._name is not None:
|
|
23
|
+
return self._name
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# rb_id_to_serial
|
|
27
|
+
# tLAST_OP_ID is in enum ruby_method_ids
|
|
28
|
+
tLAST_OP_ID = constants.get_enum('ruby_method_ids', 'tLAST_OP_ID', 163)
|
|
29
|
+
ID_ENTRY_UNIT = 512
|
|
30
|
+
ID_ENTRY_SIZE = 2
|
|
31
|
+
|
|
32
|
+
id_val = self.symbol_id
|
|
33
|
+
|
|
34
|
+
# The symbol_id is already the ID value (shifted from VALUE by RUBY_SPECIAL_SHIFT)
|
|
35
|
+
# We just need to convert it to a serial number for the global symbol table lookup
|
|
36
|
+
if id_val > tLAST_OP_ID:
|
|
37
|
+
# For dynamic symbols, the serial is stored in the upper bits
|
|
38
|
+
try:
|
|
39
|
+
RUBY_ID_SCOPE_SHIFT = int(constants.get_enum('ruby_id_types', 'RUBY_ID_SCOPE_SHIFT', 4))
|
|
40
|
+
except Exception:
|
|
41
|
+
RUBY_ID_SCOPE_SHIFT = 4 # Default value
|
|
42
|
+
serial = id_val >> RUBY_ID_SCOPE_SHIFT
|
|
43
|
+
else:
|
|
44
|
+
# For static symbols (operators, etc.), the ID is the serial
|
|
45
|
+
serial = id_val
|
|
46
|
+
|
|
47
|
+
# Access ruby_global_symbols
|
|
48
|
+
global_symbols = debugger.parse_and_eval("ruby_global_symbols")
|
|
49
|
+
|
|
50
|
+
# Ruby 3.5+ changed from last_id to next_id
|
|
51
|
+
last_id_field = global_symbols['last_id']
|
|
52
|
+
if last_id_field is not None:
|
|
53
|
+
last_id = int(last_id_field)
|
|
54
|
+
else:
|
|
55
|
+
# Ruby 3.5+ uses next_id instead
|
|
56
|
+
next_id_field = global_symbols['next_id']
|
|
57
|
+
if next_id_field is not None:
|
|
58
|
+
last_id = int(next_id_field) - 1
|
|
59
|
+
else:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
if not serial or serial > last_id:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
idx = serial // ID_ENTRY_UNIT
|
|
66
|
+
|
|
67
|
+
# Get ids array
|
|
68
|
+
ids = global_symbols['ids'].cast(constants.type_struct("struct RArray").pointer())
|
|
69
|
+
|
|
70
|
+
# Import rarray to access the array
|
|
71
|
+
import rarray
|
|
72
|
+
arr = rarray.RArray(global_symbols['ids'])
|
|
73
|
+
if arr is None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
# Get the symbols entry
|
|
77
|
+
entry_val = arr[idx]
|
|
78
|
+
|
|
79
|
+
# Get the symbol from the entry
|
|
80
|
+
entry_arr = rarray.RArray(entry_val)
|
|
81
|
+
if entry_arr is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
ids_idx = (serial % ID_ENTRY_UNIT) * ID_ENTRY_SIZE
|
|
85
|
+
sym_val = entry_arr[ids_idx]
|
|
86
|
+
|
|
87
|
+
# Get the string from the symbol
|
|
88
|
+
import rstring
|
|
89
|
+
str_obj = rstring.RString(sym_val)
|
|
90
|
+
if str_obj is None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
self._name = str_obj.to_str()
|
|
94
|
+
return self._name
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
import traceback
|
|
98
|
+
traceback.print_exc(file=sys.stderr)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def __str__(self):
|
|
102
|
+
"""Return symbol representation."""
|
|
103
|
+
name = self.to_str()
|
|
104
|
+
if name:
|
|
105
|
+
return f":{name}"
|
|
106
|
+
else:
|
|
107
|
+
return f":id_0x{self.symbol_id:x}"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class RSymbolImmediate:
|
|
111
|
+
"""Ruby immediate symbol (encoded in VALUE)."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, value):
|
|
114
|
+
self.value = value
|
|
115
|
+
self._ruby_symbol = None
|
|
116
|
+
|
|
117
|
+
def id(self):
|
|
118
|
+
"""Get the symbol ID."""
|
|
119
|
+
return get_id(self.value)
|
|
120
|
+
|
|
121
|
+
def ruby_symbol(self):
|
|
122
|
+
"""Get RubySymbol instance for this symbol."""
|
|
123
|
+
if self._ruby_symbol is None:
|
|
124
|
+
self._ruby_symbol = RubySymbol(self.id())
|
|
125
|
+
return self._ruby_symbol
|
|
126
|
+
|
|
127
|
+
def to_str(self):
|
|
128
|
+
"""Get symbol name from global symbol table, or None if not found."""
|
|
129
|
+
return self.ruby_symbol().to_str()
|
|
130
|
+
|
|
131
|
+
def __str__(self):
|
|
132
|
+
"""Return symbol representation."""
|
|
133
|
+
name = self.to_str()
|
|
134
|
+
if name:
|
|
135
|
+
return f"<T_SYMBOL> :{name}"
|
|
136
|
+
else:
|
|
137
|
+
return f"<T_SYMBOL> :id_0x{self.id():x}"
|
|
138
|
+
|
|
139
|
+
def print_to(self, terminal):
|
|
140
|
+
"""Return formatted symbol representation."""
|
|
141
|
+
name = self.to_str()
|
|
142
|
+
if name:
|
|
143
|
+
tag = terminal.print(
|
|
144
|
+
format.metadata, '<',
|
|
145
|
+
format.type, 'T_SYMBOL',
|
|
146
|
+
format.metadata, '>',
|
|
147
|
+
format.reset
|
|
148
|
+
)
|
|
149
|
+
symbol_val = terminal.print(format.symbol, f':{name}', format.reset)
|
|
150
|
+
return f"{tag} {symbol_val}"
|
|
151
|
+
else:
|
|
152
|
+
tag = terminal.print(
|
|
153
|
+
format.metadata, '<',
|
|
154
|
+
format.type, 'T_SYMBOL',
|
|
155
|
+
format.metadata, '>',
|
|
156
|
+
format.reset
|
|
157
|
+
)
|
|
158
|
+
symbol_val = terminal.print(format.symbol, f':id_0x{self.id():x}', format.reset)
|
|
159
|
+
return f"{tag} {symbol_val}"
|
|
160
|
+
|
|
161
|
+
def print_recursive(self, printer, depth):
|
|
162
|
+
"""Print this symbol (no recursion needed)."""
|
|
163
|
+
printer.print(self)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class RSymbolObject:
|
|
167
|
+
"""Ruby T_SYMBOL heap object."""
|
|
168
|
+
|
|
169
|
+
def __init__(self, value):
|
|
170
|
+
self.value = value
|
|
171
|
+
self.rsymbol = value.cast(constants.type_struct("struct RSymbol").pointer())
|
|
172
|
+
self._ruby_symbol = None
|
|
173
|
+
|
|
174
|
+
def id(self):
|
|
175
|
+
"""Get the symbol ID."""
|
|
176
|
+
return int(self.rsymbol.dereference()['id'])
|
|
177
|
+
|
|
178
|
+
def ruby_symbol(self):
|
|
179
|
+
"""Get RubySymbol instance for this symbol."""
|
|
180
|
+
if self._ruby_symbol is None:
|
|
181
|
+
self._ruby_symbol = RubySymbol(self.id())
|
|
182
|
+
return self._ruby_symbol
|
|
183
|
+
|
|
184
|
+
def fstr(self):
|
|
185
|
+
"""Get the frozen string containing the symbol name."""
|
|
186
|
+
return self.rsymbol.dereference()['fstr']
|
|
187
|
+
|
|
188
|
+
def to_str(self):
|
|
189
|
+
"""Get symbol name as a string."""
|
|
190
|
+
try:
|
|
191
|
+
import rstring
|
|
192
|
+
fstr_val = self.fstr()
|
|
193
|
+
str_obj = rstring.RString(fstr_val)
|
|
194
|
+
if str_obj:
|
|
195
|
+
return str_obj.to_str()
|
|
196
|
+
return None
|
|
197
|
+
except Exception:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def __str__(self):
|
|
201
|
+
"""Return symbol representation."""
|
|
202
|
+
name = self.to_str()
|
|
203
|
+
addr = int(self.value)
|
|
204
|
+
if name:
|
|
205
|
+
return f"<T_SYMBOL@0x{addr:x}> :{name}"
|
|
206
|
+
else:
|
|
207
|
+
fstr_val = self.fstr()
|
|
208
|
+
return f"<T_SYMBOL@0x{addr:x}> :<Symbol:0x{int(fstr_val):x}>"
|
|
209
|
+
|
|
210
|
+
def print_to(self, terminal):
|
|
211
|
+
"""Return formatted symbol representation."""
|
|
212
|
+
name = self.to_str()
|
|
213
|
+
addr = int(self.value)
|
|
214
|
+
if name:
|
|
215
|
+
tag = terminal.print(
|
|
216
|
+
format.metadata, '<',
|
|
217
|
+
format.type, 'T_SYMBOL',
|
|
218
|
+
format.metadata, f'@0x{addr:x}>',
|
|
219
|
+
format.reset
|
|
220
|
+
)
|
|
221
|
+
symbol_val = terminal.print(format.symbol, f':{name}', format.reset)
|
|
222
|
+
return f"{tag} {symbol_val}"
|
|
223
|
+
else:
|
|
224
|
+
fstr_val = self.fstr()
|
|
225
|
+
tag = terminal.print(
|
|
226
|
+
format.metadata, '<',
|
|
227
|
+
format.type, 'T_SYMBOL',
|
|
228
|
+
format.metadata, f'@0x{addr:x}>',
|
|
229
|
+
format.reset
|
|
230
|
+
)
|
|
231
|
+
symbol_val = terminal.print(format.symbol, f':<Symbol:0x{int(fstr_val):x}>', format.reset)
|
|
232
|
+
return f"{tag} {symbol_val}"
|
|
233
|
+
|
|
234
|
+
def print_recursive(self, printer, depth):
|
|
235
|
+
"""Print this symbol (no recursion needed)."""
|
|
236
|
+
printer.print(self)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def is_symbol(value):
|
|
241
|
+
"""
|
|
242
|
+
Check if a VALUE is a symbol using the logic from Ruby's .gdbinit.
|
|
243
|
+
|
|
244
|
+
From .gdbinit:
|
|
245
|
+
if (($arg0) & ~(~(VALUE)0<<RUBY_SPECIAL_SHIFT)) == RUBY_SYMBOL_FLAG
|
|
246
|
+
|
|
247
|
+
This checks if the low bits (after masking RUBY_SPECIAL_SHIFT) match RUBY_SYMBOL_FLAG.
|
|
248
|
+
"""
|
|
249
|
+
import sys
|
|
250
|
+
try:
|
|
251
|
+
val_int = int(value)
|
|
252
|
+
|
|
253
|
+
# Get Ruby constants from ruby_special_consts enum
|
|
254
|
+
RUBY_SPECIAL_SHIFT = constants.get_enum('ruby_special_consts', 'RUBY_SPECIAL_SHIFT', 8)
|
|
255
|
+
RUBY_SYMBOL_FLAG = constants.get_enum('ruby_special_consts', 'RUBY_SYMBOL_FLAG', 0x0c)
|
|
256
|
+
|
|
257
|
+
# Create mask for low bits: ~(~0 << RUBY_SPECIAL_SHIFT)
|
|
258
|
+
# This gives us RUBY_SPECIAL_SHIFT low bits set to 1
|
|
259
|
+
mask = ~(~0 << RUBY_SPECIAL_SHIFT)
|
|
260
|
+
|
|
261
|
+
# Check if masked value equals RUBY_SYMBOL_FLAG
|
|
262
|
+
result = (val_int & mask) == RUBY_SYMBOL_FLAG
|
|
263
|
+
return result
|
|
264
|
+
except Exception as e:
|
|
265
|
+
# Fallback to simple check
|
|
266
|
+
return (int(value) & 0xFF) == 0x0C
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_id(value):
|
|
270
|
+
"""
|
|
271
|
+
Extract symbol ID from a VALUE using the logic from Ruby's .gdbinit.
|
|
272
|
+
|
|
273
|
+
From .gdbinit:
|
|
274
|
+
set $id = (($arg0) >> RUBY_SPECIAL_SHIFT)
|
|
275
|
+
"""
|
|
276
|
+
import sys
|
|
277
|
+
try:
|
|
278
|
+
RUBY_SPECIAL_SHIFT = constants.get_enum('ruby_special_consts', 'RUBY_SPECIAL_SHIFT', 8)
|
|
279
|
+
val_int = int(value)
|
|
280
|
+
result = val_int >> RUBY_SPECIAL_SHIFT
|
|
281
|
+
return result
|
|
282
|
+
except Exception as e:
|
|
283
|
+
# Fallback to shift by 8
|
|
284
|
+
return int(value) >> 8
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def RSymbol(value):
|
|
288
|
+
"""
|
|
289
|
+
Factory function to create the appropriate RSymbol variant.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
RSymbolImmediate, RSymbolObject, or None
|
|
293
|
+
"""
|
|
294
|
+
# Check if it's an immediate symbol
|
|
295
|
+
if is_symbol(value):
|
|
296
|
+
return RSymbolImmediate(value)
|
|
297
|
+
|
|
298
|
+
# Check if it's a T_SYMBOL object
|
|
299
|
+
if rbasic.is_type(value, 'RUBY_T_SYMBOL'):
|
|
300
|
+
return RSymbolObject(value)
|
|
301
|
+
|
|
302
|
+
return None
|