File Coverage

blib/lib/Bifcode.pm
Criterion Covered Total %
statement 263 360 73.0
branch 128 156 82.0
condition 54 78 69.2
subroutine 60 60 100.0
pod 4 6 66.6
total 509 660 77.1


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