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   144696 use 5.010;
  5         24  
3 5     5   31 use strict;
  3         5  
  3         60  
4 3     5   12 use warnings;
  5         16  
  5         59  
5 5     5   459 use boolean ();
  3         2918  
  3         79  
6 5         20 use Exporter::Tidy all => [
7             qw( encode_bifcode2
8             decode_bifcode2
9             force_bifcode2
10             diff_bifcode2)
11 3     5   2648 ];
  5         49  
12              
13             # ABSTRACT: Serialisation similar to Bencode + undef/UTF8
14              
15             our $VERSION = '2.0.0_13';
16             our $max_depth;
17             our @CARP_NOT = (__PACKAGE__);
18              
19             sub _croak {
20 75     75   342 require Carp;
21 73   33     131 my $type = shift // Carp::croak('usage: _croak($TYPE, [$msg])');
22 73         863 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         128 my $err = 'Bifcode2::Error::' . $type;
56 75   66     264 my $msg = shift // $messages{$type}
      33        
57             // Carp::croak("Bifcode::_croak($type) has no message ");
58 75         6156 my $short = Carp::shortmess('');
59              
60 75   100     541 $msg =~ s! at$!' at input byte '. ( pos() // 0 )!e;
  54         195  
61              
62 5     5   31 eval qq[
  3     3   5  
  3     1   24  
  0     1   0  
  3     1   5430  
  3     1   21  
  73     1   8498  
  1     1   2  
  1     1   8  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   3  
  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   6  
  1     1   2  
  1     1   7  
  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   7  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  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         2  
  1         16  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         15  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         14  
  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         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         3  
  1         6  
  0         0  
  0         0  
  0         0  
  1         5  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         7  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         18  
  0         0  
  0         0  
  0         0  
  1         7  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         18  
  0         0  
  0         0  
  0         0  
  1         6  
  1         2  
  1         7  
  0         0  
  0         0  
  0         0  
  1         5  
  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         7  
  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       172 die $@ if $@;
72 75         725 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   95 unless (m/ \G (b|u) (?: ( 0 | [1-9] [0-9]* ) \. )? /gcx) {
95 6 50       23 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'DecodeKeyType';
96             }
97              
98 29 100       82 if ( $1 eq 'b' ) {
    50          
99 1   33     5 my $len = $2 // _croak 'DecodeBytes';
100 1 50       4 _croak 'DecodeBytesTrunc' if $len > length() - pos();
101              
102 3         14 my $data = substr $_, pos(), $len;
103 3         7 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     58 my $len = $2 // _croak 'DecodeUTF8';
110 28 50       53 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
111              
112 30         69 utf8::decode( my $str = substr $_, pos(), $len );
113 30         47 pos() = pos() + $len;
114              
115 30 50       82 _croak 'DecodeUTF8Term' unless m/ \G : /xgc;
116 28         51 return $str;
117             }
118             }
119              
120             sub _decode_bifcode2_chunk {
121 157 100   159   219 local $max_depth = $max_depth - 1 if defined $max_depth;
122              
123 157 100       906 unless (m/$chunk/gc) {
124 10 100       41 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'Decode';
125             }
126              
127 151 100       838 if ( $1 eq '~,' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
    0          
128 10         33 return undef;
129             }
130             elsif ( $1 eq 'f,' ) {
131 3         6 return boolean::false;
132             }
133             elsif ( $1 eq 't,' ) {
134 3         9 return boolean::true;
135             }
136             elsif ( $1 eq 'N,' ) {
137 1         5 require Math::BigInt;
138 3         16 return Math::BigInt->bnan;
139             }
140             elsif ( $1 eq '-,' ) {
141 3         10 require Math::BigInt;
142 3         15 return Math::BigInt->binf('-');
143             }
144             elsif ( $1 eq '+,' ) {
145 1         4 require Math::BigInt;
146 1         4 return Math::BigInt->binf('+');
147             }
148             elsif ( $1 eq 'b' ) {
149 6   66     20 my $len = $2 // _croak 'DecodeBytes';
150 5 100       21 _croak 'DecodeBytesTrunc' if $len > length() - pos();
151              
152 4         8 my $data = substr $_, pos(), $len;
153 4         14 pos() = pos() + $len;
154              
155 3 100       11 _croak 'DecodeBytesTerm' unless m/ \G , /xgc;
156 2         7 return $data;
157             }
158             elsif ( $1 eq 'u' ) {
159 40   100     91 my $len = $2 // _croak 'DecodeUTF8';
160 36 100       76 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
161              
162 34         90 utf8::decode( my $str = substr $_, pos(), $len );
163 34         53 pos() = pos() + $len;
164              
165 33 100       96 _croak 'DecodeUTF8Term' unless m/ \G , /xgc;
166 31         95 return $str;
167             }
168             elsif ( $1 eq 'i' ) {
169 20 100       60 return 0 + $2 if defined $2;
170 9 100       44 _croak 'DecodeIntegerTrunc' if m/ \G \z /xgc;
171 8         12 _croak 'DecodeInteger';
172             }
173             elsif ( $1 eq 'r' ) {
174 12 100       30 if ( !defined $2 ) {
175 8 100       20 _croak 'DecodeRealTrunc' if m/ \G \z /xgc;
176 7         10 _croak 'DecodeReal';
177             }
178 3 100 66     17 _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     59 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
187              
188 23         28 my @list;
189 22         43 until (m/ \G \] /xgc) {
190 35         53 push @list, _decode_bifcode2_chunk();
191             }
192 16         37 return \@list;
193             }
194             elsif ( $1 eq '{' ) {
195 30 100 100     86 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
196              
197 27         35 my $last_key;
198             my %hash;
199 27         62 until (m/ \G \} /xgc) {
200 35 100       64 _croak 'DecodeTrunc' if m/ \G \z /xgc;
201              
202 33         45 my $key = _decode_bifcode2_key();
203              
204 29 100       46 _croak 'DecodeKeyDuplicate' if exists $hash{$key};
205 29 100 100     61 _croak 'DecodeKeyOrder'
206             if defined $last_key and $key lt $last_key;
207 28 100       41 _croak 'DecodeKeyValue' if m/ \G \} /xgc;
208              
209 27         28 $last_key = $key;
210 26         32 $hash{$key} = _decode_bifcode2_chunk();
211             }
212 13         28 return \%hash;
213             }
214             elsif ( $1 eq 'B' ) {
215 0   0     0 my $len = $2 // _croak 'DecodeBifcode';
216 1 0       7 _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 61191 local $_ = shift;
227 100         119 local $max_depth = shift;
228              
229 101 100       189 _croak 'DecodeUsage', 'decode_bifcode2: too many arguments' if @_;
230 100 100       150 _croak 'DecodeUsage', 'decode_bifcode2: input undefined'
231             unless defined $_;
232 98 100       191 _croak 'DecodeUsage', 'decode_bifcode2: only accepts bytes'
233             if utf8::is_utf8($_);
234              
235 96         158 my $deserialised_data = _decode_bifcode2_chunk();
236 42 100       175 _croak 'DecodeTrailing', " For: $_" if $_ !~ m/ \G \z /xgc;
237 38         130 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 91 100 100 91   166 if ( !defined $_ ) {
  98 100       401  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
247 5         15 '~' . ',';
248             }
249             elsif ( ( my $ref = ref $_ ) eq '' ) {
250 37 100       280 if ( $_ =~ $number_qr ) {
    100          
    50          
251 25 100 100     90 if ( defined $3 or defined $5 ) {
252              
253             # normalize to BIFCODE_REAL standards
254 24   100     125 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
255             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
256 25         56 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
257 25         69 $x;
258             }
259             else {
260 2         23 'i' . $_ . ',';
261             }
262             }
263             elsif ( utf8::is_utf8($_) ) {
264 3         8 utf8::encode( my $str = $_ );
265 3         15 'u' . length($str) . '.' . $str . ',';
266             }
267             elsif ( $_ =~ m/^[\x{20}-\x{7E}]*$/ ) {
268 9         35 'u' . length($_) . '.' . $_ . ',';
269             }
270             else {
271 1         6 'b' . length($_) . '.' . $_ . ',';
272             }
273             }
274             elsif ( $ref eq 'ARRAY' ) {
275 6         22 '[' . join( '', map _encode_bifcode2($_), @$_ ) . ']';
276             }
277             elsif ( $ref eq 'HASH' ) {
278             '{' . join(
279             '',
280 8         15 do {
281 7         7 my $k;
282 7         21 my @k = sort keys %$_;
283              
284             map {
285 15         20 $k = shift @k;
286              
287 15 100       36 if ( $k =~ m/^[\x{20}-\x{7E}]*$/ ) {
    50          
288 14         47 ( 'u' . length($k) . '.' . $k . ':', $_ );
289             }
290             elsif ( utf8::is_utf8($k) ) {
291 1         3 utf8::encode($k);
292 1         9 ( '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     45 $$_ // _croak 'EncodeBytesUndef';
303 5         17 'b' . length($$_) . '.' . $$_ . ',';
304             }
305             elsif ( boolean::isBoolean($_) ) {
306 5 100       94 ( $_ ? 't' : 'f' ) . ',';
307             }
308             elsif ( $ref eq 'Bifcode2::INTEGER' ) {
309 4   66     60 $$_ // _croak 'EncodeIntegerUndef';
310 3 100       54 _croak 'EncodeInteger', 'invalid integer: ' . $$_
311             unless $$_ =~ m/\A (?: 0 | -? [1-9] [0-9]* ) \z/x;
312 1         34 sprintf 'i%s,', $$_;
313             }
314             elsif ( $ref eq 'Bifcode2::REAL' ) {
315 10   66     136 $$_ // _croak 'EncodeRealUndef';
316 9 100       94 _croak 'EncodeReal', 'invalid real: ' . $$_
317             unless $$_ =~ $number_qr;
318              
319 8   100     81 my $x = 'r' . ( 0 + $1 ) # remove leading zeros
      100        
320             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
321 7         13 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
322 7         26 $x;
323             }
324             elsif ( $ref eq 'Bifcode2::UTF8' ) {
325 3   66     49 my $str = $$_ // _croak 'EncodeUTF8Undef';
326 3         10 utf8::encode($str);
327 3         12 'u' . length($str) . '.' . $str . ',';
328             }
329 19         322 elsif ( eval { $_->is_nan } ) {
330 1         8 'N,';
331             }
332 17         197 elsif ( eval { $_->is_inf } ) {
333 6 100       51 $_->is_pos ? '+,' : '-,';
334             }
335 12         83 elsif ( my $a = eval { [ $_->is_int ] } ) {
336 11 100       99 $a->[0]
337             ? 'i' . $_->bdstr() . ','
338             : 'r' . $_->bnstr() . ',';
339             }
340             else {
341 2         21 _croak 'EncodeUnhandled', 'unhandled data type: ' . $ref;
342             }
343             } @_;
344             }
345              
346             sub encode_bifcode2 {
347 75 50 33 76 1 34929 if ( ( @_ == 2 ) && pop ) {
    100          
348 0         0 my $b = (&_encode_bifcode2)[0];
349 0         0 'B' . length($b) . '.' . $b . ',';
350             }
351             elsif ( @_ == 1 ) {
352 74         126 (&_encode_bifcode2)[0];
353             }
354             else {
355 3         6 _croak 'EncodeUsage';
356             }
357             }
358              
359             sub force_bifcode2 {
360 17     17 1 4968 my $ref = shift;
361 16         18 my $type = shift;
362              
363 16 50 33     59 _croak 'ForceUsage' unless defined $ref and defined $type;
364 16         100 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         13 $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 6 my ( $self, $cb, $maxdepth ) = @_;
409              
410             sub {
411 1 0   1   7 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         7 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         6 $cb->( $_[0], $data );
428             }
429 1         1 1;
430             }
431 1         2 );
432 1         6 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__