rlp 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3195187f64cf0bc3c2d9499028b45d66a19591f2
4
- data.tar.gz: 1d20e7ef25821a865001a5e59237418749e87aa1
3
+ metadata.gz: a740accf9d4e050d92d280aacd38699feffb6081
4
+ data.tar.gz: fdae1f3979c78452f141062f95b3254f9aac4220
5
5
  SHA512:
6
- metadata.gz: 1c1a5fcb71deda8629cdc4806499cdc050b4900af1ae65e67179e8245ce263db67f89bbf8e3fcc29bdb39c8168b688490a77591a7429d7851ae921e6f3398ce3
7
- data.tar.gz: 7bf8bc70835a72adbc41f012c21bed66fee6afdaae78a81fd95a65591b6f6615ba720612fec287e6980b053aa793f33c8c2d6a564b2c23e506aa84230210487e
6
+ metadata.gz: 5b18bb3dd56837f948f44e7797f5497e3140dedfb98d50698c53470a351db879bacce83b4861c0d1d638f2ade563bcc2450ca60cca8fc372459122a0a45a5c3d
7
+ data.tar.gz: b143e3ef2289684e754c74406581126c8734845c274f04e8a952e17e65147eabdfde80e8de7e4f8fc801f916dcef024a4ba68597a03c37586f4c158e50a6fb61
data/README.md CHANGED
@@ -1,3 +1,23 @@
1
1
  # ruby-rlp
2
2
 
3
3
  A Ruby implementation of Ethereum's Recursive Length Prefix encoding (RLP). You can find the specification of the standard in the [Ethereum wiki](https://github.com/ethereum/wiki/wiki/RLP).
4
+
5
+ ## Benchmark
6
+
7
+ ```
8
+ ruby-rlp $ ruby -v -Ilib test/speed.py
9
+ ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-linux]
10
+ Block serializations / sec: 2318.21
11
+ Block deserializations / sec: 1704.61
12
+ TX serializations / sec: 30461.76
13
+ TX deserializations / sec: 21378.70
14
+
15
+ pyrlp $ python -V
16
+ Python 2.7.11
17
+
18
+ pyrlp $ PYTHONPATH=. python tests/speed.py
19
+ Block serializations / sec: 1225.00
20
+ Block deserializations / sec: 1162.01
21
+ TX serializations / sec: 16468.41
22
+ TX deserializations / sec: 14517.31
23
+ ```
data/lib/rlp.rb CHANGED
@@ -7,9 +7,13 @@ require 'rlp/sedes'
7
7
  require 'rlp/encode'
8
8
  require 'rlp/decode'
9
9
 
10
+ require 'rlp/decode_lazy'
11
+ require 'rlp/lazy_list'
12
+
10
13
  module RLP
11
14
  include Encode
12
15
  include Decode
16
+ include DecodeLazy
13
17
 
14
18
  extend self
15
19
  end
@@ -4,10 +4,31 @@ module RLP
4
4
  include Error
5
5
  include Utils
6
6
 
7
- def decode(rlp, options={})
8
- sedes = options.delete(:sedes)
9
- strict = options.delete(:strict) {|k| true }
10
-
7
+ ##
8
+ # Decode an RLP encoded object.
9
+ #
10
+ # If the deserialized result `obj` has an attribute `_cached_rlp` (e.g. if
11
+ # `sedes` is a subclass of {RLP::Sedes::Serializable}), it will be set to
12
+ # `rlp`, which will improve performance on subsequent {RLP::Encode#encode}
13
+ # calls. Bear in mind however that `obj` needs to make sure that this value
14
+ # is updated whenever one of its fields changes or prevent such changes
15
+ # entirely ({RLP::Sedes::Serializable} does the latter).
16
+ #
17
+ # @param sedes [#deserialize] an object implementing a function
18
+ # `deserialize(code)` which will be applied after decoding, or `nil` if
19
+ # no deserialization should be performed
20
+ # @param options [Hash] additional keyword arguments that will be passed to
21
+ # the deserializer
22
+ # @param strict [Boolean] if false inputs that are longer than necessary
23
+ # don't cause an exception
24
+ #
25
+ # @return [Object] the decoded and maybe deserialized object
26
+ #
27
+ # @raise [RLP::Error::DecodingError] if the input string does not end after
28
+ # the root item and `strict` is true
29
+ # @raise [RLP::Error::DeserializationError] if the deserialization fails
30
+ #
31
+ def decode(rlp, sedes: nil, strict: true, options: {})
11
32
  rlp = str_to_bytes(rlp)
12
33
 
13
34
  begin
@@ -19,8 +40,16 @@ module RLP
19
40
  raise DecodingError.new("RLP string ends with #{rlp.size - next_start} superfluous bytes", rlp) if next_start != rlp.size && strict
20
41
 
21
42
  if sedes
22
- obj = sedes.deserialize(item) # TODO: (options)
23
- #TODO: cache flow
43
+ # FIXME: lazy man's kwargs
44
+ obj = sedes.is_a?(Sedes::Serializable) ?
45
+ sedes.deserialize(item, **options) :
46
+ sedes.deserialize(item)
47
+
48
+ if obj.respond_to?(:_cached_rlp)
49
+ obj._cached_rlp = rlp
50
+ raise "RLP::Sedes::Serializable object must be immutable after decode" if obj.is_a?(Sedes::Serializable) && obj.mutable?
51
+ end
52
+
24
53
  obj
25
54
  else
26
55
  item
@@ -0,0 +1,103 @@
1
+ module RLP
2
+ module DecodeLazy
3
+ include Decode
4
+
5
+ ##
6
+ # Decode an RLP encoded object in a lazy fashion.
7
+ #
8
+ # If the encoded object is a byte string, this function acts similar to
9
+ # {RLP::Decode#decode}. If it is a list however, a {LazyList} is returned
10
+ # instead. This object will decode the string lazily, avoiding both
11
+ # horizontal and vertical traversing as much as possible.
12
+ #
13
+ # The way `sedes` is applied depends on the decoded object: If it is a
14
+ # string `sedes` deserializes it as a whole; if it is a list, each element
15
+ # is deserialized individually. In both cases, `sedes_options` are passed
16
+ # on. Note that, if a deserializer is used, only "horizontal" but not
17
+ # "vertical lazyness" can be preserved.
18
+ #
19
+ # @param rlp [String] the RLP string to decode
20
+ # @param sedes [Object] an object implementing a method `deserialize(code)`
21
+ # which is used as described above, or `nil` if no deserialization should
22
+ # be performed
23
+ # @param sedes_options [Hash] additional keyword arguments that will be
24
+ # passed to the deserializers
25
+ #
26
+ # @return [Object] either the already decoded and deserialized object (if
27
+ # encoded as a string) or an instance of {RLP::LazyList}
28
+ #
29
+ def decode_lazy(rlp, sedes: nil, sedes_options: {})
30
+ item, next_start = consume_item_lazy(rlp, 0)
31
+
32
+ raise DecodingError.new("RLP length prefix announced wrong length", rlp) if next_start != rlp.size
33
+
34
+ if item.instance_of?(LazyList)
35
+ item.sedes = sedes
36
+ item.sedes_options = sedes_options
37
+ item
38
+ elsif sedes
39
+ # FIXME: lazy man's kwargs
40
+ sedes_options.empty? ?
41
+ sedes.deserialize(item) :
42
+ sedes.deserialize(item, **sedes_options)
43
+ else
44
+ item
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Read an item from an RLP string lazily.
50
+ #
51
+ # If the length prefix announces a string, the string is read; if it
52
+ # announces a list, a {LazyList} is created.
53
+ #
54
+ # @param rlp [String] the rlp string to read from
55
+ # @param start [Integer] the position at which to start reading
56
+ #
57
+ # @return [Array] A pair `[item, next_start]` where `item` is the read
58
+ # string or a {LazyList} and `next_start` is the position of the first
59
+ # unprocessed byte
60
+ #
61
+ def consume_item_lazy(rlp, start)
62
+ t, l, s = consume_length_prefix(rlp, start)
63
+ if t == :str
64
+ consume_payload(rlp, s, :str, l)
65
+ elsif t == :list
66
+ [LazyList.new(rlp, s, s+l), s+l]
67
+ else
68
+ raise "Invalid item type: #{t}"
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Get a specific element from an rlp encoded nested list.
74
+ #
75
+ # This method uses {RLP::DecodeLazy#decode_lazy} and, thus, decodes only
76
+ # the necessary parts of the string.
77
+ #
78
+ # @example Usage
79
+ # rlpdata = RLP.encode([1, 2, [3, [4, 5]]])
80
+ # RLP.peek(rlpdata, 0, sedes: RLP::Sedes.big_endian_int) # => 1
81
+ # RLP.peek(rlpdata, [2, 0], sedes: RLP::Sedes.big_endian_int) # => 3
82
+ #
83
+ # @param rlp [String] the rlp string
84
+ # @param index [Integer, Array] the index of the element to peek at (can be
85
+ # a list for nested data)
86
+ # @param sedes [#deserialize] a sedes used to deserialize the peeked at
87
+ # object, or `nil` if no deserialization should be performed
88
+ #
89
+ # @raise [IndexError] if `index` is invalid (out of range or too many levels)
90
+ #
91
+ def peek(rlp, index, sedes: nil)
92
+ ll = decode_lazy(rlp)
93
+ index = Array(index)
94
+
95
+ index.each do |i|
96
+ raise IndexError, "Too many indices given" if primitive?(ll)
97
+ ll = ll.fetch(i)
98
+ end
99
+
100
+ sedes ? sedes.deserialize(ll) : ll
101
+ end
102
+ end
103
+ end
@@ -39,7 +39,9 @@ module RLP
39
39
  # @raise [RLP::SerializationError] if the serialization fails
40
40
  #
41
41
  def encode(obj, sedes: nil, infer_serializer: true, cache: false)
42
- # TODO: cache flow
42
+ return obj._cached_rlp if obj.is_a?(Sedes::Serializable) && obj._cached_rlp && sedes.nil?
43
+
44
+ really_cache = obj.is_a?(Sedes::Serializable) && sedes.nil? && cache
43
45
 
44
46
  if sedes
45
47
  item = sedes.serialize(obj)
@@ -50,6 +52,12 @@ module RLP
50
52
  end
51
53
 
52
54
  result = encode_raw(item)
55
+
56
+ if really_cache
57
+ obj._cached_rlp = result
58
+ obj.make_immutable!
59
+ end
60
+
53
61
  result
54
62
  end
55
63
 
@@ -0,0 +1,86 @@
1
+ module RLP
2
+ ##
3
+ # A RLP encoded list which decodes itself when necessary.
4
+ #
5
+ # Both indexing with positive indices and iterating are supported. Getting
6
+ # the length is possible as well but requires full horizontal encoding.
7
+ #
8
+ class LazyList
9
+ include Enumerable
10
+ include DecodeLazy
11
+
12
+ attr_accessor :sedes, :sedes_options
13
+
14
+ ##
15
+ # @param rlp [String] the rlp string in which the list is encoded
16
+ # @param start [Integer] the position of the first payload byte of the
17
+ # encoded list
18
+ # @param next_start [Integer] the position of the last payload byte of the
19
+ # encoded list
20
+ # @param sedes [Object] a sedes object which deserializes each element of the
21
+ # list, or `nil` for on deserialization
22
+ # @param sedes_options [Hash] keyword arguments which will be passed on to
23
+ # the deserializer
24
+ #
25
+ def initialize(rlp, start, next_start, sedes: nil, sedes_options: nil)
26
+ @rlp = rlp
27
+ @start = start
28
+ @next_start = next_start
29
+ @index = start
30
+ @elements = []
31
+ @size = nil
32
+ @sedes = sedes
33
+ @sedes_options = sedes_options
34
+ end
35
+
36
+ def next_item
37
+ if @index == @next_start
38
+ @size = @elements.size
39
+ raise StopIteration
40
+ elsif @index < @next_start
41
+ item, @index = consume_item_lazy @rlp, @index
42
+
43
+ if @sedes
44
+ # FIXME: lazy man's kwargs
45
+ item = @sedes_options.empty? ?
46
+ @sedes.deserialize(item) :
47
+ @sedes.deserialize(item, **@sedes_options)
48
+ end
49
+
50
+ @elements.push item
51
+ item
52
+ else
53
+ raise "Assertion failed: index cannot be larger than next start"
54
+ end
55
+ end
56
+
57
+ def each(&block)
58
+ @elements.each(&block)
59
+ loop { block.call(next_item) }
60
+ end
61
+
62
+ def [](i)
63
+ fetch(i, nil)
64
+ end
65
+
66
+ def fetch(*args)
67
+ i = args[0]
68
+
69
+ loop do
70
+ raise StopIteration if @elements.size > i
71
+ next_item
72
+ end
73
+
74
+ @elements.fetch(*args)
75
+ end
76
+
77
+ def size
78
+ unless @size
79
+ loop { next_item }
80
+ end
81
+ @size
82
+ end
83
+ alias :length :size
84
+
85
+ end
86
+ end
@@ -6,6 +6,10 @@ module RLP
6
6
  Infinity = 1.0 / 0.0
7
7
 
8
8
  class <<self
9
+ def fixed_length(l, allow_empty: false)
10
+ new(min_length: l, max_length: l, allow_empty: allow_empty)
11
+ end
12
+
9
13
  def valid_type?(obj)
10
14
  obj.instance_of?(String)
11
15
  end
@@ -76,6 +76,8 @@ module RLP
76
76
  end
77
77
  end
78
78
 
79
+ attr_accessor :_cached_rlp
80
+
79
81
  def initialize(*args)
80
82
  serializable_initialize(*args)
81
83
  end
@@ -101,27 +103,36 @@ module RLP
101
103
  raise TypeError, "Not all fields initialized" unless field_set.size == 0
102
104
  end
103
105
 
104
- def ==(other)
105
- return false unless other.class.respond_to?(:serialize)
106
- self.class.serialize(self) == other.class.serialize(other)
107
- end
108
-
109
106
  def _set_field(field, value)
110
107
  unless instance_variable_defined?(:@_mutable)
111
108
  @_mutable = true
112
109
  end
113
110
 
114
- if _mutable? || !self.class.serializable_fields.has_key?(field)
111
+ if mutable? || !self.class.serializable_fields.has_key?(field)
115
112
  instance_variable_set :"@#{field}", value
116
113
  else
117
114
  raise ArgumentError, "Tried to mutate immutable object"
118
115
  end
119
116
  end
120
117
 
121
- def _mutable?
118
+ def ==(other)
119
+ return false unless other.class.respond_to?(:serialize)
120
+ self.class.serialize(self) == other.class.serialize(other)
121
+ end
122
+
123
+ def mutable?
122
124
  @_mutable
123
125
  end
124
126
 
127
+ def make_immutable!
128
+ @_mutable = true
129
+ self.class.serializable_fields.keys.each do |field|
130
+ ::RLP::Utils.make_immutable! send(field)
131
+ end
132
+
133
+ @_mutable = false
134
+ self
135
+ end
125
136
  end
126
137
  end
127
138
  end
@@ -1,5 +1,30 @@
1
1
  module RLP
2
2
  module Utils
3
+ class <<self
4
+ ##
5
+ # Do your best to make `obj` as immutable as possible.
6
+ #
7
+ # If `obj` is a list, apply this function recursively to all elements and
8
+ # return a new list containing them. If `obj` is an instance of
9
+ # {RLP::Sedes::Serializable}, apply this function to its fields, and set
10
+ # `@_mutable` to `false`. If `obj` is neither of the above, just return
11
+ # `obj`.
12
+ #
13
+ # @return [Object] `obj` after making it immutable
14
+ #
15
+ def make_immutable!(obj)
16
+ if obj.is_a?(Sedes::Serializable)
17
+ obj.make_immutable!
18
+ elsif list?(obj)
19
+ obj.map {|e| make_immutable!(e) }
20
+ else
21
+ obj
22
+ end
23
+ end
24
+ end
25
+
26
+ extend self
27
+
3
28
  def primitive?(item)
4
29
  item.instance_of?(String)
5
30
  end
@@ -1,3 +1,3 @@
1
1
  module RLP
2
- VERSION = '0.3.0'
2
+ VERSION = '0.5.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rlp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Xie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-25 00:00:00.000000000 Z
11
+ date: 2016-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -66,8 +66,10 @@ files:
66
66
  - lib/rlp/constant.rb
67
67
  - lib/rlp/data.rb
68
68
  - lib/rlp/decode.rb
69
+ - lib/rlp/decode_lazy.rb
69
70
  - lib/rlp/encode.rb
70
71
  - lib/rlp/error.rb
72
+ - lib/rlp/lazy_list.rb
71
73
  - lib/rlp/sedes.rb
72
74
  - lib/rlp/sedes/big_endian_int.rb
73
75
  - lib/rlp/sedes/binary.rb