From 88c07c7af1d1edf3783a41112690ae3a691892f0 Mon Sep 17 00:00:00 2001 From: samuelarogbonlo Date: Thu, 28 Nov 2024 16:13:52 +0100 Subject: [PATCH] fix: handle single-byte strings in RLP encoding/decoding --- lib/eth/rlp/decoder.rb | 36 +++++++++++++++++++++++++----------- lib/eth/rlp/encoder.rb | 37 +++++++++++++++++++++++++------------ lib/eth/util.rb | 22 +++++++++------------- spec/eth/rlp_spec.rb | 9 +++++++++ 4 files changed, 68 insertions(+), 36 deletions(-) diff --git a/lib/eth/rlp/decoder.rb b/lib/eth/rlp/decoder.rb index 0f82bc2a..079e48c1 100644 --- a/lib/eth/rlp/decoder.rb +++ b/lib/eth/rlp/decoder.rb @@ -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 @@ -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 @@ -111,4 +125,4 @@ def consume_payload(rlp, start, type, length) end end end -end + end \ No newline at end of file diff --git a/lib/eth/rlp/encoder.rb b/lib/eth/rlp/encoder.rb index 24fe3b97..e167a1fe 100644 --- a/lib/eth/rlp/encoder.rb +++ b/lib/eth/rlp/encoder.rb @@ -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 @@ -43,22 +45,33 @@ 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. @@ -66,7 +79,7 @@ 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 @@ -75,4 +88,4 @@ def length_prefix(length, offset) end end end -end +end \ No newline at end of file diff --git a/lib/eth/util.rb b/lib/eth/util.rb index 0eeef457..eb5b1abf 100644 --- a/lib/eth/util.rb +++ b/lib/eth/util.rb @@ -47,23 +47,19 @@ 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 - [hex].pack("H*") + return hex unless hex?(hex) + hex = hex.gsub(/\A0x/, '') + [hex].pack('H*') end - + # Prefixes a hexa-decimal string with `0x`. # # @param hex [String] a hex-string to be prefixed. diff --git a/spec/eth/rlp_spec.rb b/spec/eth/rlp_spec.rb index ea8293a8..5f143961 100644 --- a/spec/eth/rlp_spec.rb +++ b/spec/eth/rlp_spec.rb @@ -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