File Coverage

blib/lib/Blockchain/Ethereum/RLP.pm
Criterion Covered Total %
statement 90 92 97.8
branch 32 38 84.2
condition 17 24 70.8
subroutine 10 10 100.0
pod 2 2 100.0
total 151 166 90.9


line stmt bran cond sub pod time code
1 3     3   316898 use v5.26;
  3         41  
2 3     3   1885 use Object::Pad;
  3         34662  
  3         18  
3             # ABSTRACT: Ethereum RLP encoding/decoding utility
4              
5             package Blockchain::Ethereum::RLP;
6             class Blockchain::Ethereum::RLP;
7              
8             our $AUTHORITY = 'cpan:REFECO'; # AUTHORITY
9             our $VERSION = '0.008'; # VERSION
10              
11 3     3   1424 use Carp;
  3         6  
  3         291  
12              
13             use constant {
14 3         7350 STRING => 'string',
15             LIST => 'list',
16             SINGLE_BYTE_MAX_LENGTH => 128,
17             SHORT_STRING_MAX_LENGTH => 183,
18             LONG_STRING_MAX_LENGTH => 192,
19             LIST_MAX_LENGTH => 247,
20             LONG_LIST_MAX_LENGTH => 255,
21             BYTE_LENGTH_DELIMITER => 55,
22             INPUT_LENGTH_DELIMITER => 256,
23 3     3   24 };
  3         6  
24              
25 65     65 1 10802 method encode ($input) {
  65         94  
  65         98  
  65         83  
26              
27 65 50       129 croak 'No input given' unless defined $input;
28              
29 65 100       145 if (ref $input eq 'ARRAY') {
30 16         30 my $output = '';
31 16         55 $output .= $self->encode($_) for $input->@*;
32              
33 16         40 return $self->_encode_length(length($output), LONG_STRING_MAX_LENGTH) . $output;
34             }
35              
36 49         162 my $hex = $input =~ s/^0x//r;
37              
38             # zero will be considered empty as per RLP specification
39 49 100       102 unless ($hex) {
40 11         23 $hex = chr(0x80);
41 11         34 return $hex;
42             }
43              
44             # pack will add a null character at the end if the length is odd
45             # RLP expects this to be added at the left instead.
46 38 100       94 $hex = "0$hex" if length($hex) % 2 != 0;
47 38         113 $hex = pack("H*", $hex);
48              
49 38         58 my $input_length = length $hex;
50              
51 38 100 100     110 return $hex if $input_length == 1 && ord $hex < SINGLE_BYTE_MAX_LENGTH;
52 31         93 return $self->_encode_length($input_length, SINGLE_BYTE_MAX_LENGTH) . $hex;
53             }
54              
55 47     47   80 method _encode_length ($length, $offset) {
  47         92  
  47         65  
  47         56  
  47         66  
56              
57 47 100       246 return chr($length + $offset) if $length <= BYTE_LENGTH_DELIMITER;
58              
59 5 50       39 if ($length < INPUT_LENGTH_DELIMITER**8) {
60 5         17 my $bl = $self->_to_binary($length);
61 5         45 return chr(length($bl) + $offset + BYTE_LENGTH_DELIMITER) . $bl;
62             }
63              
64 0         0 croak "Input too long";
65             }
66              
67 10     10   34 method _to_binary ($x) {
  10         16  
  10         16  
  10         15  
68              
69 10 100       39 return '' unless $x;
70 5         29 return $self->_to_binary(int($x / INPUT_LENGTH_DELIMITER)) . chr($x % INPUT_LENGTH_DELIMITER);
71             }
72              
73 65     65 1 15331 method decode ($input) {
  65         98  
  65         120  
  65         92  
74              
75 65 50       129 return [] unless length $input;
76              
77 65         116 my ($offset, $data_length, $type) = $self->_decode_length($input);
78              
79             # string
80 65 100       149 if ($type eq STRING) {
81 49         145 my $hex = unpack("H*", substr($input, $offset, $data_length));
82             # same as for the encoding we do expect an prefixed 0 for
83             # odd length hexadecimal values, this just removes the 0 prefix.
84 49 100 66     191 $hex = substr($hex, 1) if $hex =~ /^0/ && (length($hex) - 1) % 2 != 0;
85 49         147 return '0x' . $hex;
86             }
87              
88             # list
89 16         23 my @output;
90 16         37 my $list_data = substr($input, $offset, $data_length);
91 16         23 my $list_offset = 0;
92             # recursive arrays
93 16         57 while ($list_offset < length($list_data)) {
94 51         124 my ($item_offset, $item_length, $item_type) = $self->_decode_length(substr($list_data, $list_offset));
95 51         184 my $list_item = $self->decode(substr($list_data, $list_offset, $item_offset + $item_length));
96 51         115 push @output, $list_item;
97 51         109 $list_offset += $item_offset + $item_length;
98             }
99              
100 16         57 return \@output;
101             }
102              
103 116     116   203 method _decode_length ($input) {
  116         159  
  116         203  
  116         147  
104              
105 116         165 my $length = length($input);
106 116 50       197 croak "Invalid empty input" unless $length;
107              
108 116         197 my $prefix = ord(substr($input, 0, 1));
109              
110 116         160 my $short_string = $prefix - SINGLE_BYTE_MAX_LENGTH;
111 116         174 my $long_string = $prefix - SHORT_STRING_MAX_LENGTH;
112 116         150 my $list = $prefix - LONG_STRING_MAX_LENGTH;
113 116         161 my $long_list = $prefix - LIST_MAX_LENGTH;
114              
115 116 100 66     511 if ($prefix < SINGLE_BYTE_MAX_LENGTH) {
    100 100        
    100 100        
    100 66        
    50 33        
      33        
116             # single byte
117 13         47 return (0, 1, STRING);
118             } elsif ($prefix <= SHORT_STRING_MAX_LENGTH && $length > $short_string) {
119             # short string
120 75         205 return (1, $short_string, STRING);
121             } elsif ($prefix <= LONG_STRING_MAX_LENGTH
122             && $length > $long_string
123             && $length > $long_string + $self->_to_integer(substr($input, 1, $long_string)))
124             {
125             # long string
126 3         16 my $str_length = $self->_to_integer(substr($input, 1, $long_string));
127 3         16 return (1 + $long_string, $str_length, STRING);
128             } elsif ($prefix < LIST_MAX_LENGTH && $length > $list) {
129             # list
130 22         72 return (1, $list, LIST);
131             } elsif ($prefix <= LONG_LIST_MAX_LENGTH
132             && $length > $long_list
133             && $length > $long_list + $self->_to_integer(substr($input, 1, $long_list)))
134             {
135             # long list
136 3         14 my $list_length = $self->_to_integer(substr($input, 1, $long_list));
137 3         23 return (1 + $long_list, $list_length, LIST);
138             }
139              
140 0         0 croak "Invalid RLP input";
141             }
142              
143 30     30   51 method _to_integer ($b) {
  30         55  
  30         59  
  30         35  
144              
145 30         52 my $length = length($b);
146 30 50       50 croak "Invalid empty input" unless $length;
147              
148 30 100       127 return ord($b) if $length == 1;
149              
150 16         49 return ord(substr($b, -1)) + $self->_to_integer(substr($b, 0, -1)) * INPUT_LENGTH_DELIMITER;
151             }
152              
153             1;
154              
155             __END__