lib/eth/rlp/decoder.rb

Summary

Maintainability
A
1 hr
Test Coverage
# Copyright (c) 2016-2023 The Ruby-Eth Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# -*- encoding : ascii-8bit -*-

# Provides the {Eth} module.
module Eth

  # Provides an recursive-length prefix (RLP) encoder and decoder.
  module Rlp

    # Provides an RLP-decoder.
    module Decoder
      extend self

      # Decodes an RLP-encoded object.
      #
      # @param rlp [String] an RLP-encoded object.
      # @return [Object] the decoded and maybe deserialized object.
      # @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
        begin
          item, next_start = consume_item rlp, 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
      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
      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
          raise DecodingError, "Encoded as short string although single byte was possible" if (b0 - Constant::PRIMITIVE_PREFIX_OFFSET == 1) && rlp[start + 1].ord < Constant::PRIMITIVE_PREFIX_OFFSET

          # short string
          [:str, b0 - Constant::PRIMITIVE_PREFIX_OFFSET, start + 1]
        elsif b0 < Constant::LIST_PREFIX_OFFSET
          enforce_no_zero_bytes rlp, start

          # long string
          ll = b0 - Constant::PRIMITIVE_PREFIX_OFFSET - Constant::SHORT_LENGTH_LIMIT + 1
          l = Util.big_endian_to_int rlp[(start + 1)...(start + 1 + ll)]
          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
          enforce_no_zero_bytes rlp, start

          # long list
          ll = b0 - Constant::LIST_PREFIX_OFFSET - Constant::SHORT_LENGTH_LIMIT + 1
          l = Util.big_endian_to_int rlp[(start + 1)...(start + 1 + ll)]
          raise DecodingError, "Long list prefix used for short list" if l < Constant::SHORT_LENGTH_LIMIT
          [:list, l, start + 1 + ll]
        end
      end

      # Enforce RLP slices to not start with empty bytes.
      def enforce_no_zero_bytes(rlp, start)
        raise DecodingError, "Length starts with zero bytes" if rlp.slice(start + 1) == Constant::BYTE_ZERO
      end

      # Consume an RLP payload at the given position of given type and size.
      def consume_payload(rlp, start, type, length)
        case type
        when :str
          [rlp[start...(start + length)], start + length]
        when :list
          items = []
          next_item_start = start
          payload_end = next_item_start + length
          while next_item_start < payload_end
            item, next_item_start = consume_item rlp, next_item_start
            items.push item
          end
          raise DecodingError, "List length prefix announced a too small length" if next_item_start > payload_end
          [items, next_item_start]
        else
          raise TypeError, "Type must be either :str or :list"
        end
      end
    end
  end
end