Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: handle single-byte RLP strings, bump to v0.5.14 #293

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions lib/eth/rlp/decoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,30 +31,45 @@ module Decoder
# @raise [Eth::Rlp::DecodingError] if the input string does not end after
# the root item.
def perform(rlp)
rlp = Util.hex_to_bin rlp if Util.hex? rlp
rlp = Util.str_to_bytes rlp
input = case rlp
when Rlp::Data
rlp
when String
# Handle hex strings
if Util.hex?(rlp)
Util.hex_to_bin(rlp)
else
rlp.dup.force_encoding(Encoding::ASCII_8BIT)
end
else
raise TypeError, "RLP input must be String or Rlp::Data"
end

begin
item, next_start = consume_item rlp, 0
item, next_start = consume_item(input, 0)
rescue Exception => e
raise DecodingError, "Cannot decode rlp string: #{e}"
end
raise DecodingError, "RLP string ends with #{rlp.size - next_start} superfluous bytes" if next_start != rlp.size
return item

# Check if we consumed the whole input
if next_start != input.bytesize
raise DecodingError, "RLP string ends with #{input.bytesize - next_start} superfluous bytes"
end

item
end

private

# Consume an RLP-encoded item from the given start.
def consume_item(rlp, start)
t, l, s = consume_length_prefix rlp, start
consume_payload rlp, s, t, l
type, length, pos = consume_length_prefix(rlp, start)
consume_payload(rlp, pos, type, length)
end

# Consume an RLP length prefix at the given position.
def consume_length_prefix(rlp, start)
b0 = rlp[start].ord
if b0 < Constant::PRIMITIVE_PREFIX_OFFSET

# single byte
[:str, 1, start]
elsif b0 < Constant::PRIMITIVE_PREFIX_OFFSET + Constant::SHORT_LENGTH_LIMIT
Expand All @@ -71,7 +86,6 @@ def consume_length_prefix(rlp, start)
raise DecodingError, "Long string prefix used for short string" if l < Constant::SHORT_LENGTH_LIMIT
[:str, l, start + 1 + ll]
elsif b0 < Constant::LIST_PREFIX_OFFSET + Constant::SHORT_LENGTH_LIMIT

# short list
[:list, b0 - Constant::LIST_PREFIX_OFFSET, start + 1]
else
Expand Down
35 changes: 24 additions & 11 deletions lib/eth/rlp/encoder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ module Encoder
# @raise [Eth::Rlp::SerializationError] if the serialization fails.
def perform(obj)
item = Sedes.infer(obj).serialize(obj)
result = encode_raw item
result = encode_raw(item)
# Ensure result is always Rlp::Data
result.is_a?(Rlp::Data) ? result : Rlp::Data.new(result.force_encoding(Encoding::ASCII_8BIT))
end

private
Expand All @@ -43,30 +45,41 @@ def encode_raw(item)
return item if item.instance_of? Rlp::Data
return encode_primitive item if Util.primitive? item
return encode_list item if Util.list? item
raise EncodingError "Cannot encode object of type #{item.class.name}"
raise EncodingError, "Cannot encode object of type #{item.class.name}"
end

# Encodes a single primitive.
# Encodes a primitive (string or number).
def encode_primitive(item)
return Util.str_to_bytes item if item.size == 1 && item.ord < Constant::PRIMITIVE_PREFIX_OFFSET
payload = Util.str_to_bytes item
prefix = length_prefix payload.size, Constant::PRIMITIVE_PREFIX_OFFSET
"#{prefix}#{payload}"
return item if item.is_a?(Rlp::Data)

bytes = case item
when String
item.dup.force_encoding(Encoding::ASCII_8BIT)
else
item.to_s.force_encoding(Encoding::ASCII_8BIT)
end

if bytes.size == 1 && bytes.getbyte(0) < Constant::PRIMITIVE_PREFIX_OFFSET
Rlp::Data.new(bytes)
else
prefix = length_prefix(bytes.size, Constant::PRIMITIVE_PREFIX_OFFSET)
Rlp::Data.new("#{prefix}#{bytes}")
end
end

# Encodes a single list.
def encode_list(list)
payload = list.map { |item| encode_raw item }.join
prefix = length_prefix payload.size, Constant::LIST_PREFIX_OFFSET
"#{prefix}#{payload}"
payload = list.map { |item| encode_raw(item) }.join
prefix = length_prefix(payload.size, Constant::LIST_PREFIX_OFFSET)
Rlp::Data.new("#{prefix}#{payload}")
end

# Determines a length prefix.
def length_prefix(length, offset)
if length < Constant::SHORT_LENGTH_LIMIT
(offset + length).chr
elsif length < Constant::LONG_LENGTH_LIMIT
length_string = Util.int_to_big_endian length
length_string = Util.int_to_big_endian(length)
length_len = (offset + Constant::SHORT_LENGTH_LIMIT - 1 + length_string.size).chr
"#{length_len}#{length_string}"
else
Expand Down
18 changes: 7 additions & 11 deletions lib/eth/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,16 @@ def keccak256(str)
# @return [String] a hexa-decimal string.
# @raise [TypeError] if value is not a string.
def bin_to_hex(bin)
raise TypeError, "Value must be an instance of String" unless bin.instance_of? String
bin.unpack("H*").first
return nil unless bin

# Handle both String and Rlp::Data
bin_str = bin.is_a?(String) ? bin : bin.to_s
bin_str.unpack("H*")[0]
end

# Packs a hexa-decimal string into a binary string. Also works with
# `0x`-prefixed strings.
#
# @param hex [String] a hexa-decimal string to be packed.
# @return [String] a packed binary string.
# @raise [TypeError] if value is not a string or string is not hex.
def hex_to_bin(hex)
raise TypeError, "Value must be an instance of String" unless hex.instance_of? String
hex = remove_hex_prefix hex
raise TypeError, "Non-hexadecimal digit found" unless hex? hex
return hex unless hex?(hex)
hex = hex.gsub(/\A0x/, "")
[hex].pack("H*")
end

Expand Down
2 changes: 1 addition & 1 deletion lib/eth/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module Eth
MINOR = 5.freeze

# Defines the patch version of the {Eth} module.
PATCH = 13.freeze
PATCH = 14.freeze

# Defines the version string of the {Eth} module.
VERSION = [MAJOR, MINOR, PATCH].join(".").freeze
Expand Down
9 changes: 9 additions & 0 deletions spec/eth/rlp_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,14 @@
expect(encoded_again).to eq Util.hex_to_bin rlp
end
end

it "properly handles single-byte strings" do
test_cases = ["a", "b", "1", "\x01"]
test_cases.each do |input|
encoded = Eth::Rlp.encode(input)
decoded = Eth::Rlp.decode(encoded)
expect(decoded).to eq(input)
end
end
end
end