toolbox 0.3.0 → 0.4.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/data/toolbox/heap.py +183 -2
- data/data/toolbox/print.py +1 -0
- data/data/toolbox/rarray.py +4 -0
- data/data/toolbox/rbasic.py +6 -2
- data/data/toolbox/rbignum.py +4 -0
- data/data/toolbox/rfloat.py +14 -0
- data/data/toolbox/rhash.py +4 -0
- data/data/toolbox/robject.py +219 -0
- data/data/toolbox/rstring.py +9 -0
- data/data/toolbox/rvalue.py +15 -0
- data/lib/toolbox/version.rb +1 -1
- data/license.md +1 -1
- data/readme.md +21 -0
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +4 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47056303f26efc916f64b03caf6c67278818e8c2bfb79b00b23b9ed752ce393f
|
|
4
|
+
data.tar.gz: 3a3860fadb6e0a7e4f5d591344801d459480da891d95c87ecc2b1cb2044c9ce8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 272ee139aec5ae2ce7ba7b08ae57d329e64dfd4f370cfa99649621978c9d588266de01e902d5f88a26ad90981bbc062f270015d8422d60dbc7ed1f8421f5df5c
|
|
7
|
+
data.tar.gz: b96fc5c29410655d1f3538df655eaa865237f549c553250ef4ea3586ac0b0d01d0902d27505e9d17cf31a70cf2b3822352a0e59b6f9215a1d40ec1907909931e
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/data/toolbox/heap.py
CHANGED
|
@@ -531,8 +531,18 @@ class RubyHeapScanHandler:
|
|
|
531
531
|
"""
|
|
532
532
|
import constants
|
|
533
533
|
|
|
534
|
-
# Try as a constant
|
|
535
|
-
|
|
534
|
+
# Try as a Ruby type constant (RUBY_T_*) first, using defaults table
|
|
535
|
+
if type_arg.startswith('RUBY_T_'):
|
|
536
|
+
try:
|
|
537
|
+
return constants.type(type_arg)
|
|
538
|
+
except Exception:
|
|
539
|
+
pass
|
|
540
|
+
|
|
541
|
+
# Try as a general constant name
|
|
542
|
+
try:
|
|
543
|
+
type_value = constants.get(type_arg)
|
|
544
|
+
except Exception:
|
|
545
|
+
type_value = None
|
|
536
546
|
|
|
537
547
|
if type_value is None:
|
|
538
548
|
# Try parsing as a number (hex or decimal)
|
|
@@ -675,5 +685,176 @@ class RubyHeapScanHandler:
|
|
|
675
685
|
traceback.print_exc()
|
|
676
686
|
|
|
677
687
|
|
|
688
|
+
class RubyHeapDumpHandler:
|
|
689
|
+
"""Dump the Ruby heap to a JSON file.
|
|
690
|
+
|
|
691
|
+
Usage: rb-heap-dump --output FILE [--type TYPE] [--pretty]
|
|
692
|
+
|
|
693
|
+
Iterates the entire Ruby heap and writes each live object as a JSON record.
|
|
694
|
+
Each record includes the object's address, type, and type-specific metadata.
|
|
695
|
+
|
|
696
|
+
Options:
|
|
697
|
+
--output FILE Path to the output JSON file (required)
|
|
698
|
+
--type TYPE Filter by Ruby type (e.g., RUBY_T_STRING, 0x05); omit for all types
|
|
699
|
+
--pretty Pretty-print the JSON output (default: compact)
|
|
700
|
+
|
|
701
|
+
JSON format:
|
|
702
|
+
{
|
|
703
|
+
"total": N,
|
|
704
|
+
"objects": [
|
|
705
|
+
{ "address": "0x...", "type": "T_STRING", "properties": { ... } },
|
|
706
|
+
...
|
|
707
|
+
]
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
Examples:
|
|
711
|
+
rb-heap-dump --output /tmp/heap.json
|
|
712
|
+
rb-heap-dump --output /tmp/strings.json --type RUBY_T_STRING --pretty
|
|
713
|
+
"""
|
|
714
|
+
|
|
715
|
+
USAGE = command.Usage(
|
|
716
|
+
summary="Dump the Ruby heap to a JSON file",
|
|
717
|
+
parameters=[],
|
|
718
|
+
options={
|
|
719
|
+
'output': (str, None, 'Output file path (required)'),
|
|
720
|
+
'type': (str, None, 'Filter by Ruby type (e.g., RUBY_T_STRING, or 0x05)'),
|
|
721
|
+
'limit': (int, None, 'Stop after dumping N objects'),
|
|
722
|
+
},
|
|
723
|
+
flags=[('pretty', 'Pretty-print the JSON output')],
|
|
724
|
+
examples=[
|
|
725
|
+
("rb-heap-dump --output /tmp/heap.json", "Dump the entire heap"),
|
|
726
|
+
("rb-heap-dump --output /tmp/strings.json --type RUBY_T_STRING --pretty", "Dump all strings, pretty-printed"),
|
|
727
|
+
("rb-heap-dump --output /tmp/strings.json --type RUBY_T_STRING --limit 1 --pretty", "Dump first string, pretty-printed"),
|
|
728
|
+
]
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def _parse_type(self, type_arg):
|
|
732
|
+
"""Parse a type argument and return the type value (reuse scan handler logic)."""
|
|
733
|
+
import constants
|
|
734
|
+
|
|
735
|
+
if type_arg.startswith('RUBY_T_'):
|
|
736
|
+
try:
|
|
737
|
+
return constants.type(type_arg)
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
try:
|
|
742
|
+
type_value = constants.get(type_arg)
|
|
743
|
+
except Exception:
|
|
744
|
+
type_value = None
|
|
745
|
+
|
|
746
|
+
if type_value is None:
|
|
747
|
+
try:
|
|
748
|
+
if type_arg.startswith('0x') or type_arg.startswith('0X'):
|
|
749
|
+
type_value = int(type_arg, 16)
|
|
750
|
+
else:
|
|
751
|
+
type_value = int(type_arg)
|
|
752
|
+
except ValueError:
|
|
753
|
+
print(f"Error: Unknown type constant '{type_arg}'")
|
|
754
|
+
print("Use a constant like RUBY_T_STRING or a numeric value like 0x05")
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
return type_value
|
|
758
|
+
|
|
759
|
+
def _object_to_dict(self, obj, flags):
|
|
760
|
+
"""Serialize a heap object to a JSON-compatible dict.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
obj: GDB VALUE for the heap object
|
|
764
|
+
flags: Integer flags value read from the object's RBasic header
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
Dict with 'address', 'type', and 'data' keys
|
|
768
|
+
"""
|
|
769
|
+
import rvalue
|
|
770
|
+
import rbasic as rbasic_mod
|
|
771
|
+
|
|
772
|
+
addr = int(obj)
|
|
773
|
+
type_name = rbasic_mod.type_name(obj)
|
|
774
|
+
|
|
775
|
+
record = {
|
|
776
|
+
"address": f"0x{addr:x}",
|
|
777
|
+
"type": type_name,
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
interpreted = rvalue.interpret(obj)
|
|
782
|
+
properties = interpreted.as_json()
|
|
783
|
+
if properties:
|
|
784
|
+
record["properties"] = properties
|
|
785
|
+
except Exception:
|
|
786
|
+
pass
|
|
787
|
+
|
|
788
|
+
return record
|
|
789
|
+
|
|
790
|
+
def invoke(self, arguments, terminal):
|
|
791
|
+
"""Execute the heap dump command."""
|
|
792
|
+
import json
|
|
793
|
+
|
|
794
|
+
output_path = arguments.get_option('output')
|
|
795
|
+
if output_path is None:
|
|
796
|
+
print("Error: --output FILE is required")
|
|
797
|
+
return
|
|
798
|
+
|
|
799
|
+
pretty = arguments.has_flag('pretty')
|
|
800
|
+
|
|
801
|
+
limit = None
|
|
802
|
+
limit_option = arguments.get_option('limit')
|
|
803
|
+
if limit_option is not None:
|
|
804
|
+
try:
|
|
805
|
+
limit = int(limit_option)
|
|
806
|
+
except ValueError:
|
|
807
|
+
print("Error: --limit must be a number")
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
type_value = None
|
|
811
|
+
type_option = arguments.get_option('type')
|
|
812
|
+
if type_option is not None:
|
|
813
|
+
type_value = self._parse_type(type_option)
|
|
814
|
+
if type_value is None:
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
heap = RubyHeap()
|
|
818
|
+
if not heap.initialize():
|
|
819
|
+
return
|
|
820
|
+
|
|
821
|
+
if type_value is not None:
|
|
822
|
+
type_desc = f"type 0x{type_value:02x}"
|
|
823
|
+
else:
|
|
824
|
+
type_desc = "all types"
|
|
825
|
+
|
|
826
|
+
print(f"Scanning heap ({type_desc}), writing to {output_path}...")
|
|
827
|
+
|
|
828
|
+
objects = []
|
|
829
|
+
count = 0
|
|
830
|
+
|
|
831
|
+
for obj, flags, address in heap.iterate_heap():
|
|
832
|
+
if type_value is not None and (flags & RBASIC_FLAGS_TYPE_MASK) != type_value:
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
count += 1
|
|
836
|
+
if count % 10000 == 0:
|
|
837
|
+
print(f" Processed {count} objects...", file=sys.stderr)
|
|
838
|
+
|
|
839
|
+
record = self._object_to_dict(obj, flags)
|
|
840
|
+
objects.append(record)
|
|
841
|
+
|
|
842
|
+
if limit is not None and len(objects) >= limit:
|
|
843
|
+
break
|
|
844
|
+
|
|
845
|
+
result = {"total": len(objects), "objects": objects}
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
849
|
+
if pretty:
|
|
850
|
+
json.dump(result, f, indent=2, ensure_ascii=False)
|
|
851
|
+
else:
|
|
852
|
+
json.dump(result, f, ensure_ascii=False)
|
|
853
|
+
print(f"Wrote {len(objects)} object(s) to {output_path}")
|
|
854
|
+
except OSError as e:
|
|
855
|
+
print(f"Error writing {output_path}: {e}")
|
|
856
|
+
|
|
857
|
+
|
|
678
858
|
# Register commands
|
|
679
859
|
debugger.register("rb-heap-scan", RubyHeapScanHandler, usage=RubyHeapScanHandler.USAGE)
|
|
860
|
+
debugger.register("rb-heap-dump", RubyHeapDumpHandler, usage=RubyHeapDumpHandler.USAGE)
|
data/data/toolbox/print.py
CHANGED
data/data/toolbox/rarray.py
CHANGED
|
@@ -56,6 +56,10 @@ class RArrayBase:
|
|
|
56
56
|
except Exception as e:
|
|
57
57
|
print(f"Error accessing element {i}: {e}")
|
|
58
58
|
|
|
59
|
+
def as_json(self):
|
|
60
|
+
"""Return a JSON-serializable dict of this array's data."""
|
|
61
|
+
return {"length": self.length()}
|
|
62
|
+
|
|
59
63
|
class RArrayEmbedded(RArrayBase):
|
|
60
64
|
"""Embedded array (small arrays stored directly in struct)."""
|
|
61
65
|
|
data/data/toolbox/rbasic.py
CHANGED
|
@@ -24,7 +24,7 @@ def is_type(value, ruby_type_constant):
|
|
|
24
24
|
True if the value is of the specified type, False otherwise
|
|
25
25
|
"""
|
|
26
26
|
type_flag = type_of(value)
|
|
27
|
-
expected_type = constants.
|
|
27
|
+
expected_type = constants.type(ruby_type_constant)
|
|
28
28
|
return type_flag == expected_type
|
|
29
29
|
|
|
30
30
|
# Map of type constants to their names for display
|
|
@@ -67,7 +67,7 @@ def type_name(value):
|
|
|
67
67
|
|
|
68
68
|
# Try to find matching type name
|
|
69
69
|
for const_name, display_name in TYPE_NAMES.items():
|
|
70
|
-
if constants.
|
|
70
|
+
if constants.type(const_name) == type_flag:
|
|
71
71
|
return display_name
|
|
72
72
|
|
|
73
73
|
return f'Unknown(0x{type_flag:x})'
|
|
@@ -94,6 +94,10 @@ class RBasic:
|
|
|
94
94
|
# Use print_type_tag for consistency with other types
|
|
95
95
|
terminal.print_type_tag(type_str, addr)
|
|
96
96
|
|
|
97
|
+
def as_json(self):
|
|
98
|
+
"""Return a JSON-serializable dict of this object's data."""
|
|
99
|
+
return {}
|
|
100
|
+
|
|
97
101
|
def print_recursive(self, printer, depth):
|
|
98
102
|
"""Print this basic object (no recursion)."""
|
|
99
103
|
printer.print(self)
|
data/data/toolbox/rbignum.py
CHANGED
|
@@ -37,6 +37,10 @@ class RBignumObject:
|
|
|
37
37
|
details = f"{storage} length={len(self)}"
|
|
38
38
|
terminal.print_type_tag('T_BIGNUM', addr, details)
|
|
39
39
|
|
|
40
|
+
def as_json(self):
|
|
41
|
+
"""Return a JSON-serializable dict of this bignum's data."""
|
|
42
|
+
return {"embedded": self.is_embedded(), "digits": len(self)}
|
|
43
|
+
|
|
40
44
|
def print_recursive(self, printer, depth):
|
|
41
45
|
"""Print this bignum (no recursion needed)."""
|
|
42
46
|
printer.print(self)
|
data/data/toolbox/rfloat.py
CHANGED
|
@@ -37,6 +37,13 @@ class RFloatImmediate:
|
|
|
37
37
|
terminal.print(' ', end='')
|
|
38
38
|
terminal.print(format.number, str(self.float_value()), format.reset, end='')
|
|
39
39
|
|
|
40
|
+
def as_json(self):
|
|
41
|
+
"""Return a JSON-serializable dict of this float's data."""
|
|
42
|
+
try:
|
|
43
|
+
return {"value": self.float_value()}
|
|
44
|
+
except Exception:
|
|
45
|
+
return {"value": None}
|
|
46
|
+
|
|
40
47
|
def print_recursive(self, printer, depth):
|
|
41
48
|
"""Print this float (no recursion needed)."""
|
|
42
49
|
printer.print(self)
|
|
@@ -60,6 +67,13 @@ class RFloatObject:
|
|
|
60
67
|
terminal.print(' ', end='')
|
|
61
68
|
terminal.print(format.number, str(self.float_value()), format.reset, end='')
|
|
62
69
|
|
|
70
|
+
def as_json(self):
|
|
71
|
+
"""Return a JSON-serializable dict of this float's data."""
|
|
72
|
+
try:
|
|
73
|
+
return {"value": self.float_value()}
|
|
74
|
+
except Exception:
|
|
75
|
+
return {"value": None}
|
|
76
|
+
|
|
63
77
|
def print_recursive(self, printer, depth):
|
|
64
78
|
"""Print this float (no recursion needed)."""
|
|
65
79
|
printer.print(self)
|
data/data/toolbox/rhash.py
CHANGED
|
@@ -70,6 +70,10 @@ class RHashSTTable(RHashBase):
|
|
|
70
70
|
printer.print_value_label(printer.max_depth - depth)
|
|
71
71
|
printer.print_value(value, depth - 1)
|
|
72
72
|
|
|
73
|
+
def as_json(self):
|
|
74
|
+
"""Return a JSON-serializable dict of this hash's data."""
|
|
75
|
+
return {"size": self.size()}
|
|
76
|
+
|
|
73
77
|
class RHashARTable(RHashBase):
|
|
74
78
|
"""Hash using AR table structure (newer, smaller hashes)."""
|
|
75
79
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import debugger
|
|
2
|
+
import constants
|
|
3
|
+
import format
|
|
4
|
+
import rclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RObjectBase:
|
|
8
|
+
"""Base class for Ruby T_OBJECT instances (regular class instances like User.new)."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, value):
|
|
11
|
+
"""value is a VALUE pointing to a T_OBJECT."""
|
|
12
|
+
self.value = value
|
|
13
|
+
self.basic = value.cast(constants.type_struct('struct RBasic').pointer())
|
|
14
|
+
self.flags = int(self.basic.dereference()['flags'])
|
|
15
|
+
self._class_name = None
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def class_name(self):
|
|
19
|
+
if self._class_name is None:
|
|
20
|
+
try:
|
|
21
|
+
klass = self.basic['klass']
|
|
22
|
+
self._class_name = rclass.get_class_name(klass)
|
|
23
|
+
except Exception:
|
|
24
|
+
self._class_name = f"#<Class:0x{int(self.value):x}>"
|
|
25
|
+
return self._class_name
|
|
26
|
+
|
|
27
|
+
def numiv(self):
|
|
28
|
+
"""Get the number of instance variables. Subclasses must implement."""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def ivptr(self):
|
|
32
|
+
"""Get pointer to instance variable values. Subclasses must implement."""
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def __str__(self):
|
|
36
|
+
addr = int(self.value)
|
|
37
|
+
n = self.numiv()
|
|
38
|
+
if n > 0:
|
|
39
|
+
return f"<{self.class_name}@0x{addr:x} ivars={n}>"
|
|
40
|
+
return f"<{self.class_name}@0x{addr:x}>"
|
|
41
|
+
|
|
42
|
+
def print_to(self, terminal):
|
|
43
|
+
addr = int(self.value)
|
|
44
|
+
n = self.numiv()
|
|
45
|
+
details = f"ivars={n}" if n > 0 else None
|
|
46
|
+
terminal.print_type_tag(self.class_name, addr, details)
|
|
47
|
+
|
|
48
|
+
def as_json(self):
|
|
49
|
+
"""Return a JSON-serializable dict of this object's data."""
|
|
50
|
+
data = {"class": self.class_name}
|
|
51
|
+
try:
|
|
52
|
+
data["ivars"] = self.numiv()
|
|
53
|
+
except Exception:
|
|
54
|
+
data["ivars"] = None
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
def print_recursive(self, printer, depth):
|
|
58
|
+
printer.print(self)
|
|
59
|
+
|
|
60
|
+
n = self.numiv()
|
|
61
|
+
if depth <= 0 or n <= 0:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
ptr = self.ivptr()
|
|
65
|
+
if ptr is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
for i in range(n):
|
|
69
|
+
try:
|
|
70
|
+
printer.print_item_label(printer.max_depth - depth, i)
|
|
71
|
+
printer.print_value(ptr[i], depth - 1)
|
|
72
|
+
except Exception:
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RObjectEmbedded(RObjectBase):
|
|
77
|
+
"""T_OBJECT with instance variables stored inline (small objects)."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, value, robject):
|
|
80
|
+
super().__init__(value)
|
|
81
|
+
self.robject = robject
|
|
82
|
+
self._numiv = None
|
|
83
|
+
|
|
84
|
+
def numiv(self):
|
|
85
|
+
if self._numiv is not None:
|
|
86
|
+
return self._numiv
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Try shape-based numiv (Ruby 3.4+)
|
|
90
|
+
self._numiv = self._numiv_from_shape()
|
|
91
|
+
if self._numiv is not None:
|
|
92
|
+
return self._numiv
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Fallback: count non-zero slots in the embedded array
|
|
97
|
+
self._numiv = self._count_embedded_slots()
|
|
98
|
+
return self._numiv
|
|
99
|
+
|
|
100
|
+
def _numiv_from_shape(self):
|
|
101
|
+
"""Try to get ivar count from the object's shape (Ruby 3.4+)."""
|
|
102
|
+
try:
|
|
103
|
+
shape_id_mask = 0xFFFF # shape_id is in bits 16..31 of flags typically
|
|
104
|
+
# In Ruby 3.4+, shape_id_t is stored in flags
|
|
105
|
+
# SHAPE_FLAG_SHIFT is typically 16
|
|
106
|
+
shape_id = (self.flags >> 16) & 0xFFFF
|
|
107
|
+
if shape_id == 0:
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
# Try to read from shape table
|
|
111
|
+
shape_list = debugger.parse_and_eval('rb_shape_tree.shape_list')
|
|
112
|
+
shape = shape_list[shape_id]
|
|
113
|
+
next_iv = int(shape['next_iv'])
|
|
114
|
+
return next_iv
|
|
115
|
+
except Exception:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _count_embedded_slots(self):
|
|
119
|
+
"""Count non-Qundef/non-zero slots in the embedded array."""
|
|
120
|
+
try:
|
|
121
|
+
ary = self.robject.dereference()['as']['ary']
|
|
122
|
+
count = 0
|
|
123
|
+
# ROBJECT_EMBED_LEN_MAX is typically 3 (Ruby 3.2) or varies by slot size
|
|
124
|
+
for i in range(12):
|
|
125
|
+
try:
|
|
126
|
+
val = int(ary[i])
|
|
127
|
+
if val == 0 or val == 0x34: # Qundef
|
|
128
|
+
break
|
|
129
|
+
count += 1
|
|
130
|
+
except Exception:
|
|
131
|
+
break
|
|
132
|
+
return count
|
|
133
|
+
except Exception:
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
def ivptr(self):
|
|
137
|
+
try:
|
|
138
|
+
return self.robject.dereference()['as']['ary']
|
|
139
|
+
except Exception:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class RObjectHeap(RObjectBase):
|
|
144
|
+
"""T_OBJECT with instance variables stored on the heap (many ivars)."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, value, robject):
|
|
147
|
+
super().__init__(value)
|
|
148
|
+
self.robject = robject
|
|
149
|
+
self._numiv = None
|
|
150
|
+
|
|
151
|
+
def numiv(self):
|
|
152
|
+
if self._numiv is not None:
|
|
153
|
+
return self._numiv
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
self._numiv = int(self.robject.dereference()['as']['heap']['numiv'])
|
|
157
|
+
except Exception:
|
|
158
|
+
self._numiv = 0
|
|
159
|
+
return self._numiv
|
|
160
|
+
|
|
161
|
+
def ivptr(self):
|
|
162
|
+
try:
|
|
163
|
+
return self.robject.dereference()['as']['heap']['ivptr']
|
|
164
|
+
except Exception:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class RObjectGeneric(RObjectBase):
|
|
169
|
+
"""Fallback T_OBJECT when struct RObject is unavailable or has unknown layout."""
|
|
170
|
+
|
|
171
|
+
def numiv(self):
|
|
172
|
+
return 0
|
|
173
|
+
|
|
174
|
+
def ivptr(self):
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def RObject(value):
|
|
179
|
+
"""Factory function that detects the RObject variant and returns the appropriate instance.
|
|
180
|
+
|
|
181
|
+
Caller should ensure value is a RUBY_T_OBJECT before calling.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
robject = value.cast(constants.type_struct('struct RObject').pointer())
|
|
185
|
+
except Exception:
|
|
186
|
+
return RObjectGeneric(value)
|
|
187
|
+
|
|
188
|
+
# Detect embedded vs heap storage
|
|
189
|
+
try:
|
|
190
|
+
FL_USER1 = constants.flag('RUBY_FL_USER1')
|
|
191
|
+
basic = value.cast(constants.type_struct('struct RBasic').pointer())
|
|
192
|
+
flags = int(basic.dereference()['flags'])
|
|
193
|
+
|
|
194
|
+
if flags & FL_USER1:
|
|
195
|
+
# ROBJECT_EMBED flag set — ivars are inline
|
|
196
|
+
return RObjectEmbedded(value, robject)
|
|
197
|
+
else:
|
|
198
|
+
# Heap-allocated ivars
|
|
199
|
+
return RObjectHeap(value, robject)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
# Feature detection: check what fields exist
|
|
204
|
+
try:
|
|
205
|
+
as_union = robject.dereference()['as']
|
|
206
|
+
if as_union is not None:
|
|
207
|
+
heap = as_union['heap']
|
|
208
|
+
if heap is not None:
|
|
209
|
+
numiv_field = heap['numiv']
|
|
210
|
+
if numiv_field is not None:
|
|
211
|
+
return RObjectHeap(value, robject)
|
|
212
|
+
|
|
213
|
+
ary = as_union['ary']
|
|
214
|
+
if ary is not None:
|
|
215
|
+
return RObjectEmbedded(value, robject)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
return RObjectGeneric(value)
|
data/data/toolbox/rstring.py
CHANGED
|
@@ -77,6 +77,15 @@ class RStringBase:
|
|
|
77
77
|
# Use repr() to properly escape quotes, newlines, etc.
|
|
78
78
|
terminal.print(format.string, repr(content), format.reset, end='')
|
|
79
79
|
|
|
80
|
+
def as_json(self):
|
|
81
|
+
"""Return a JSON-serializable dict of this string's data."""
|
|
82
|
+
data = {"length": self.length(), "embedded": self._is_embedded()}
|
|
83
|
+
try:
|
|
84
|
+
data["value"] = self.to_str()
|
|
85
|
+
except Exception:
|
|
86
|
+
data["value"] = None
|
|
87
|
+
return data
|
|
88
|
+
|
|
80
89
|
def print_recursive(self, printer, depth):
|
|
81
90
|
"""Print this string (no recursion needed for strings)."""
|
|
82
91
|
printer.print(self)
|
data/data/toolbox/rvalue.py
CHANGED
|
@@ -11,6 +11,7 @@ import rarray
|
|
|
11
11
|
import rhash
|
|
12
12
|
import rstruct
|
|
13
13
|
import rbignum
|
|
14
|
+
import robject
|
|
14
15
|
|
|
15
16
|
class RImmediate:
|
|
16
17
|
"""Wrapper for Ruby immediate values (fixnum, nil, true, false)."""
|
|
@@ -50,6 +51,18 @@ class RImmediate:
|
|
|
50
51
|
# Unknown immediate
|
|
51
52
|
terminal.print_type_tag('Immediate', self.val_int)
|
|
52
53
|
|
|
54
|
+
def as_json(self):
|
|
55
|
+
"""Return a JSON-serializable dict of this immediate value's data."""
|
|
56
|
+
if self.val_int == 0:
|
|
57
|
+
return {"value": False}
|
|
58
|
+
elif self.val_int == 0x04 or self.val_int == 0x08:
|
|
59
|
+
return {"value": None}
|
|
60
|
+
elif self.val_int == 0x14:
|
|
61
|
+
return {"value": True}
|
|
62
|
+
elif (self.val_int & 0x01) != 0:
|
|
63
|
+
return {"value": self.val_int >> 1}
|
|
64
|
+
return {}
|
|
65
|
+
|
|
53
66
|
def print_recursive(self, printer, depth):
|
|
54
67
|
"""Print this immediate value (no recursion needed)."""
|
|
55
68
|
printer.print(self)
|
|
@@ -174,6 +187,8 @@ def interpret(value):
|
|
|
174
187
|
return rfloat.RFloat(value)
|
|
175
188
|
elif type_flag == constants.type("RUBY_T_BIGNUM"):
|
|
176
189
|
return rbignum.RBignum(value)
|
|
190
|
+
elif type_flag == constants.type("RUBY_T_OBJECT"):
|
|
191
|
+
return robject.RObject(value)
|
|
177
192
|
else:
|
|
178
193
|
# Unknown type - return generic RBasic
|
|
179
194
|
return rbasic.RBasic(value)
|
data/lib/toolbox/version.rb
CHANGED
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -31,6 +31,11 @@ Please see the [project documentation](https://socketry.github.io/toolbox/) for
|
|
|
31
31
|
|
|
32
32
|
Please see the [project releases](https://socketry.github.io/toolbox/releases/index) for all releases.
|
|
33
33
|
|
|
34
|
+
### v0.4.0
|
|
35
|
+
|
|
36
|
+
- Better support for printing `T_OBJECT` values, including class name.
|
|
37
|
+
- Introduce `rb-heap-dump` command to dump the Ruby heap to a JSON file, with optional type filtering and limit.
|
|
38
|
+
|
|
34
39
|
### v0.2.0
|
|
35
40
|
|
|
36
41
|
- Introduce support for `lldb` using debugger shims.
|
|
@@ -55,6 +60,22 @@ We welcome contributions to this project.
|
|
|
55
60
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
56
61
|
5. Create new Pull Request.
|
|
57
62
|
|
|
63
|
+
### Running Tests
|
|
64
|
+
|
|
65
|
+
To run the test suite:
|
|
66
|
+
|
|
67
|
+
``` shell
|
|
68
|
+
bundle exec sus
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Making Releases
|
|
72
|
+
|
|
73
|
+
To make a new release:
|
|
74
|
+
|
|
75
|
+
``` shell
|
|
76
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
77
|
+
```
|
|
78
|
+
|
|
58
79
|
### Developer Certificate of Origin
|
|
59
80
|
|
|
60
81
|
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- Better support for printing `T_OBJECT` values, including class name.
|
|
6
|
+
- Introduce `rb-heap-dump` command to dump the Ruby heap to a JSON file, with optional type filtering and limit.
|
|
7
|
+
|
|
3
8
|
## v0.2.0
|
|
4
9
|
|
|
5
10
|
- Introduce support for `lldb` using debugger shims.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toolbox
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- data/toolbox/rexception.py
|
|
71
71
|
- data/toolbox/rfloat.py
|
|
72
72
|
- data/toolbox/rhash.py
|
|
73
|
+
- data/toolbox/robject.py
|
|
73
74
|
- data/toolbox/rstring.py
|
|
74
75
|
- data/toolbox/rstruct.py
|
|
75
76
|
- data/toolbox/rsymbol.py
|
|
@@ -95,14 +96,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
95
96
|
requirements:
|
|
96
97
|
- - ">="
|
|
97
98
|
- !ruby/object:Gem::Version
|
|
98
|
-
version: '3.
|
|
99
|
+
version: '3.3'
|
|
99
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
101
|
requirements:
|
|
101
102
|
- - ">="
|
|
102
103
|
- !ruby/object:Gem::Version
|
|
103
104
|
version: '0'
|
|
104
105
|
requirements: []
|
|
105
|
-
rubygems_version:
|
|
106
|
+
rubygems_version: 4.0.6
|
|
106
107
|
specification_version: 4
|
|
107
108
|
summary: Ruby debugging toolbox for GDB and LLDB
|
|
108
109
|
test_files: []
|
metadata.gz.sig
CHANGED
|
Binary file
|