File Coverage

blib/lib/Bifcode2.pm
Criterion Covered Total %
statement 255 360 70.8
branch 126 156 80.7
condition 51 78 65.3
subroutine 60 60 100.0
pod 4 6 66.6
total 496 660 75.1


line stmt bran cond sub pod time code
1             package Bifcode2;
2 5     5   67468 use 5.010;
  5         23  
3 5     5   33 use strict;
  3         3  
  3         49  
4 3     5   12 use warnings;
  5         16  
  5         70  
5 5     5   540 use boolean ();
  3         2890  
  3         69  
6 5         20 use Exporter::Tidy all => [
7             qw( encode_bifcode2
8             decode_bifcode2
9             force_bifcode2
10             diff_bifcode2)
11 3     5   2741 ];
  5         47  
12              
13             # ABSTRACT: Serialisation similar to Bencode + undef/UTF8
14              
15             our $VERSION = '2.0.0_14';
16             our $max_depth;
17             our @CARP_NOT = (__PACKAGE__);
18              
19             sub _croak {
20 75     75   356 require Carp;
21 73   33     135 my $type = shift // Carp::croak('usage: _croak($TYPE, [$msg])');
22 73         900 my %messages = (
23             Decode => 'garbage at',
24             DecodeBifcodeTerm => 'missing BIFCODE terminator at',
25             DecodeBytes => 'malformed BYTES length at',
26             DecodeBytesTrunc => 'unexpected BYTES end of data at',
27             DecodeBytesTerm => 'missing BYTES termination at',
28             DecodeDepth => 'nesting depth exceeded at',
29             DecodeTrunc => 'unexpected end of data at',
30             DecodeReal => 'malformed REAL data at',
31             DecodeRealTrunc => 'unexpected REAL end of data at',
32             DecodeInteger => 'malformed INTEGER data at',
33             DecodeIntegerTrunc => 'unexpected INTEGER end of data at',
34             DecodeTrailing => 'trailing garbage at',
35             DecodeUTF8 => 'malformed UTF8 string length at',
36             DecodeUTF8Trunc => 'unexpected UTF8 end of data at',
37             DecodeUTF8Term => 'missing UTF8 termination at',
38             DecodeUsage => undef,
39             DiffUsage => 'usage: diff_bifcode2($b1, $b2, [$diff_args])',
40             EncodeBytesUndef => 'Bifcode2::BYTES ref is undefined',
41             EncodeReal => undef,
42             EncodeRealUndef => 'Bifcode2::REAL ref is undefined',
43             EncodeInteger => undef,
44             EncodeIntegerUndef => 'Bifcode2::INTEGER ref is undefined',
45             DecodeKeyType => 'dict key is not BYTES or UTF8 at',
46             DecodeKeyDuplicate => 'duplicate dict key at',
47             DecodeKeyOrder => 'dict key not in sort order at',
48             DecodeKeyValue => 'dict key is missing value at',
49             EncodeUTF8Undef => 'Bifcode2::UTF8 ref is undefined',
50             EncodeUnhandled => undef,
51             EncodeUsage => 'usage: encode_bifcode2($arg)',
52             ForceUsage => 'ref and type must be defined',
53             );
54              
55 73         130 my $err = 'Bifcode2::Error::' . $type;
56 75   66     256 my $msg = shift // $messages{$type}
      33        
57             // Carp::croak("Bifcode::_croak($type) has no message ");
58 75         6165 my $short = Carp::shortmess('');
59              
60 75   100     563 $msg =~ s! at$!' at input byte '. ( pos() // 0 )!e;
  54         181  
61              
62 5     5   34 eval qq[
  3     2   7  
  3     1   41  
  0     1   0  
  3     1   5286  
  3     1   15  
  73     1   8804  
  1     1   2  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   3  
  1     1   7  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   1  
  1     1   16  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   10  
  1     1   1  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   1  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   2  
  1     1   7  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   5  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         28  
  0         0  
  0         0  
  0         0  
  1         5  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         5  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         8  
  0         0  
  0         0  
  0         0  
  1         5  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         6  
  0         0  
  0         0  
  0         0  
  1         8  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         10  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         17  
  0         0  
  0         0  
  0         0  
  1         5  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0            
  0            
  0            
63             package $err;
64             use overload
65             bool => sub { 1 },
66             '""' => sub { \${ \$_[0] } . ' (' . ( ref \$_[0] ) . ')$short' },
67             fallback => 1;
68             1;
69             ];
70              
71 73 50       186 die $@ if $@;
72 75         735 die bless \$msg, $err;
73             }
74              
75             my $chunk = qr/ \G (?|
76             (~,)
77             | (f,)
78             | (t,)
79             | (N,)
80             | (-,)
81             | (\+,)
82             | (B|b|u) (?: ( 0 | [1-9] [0-9]* ) \. )?
83             | (i) (?: ( 0 | -? [1-9] [0-9]* ) , )?
84             | (r) (?: ( 0 | -? [1-9] [0-9]* )
85             \. ( 0 | [0-9]* [1-9] )
86             e ( (?: 0 | -? [1-9] ) [0-9]* ) ,
87             )?
88             | (\[)
89             | (\{)
90             ) /x;
91              
92             sub _decode_bifcode2_key {
93              
94 35 100   35   98 unless (m/ \G (b|u) (?: ( 0 | [1-9] [0-9]* ) \. )? /gcx) {
95 6 50       26 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'DecodeKeyType';
96             }
97              
98 29 100       80 if ( $1 eq 'b' ) {
    50          
99 1   33     4 my $len = $2 // _croak 'DecodeBytes';
100 1 50       4 _croak 'DecodeBytesTrunc' if $len > length() - pos();
101              
102 3         18 my $data = substr $_, pos(), $len;
103 3         6 pos() = pos() + $len;
104              
105 3 50       18 _croak 'DecodeBytesTerm' unless m/ \G : /xgc;
106 1         2 return $data;
107             }
108             elsif ( $1 eq 'u' ) {
109 28   33     59 my $len = $2 // _croak 'DecodeUTF8';
110 28 50       46 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
111              
112 30         71 utf8::decode( my $str = substr $_, pos(), $len );
113 30         46 pos() = pos() + $len;
114              
115 30 50       80 _croak 'DecodeUTF8Term' unless m/ \G : /xgc;
116 28         56 return $str;
117             }
118             }
119              
120             sub _decode_bifcode2_chunk {
121 157 100   159   189 local $max_depth = $max_depth - 1 if defined $max_depth;
122              
123 157 100       865 unless (m/$chunk/gc) {
124 10 100       41 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'Decode';
125             }
126              
127 151 100       777 if ( $1 eq '~,' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
128 10         34 return undef;
129             }
130             elsif ( $1 eq 'f,' ) {
131 3         7 return boolean::false;
132             }
133             elsif ( $1 eq 't,' ) {
134 3         5 return boolean::true;
135             }
136             elsif ( $1 eq 'N,' ) {
137 1         8 require Math::BigInt;
138 3         16 return Math::BigInt->bnan;
139             }
140             elsif ( $1 eq '-,' ) {
141 3         8 require Math::BigInt;
142 3         29 return Math::BigInt->binf('-');
143             }
144             elsif ( $1 eq '+,' ) {
145 1         5 require Math::BigInt;
146 1         4 return Math::BigInt->binf('+');
147             }
148             elsif ( $1 eq 'b' ) {
149 6   66     27 my $len = $2 // _croak 'DecodeBytes';
150 5 100       22 _croak 'DecodeBytesTrunc' if $len > length() - pos();
151              
152 4         6 my $data = substr $_, pos(), $len;
153 4         13 pos() = pos() + $len;
154              
155 3 100       11 _croak 'DecodeBytesTerm' unless m/ \G , /xgc;
156 2         6 return $data;
157             }
158             elsif ( $1 eq 'u' ) {
159 40   100     91 my $len = $2 // _croak 'DecodeUTF8';
160 36 100       74 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
161              
162 34         95 utf8::decode( my $str = substr $_, pos(), $len );
163 34         62 pos() = pos() + $len;
164              
165 33 100       81 _croak 'DecodeUTF8Term' unless m/ \G , /xgc;
166 31         91 return $str;
167             }
168             elsif ( $1 eq 'i' ) {
169 20 100       59 return 0 + $2 if defined $2;
170 9 100       35 _croak 'DecodeIntegerTrunc' if m/ \G \z /xgc;
171 8         14 _croak 'DecodeInteger';
172             }
173             elsif ( $1 eq 'r' ) {
174 12 100       25 if ( !defined $2 ) {
175 8 100       21 _croak 'DecodeRealTrunc' if m/ \G \z /xgc;
176 7         11 _croak 'DecodeReal';
177             }
178 3 100 66     20 _croak 'DecodeReal'
      100        
179             if $2 eq '0' # mantissa 0.
180             and $3 eq '0' # mantissa 0.0
181             and $4 ne '0'; # sign or exponent 0.0e0
182              
183 3         17 return 0.0 + ( $2 . '.' . $3 . 'e' . $4 );
184             }
185             elsif ( $1 eq '[' ) {
186 27 100 100     67 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
187              
188 23         27 my @list;
189 22         45 until (m/ \G \] /xgc) {
190 35         53 push @list, _decode_bifcode2_chunk();
191             }
192 16         33 return \@list;
193             }
194             elsif ( $1 eq '{' ) {
195 30 100 100     78 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
196              
197 27         36 my $last_key;
198             my %hash;
199 27         62 until (m/ \G \} /xgc) {
200 35 100       66 _croak 'DecodeTrunc' if m/ \G \z /xgc;
201              
202 33         43 my $key = _decode_bifcode2_key();
203              
204 29 100       48 _croak 'DecodeKeyDuplicate' if exists $hash{$key};
205 29 100 100     64 _croak 'DecodeKeyOrder'
206             if defined $last_key and $key lt $last_key;
207 28 100       42 _croak 'DecodeKeyValue' if m/ \G \} /xgc;
208              
209 27         30 $last_key = $key;
210 26         31 $hash{$key} = _decode_bifcode2_chunk();
211             }
212 13         24 return \%hash;
213             }
214             elsif ( $1 eq 'B' ) {
215 0   0     0 my $len = $2 // _croak 'DecodeBifcode';
216 1 0       6 _croak 'DecodeBifcodeTrunc' if $len > length() - pos();
217              
218 1         1 my $res = _decode_bifcode2_chunk();
219 1 0       7 _croak 'DecodeBifcodeTerm' unless m/ \G , /xgc;
220              
221 0         0 return $res;
222             }
223             }
224              
225             sub decode_bifcode2 {
226 100     101 1 60067 local $_ = shift;
227 100         121 local $max_depth = shift;
228              
229 101 100       179 _croak 'DecodeUsage', 'decode_bifcode2: too many arguments' if @_;
230 100 100       147 _croak 'DecodeUsage', 'decode_bifcode2: input undefined'
231             unless defined $_;
232 98 100       177 _croak 'DecodeUsage', 'decode_bifcode2: only accepts bytes'
233             if utf8::is_utf8($_);
234              
235 96         136 my $deserialised_data = _decode_bifcode2_chunk();
236 42 100       173 _croak 'DecodeTrailing', " For: $_" if $_ !~ m/ \G \z /xgc;
237 38         121 return $deserialised_data;
238             }
239              
240             my $number_qr = qr/\A ( 0 | -? [1-9] [0-9]* )
241             ( \. ( [0-9]+? ) 0* )?
242             ( e ( -? [0-9]+ ) )? \z/xi;
243              
244             sub _encode_bifcode2 {
245             map {
246 94 100 100 94   127 if ( !defined $_ ) {
  101 100       360  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
247 5         26 '~' . ',';
248             }
249             elsif ( ( my $ref = ref $_ ) eq '' ) {
250 48 100       376 if ( $_ =~ $number_qr ) {
    100          
    50          
251 36 100 100     116 if ( defined $3 or defined $5 ) {
252              
253             # normalize to BIFCODE_REAL standards
254 26   100     144 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
255             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
256 27         65 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
257 27         80 $x;
258             }
259             else {
260 11         44 'i' . $_ . ',';
261             }
262             }
263             elsif ( utf8::is_utf8($_) ) {
264 3         13 utf8::encode( my $str = $_ );
265 3         25 'u' . length($str) . '.' . $str . ',';
266             }
267             elsif ( $_ =~ m/^[\x{20}-\x{7E}]*$/ ) {
268 9         38 'u' . length($_) . '.' . $_ . ',';
269             }
270             else {
271 1         6 'b' . length($_) . '.' . $_ . ',';
272             }
273             }
274             elsif ( $ref eq 'ARRAY' ) {
275 6         23 '[' . join( '', map _encode_bifcode2($_), @$_ ) . ']';
276             }
277             elsif ( $ref eq 'HASH' ) {
278             '{' . join(
279             '',
280 8         15 do {
281 7         17 my $k;
282 7         31 my @k = sort keys %$_;
283              
284             map {
285 15         24 $k = shift @k;
286              
287 15 100       33 if ( $k =~ m/^[\x{20}-\x{7E}]*$/ ) {
    50          
288 14         45 ( 'u' . length($k) . '.' . $k . ':', $_ );
289             }
290             elsif ( utf8::is_utf8($k) ) {
291 1         3 utf8::encode($k);
292 1         8 ( 'u' . length($k) . '.' . $k . ':', $_ );
293             }
294             else {
295 0         0 ( 'b' . length($k) . '.' . $k . ':', $_ );
296             }
297 7         15 } _encode_bifcode2( @$_{@k} );
298             }
299             ) . '}';
300             }
301             elsif ( $ref eq 'SCALAR' or $ref eq 'Bifcode2::BYTES' ) {
302 7   66     47 $$_ // _croak 'EncodeBytesUndef';
303 5         19 'b' . length($$_) . '.' . $$_ . ',';
304             }
305             elsif ( boolean::isBoolean($_) ) {
306 5 100       123 ( $_ ? 't' : 'f' ) . ',';
307             }
308             elsif ( $ref eq 'Bifcode2::INTEGER' ) {
309 4   66     67 $$_ // _croak 'EncodeIntegerUndef';
310 3 100       23 _croak 'EncodeInteger', 'invalid integer: ' . $$_
311             unless $$_ =~ m/\A (?: 0 | -? [1-9] [0-9]* ) \z/x;
312 1         14 sprintf 'i%s,', $$_;
313             }
314             elsif ( $ref eq 'Bifcode2::REAL' ) {
315 10   66     155 $$_ // _croak 'EncodeRealUndef';
316 9 100       84 _croak 'EncodeReal', 'invalid real: ' . $$_
317             unless $$_ =~ $number_qr;
318              
319 8   100     55 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
320             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
321 7         14 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
322 7         24 $x;
323             }
324             elsif ( $ref eq 'Bifcode2::UTF8' ) {
325 3   66     63 my $str = $$_ // _croak 'EncodeUTF8Undef';
326 3         9 utf8::encode($str);
327 3         10 'u' . length($str) . '.' . $str . ',';
328             }
329 11         225 elsif ( eval { $_->is_nan } ) {
330 1         10 'N,';
331             }
332 9         57 elsif ( eval { $_->is_inf } ) {
333 6 100       57 $_->is_pos ? '+,' : '-,';
334             }
335 4         74 elsif ( my $a = eval { [ $_->is_int ] } ) {
336 3 100       40 $a->[0]
337             ? 'i' . $_->bdstr() . ','
338             : 'r' . $_->bnstr() . ',';
339             }
340             else {
341 2         12 _croak 'EncodeUnhandled', 'unhandled data type: ' . $ref;
342             }
343             } @_;
344             }
345              
346             sub encode_bifcode2 {
347 78 50 33 79 1 50252 if ( ( @_ == 2 ) && pop ) {
    100          
348 0         0 my $b = (&_encode_bifcode2)[0];
349 0         0 'B' . length($b) . '.' . $b . ',';
350             }
351             elsif ( @_ == 1 ) {
352 77         130 (&_encode_bifcode2)[0];
353             }
354             else {
355 3         7 _croak 'EncodeUsage';
356             }
357             }
358              
359             sub force_bifcode2 {
360 17     17 1 4107 my $ref = shift;
361 16         19 my $type = shift;
362              
363 16 50 33     68 _croak 'ForceUsage' unless defined $ref and defined $type;
364 16         110 bless \$ref, 'Bifcode2::' . uc($type);
365             }
366              
367             sub _expand_bifcode {
368 1     1   6 my $bifcode = shift;
369 1         2 $bifcode =~ s/ (
370             [\[\]\{\}]
371             | ~,
372             | (B|u|b) [0-9]+ \.
373             | r -? [0-9]+ \. [0-9]+ e -? [0-9]+ ,
374             | i [0-9]+ ,
375             ) /\n$1/gmx;
376 1         6 $bifcode =~ s/ \A \n //mx;
377 0         0 $bifcode . "\n";
378             }
379              
380             sub diff_bifcode2 {
381 0 0 0 1 1 0 _croak 'DiffUsage' unless @_ >= 2 and @_ <= 3;
382 0         0 my $b1 = shift;
383 1         6 my $b2 = shift;
384 1   0     2 my $diff_args = shift || { STYLE => 'Unified' };
385              
386 1         7 require Text::Diff;
387              
388 0         0 $b1 = _expand_bifcode($b1);
389 0         0 $b2 = _expand_bifcode($b2);
390 0         0 return Text::Diff::diff( \$b1, \$b2, $diff_args );
391             }
392              
393             # Looking for something like "B48." that may be truncated at any point
394             my $qr_bcbc = qr/
395             ^
396             ( # 1
397             B
398             (?:
399             \Z | (?:
400             ( 0 | [1-9][0-9]* ) # 2
401             ( \Z | (\.) ) # 3
402             )
403             )
404             )
405             /x;
406              
407             sub anyevent_read_type {
408 1     1 0 5 my ( $self, $cb, $maxdepth ) = @_;
409              
410             sub {
411 1 0   1   6 return unless defined $_[0]->{rbuf};
412              
413 0         0 $_[0]->{rbuf} =~ s/^[\r\n]*//;
414 0 0       0 return unless length $_[0]->{rbuf};
415              
416 0         0 $_[0]->{rbuf} =~ $qr_bcbc;
417              
418 1 0       6 if ( length $3 ) {
    0          
419             $_[0]->unshift_read(
420             chunk => length($1) + $2 + 1,
421             sub {
422 1         6 my $data = eval { decode_bifcode2( $_[1], $maxdepth ) };
  0         0  
423 0 0       0 if ($@) {
424 0         0 $_[0]->_error( Errno::EBADMSG(), undef, $@ );
425             }
426             else {
427 1         5 $cb->( $_[0], $data );
428             }
429 1         2 1;
430             }
431 1         2 );
432 1         5 return 1;
433             }
434             elsif ( not length $1 ) {
435 0         0 $_[0]->_error( Errno::EBADMSG() );
436 0         0 return;
437             }
438             else {
439 0         0 return; # not enough data yet
440             }
441 1         2 };
442             }
443              
444             sub anyevent_write_type {
445 1     1 0 6 encode_bifcode2( $_[1], 1 ) . "\n";
446             }
447              
448             1;
449              
450             __END__