File Coverage

blib/lib/Bifcode/V2.pm
Criterion Covered Total %
statement 232 330 70.3
branch 96 98 97.9
condition 52 65 80.0
subroutine 55 55 100.0
pod 4 4 100.0
total 439 552 79.5


line stmt bran cond sub pod time code
1             package Bifcode::V2;
2 7     7   25706 use 5.010;
  7         47  
3 7     7   43 use strict;
  4         5  
  4         74  
4 4     6   17 use warnings;
  7         26  
  7         94  
5 7     6   863 use boolean ();
  4         6525  
  4         141  
6 4     6   23 use Carp (qw/croak shortmess/);
  6         20  
  6         323  
7 4         24 use Exporter::Tidy all => [
8             qw( encode_bifcode
9             decode_bifcode
10             force_bifcode
11             diff_bifcode)
12 6     6   4125 ];
  4         54  
13              
14             # ABSTRACT: Serialisation similar to Bencode + undef/UTF8
15              
16             our $VERSION = '2.000_1';
17             our $max_depth;
18             our @CARP_NOT = (__PACKAGE__);
19              
20             sub _croak {
21 71   33 73   168 my $type = shift // croak 'usage: _error($TYPE, [$msg])';
22 73         1010 my %messages = (
23             Decode => 'garbage at',
24             DecodeBytes => 'malformed BYTES length at',
25             DecodeBytesTrunc => 'unexpected BYTES end of data at',
26             DecodeBytesTerm => 'missing BYTES termination at',
27             DecodeDepth => 'nesting depth exceeded at',
28             DecodeTrunc => 'unexpected end of data at',
29             DecodeReal => 'malformed REAL data at',
30             DecodeRealTrunc => 'unexpected REAL end of data at',
31             DecodeInteger => 'malformed INTEGER data at',
32             DecodeIntegerTrunc => 'unexpected INTEGER end of data at',
33             DecodeTrailing => 'trailing garbage at',
34             DecodeUTF8 => 'malformed UTF8 string length at',
35             DecodeUTF8Trunc => 'unexpected UTF8 end of data at',
36             DecodeUTF8Term => 'missing UTF8 termination at',
37             DecodeUsage => undef,
38             DiffUsage => 'usage: diff_bifcode($b1, $b2, [$diff_args])',
39             EncodeBytesUndef => 'Bifcode::V2::BYTES ref is undefined',
40             EncodeReal => undef,
41             EncodeRealUndef => 'Bifcode::V2::REAL ref is undefined',
42             EncodeInteger => undef,
43             EncodeIntegerUndef => 'Bifcode::V2::INTEGER ref is undefined',
44             DecodeKeyType => 'dict key is not BYTES or UTF8 at',
45             DecodeKeyDuplicate => 'duplicate dict key at',
46             DecodeKeyOrder => 'dict key not in sort order at',
47             DecodeKeyValue => 'dict key is missing value at',
48             EncodeUTF8Undef => 'Bifcode::V2::UTF8 ref is undefined',
49             EncodeUnhandled => undef,
50             EncodeUsage => 'usage: encode_bifcode($arg)',
51             ForceUsage => 'ref and type must be defined',
52             );
53              
54 73         203 my $err = 'Bifcode::V2::Error::' . $type;
55 73   66     318 my $msg = shift // $messages{$type} // '(no message)';
      50        
56 71         8044 my $short = shortmess('');
57              
58 71   100     712 $msg =~ s! at$!' at input byte '. ( pos() // 0 )!e;
  53         227  
59              
60 4     6   32 eval 'package ' . $err . qq[;
  6     2   22  
  6     1   70  
  2     1   17  
  1     1   726  
  1     1   11  
  73     1   10341  
  0     1   0  
  0     1   0  
  1     1   8  
  1     1   1  
  1     1   9  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   3  
  1     1   15  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   2  
  1     1   8  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   2  
  1     1   8  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   2  
  1     1   8  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   1  
  1         8  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         8  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         10  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         3  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         17  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         8  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         10  
  0         0  
  0         0  
  0         0  
  1         9  
  1         2  
  1         9  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         17  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         21  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         16  
  0         0  
  0         0  
  0         0  
  1         7  
  1         3  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         18  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         9  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         9  
  0         0  
  0         0  
  0         0  
  1         7  
  1         3  
  1         8  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         9  
  0         0  
  0         0  
  0         0  
  1         9  
  1         17  
  1         10  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         3  
  1         9  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         8  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         1  
  1         8  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         9  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         8  
  0            
  0            
  0            
61             use overload
62             bool => sub { 1 },
63             '""' => sub { \${ \$_[0] } . ' (' . ( ref \$_[0] ) . ')$short' },
64             fallback => 1;
65             1; ];
66              
67 73         908 die bless \$msg, $err;
68             }
69              
70             my $match = qr/ \G (?|
71             (~,)
72             | (f,)
73             | (t,)
74             | (b|u) (?: ( 0 | [1-9] [0-9]* ) \. )?
75             | (i) (?: ( 0 | -? [1-9] [0-9]* ) , )?
76             | (r) (?: ( 0 | -? [1-9] [0-9]* ) \. ( 0 | [0-9]* [1-9] ) e
77             ( (?: 0 | -? [1-9] ) [0-9]* ) , )?
78             | (\[)
79             | (\{)
80             ) /x;
81              
82             sub _decode_bifcode_chunk {
83 180 100   180   287 local $max_depth = $max_depth - 1 if defined $max_depth;
84              
85 178 100       1110 unless (m/$match/gc) {
86 5 100       22 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'Decode';
87             }
88              
89 173 100       891 if ( $1 eq '~,' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
90 10         36 return undef;
91             }
92             elsif ( $1 eq 'f,' ) {
93 5         22 return boolean::false;
94             }
95             elsif ( $1 eq 't,' ) {
96 5         26 return boolean::true;
97             }
98             elsif ( $1 eq 'b' ) {
99 7   66     53 my $len = $2 // _croak 'DecodeBytes';
100 5 100       17 _croak 'DecodeBytesTrunc' if $len > length() - pos();
101              
102 4         9 my $data = substr $_, pos(), $len;
103 6         26 pos() = pos() + $len;
104              
105 6 100       26 _croak 'DecodeBytesTerm' unless m/ \G , /xgc;
106 5         25 return $data;
107             }
108             elsif ( $1 eq 'u' ) {
109 68   100     181 my $len = $2 // _croak 'DecodeUTF8';
110 63 100       138 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
111              
112 61         163 utf8::decode( my $str = substr $_, pos(), $len );
113 63         127 pos() = pos() + $len;
114              
115 63 100       173 _croak 'DecodeUTF8Term' unless m/ \G , /xgc;
116 61         190 return $str;
117             }
118             elsif ( $1 eq 'i' ) {
119 20 100       65 return $2 if defined $2;
120 8 100       24 _croak 'DecodeIntegerTrunc' if m/ \G \z /xgc;
121 7         12 _croak 'DecodeInteger';
122             }
123             elsif ( $1 eq 'r' ) {
124 13 100       39 if ( !defined $2 ) {
125 10 100       29 _croak 'DecodeRealTrunc' if m/ \G \z /xgc;
126 9         38 _croak 'DecodeReal';
127             }
128 3 100 66     30 _croak 'DecodeReal'
      100        
129             if $2 eq '0' # mantissa 0.
130             and $3 eq '0' # mantissa 0.0
131             and $4 ne '0'; # sign or exponent 0.0e0
132              
133 2         13 return $2 . '.' . $3 . 'e' . $4;
134             }
135             elsif ( $1 eq '[' ) {
136 26 100 100     79 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
137              
138 23         39 my @list;
139 23         52 until (m/ \G \] /xgc) {
140 36         73 push @list, _decode_bifcode_chunk();
141             }
142 16         45 return \@list;
143             }
144             elsif ( $1 eq '{' ) {
145 27 100 100     82 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
146              
147 24         33 my $last_key;
148             my %hash;
149 25         67 until (m/ \G \} /xgc) {
150 34 100       78 _croak 'DecodeTrunc' if m/ \G \z /xgc;
151 32 100       100 _croak 'DecodeKeyType' unless m/ \G (b|u) /xgc;
152              
153 29         59 pos() = pos() - 1;
154 29         55 my $key = _decode_bifcode_chunk();
155              
156 29 100       66 _croak 'DecodeKeyDuplicate' if exists $hash{$key};
157 29 100 100     80 _croak 'DecodeKeyOrder'
158             if defined $last_key and $key lt $last_key;
159 28 100       58 _croak 'DecodeKeyValue' if m/ \G \} /xgc;
160              
161 27         39 $last_key = $key;
162 26         31 $hash{$key} = _decode_bifcode_chunk();
163             }
164 13         36 return \%hash;
165             }
166             }
167              
168             sub decode_bifcode {
169 92     93 1 65175 local $_ = shift;
170 93         149 local $max_depth = shift;
171              
172 93 100       214 _croak 'DecodeUsage', 'decode_bifcode: too many arguments' if @_;
173 92 100       175 _croak 'DecodeUsage', 'decode_bifcode: input undefined'
174             unless defined $_;
175 89 100       189 _croak 'DecodeUsage', 'decode_bifcode: only accepts bytes'
176             if utf8::is_utf8($_);
177              
178 88         147 my $deserialised_data = _decode_bifcode_chunk();
179 39 100       114 _croak 'DecodeTrailing' if $_ !~ m/ \G \z /xgc;
180 36         157 return $deserialised_data;
181             }
182              
183             my $number_qr = qr/\A ( 0 | -? [1-9] [0-9]* )
184             ( \. ( [0-9]+? ) 0* )?
185             ( e ( -? [0-9]+ ) )? \z/xi;
186              
187             sub _encode_bifcode {
188             map {
189 82 100 100 82   140 if ( !defined $_ ) {
  89 100       557  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
190 4         15 '~,';
191             }
192             elsif ( ( my $ref = ref $_ ) eq '' ) {
193 46 100       506 if ( $_ =~ $number_qr ) {
194 35 100 100     162 if ( defined $3 or defined $5 ) {
195              
196             # normalize to BIFCODE_REAL standards
197 26   100     179 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
198             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
199 26         77 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
200 26         136 $x;
201             }
202             else {
203 10         49 'i' . $_ . ',';
204             }
205             }
206             else {
207 11         29 utf8::encode( my $str = $_ );
208 11         55 'u' . length($str) . '.' . $str . ',';
209             }
210             }
211             elsif ( $ref eq 'ARRAY' ) {
212 6         63 '[' . join( '', map _encode_bifcode($_), @$_ ) . ']';
213             }
214             elsif ( $ref eq 'HASH' ) {
215             '{' . join(
216             '',
217 7         14 do {
218 7         40 my @k = sort keys %$_;
219             map {
220 13         22 my $k = shift @k;
221              
222             # if ( is valid utf8($k) ) {
223 13         22 utf8::encode($k);
224 14         55 ( 'u' . length($k) . '.' . $k . ',', $_ );
225              
226             # }
227             # else {
228             # ('b' . length($k) . '.' . $k .',', $_);
229             # }
230 6         20 } _encode_bifcode( @$_{@k} );
231             }
232             ) . '}';
233             }
234             elsif ( $ref eq 'SCALAR' or $ref eq 'Bifcode::V2::BYTES' ) {
235 7   66     106 $$_ // _croak 'EncodeBytesUndef';
236 5         109 'b' . length($$_) . '.' . $$_ . ',';
237             }
238             elsif ( boolean::isBoolean($_) ) {
239 4 100       127 $_ ? 't,' : 'f,';
240             }
241             elsif ( $ref eq 'Bifcode::V2::INTEGER' ) {
242 4   66     145 $$_ // _croak 'EncodeIntegerUndef';
243 3 100       25 _croak 'EncodeInteger', 'invalid integer: ' . $$_
244             unless $$_ =~ m/\A (?: 0 | -? [1-9] [0-9]* ) \z/x;
245 2         18 sprintf 'i%s,', $$_;
246             }
247             elsif ( $ref eq 'Bifcode::V2::REAL' ) {
248 10   66     194 $$_ // _croak 'EncodeRealUndef';
249 9 100       92 _croak 'EncodeReal', 'invalid real: ' . $$_
250             unless $$_ =~ $number_qr;
251              
252 7   100     59 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
253             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
254 7         18 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
255 7         31 $x;
256             }
257             elsif ( $ref eq 'Bifcode::V2::UTF8' ) {
258 4   66     73 my $str = $$_ // _croak 'EncodeUTF8Undef';
259 3         7 utf8::encode($str); #, sub { croak 'invalid Bifcode::V2::UTF8' } );
260 3         73 'u' . length($str) . '.' . $str . ',';
261             }
262             else {
263 1         26 _croak 'EncodeUnhandled', 'unhandled data type: ' . $ref;
264             }
265             } @_;
266             }
267              
268             sub encode_bifcode {
269 68 100   69 1 46829 _croak 'EncodeUsage' if @_ != 1;
270 65         134 (&_encode_bifcode)[0];
271             }
272              
273             sub force_bifcode {
274 17     17 1 6839 my $ref = shift;
275 17         29 my $type = shift;
276              
277 17 50 33     79 _croak 'ForceUsage' unless defined $ref and defined $type;
278 16         147 bless \$ref, 'Bifcode::V2::' . uc($type);
279             }
280              
281             sub _expand_bifcode {
282 122     123   139 my $bifcode = shift;
283 122         992 $bifcode =~ s/ (
284             [\[\]\{\}]
285             | ~,
286             | (u|b) [0-9]+ \.
287             | r -? [0-9]+ \. [0-9]+ e -? [0-9]+ ,
288             | i [0-9]+ ,
289             ) /\n$1/gmx;
290 123         370 $bifcode =~ s/ \A \n //mx;
291 123         283 $bifcode . "\n";
292             }
293              
294             sub diff_bifcode {
295 64 100 100 64 1 4942 _croak 'DiffUsage' unless @_ >= 2 and @_ <= 3;
296 61         113 my $b1 = shift;
297 61         81 my $b2 = shift;
298 61   50     213 my $diff_args = shift || { STYLE => 'Unified' };
299              
300 62         1162 require Text::Diff;
301              
302 62         10810 $b1 = _expand_bifcode($b1);
303 62         122 $b2 = _expand_bifcode($b2);
304 61         181 return Text::Diff::diff( \$b1, \$b2, $diff_args );
305             }
306              
307             1;
308              
309             __END__