File Coverage

blib/lib/Bifcode/V1.pm
Criterion Covered Total %
statement 232 330 70.3
branch 94 96 97.9
condition 51 65 78.4
subroutine 55 55 100.0
pod 4 4 100.0
total 436 550 79.2


line stmt bran cond sub pod time code
1             package Bifcode::V1;
2 7     7   45918 use 5.010;
  7         22  
3 7     7   75 use strict;
  4         37  
  4         153  
4 4     6   42 use warnings;
  7         35  
  7         295  
5 7     6   1106 use boolean ();
  4         8990  
  4         175  
6 4     6   48 use Carp (qw/croak shortmess/);
  6         25  
  6         447  
7 4         32 use Exporter::Tidy all => [
8             qw( encode_bifcode
9             decode_bifcode
10             force_bifcode
11             diff_bifcode)
12 6     6   6282 ];
  4         73  
13              
14             # ABSTRACT: Serialisation similar to Bencode + undef/UTF8
15              
16             our $VERSION = 'v2.0.16';
17             our $max_depth;
18             our @CARP_NOT = (__PACKAGE__);
19              
20             sub _croak {
21 71   33 73   196 my $type = shift // croak 'usage: _error($TYPE, [$msg])';
22 73         1357 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             DecodeFloat => 'malformed FLOAT data at',
30             DecodeFloatTrunc => 'unexpected FLOAT 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::BYTES ref is undefined',
40             EncodeFloat => undef,
41             EncodeFloatUndef => 'Bifcode::FLOAT ref is undefined',
42             EncodeInteger => undef,
43             EncodeIntegerUndef => 'Bifcode::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::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         163 my $err = 'Bifcode::Error::' . $type;
55 73   66     392 my $msg = shift // $messages{$type} // '(no message)';
      50        
56 71         10495 my $short = shortmess('');
57              
58 71   100     495 $msg =~ s! at$!' at input byte '. ( pos() // 0 )!e;
  53         321  
59              
60 4     6   54 eval 'package ' . $err . qq[;
  6     2   22  
  6     1   59  
  2     1   30  
  1     1   9592  
  1     1   19  
  73     1   13877  
  0     1   0  
  0     1   0  
  1     1   8  
  1     1   2  
  1     1   23  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   8  
  1     1   2  
  1     1   9  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   8  
  1     1   2  
  1     1   11  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   2  
  1     1   11  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   7  
  1     1   3  
  1     1   11  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   8  
  1     1   2  
  1         27  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         10  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         10  
  0         0  
  0         0  
  0         0  
  1         11  
  1         2  
  1         14  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         8  
  1         2  
  1         12  
  0         0  
  0         0  
  0         0  
  1         10  
  1         3  
  1         12  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         30  
  0         0  
  0         0  
  0         0  
  1         12  
  1         2  
  1         12  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         10  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         12  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         32  
  0         0  
  0         0  
  0         0  
  1         8  
  1         4  
  1         11  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         26  
  0         0  
  0         0  
  0         0  
  1         9  
  1         3  
  1         10  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         12  
  0         0  
  0         0  
  0         0  
  1         10  
  1         2  
  1         11  
  0         0  
  0         0  
  0         0  
  1         9  
  1         2  
  1         11  
  0         0  
  0         0  
  0         0  
  1         10  
  1         2  
  1         9  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         8  
  1         3  
  1         11  
  0         0  
  0         0  
  0         0  
  1         62  
  1         3  
  1         12  
  0         0  
  0         0  
  0         0  
  1         7  
  1         3  
  1         25  
  0            
  0            
  0            
61             use overload
62             bool => sub { 1 },
63             '""' => sub { \${ \$_[0] } . ' (' . ( ref \$_[0] ) . ')$short' },
64             fallback => 1;
65             1; ];
66              
67 73         1391 die bless \$msg, $err;
68             }
69              
70             my $match = qr/ \G (?|
71             (~)
72             | (0)
73             | (1)
74             | (B|U) (?: ( 0 | [1-9] [0-9]* ) : )?
75             | (I) (?: ( 0 | -? [1-9] [0-9]* ) , )?
76             | (F) (?: ( 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 184 100   184   450 local $max_depth = $max_depth - 1 if defined $max_depth;
84              
85 182 100       1958 unless (m/$match/gc) {
86 5 100       30 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'Decode';
87             }
88              
89 177 100       1328 if ( $1 eq '~' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
90 10         46 return undef;
91             }
92             elsif ( $1 eq '0' ) {
93 5         15 return boolean::false;
94             }
95             elsif ( $1 eq '1' ) {
96 5         37 return boolean::true;
97             }
98             elsif ( $1 eq 'B' ) {
99 7   66     37 my $len = $2 // _croak 'DecodeBytes';
100 5 100       24 _croak 'DecodeBytesTrunc' if $len > length() - pos();
101              
102 4         14 my $data = substr $_, pos(), $len;
103 6         29 pos() = pos() + $len;
104              
105 6 100       25 _croak 'DecodeBytesTerm' unless m/ \G , /xgc;
106 5         32 return $data;
107             }
108             elsif ( $1 eq 'U' ) {
109 68   66     233 my $len = $2 // _croak 'DecodeUTF8';
110 63 100       185 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
111              
112 61         239 utf8::decode( my $str = substr $_, pos(), $len );
113 63         190 pos() = pos() + $len;
114              
115 63 100       269 _croak 'DecodeUTF8Term' unless m/ \G , /xgc;
116 61         316 return $str;
117             }
118             elsif ( $1 eq 'I' ) {
119 24 100       109 return $2 if defined $2;
120 8 100       36 _croak 'DecodeIntegerTrunc' if m/ \G \z /xgc;
121 7         15 _croak 'DecodeInteger';
122             }
123             elsif ( $1 eq 'F' ) {
124 13 100       49 if ( !defined $2 ) {
125 10 100       37 _croak 'DecodeFloatTrunc' if m/ \G \z /xgc;
126 9         35 _croak 'DecodeFloat';
127             }
128 3 100 66     28 _croak 'DecodeFloat'
      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     112 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
137              
138 23         44 my @list;
139 23         73 until (m/ \G \] /xgc) {
140 36         98 push @list, _decode_bifcode_chunk();
141             }
142 16         55 return \@list;
143             }
144             elsif ( $1 eq '{' ) {
145 27 100 100     109 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
146              
147 24         50 my $last_key;
148             my %hash;
149 25         96 until (m/ \G \} /xgc) {
150 34 100       122 _croak 'DecodeTrunc' if m/ \G \z /xgc;
151 32 100       174 _croak 'DecodeKeyType' unless m/ \G (B|U) /xgc;
152              
153 29         92 pos() = pos() - 1;
154 29         75 my $key = _decode_bifcode_chunk();
155              
156 29 100       81 _croak 'DecodeKeyDuplicate' if exists $hash{$key};
157 29 100 100     105 _croak 'DecodeKeyOrder'
158             if defined $last_key and $key lt $last_key;
159 28 100       108 _croak 'DecodeKeyValue' if m/ \G \} /xgc;
160              
161 27         58 $last_key = $key;
162 26         52 $hash{$key} = _decode_bifcode_chunk();
163             }
164 13         42 return \%hash;
165             }
166             }
167              
168             sub decode_bifcode {
169 96     97 1 1116152 local $_ = shift;
170 97         231 local $max_depth = shift;
171              
172 97 100       333 _croak 'DecodeUsage', 'decode_bifcode: too many arguments' if @_;
173 96 100       276 _croak 'DecodeUsage', 'decode_bifcode: input undefined'
174             unless defined $_;
175 93 100       386 _croak 'DecodeUsage', 'decode_bifcode: only accepts bytes'
176             if utf8::is_utf8($_);
177              
178 92         283 my $deserialised_data = _decode_bifcode_chunk();
179 43 100       168 _croak 'DecodeTrailing' if $_ !~ m/ \G \z /xgc;
180 40         305 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 154 100 100 154   188 if ( !defined $_ ) {
  168 100       516  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
190 8         25 '~';
191             }
192             elsif ( ( my $ref = ref $_ ) eq '' ) {
193 92 100       572 if ( $_ =~ $number_qr ) {
194 70 100 100     217 if ( defined $3 or defined $5 ) {
195              
196             # normalize to BIFCODE_FLOAT standards
197 51   100     230 my $x = 'F' . ( 0 + $1 ) # remove leading zeros
      100        
198             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
199 51         105 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
200 51         175 $x;
201             }
202             else {
203 20         59 'I' . $_ . ',';
204             }
205             }
206             else {
207 22         54 utf8::encode( my $str = $_ );
208 22         77 'U' . length($str) . ':' . $str . ',';
209             }
210             }
211             elsif ( $ref eq 'ARRAY' ) {
212 11         39 '[' . join( '', map _encode_bifcode($_), @$_ ) . ']';
213             }
214             elsif ( $ref eq 'HASH' ) {
215             '{' . join(
216             '',
217 13         17 do {
218 13         47 my @k = sort keys %$_;
219             map {
220 26         28 my $k = shift @k;
221              
222             # if ( is valid utf8($k) ) {
223 26         44 utf8::encode($k);
224 27         76 ( 'U' . length($k) . ':' . $k . ',', $_ );
225              
226             # }
227             # else {
228             # ('B' . length($k) . ':' . $k .',', $_);
229             # }
230 12         48 } _encode_bifcode( @$_{@k} );
231             }
232             ) . '}';
233             }
234             elsif ( $ref eq 'SCALAR' or $ref eq 'Bifcode::BYTES' ) {
235 11   66     27 $$_ // _croak 'EncodeBytesUndef';
236 9         48 'B' . length($$_) . ':' . $$_ . ',';
237             }
238             elsif ( boolean::isBoolean($_) ) {
239 8         148 $_;
240             }
241             elsif ( $ref eq 'Bifcode::INTEGER' ) {
242 5   66     75 $$_ // _croak 'EncodeIntegerUndef';
243 4 100       34 _croak 'EncodeInteger', 'invalid integer: ' . $$_
244             unless $$_ =~ m/\A (?: 0 | -? [1-9] [0-9]* ) \z/x;
245 3         16 sprintf 'I%s,', $$_;
246             }
247             elsif ( $ref eq 'Bifcode::FLOAT' ) {
248 17   66     205 $$_ // _croak 'EncodeFloatUndef';
249 16 100       127 _croak 'EncodeFloat', 'invalid float: ' . $$_
250             unless $$_ =~ $number_qr;
251              
252 14   100     76 my $x = 'F' . ( 0 + $1 ) # remove leading zeros
      100        
253             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
254 14         37 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
255 14         56 $x;
256             }
257             elsif ( $ref eq 'Bifcode::UTF8' ) {
258 6   66     104 my $str = $$_ // _croak 'EncodeUTF8Undef';
259 5         12 utf8::encode($str); #, sub { croak 'invalid Bifcode::UTF8' } );
260 5         23 'U' . length($str) . ':' . $str . ',';
261             }
262             else {
263 1         23 _croak 'EncodeUnhandled', 'unhandled data type: ' . $ref;
264             }
265             } @_;
266             }
267              
268             sub encode_bifcode {
269 124 100   125 1 352451 _croak 'EncodeUsage' if @_ != 1;
270 121         166 (&_encode_bifcode)[0];
271             }
272              
273             sub force_bifcode {
274 17     17 1 3613 my $ref = shift;
275 17         23 my $type = shift;
276              
277 17 50 33     99 _croak 'ForceUsage' unless defined $ref and defined $type;
278 16         124 bless \$ref, 'Bifcode::' . uc($type);
279             }
280              
281             sub _expand_bifcode {
282 10     11   15 my $bifcode = shift;
283 10         151 $bifcode =~ s/ (
284             [~\[\]\{\}]
285             | (U|B) [0-9]+ :
286             | F -? [0-9]+ \. [0-9]+ e -? [0-9]+ ,
287             | I [0-9]+ ,
288             ) /\n$1/gmx;
289 11         44 $bifcode =~ s/ \A \n //mx;
290 11         34 $bifcode . "\n";
291             }
292              
293             sub diff_bifcode {
294 8 100 100 8 1 530847 _croak 'DiffUsage' unless @_ >= 2 and @_ <= 3;
295 5         11 my $b1 = shift;
296 5         9 my $b2 = shift;
297 5   50     26 my $diff_args = shift || { STYLE => 'Unified' };
298              
299 6         47 require Text::Diff;
300              
301 6         18 $b1 = _expand_bifcode($b1);
302 6         22 $b2 = _expand_bifcode($b2);
303 5         20 return Text::Diff::diff( \$b1, \$b2, $diff_args );
304             }
305              
306             decode_bifcode('I1,');
307              
308             __END__