binding_dumper 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +13 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/Rakefile +7 -0
- data/bin/console +15 -0
- data/bin/dummy_rails +3 -0
- data/bin/dummy_rake +4 -0
- data/bin/multitest +32 -0
- data/bin/setup +7 -0
- data/binding_dumper.gemspec +24 -0
- data/lib/binding_dumper.rb +28 -0
- data/lib/binding_dumper/core_ext/binding_ext.rb +67 -0
- data/lib/binding_dumper/core_ext/local_binding_patch_builder.rb +102 -0
- data/lib/binding_dumper/core_ext/magic_context_patch_builder.rb +55 -0
- data/lib/binding_dumper/dumpers/abstract.rb +73 -0
- data/lib/binding_dumper/dumpers/array_dumper.rb +64 -0
- data/lib/binding_dumper/dumpers/class_dumper.rb +111 -0
- data/lib/binding_dumper/dumpers/existing_object_dumper.rb +89 -0
- data/lib/binding_dumper/dumpers/hash_dumper.rb +69 -0
- data/lib/binding_dumper/dumpers/magic_dumper.rb +75 -0
- data/lib/binding_dumper/dumpers/object_dumper.rb +106 -0
- data/lib/binding_dumper/dumpers/primitive_dumper.rb +48 -0
- data/lib/binding_dumper/dumpers/proc_dumper.rb +44 -0
- data/lib/binding_dumper/magic_objects.rb +98 -0
- data/lib/binding_dumper/memories.rb +51 -0
- data/lib/binding_dumper/universal_dumper.rb +118 -0
- data/lib/binding_dumper/version.rb +3 -0
- data/test.rb +17 -0
- metadata +117 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# Class for buliding patch that adds method '_local_binding'
|
2
|
+
# to existing object
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# data = {
|
6
|
+
# file: '/path/to/file.rb',
|
7
|
+
# line: 17,
|
8
|
+
# method: 'do_something',
|
9
|
+
# lvars: { a: 'b' }
|
10
|
+
# }
|
11
|
+
# patch = BindingDumper::CoreExt::MagicContextPatchBuilder.new(data).patch
|
12
|
+
# context = Object.new.extend(patch)
|
13
|
+
#
|
14
|
+
# context._local_binding
|
15
|
+
# # => #<Binding>
|
16
|
+
# context._local_binding.eval('a')
|
17
|
+
# # => 'b'
|
18
|
+
# context._local_binding.eval('__FILE__')
|
19
|
+
# # => '/path/to/file.rb'
|
20
|
+
# context._local_binding.eval('__LINE__')
|
21
|
+
# # => 17
|
22
|
+
# context._local_binding.eval('__method__')
|
23
|
+
# # => 'do_something'
|
24
|
+
#
|
25
|
+
module BindingDumper
|
26
|
+
module CoreExt
|
27
|
+
class MagicContextPatchBuilder
|
28
|
+
attr_reader :undumped
|
29
|
+
|
30
|
+
def initialize(undumped)
|
31
|
+
@undumped = undumped
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns module that is ready for patching existing context
|
35
|
+
#
|
36
|
+
# @return [Module]
|
37
|
+
#
|
38
|
+
def patch
|
39
|
+
undumped = self.undumped
|
40
|
+
Module.new do
|
41
|
+
define_method :_local_binding do
|
42
|
+
result = binding
|
43
|
+
|
44
|
+
undumped[:lvars].each do |lvar_name, lvar|
|
45
|
+
result.eval("#{lvar_name} = ObjectSpace._id2ref(#{lvar.object_id})")
|
46
|
+
end
|
47
|
+
|
48
|
+
mod = LocalBindingPatchBuilder.new(undumped).patch
|
49
|
+
result.extend(mod)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Class with common functionality of all dumpers
|
2
|
+
#
|
3
|
+
class BindingDumper::Dumpers::Abstract
|
4
|
+
attr_reader :dumped_ids
|
5
|
+
|
6
|
+
# @param abstract_object [Object] any object
|
7
|
+
# @param dumped_ids [Array<Fixnum>] list of object ids that are already dumped
|
8
|
+
#
|
9
|
+
def initialize(abstract_object, dumped_ids = [])
|
10
|
+
@abstract_object = abstract_object
|
11
|
+
@dumped_ids = dumped_ids
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# Returns abstract object
|
17
|
+
# Sometimes it's a Hash that represents object structure
|
18
|
+
# Sometimes it's just that object (if its' primitive)
|
19
|
+
#
|
20
|
+
# @return [Object]
|
21
|
+
#
|
22
|
+
def abstract_object
|
23
|
+
if @abstract_object.is_a?(Hash) && @abstract_object.has_key?(:_object_data)
|
24
|
+
@abstract_object[:_object_data]
|
25
|
+
else
|
26
|
+
@abstract_object
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns +true+ if +abstract_object+ should be converted
|
31
|
+
#
|
32
|
+
# @return [true, false]
|
33
|
+
#
|
34
|
+
def should_convert?
|
35
|
+
!dumped_ids.include?(abstract_object.object_id)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns true if +abstract_object+ can be dumped using Marshal.dump
|
39
|
+
#
|
40
|
+
# @return [true, false]
|
41
|
+
#
|
42
|
+
def can_be_fully_dumped?(object)
|
43
|
+
begin
|
44
|
+
Marshal.dump(object)
|
45
|
+
true
|
46
|
+
rescue TypeError, IOError
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns +true+ if undumpable object (like StringIO.new)
|
52
|
+
# can't be dumped itself, but it's blank copy can be dumped
|
53
|
+
#
|
54
|
+
# @return [true, false]
|
55
|
+
#
|
56
|
+
def can_be_dumped_as_copy?(object)
|
57
|
+
begin
|
58
|
+
copy = object.class.allocate
|
59
|
+
Marshal.dump(copy)
|
60
|
+
true
|
61
|
+
rescue TypeError, IOError
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns +true+ if object can't be marshaled
|
67
|
+
#
|
68
|
+
# @return [true, false]
|
69
|
+
#
|
70
|
+
def undumpable?(object)
|
71
|
+
!can_be_fully_dumped?(object) && !can_be_dumped_as_copy?(object)
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting arrays to marshalable Hash
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# array = [1,2,3]
|
6
|
+
# dump = BindingDumper::Dumpers::Array.new(array).convert
|
7
|
+
# # => { marshalable: :hash }
|
8
|
+
# BindingDumper::Dumpers::Array.new(dump).deconvert
|
9
|
+
# # => [1,2,3]
|
10
|
+
#
|
11
|
+
class Dumpers::ArrayDumper < Dumpers::Abstract
|
12
|
+
alias_method :array, :abstract_object
|
13
|
+
|
14
|
+
# Returns true if ArrayDumper can convert +abstract_object+
|
15
|
+
#
|
16
|
+
# @return [true, false]
|
17
|
+
#
|
18
|
+
def can_convert?
|
19
|
+
array.is_a?(Array)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if ArrayDumper can deconvert +abstract_object+
|
23
|
+
#
|
24
|
+
# @return [true, false]
|
25
|
+
#
|
26
|
+
def can_deconvert?
|
27
|
+
array.is_a?(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Converts +abstract_object+ to marshalable Hash
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
#
|
34
|
+
def convert
|
35
|
+
unless should_convert?
|
36
|
+
return { _existing_object_id: array.object_id }
|
37
|
+
end
|
38
|
+
|
39
|
+
dumped_ids << array.object_id
|
40
|
+
|
41
|
+
result = array.map do |item|
|
42
|
+
UniversalDumper.convert(item, dumped_ids)
|
43
|
+
end
|
44
|
+
|
45
|
+
{
|
46
|
+
_old_object_id: array.object_id,
|
47
|
+
_object_data: result
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Deconverts passed +abstract_object+ back to the original state
|
52
|
+
#
|
53
|
+
# @return [Array]
|
54
|
+
#
|
55
|
+
def deconvert
|
56
|
+
result = []
|
57
|
+
yield result
|
58
|
+
array.each do |converted_item|
|
59
|
+
result << UniversalDumper.deconvert(converted_item)
|
60
|
+
end
|
61
|
+
result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting classes to marshalable Hash
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# class MyClass
|
6
|
+
# @a = 1
|
7
|
+
# @@b = 2
|
8
|
+
# end
|
9
|
+
# dump = BindingDumper::Dumpers::ClassDumper.new(MyClass).convert
|
10
|
+
# # => { marshalable: :hash }
|
11
|
+
# BindingDumper::Dumpers::ClassDumper.new(MyClass).deconvert
|
12
|
+
# # => MyClass
|
13
|
+
#
|
14
|
+
class Dumpers::ClassDumper < Dumpers::Abstract
|
15
|
+
alias_method :klass, :abstract_object
|
16
|
+
|
17
|
+
# Returns +true+ if ClassDumper can convert passed +abstract_object+
|
18
|
+
#
|
19
|
+
# @return [true, false]
|
20
|
+
#
|
21
|
+
def can_convert?
|
22
|
+
klass.is_a?(Class)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns +true+ if ClassDumper can deconvert passed +abstract_object+
|
26
|
+
#
|
27
|
+
# @return [true, false]
|
28
|
+
#
|
29
|
+
def can_deconvert?
|
30
|
+
abstract_object.is_a?(Hash) &&
|
31
|
+
(abstract_object.has_key?(:_cvars) || abstract_object.has_key?(:_anonymous))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
35
|
+
#
|
36
|
+
# @return [Hash]
|
37
|
+
#
|
38
|
+
def convert
|
39
|
+
return unless should_convert?
|
40
|
+
dumped_ids << klass.object_id
|
41
|
+
|
42
|
+
if klass.name
|
43
|
+
{
|
44
|
+
_klass: klass,
|
45
|
+
_ivars: converted_ivars(dumped_ids),
|
46
|
+
_cvars: converted_cvars(dumped_ids)
|
47
|
+
}
|
48
|
+
else
|
49
|
+
{
|
50
|
+
_anonymous: true
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# Deconverts passed +abstract_object+ back to the original state
|
57
|
+
#
|
58
|
+
# @return [Class]
|
59
|
+
#
|
60
|
+
def deconvert
|
61
|
+
return Class.new if abstract_object[:_anonymous]
|
62
|
+
klass, converted_ivars, converted_cvars = abstract_object[:_klass], abstract_object[:_ivars], abstract_object[:_cvars]
|
63
|
+
|
64
|
+
converted_ivars.each do |ivar_name, converted_ivar|
|
65
|
+
ivar = UniversalDumper.deconvert(converted_ivar)
|
66
|
+
klass.instance_variable_set(ivar_name, ivar)
|
67
|
+
end
|
68
|
+
|
69
|
+
converted_cvars.each do |cvar_name, converted_cvar|
|
70
|
+
cvar = UniversalDumper.deconvert(converted_cvar)
|
71
|
+
klass.class_variable_set(cvar_name, cvar)
|
72
|
+
end
|
73
|
+
|
74
|
+
klass
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Returns converted mapping of instance variables like
|
80
|
+
# { instance variable name => instance variable value }
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
#
|
84
|
+
def converted_ivars(dumped_ids = [])
|
85
|
+
converted = klass.instance_variables.map do |ivar_name|
|
86
|
+
ivar = klass.instance_variable_get(ivar_name)
|
87
|
+
conveted_ivar = UniversalDumper.convert(ivar, dumped_ids)
|
88
|
+
[ivar_name, conveted_ivar]
|
89
|
+
end
|
90
|
+
Hash[converted]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns converted mapping of class variables like
|
94
|
+
# { class variable name => class variable vakue }
|
95
|
+
#
|
96
|
+
# @return [Hash]
|
97
|
+
#
|
98
|
+
def converted_cvars(dumped_ids = [])
|
99
|
+
converted = klass.class_variables.map do |cvar_name|
|
100
|
+
ivar = klass.class_variable_get(cvar_name)
|
101
|
+
if dumped_ids.include?(ivar.object_id)
|
102
|
+
[]
|
103
|
+
else
|
104
|
+
conveted_ivar = UniversalDumper.convert(ivar, dumped_ids)
|
105
|
+
[cvar_name, conveted_ivar]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
Hash[converted]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for restoring recurring objects
|
3
|
+
#
|
4
|
+
# If you dump an object twice you will get:
|
5
|
+
# First time: { marshalable: :hash }
|
6
|
+
# Second time: { _existing_object_id: 1234 }
|
7
|
+
#
|
8
|
+
# This _existing_object_id is actually an object_id of dumped object
|
9
|
+
# in original memory
|
10
|
+
#
|
11
|
+
# So, after deconverting the hash { _existing_object_id: ojbect_id }
|
12
|
+
# You will get an object from your current memory
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class Profile
|
16
|
+
# attr_accessor :first_name, :last_name
|
17
|
+
# end
|
18
|
+
|
19
|
+
# profile = Profile.allocate
|
20
|
+
# profile.first_name = profile # <-- so the object is recursive
|
21
|
+
# profile.last_name = StringIO.new # <-- and unmarshalable
|
22
|
+
# dump = BindingDumper::UniversalDumper.convert(profile)
|
23
|
+
# =>
|
24
|
+
# {
|
25
|
+
# :_klass => Profile,
|
26
|
+
# :_ivars => {
|
27
|
+
# :@first_name => {
|
28
|
+
# :_existing_object_id => 47687640 # <-- right here
|
29
|
+
# },
|
30
|
+
# :@last_name => {
|
31
|
+
# :_klass => StringIO,
|
32
|
+
# :_undumpable => true
|
33
|
+
# }
|
34
|
+
# },
|
35
|
+
# :_old_object_id => 47687640
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# restored = BindingDumper::UniversalDumper.deconvert(profile)
|
39
|
+
# restored.profile.equal?(restored)
|
40
|
+
# # => true # (they have the same object id)
|
41
|
+
#
|
42
|
+
class Dumpers::ExistingObjectDumper < Dumpers::Abstract
|
43
|
+
alias_method :hash, :abstract_object
|
44
|
+
|
45
|
+
# Returns false, this class is only for deconverting
|
46
|
+
#
|
47
|
+
def can_convert?
|
48
|
+
false # really, it's only for deconverting
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns true if ExistingObjectDumper can deconvert passed +abstract_object+
|
52
|
+
#
|
53
|
+
# @return [true, false]
|
54
|
+
#
|
55
|
+
def can_deconvert?
|
56
|
+
hash.is_a?(Hash) && hash.has_key?(:_existing_object_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Raises an exception, don't use this class for converting
|
60
|
+
#
|
61
|
+
# @raise [NotImplementedError]
|
62
|
+
#
|
63
|
+
def convert
|
64
|
+
raise NotImplementedError
|
65
|
+
end
|
66
|
+
|
67
|
+
# Deconverts passed +abstract_object+ back to the original state
|
68
|
+
#
|
69
|
+
# @return [Object]
|
70
|
+
#
|
71
|
+
# @raise [RuntimeError] when object doesn't exist in the memory
|
72
|
+
#
|
73
|
+
def deconvert
|
74
|
+
data_without_object_id = hash.dup.delete(:_existing_object_id)
|
75
|
+
|
76
|
+
unless UniversalDumper.memories.has_key?(existing_object_id)
|
77
|
+
raise "Object with id #{existing_object_id} wasn't dumped. Something is wrong."
|
78
|
+
end
|
79
|
+
|
80
|
+
UniversalDumper.memories[existing_object_id]
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def existing_object_id
|
86
|
+
hash[:_existing_object_id]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module BindingDumper
|
2
|
+
# Class responsible for converting arbitary hashes to marshalable hashes
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# hash = { key: 'value' }
|
6
|
+
# dump = BindingDumper::Dumpers::HashDumper.new(hash).convert
|
7
|
+
# BindingDumper::Dumpers::HashDumper.new(dump).deconvert
|
8
|
+
# # => { key: 'value' }
|
9
|
+
#
|
10
|
+
class Dumpers::HashDumper < Dumpers::Abstract
|
11
|
+
alias_method :hash, :abstract_object
|
12
|
+
|
13
|
+
# Returns true if HashDumper can convert passed +abstract_object+
|
14
|
+
#
|
15
|
+
# @return [true, false]
|
16
|
+
#
|
17
|
+
def can_convert?
|
18
|
+
hash.is_a?(Hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns true if HashDumper can deconvert passed +abstract_object+
|
22
|
+
#
|
23
|
+
# @return [true, false]
|
24
|
+
#
|
25
|
+
def can_deconvert?
|
26
|
+
abstract_object.is_a?(Hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Converts passed +abstract_object+ to marshalable Hash
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
#
|
33
|
+
def convert
|
34
|
+
unless should_convert?
|
35
|
+
return { _existing_object_id: hash.object_id }
|
36
|
+
end
|
37
|
+
|
38
|
+
dumped_ids << hash.object_id
|
39
|
+
|
40
|
+
prepared = hash.map do |k, v|
|
41
|
+
converted_k = UniversalDumper.convert(k, dumped_ids)
|
42
|
+
converted_v = UniversalDumper.convert(v, dumped_ids)
|
43
|
+
[converted_k, converted_v]
|
44
|
+
end
|
45
|
+
|
46
|
+
result = Hash[prepared]
|
47
|
+
|
48
|
+
{
|
49
|
+
_old_object_id: hash.object_id,
|
50
|
+
_object_data: result
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deconverts passed +abstract_object+ back to the original state
|
55
|
+
#
|
56
|
+
# @return [Hash]
|
57
|
+
#
|
58
|
+
def deconvert
|
59
|
+
result = {}
|
60
|
+
yield result
|
61
|
+
hash.each do |converted_k, converted_v|
|
62
|
+
k = UniversalDumper.deconvert(converted_k)
|
63
|
+
v = UniversalDumper.deconvert(converted_v)
|
64
|
+
result[k] = v
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|