File Coverage

blib/lib/Bifcode2.pm
Criterion Covered Total %
statement 240 342 70.1
branch 110 140 78.5
condition 51 78 65.3
subroutine 57 57 100.0
pod 4 6 66.6
total 462 623 74.1


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