File Coverage

blib/lib/Bifcode/V2.pm
Criterion Covered Total %
statement 246 342 71.9
branch 111 132 84.0
condition 54 77 70.1
subroutine 57 57 100.0
pod 4 6 66.6
total 472 614 76.8


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