rlp 0.3.0 → 0.5.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 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