File Coverage

blib/lib/Blockchain/Ethereum/RLP.pm
Criterion Covered Total %
statement 77 79 97.4
branch 32 38 84.2
condition 17 24 70.8
subroutine 12 12 100.0
pod 2 3 66.6
total 140 156 89.7


line stmt bran cond sub pod time code
1             package Blockchain::Ethereum::RLP;
2              
3 14     14   560821 use v5.26;
  14         52  
4 14     14   106 use strict;
  14         44  
  14         441  
5 14     14   71 use warnings;
  14         24  
  14         1367  
6              
7             # ABSTRACT: Ethereum RLP encoding/decoding utility
8             our $AUTHORITY = 'cpan:REFECO'; # AUTHORITY
9             our $VERSION = '0.021'; # VERSION
10              
11 14     14   98 use Carp;
  14         75  
  14         1848  
12              
13             use constant {
14 14         17229 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 14     14   114 };
  14         30  
24              
25             sub new {
26 16     16 0 808159 my $class = shift;
27 16         37 my $self = {};
28 16         107 return bless $self, $class;
29             }
30              
31             sub encode {
32 360     360 1 12792 my ($self, $input) = @_;
33              
34 360 50       691 croak 'No input given' unless defined $input;
35              
36 360 100       735 if (ref $input eq 'ARRAY') {
37 82         139 my $output = '';
38 82         280 $output .= $self->encode($_) for $input->@*;
39              
40 82         181 return $self->_encode_length(length($output), LONG_STRING_MAX_LENGTH) . $output;
41             }
42              
43 278         766 my $hex = $input =~ s/^0x//r;
44              
45             # zero will be considered empty as per RLP specification
46 278 100       610 unless ($hex) {
47 54         87 $hex = chr(0x80);
48 54         182 return $hex;
49             }
50              
51             # pack will add a null character at the end if the length is odd
52             # RLP expects this to be added at the left instead.
53 224 100       490 $hex = "0$hex" if length($hex) % 2 != 0;
54 224         568 $hex = pack("H*", $hex);
55              
56 224         309 my $input_length = length $hex;
57              
58 224 100 100     620 return $hex if $input_length == 1 && ord $hex < SINGLE_BYTE_MAX_LENGTH;
59 186         363 return $self->_encode_length($input_length, SINGLE_BYTE_MAX_LENGTH) . $hex;
60             }
61              
62             sub _encode_length {
63 268     268   489 my ($self, $length, $offset) = @_;
64              
65 268 100       1196 return chr($length + $offset) if $length <= BYTE_LENGTH_DELIMITER;
66              
67 40 50       110 if ($length < INPUT_LENGTH_DELIMITER**8) {
68 40         105 my $bl = $self->_to_binary($length);
69 40         338 return chr(length($bl) + $offset + BYTE_LENGTH_DELIMITER) . $bl;
70             }
71              
72 0         0 croak "Input too long";
73             }
74              
75             sub _to_binary {
76 87     87   170 my ($self, $x) = @_;
77              
78 87 100       290 return '' unless $x;
79 47         152 return $self->_to_binary(int($x / INPUT_LENGTH_DELIMITER)) . chr($x % INPUT_LENGTH_DELIMITER);
80             }
81              
82             sub decode {
83 101     101 1 19697 my ($self, $input) = @_;
84              
85 101 50       258 return [] unless length $input;
86              
87 101         232 my ($offset, $data_length, $type) = $self->_decode_length($input);
88              
89             # string
90 101 100       269 if ($type eq STRING) {
91 80         287 my $hex = unpack("H*", substr($input, $offset, $data_length));
92             # same as for the encoding we do expect an prefixed 0 for
93             # odd length hexadecimal values, this just removes the 0 prefix.
94 80 100 66     334 $hex = substr($hex, 1) if $hex =~ /^0/ && (length($hex) - 1) % 2 != 0;
95 80         310 return '0x' . $hex;
96             }
97              
98             # list
99 21         33 my @output;
100 21         55 my $list_data = substr($input, $offset, $data_length);
101 21         54 my $list_offset = 0;
102             # recursive arrays
103 21         55 while ($list_offset < length($list_data)) {
104 84         217 my ($item_offset, $item_length, $item_type) = $self->_decode_length(substr($list_data, $list_offset));
105 84         306 my $list_item = $self->decode(substr($list_data, $list_offset, $item_offset + $item_length));
106 84         210 push @output, $list_item;
107 84         227 $list_offset += $item_offset + $item_length;
108             }
109              
110 21         63 return \@output;
111             }
112              
113             sub _decode_length {
114 185     185   456 my ($self, $input) = @_;
115              
116 185         288 my $length = length($input);
117 185 50       405 croak "Invalid empty input" unless $length;
118              
119 185         362 my $prefix = ord(substr($input, 0, 1));
120              
121 185         315 my $short_string = $prefix - SINGLE_BYTE_MAX_LENGTH;
122 185         275 my $long_string = $prefix - SHORT_STRING_MAX_LENGTH;
123 185         273 my $list = $prefix - LONG_STRING_MAX_LENGTH;
124 185         298 my $long_list = $prefix - LIST_MAX_LENGTH;
125              
126 185 100 66     973 if ($prefix < SINGLE_BYTE_MAX_LENGTH) {
    100 100        
    100 100        
    100 66        
    50 33        
      33        
127             # single byte
128 23         68 return (0, 1, STRING);
129             } elsif ($prefix <= SHORT_STRING_MAX_LENGTH && $length > $short_string) {
130             # short string
131 125         404 return (1, $short_string, STRING);
132             } elsif ($prefix <= LONG_STRING_MAX_LENGTH
133             && $length > $long_string
134             && $length > $long_string + $self->_to_integer(substr($input, 1, $long_string)))
135             {
136             # long string
137 5         20 my $str_length = $self->_to_integer(substr($input, 1, $long_string));
138 5         67 return (1 + $long_string, $str_length, STRING);
139             } elsif ($prefix < LIST_MAX_LENGTH && $length > $list) {
140             # list
141 26         74 return (1, $list, LIST);
142             } elsif ($prefix <= LONG_LIST_MAX_LENGTH
143             && $length > $long_list
144             && $length > $long_list + $self->_to_integer(substr($input, 1, $long_list)))
145             {
146             # long list
147 6         22 my $list_length = $self->_to_integer(substr($input, 1, $long_list));
148 6         33 return (1 + $long_list, $list_length, LIST);
149             }
150              
151 0         0 croak "Invalid RLP input";
152             }
153              
154             sub _to_integer {
155 64     64   213 my ($self, $b) = @_;
156              
157 64         131 my $length = length($b);
158 64 50       138 croak "Invalid empty input" unless $length;
159              
160 64 100       270 return ord($b) if $length == 1;
161              
162 38         168 return ord(substr($b, -1)) + $self->_to_integer(substr($b, 0, -1)) * INPUT_LENGTH_DELIMITER;
163             }
164              
165             1;
166              
167             __END__