marshal-md 0.1.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
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/lib/marshal-md/dumper.rb +721 -0
- data/lib/marshal-md/loader.rb +1074 -0
- data/lib/marshal-md/object_registry.rb +43 -0
- data/lib/marshal-md/patch.rb +40 -0
- data/lib/marshal-md/version.rb +5 -0
- data/lib/marshal-md.rb +98 -0
- metadata +69 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module MarshalMd
|
|
6
|
+
class Dumper
|
|
7
|
+
UNDUMPABLE = [Proc, IO, Thread, Binding, Method, UnboundMethod].freeze
|
|
8
|
+
IMMEDIATE_CLASSES = [Integer, Symbol, TrueClass, FalseClass, NilClass].freeze
|
|
9
|
+
|
|
10
|
+
# Built-in types that can have subclasses with special handling
|
|
11
|
+
BUILTIN_TYPES = [Array, Hash, String, Regexp, Range, Time].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(obj, limit: -1)
|
|
14
|
+
@root = obj
|
|
15
|
+
@limit = limit
|
|
16
|
+
@registry = ObjectRegistry.new
|
|
17
|
+
@needs_anchor = {} # object_id => true
|
|
18
|
+
@scan_stack = {} # object_id => true (cycle detection)
|
|
19
|
+
@seen = {} # object_id => true (multi-ref detection)
|
|
20
|
+
@emitted = {} # object_id => true (pass 2)
|
|
21
|
+
@marshal_dump_stack = {} # class => true (recursive marshal_dump detection)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dump
|
|
25
|
+
check_dumpable!(@root)
|
|
26
|
+
scan(@root)
|
|
27
|
+
@depth_remaining = @limit
|
|
28
|
+
emit(@root, 0).rstrip
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_dumpable!(obj)
|
|
34
|
+
UNDUMPABLE.each do |klass|
|
|
35
|
+
if obj.is_a?(klass)
|
|
36
|
+
raise TypeError, "no _dump_data is defined for class #{obj.class}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Singleton classes/methods
|
|
41
|
+
if obj.respond_to?(:singleton_methods) && !obj.singleton_methods(false).empty?
|
|
42
|
+
raise TypeError, "singleton can't be dumped"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Singleton class with instance variables or constants
|
|
46
|
+
unless immediate?(obj) || obj.is_a?(Class) || obj.is_a?(Module)
|
|
47
|
+
begin
|
|
48
|
+
sc = obj.singleton_class
|
|
49
|
+
if !sc.instance_variables.empty? || sc.constants(false).any?
|
|
50
|
+
raise TypeError, "singleton can't be dumped"
|
|
51
|
+
end
|
|
52
|
+
rescue TypeError => e
|
|
53
|
+
raise if e.message.include?("singleton can't be dumped")
|
|
54
|
+
# can't define singleton on immediate values — that's fine
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Special undumpable objects
|
|
59
|
+
if obj.equal?(ARGF) || obj.equal?(ENV)
|
|
60
|
+
raise TypeError, "can't dump #{obj.class}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Anonymous classes and modules
|
|
64
|
+
if obj.is_a?(Class) || obj.is_a?(Module)
|
|
65
|
+
if obj.name.nil? || obj.name.empty?
|
|
66
|
+
raise TypeError, "can't dump anonymous #{obj.is_a?(Class) ? 'Class' : 'Module'} #{obj}"
|
|
67
|
+
end
|
|
68
|
+
# Classes/modules with non-portable names (e.g. defined in singleton classes)
|
|
69
|
+
if obj.name.include?("#<")
|
|
70
|
+
raise TypeError, "can't dump anonymous #{obj.is_a?(Class) ? 'Class' : 'Module'} #{obj}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Instances of anonymous classes
|
|
75
|
+
if obj.class.name.nil? || obj.class.name.empty?
|
|
76
|
+
raise TypeError, "can't dump anonymous class #{obj.class}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Hash with default proc
|
|
80
|
+
if obj.is_a?(Hash) && obj.default_proc
|
|
81
|
+
raise TypeError, "can't dump hash with default proc"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def immediate?(obj)
|
|
86
|
+
IMMEDIATE_CLASSES.any? { |k| obj.is_a?(k) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def builtin_type(obj)
|
|
90
|
+
BUILTIN_TYPES.find { |t| obj.is_a?(t) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def subclassed_builtin?(obj)
|
|
94
|
+
bt = builtin_type(obj)
|
|
95
|
+
bt && obj.class != bt
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Extra instance variables (beyond what the built-in type provides)
|
|
99
|
+
def extra_ivars(obj)
|
|
100
|
+
obj.instance_variables.sort
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def has_extra_ivars?(obj)
|
|
104
|
+
!obj.instance_variables.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def has_extensions?(obj)
|
|
108
|
+
return false if immediate?(obj)
|
|
109
|
+
begin
|
|
110
|
+
mods = obj.singleton_class.ancestors - obj.class.ancestors
|
|
111
|
+
mods.any? { |m| m.is_a?(Module) && !m.is_a?(Class) }
|
|
112
|
+
rescue TypeError
|
|
113
|
+
false
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def needs_wrapped_format?(obj)
|
|
118
|
+
has_extra_ivars?(obj) || has_extensions?(obj) || subclassed_builtin?(obj)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Pass 1: scan object graph to find shared/circular references
|
|
122
|
+
def scan(obj)
|
|
123
|
+
return if immediate?(obj)
|
|
124
|
+
|
|
125
|
+
oid = obj.object_id
|
|
126
|
+
|
|
127
|
+
if @scan_stack[oid]
|
|
128
|
+
@needs_anchor[oid] = true
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if @seen[oid]
|
|
133
|
+
@needs_anchor[oid] = true
|
|
134
|
+
return
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@seen[oid] = true
|
|
138
|
+
@scan_stack[oid] = true
|
|
139
|
+
|
|
140
|
+
case obj
|
|
141
|
+
when String, Float
|
|
142
|
+
# leaf-ish, but may have ivars
|
|
143
|
+
scan_ivars(obj)
|
|
144
|
+
when Regexp
|
|
145
|
+
scan_ivars(obj)
|
|
146
|
+
when Range
|
|
147
|
+
scan(obj.begin) if obj.begin
|
|
148
|
+
scan(obj.end) if obj.end
|
|
149
|
+
scan_ivars(obj)
|
|
150
|
+
when Time
|
|
151
|
+
scan_ivars(obj)
|
|
152
|
+
when Array
|
|
153
|
+
obj.each { |el| scan(el) }
|
|
154
|
+
scan_ivars(obj)
|
|
155
|
+
when Hash
|
|
156
|
+
obj.each { |k, v| scan(k); scan(v) }
|
|
157
|
+
scan_ivars(obj)
|
|
158
|
+
when Struct
|
|
159
|
+
obj.each_pair { |_, v| scan(v) }
|
|
160
|
+
scan_ivars(obj)
|
|
161
|
+
when Class, Module, Encoding
|
|
162
|
+
# leaf
|
|
163
|
+
else
|
|
164
|
+
if obj.respond_to?(:marshal_dump)
|
|
165
|
+
# Don't call marshal_dump during scan — it may have side effects.
|
|
166
|
+
# Just scan instance variables to detect shared/circular refs.
|
|
167
|
+
scan_ivars(obj)
|
|
168
|
+
elsif obj.respond_to?(:_dump)
|
|
169
|
+
# _dump returns a string
|
|
170
|
+
else
|
|
171
|
+
scan_ivars(obj)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
@scan_stack.delete(oid)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def scan_ivars(obj)
|
|
179
|
+
obj.instance_variables.each do |ivar|
|
|
180
|
+
scan(obj.instance_variable_get(ivar))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Pass 2: emit markdown
|
|
185
|
+
def emit(obj, indent)
|
|
186
|
+
check_dumpable!(obj)
|
|
187
|
+
|
|
188
|
+
# Depth limit check (Marshal compat: limit >= 0 means limited depth)
|
|
189
|
+
if @depth_remaining == 0 && @limit >= 0
|
|
190
|
+
raise ArgumentError, "exceed depth limit"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
if !immediate?(obj) && @needs_anchor[obj.object_id]
|
|
194
|
+
if @emitted[obj.object_id]
|
|
195
|
+
anchor = @registry.anchor_for(obj) || @registry.register(obj)
|
|
196
|
+
return "#{" " * indent}*#{anchor} (ref)\n"
|
|
197
|
+
end
|
|
198
|
+
anchor = @registry.register(obj)
|
|
199
|
+
@emitted[obj.object_id] = true
|
|
200
|
+
return emit_anchored(obj, indent, anchor)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
if !immediate?(obj)
|
|
204
|
+
@emitted[obj.object_id] = true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if @limit >= 0
|
|
208
|
+
@depth_remaining -= 1
|
|
209
|
+
result = emit_value(obj, indent)
|
|
210
|
+
@depth_remaining += 1
|
|
211
|
+
result
|
|
212
|
+
else
|
|
213
|
+
emit_value(obj, indent)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def emit_anchored(obj, indent, anchor)
|
|
218
|
+
prefix = " " * indent
|
|
219
|
+
lines = emit_value(obj, indent)
|
|
220
|
+
first_line = lines.lines.first
|
|
221
|
+
rest = lines.lines[1..].join
|
|
222
|
+
stripped = first_line.lstrip
|
|
223
|
+
"#{prefix}&#{anchor} #{stripped}#{rest}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def emit_value(obj, indent)
|
|
227
|
+
prefix = " " * indent
|
|
228
|
+
|
|
229
|
+
case obj
|
|
230
|
+
when NilClass
|
|
231
|
+
"#{prefix}nil (NilClass)\n"
|
|
232
|
+
when TrueClass
|
|
233
|
+
"#{prefix}true (Boolean)\n"
|
|
234
|
+
when FalseClass
|
|
235
|
+
"#{prefix}false (Boolean)\n"
|
|
236
|
+
when Integer
|
|
237
|
+
"#{prefix}#{obj} (Integer)\n"
|
|
238
|
+
when Float
|
|
239
|
+
"#{prefix}#{format_float(obj)} (Float)\n"
|
|
240
|
+
when Complex
|
|
241
|
+
"#{prefix}(#{obj.real}+#{obj.imaginary}i) (Complex)\n"
|
|
242
|
+
when Rational
|
|
243
|
+
"#{prefix}#{obj.numerator}/#{obj.denominator} (Rational)\n"
|
|
244
|
+
when Symbol
|
|
245
|
+
"#{prefix}:#{obj} (Symbol)\n"
|
|
246
|
+
when Encoding
|
|
247
|
+
"#{prefix}#{obj.name} (Encoding)\n"
|
|
248
|
+
when Class
|
|
249
|
+
"#{prefix}#{obj.name} (Class)\n"
|
|
250
|
+
when Module
|
|
251
|
+
"#{prefix}#{obj.name} (Module)\n"
|
|
252
|
+
when Struct
|
|
253
|
+
emit_struct(obj, indent)
|
|
254
|
+
when Range
|
|
255
|
+
emit_range(obj, indent)
|
|
256
|
+
when Regexp
|
|
257
|
+
emit_regexp(obj, indent)
|
|
258
|
+
when Time
|
|
259
|
+
emit_time(obj, indent)
|
|
260
|
+
when String
|
|
261
|
+
emit_string(obj, indent)
|
|
262
|
+
when Array
|
|
263
|
+
emit_array(obj, indent)
|
|
264
|
+
when Hash
|
|
265
|
+
emit_hash(obj, indent)
|
|
266
|
+
else
|
|
267
|
+
emit_custom(obj, indent)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def format_float(f)
|
|
272
|
+
if f.infinite? == 1
|
|
273
|
+
"Infinity"
|
|
274
|
+
elsif f.infinite? == -1
|
|
275
|
+
"-Infinity"
|
|
276
|
+
elsif f.nan?
|
|
277
|
+
"NaN"
|
|
278
|
+
elsif f.zero? && (1.0 / f) == -Float::INFINITY
|
|
279
|
+
"-0.0"
|
|
280
|
+
else
|
|
281
|
+
# Use enough precision to round-trip
|
|
282
|
+
f.to_s
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def format_string_value(str, bare: false)
|
|
287
|
+
if str.encoding == Encoding::ASCII_8BIT
|
|
288
|
+
encoded = Base64.strict_encode64(str)
|
|
289
|
+
"base64:#{encoded} (String, ASCII-8BIT, #{str.bytesize} bytes)"
|
|
290
|
+
elsif str.encoding != Encoding::UTF_8 && str.encoding != Encoding::US_ASCII
|
|
291
|
+
begin
|
|
292
|
+
utf8_str = str.encode(Encoding::UTF_8)
|
|
293
|
+
"\"#{escape_string(utf8_str)}\" (String, #{str.encoding})"
|
|
294
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
|
|
295
|
+
encoded = Base64.strict_encode64(str)
|
|
296
|
+
"base64:#{encoded} (String, #{str.encoding}, #{str.bytesize} bytes)"
|
|
297
|
+
end
|
|
298
|
+
else
|
|
299
|
+
bare ? "\"#{escape_string(str)}\"" : "\"#{escape_string(str)}\" (String)"
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def escape_string(str)
|
|
304
|
+
str.gsub("\\", "\\\\\\\\")
|
|
305
|
+
.gsub("\"", "\\\"")
|
|
306
|
+
.gsub("\n", "\\n")
|
|
307
|
+
.gsub("\r", "\\r")
|
|
308
|
+
.gsub("\t", "\\t")
|
|
309
|
+
.gsub(/[\x00-\x1f]/) { |c| "\\x#{c.ord.to_s(16).rjust(2, '0')}" }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Bare inline representation for use inside [...] and {...} and after @ivar:
|
|
313
|
+
def inline_value(obj)
|
|
314
|
+
# Check for reference first (for any non-immediate that's already emitted)
|
|
315
|
+
if !immediate?(obj) && @needs_anchor[obj.object_id] && @emitted[obj.object_id]
|
|
316
|
+
anchor = @registry.anchor_for(obj)
|
|
317
|
+
return "*#{anchor} (ref)"
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
case obj
|
|
321
|
+
when NilClass then "nil"
|
|
322
|
+
when TrueClass then "true"
|
|
323
|
+
when FalseClass then "false"
|
|
324
|
+
when Integer then obj.to_s
|
|
325
|
+
when Float then format_float(obj)
|
|
326
|
+
when Symbol then ":#{obj}"
|
|
327
|
+
when String then format_string_value(obj, bare: true)
|
|
328
|
+
else
|
|
329
|
+
nil # not inlineable
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def simple_value?(obj)
|
|
334
|
+
return false if !immediate?(obj) && has_extensions?(obj)
|
|
335
|
+
case obj
|
|
336
|
+
when NilClass, TrueClass, FalseClass, Integer, Float, Symbol
|
|
337
|
+
true
|
|
338
|
+
when String
|
|
339
|
+
!has_extra_ivars?(obj) && obj.encoding != Encoding::ASCII_8BIT && !obj.include?("\n")
|
|
340
|
+
else
|
|
341
|
+
false
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def all_simple?(arr)
|
|
346
|
+
arr.all? { |el| simple_value?(el) || (el.is_a?(String) && !el.include?("\n")) }
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# --- String ---
|
|
350
|
+
def emit_string(obj, indent)
|
|
351
|
+
prefix = " " * indent
|
|
352
|
+
class_name = obj.class.name
|
|
353
|
+
ivars = extra_ivars(obj)
|
|
354
|
+
|
|
355
|
+
if needs_wrapped_format?(obj)
|
|
356
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
357
|
+
result += "#{prefix} __value__: #{format_string_value(obj)}\n"
|
|
358
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
359
|
+
ivars.each do |ivar|
|
|
360
|
+
val = obj.instance_variable_get(ivar)
|
|
361
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
362
|
+
end
|
|
363
|
+
result
|
|
364
|
+
else
|
|
365
|
+
"#{prefix}#{format_string_value(obj)}\n"
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# --- Array ---
|
|
370
|
+
def emit_array(obj, indent)
|
|
371
|
+
prefix = " " * indent
|
|
372
|
+
class_name = obj.class.name
|
|
373
|
+
ivars = extra_ivars(obj)
|
|
374
|
+
is_subclass = class_name != "Array"
|
|
375
|
+
|
|
376
|
+
if needs_wrapped_format?(obj)
|
|
377
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
378
|
+
result += "#{prefix} __elements__:\n"
|
|
379
|
+
if obj.empty?
|
|
380
|
+
result += "#{prefix} [] (Array)\n"
|
|
381
|
+
else
|
|
382
|
+
result += "#{prefix} (Array)\n"
|
|
383
|
+
obj.each { |el| result += emit(el, indent + 3) }
|
|
384
|
+
end
|
|
385
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
386
|
+
ivars.each do |ivar|
|
|
387
|
+
val = obj.instance_variable_get(ivar)
|
|
388
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
389
|
+
end
|
|
390
|
+
result
|
|
391
|
+
else
|
|
392
|
+
emit_simple_array(obj, indent)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def emit_simple_array(obj, indent)
|
|
397
|
+
prefix = " " * indent
|
|
398
|
+
if obj.empty?
|
|
399
|
+
return "#{prefix}[] (Array)\n"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
if all_simple?(obj) && obj.sum { |el| (inline_value(el) || "").length + 2 } < 80
|
|
403
|
+
items = obj.map { |el| inline_value(el) }.join(", ")
|
|
404
|
+
"#{prefix}[#{items}] (Array)\n"
|
|
405
|
+
else
|
|
406
|
+
len_before = obj.length
|
|
407
|
+
result = "#{prefix}(Array)\n"
|
|
408
|
+
obj.each { |el| result += emit(el, indent + 1) }
|
|
409
|
+
if obj.length != len_before
|
|
410
|
+
raise RuntimeError, "array modified during dump"
|
|
411
|
+
end
|
|
412
|
+
result
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# --- Hash ---
|
|
417
|
+
def emit_hash(obj, indent)
|
|
418
|
+
prefix = " " * indent
|
|
419
|
+
class_name = obj.class.name
|
|
420
|
+
ivars = extra_ivars(obj)
|
|
421
|
+
is_subclass = class_name != "Hash"
|
|
422
|
+
has_default = !obj.default.nil?
|
|
423
|
+
|
|
424
|
+
has_identity = obj.respond_to?(:compare_by_identity?) && obj.compare_by_identity?
|
|
425
|
+
|
|
426
|
+
if needs_wrapped_format?(obj) || has_default || has_identity
|
|
427
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
428
|
+
if has_identity
|
|
429
|
+
result += "#{prefix} __compare_by_identity__: true\n"
|
|
430
|
+
end
|
|
431
|
+
if has_default
|
|
432
|
+
result += emit_ivar(prefix + " ", "__default__", obj.default, indent + 1)
|
|
433
|
+
end
|
|
434
|
+
result += "#{prefix} __entries__:\n"
|
|
435
|
+
if obj.empty?
|
|
436
|
+
result += "#{prefix} {} (Hash)\n"
|
|
437
|
+
else
|
|
438
|
+
result += "#{prefix} (Hash)\n"
|
|
439
|
+
obj.each do |k, v|
|
|
440
|
+
iv = inline_value(v)
|
|
441
|
+
ik = inline_value(k)
|
|
442
|
+
if ik && iv
|
|
443
|
+
result += "#{prefix} #{ik} => #{iv}\n"
|
|
444
|
+
elsif ik
|
|
445
|
+
result += "#{prefix} #{ik} =>\n"
|
|
446
|
+
result += emit(v, indent + 4)
|
|
447
|
+
else
|
|
448
|
+
result += "#{prefix} (entry)\n"
|
|
449
|
+
result += emit(k, indent + 4)
|
|
450
|
+
result += "#{prefix} =>\n"
|
|
451
|
+
result += emit(v, indent + 4)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
456
|
+
ivars.each do |ivar|
|
|
457
|
+
val = obj.instance_variable_get(ivar)
|
|
458
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
459
|
+
end
|
|
460
|
+
result
|
|
461
|
+
else
|
|
462
|
+
emit_simple_hash(obj, indent)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def emit_simple_hash(obj, indent)
|
|
467
|
+
prefix = " " * indent
|
|
468
|
+
if obj.empty?
|
|
469
|
+
return "#{prefix}{} (Hash)\n"
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
if obj.all? { |k, v| k.is_a?(Symbol) && simple_value?(v) }
|
|
473
|
+
pairs = obj.map { |k, v| "#{k}: #{inline_value(v)}" }.join(", ")
|
|
474
|
+
candidate = "{#{pairs}} (Hash)"
|
|
475
|
+
if candidate.length < 80
|
|
476
|
+
return "#{prefix}#{candidate}\n"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
result = "#{prefix}(Hash)\n"
|
|
481
|
+
obj.each do |k, v|
|
|
482
|
+
ik = inline_value(k)
|
|
483
|
+
iv = inline_value(v)
|
|
484
|
+
if ik && iv
|
|
485
|
+
result += "#{prefix} #{ik} => #{iv}\n"
|
|
486
|
+
elsif ik
|
|
487
|
+
result += "#{prefix} #{ik} =>\n"
|
|
488
|
+
result += emit(v, indent + 2)
|
|
489
|
+
else
|
|
490
|
+
result += "#{prefix} (entry)\n"
|
|
491
|
+
result += emit(k, indent + 2)
|
|
492
|
+
result += "#{prefix} =>\n"
|
|
493
|
+
result += emit(v, indent + 2)
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
result
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# --- Range ---
|
|
500
|
+
def emit_range(obj, indent)
|
|
501
|
+
prefix = " " * indent
|
|
502
|
+
class_name = obj.class.name
|
|
503
|
+
ivars = extra_ivars(obj)
|
|
504
|
+
is_subclass = class_name != "Range"
|
|
505
|
+
dots = obj.exclude_end? ? "..." : ".."
|
|
506
|
+
|
|
507
|
+
if is_subclass || !ivars.empty?
|
|
508
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
509
|
+
result += "#{prefix} __begin__:\n"
|
|
510
|
+
result += emit(obj.begin, indent + 2) if obj.begin
|
|
511
|
+
result += "#{prefix} __end__:\n"
|
|
512
|
+
result += emit(obj.end, indent + 2) if obj.end
|
|
513
|
+
result += "#{prefix} __exclude_end__: #{obj.exclude_end?}\n"
|
|
514
|
+
ivars.each do |ivar|
|
|
515
|
+
val = obj.instance_variable_get(ivar)
|
|
516
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
517
|
+
end
|
|
518
|
+
result
|
|
519
|
+
else
|
|
520
|
+
ib = inline_value(obj.begin)
|
|
521
|
+
ie = inline_value(obj.end)
|
|
522
|
+
if ib && ie
|
|
523
|
+
"#{prefix}#{ib}#{dots}#{ie} (Range)\n"
|
|
524
|
+
else
|
|
525
|
+
result = "#{prefix}(Range)\n"
|
|
526
|
+
result += "#{prefix} __begin__:\n"
|
|
527
|
+
result += emit(obj.begin, indent + 2) if obj.begin
|
|
528
|
+
result += "#{prefix} __end__:\n"
|
|
529
|
+
result += emit(obj.end, indent + 2) if obj.end
|
|
530
|
+
result += "#{prefix} __exclude_end__: #{obj.exclude_end?}\n"
|
|
531
|
+
result
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# --- Regexp ---
|
|
537
|
+
def emit_regexp(obj, indent)
|
|
538
|
+
prefix = " " * indent
|
|
539
|
+
class_name = obj.class.name
|
|
540
|
+
ivars = extra_ivars(obj)
|
|
541
|
+
is_subclass = class_name != "Regexp"
|
|
542
|
+
|
|
543
|
+
flags = ""
|
|
544
|
+
flags += "i" if (obj.options & Regexp::IGNORECASE) != 0
|
|
545
|
+
flags += "x" if (obj.options & Regexp::EXTENDED) != 0
|
|
546
|
+
flags += "m" if (obj.options & Regexp::MULTILINE) != 0
|
|
547
|
+
|
|
548
|
+
if is_subclass || !ivars.empty?
|
|
549
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
550
|
+
result += "#{prefix} __pattern__: /#{obj.source}/#{flags} (Regexp)\n"
|
|
551
|
+
ivars.each do |ivar|
|
|
552
|
+
val = obj.instance_variable_get(ivar)
|
|
553
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
554
|
+
end
|
|
555
|
+
result
|
|
556
|
+
else
|
|
557
|
+
"#{prefix}/#{obj.source}/#{flags} (Regexp)\n"
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# --- Time ---
|
|
562
|
+
def emit_time(obj, indent)
|
|
563
|
+
prefix = " " * indent
|
|
564
|
+
class_name = obj.class.name
|
|
565
|
+
ivars = extra_ivars(obj)
|
|
566
|
+
is_subclass = class_name != "Time"
|
|
567
|
+
|
|
568
|
+
# Use enough precision for usec round-trip
|
|
569
|
+
time_str = "#{obj.strftime('%Y-%m-%d %H:%M:%S')}.#{obj.usec.to_s.rjust(6, '0')} #{obj.strftime('%z')}"
|
|
570
|
+
|
|
571
|
+
if is_subclass || !ivars.empty?
|
|
572
|
+
result = "#{prefix}#<#{class_name}> (#{class_name})\n"
|
|
573
|
+
result += "#{prefix} __time__: #{time_str} (Time)\n"
|
|
574
|
+
ivars.each do |ivar|
|
|
575
|
+
val = obj.instance_variable_get(ivar)
|
|
576
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
577
|
+
end
|
|
578
|
+
result
|
|
579
|
+
else
|
|
580
|
+
"#{prefix}#{time_str} (Time)\n"
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# --- Struct ---
|
|
585
|
+
def emit_struct(obj, indent)
|
|
586
|
+
# If struct has marshal_dump, use custom object path
|
|
587
|
+
if obj.respond_to?(:marshal_dump)
|
|
588
|
+
return emit_custom(obj, indent)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
prefix = " " * indent
|
|
592
|
+
class_name = obj.class.name
|
|
593
|
+
ivars = extra_ivars(obj)
|
|
594
|
+
|
|
595
|
+
result = "#{prefix}#<#{class_name}> (#{class_name}, Struct)\n"
|
|
596
|
+
obj.each_pair do |member, val|
|
|
597
|
+
result += emit_ivar(prefix + " ", member.to_s, val, indent + 1)
|
|
598
|
+
end
|
|
599
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
600
|
+
ivars.each do |ivar|
|
|
601
|
+
val = obj.instance_variable_get(ivar)
|
|
602
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
603
|
+
end
|
|
604
|
+
result
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# --- Custom object ---
|
|
608
|
+
def emit_custom(obj, indent)
|
|
609
|
+
prefix = " " * indent
|
|
610
|
+
|
|
611
|
+
if obj.respond_to?(:marshal_dump)
|
|
612
|
+
klass = obj.class
|
|
613
|
+
if @marshal_dump_stack[klass]
|
|
614
|
+
raise RuntimeError, "Marshal.dump reentered at marshal_dump for #{klass}: same class instance"
|
|
615
|
+
end
|
|
616
|
+
@marshal_dump_stack[klass] = true
|
|
617
|
+
data = obj.marshal_dump
|
|
618
|
+
result = "#{prefix}#<#{obj.class}> (#{obj.class}, marshal_dump)\n"
|
|
619
|
+
result += emit(data, indent + 1)
|
|
620
|
+
@marshal_dump_stack.delete(klass)
|
|
621
|
+
return result
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
if obj.respond_to?(:_dump)
|
|
625
|
+
data = obj._dump(-1)
|
|
626
|
+
raise TypeError, "_dump() must return string" unless data.is_a?(String)
|
|
627
|
+
result = "#{prefix}(#{obj.class}, _dump)\n"
|
|
628
|
+
result += "#{prefix} #{format_string_value(data)}\n"
|
|
629
|
+
return result
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Exception special handling: store message explicitly
|
|
633
|
+
if obj.is_a?(Exception)
|
|
634
|
+
result = "#{prefix}#<#{obj.class}> (#{obj.class})\n"
|
|
635
|
+
result += "#{prefix} __message__: #{format_string_value(obj.message, bare: true)}\n" if obj.message
|
|
636
|
+
result += "#{prefix} __backtrace__:\n"
|
|
637
|
+
if obj.backtrace
|
|
638
|
+
result += emit(obj.backtrace, indent + 2)
|
|
639
|
+
else
|
|
640
|
+
result += "#{prefix} nil (NilClass)\n"
|
|
641
|
+
end
|
|
642
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
643
|
+
obj.instance_variables.sort.each do |ivar|
|
|
644
|
+
# Skip internal exception ivars that we handle specially
|
|
645
|
+
next if ivar == :@mesg || ivar == :@bt
|
|
646
|
+
val = obj.instance_variable_get(ivar)
|
|
647
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
648
|
+
end
|
|
649
|
+
return result
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
ivars_before = obj.instance_variables.sort
|
|
653
|
+
result = "#{prefix}#<#{obj.class}> (#{obj.class})\n"
|
|
654
|
+
result += emit_extensions_str(obj, indent + 1)
|
|
655
|
+
ivars_before.each do |ivar|
|
|
656
|
+
val = obj.instance_variable_get(ivar)
|
|
657
|
+
result += emit_ivar(prefix + " ", ivar, val, indent + 1)
|
|
658
|
+
end
|
|
659
|
+
ivars_after = obj.instance_variables.sort
|
|
660
|
+
if ivars_after != ivars_before
|
|
661
|
+
added = ivars_after - ivars_before
|
|
662
|
+
removed = ivars_before - ivars_after
|
|
663
|
+
if added.any?
|
|
664
|
+
raise RuntimeError, "instance variable added to #{obj.class} during dump"
|
|
665
|
+
end
|
|
666
|
+
if removed.any?
|
|
667
|
+
raise RuntimeError, "instance variable removed from #{obj.class} during dump"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
result
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def emit_ivar(prefix, name, val, parent_indent)
|
|
674
|
+
if simple_value?(val)
|
|
675
|
+
iv = inline_value(val)
|
|
676
|
+
if iv
|
|
677
|
+
return "#{prefix}#{name}: #{iv}\n"
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
"#{prefix}#{name}:\n" + emit(val, parent_indent + 1)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def emit_extensions_str(obj, indent)
|
|
684
|
+
prefix = " " * indent
|
|
685
|
+
ext = +""
|
|
686
|
+
begin
|
|
687
|
+
sc = obj.singleton_class
|
|
688
|
+
sc_ancestors = sc.ancestors
|
|
689
|
+
class_ancestors = obj.class.ancestors
|
|
690
|
+
rescue TypeError
|
|
691
|
+
return +""
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Find modules added to singleton class
|
|
695
|
+
# Modules before singleton_class in ancestors are prepended
|
|
696
|
+
# Modules after singleton_class but not in class ancestors are extended
|
|
697
|
+
sc_idx = sc_ancestors.index(sc)
|
|
698
|
+
return +"" unless sc_idx
|
|
699
|
+
|
|
700
|
+
prepended = sc_ancestors[0...sc_idx].select { |m| m.is_a?(Module) && !m.is_a?(Class) && !class_ancestors.include?(m) }
|
|
701
|
+
extended = sc_ancestors[(sc_idx + 1)..].select { |m| m.is_a?(Module) && !m.is_a?(Class) && !class_ancestors.include?(m) }
|
|
702
|
+
|
|
703
|
+
# Emit in reverse so they apply in correct order during load
|
|
704
|
+
extended.reverse.each do |mod|
|
|
705
|
+
if mod.name.nil? || mod.name.empty?
|
|
706
|
+
raise TypeError, "can't dump anonymous module"
|
|
707
|
+
end
|
|
708
|
+
ext << "#{prefix}__extend__: #{mod.name}\n"
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
prepended.reverse.each do |mod|
|
|
712
|
+
if mod.name.nil? || mod.name.empty?
|
|
713
|
+
raise TypeError, "can't dump anonymous module"
|
|
714
|
+
end
|
|
715
|
+
ext << "#{prefix}__prepend__: #{mod.name}\n"
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
ext
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
end
|