File Coverage

blib/lib/Bifcode/V1.pm
Criterion Covered Total %
statement 232 330 70.3
branch 94 96 97.9
condition 52 65 80.0
subroutine 55 55 100.0
pod 4 4 100.0
total 437 550 79.4


line stmt bran cond sub pod time code
1             package Bifcode::V1;
2 7     7   18638 use 5.010;
  7         35  
3 7     7   41 use strict;
  4         4  
  4         77  
4 4     6   14 use warnings;
  7         22  
  7         87  
5 7     6   670 use boolean ();
  4         5473  
  4         94  
6 4     6   19 use Carp (qw/croak shortmess/);
  6         17  
  6         276  
7 4         19 use Exporter::Tidy all => [
8             qw( encode_bifcode
9             decode_bifcode
10             force_bifcode
11             diff_bifcode)
12 6     6   3312 ];
  4         45  
13              
14             # ABSTRACT: Serialisation similar to Bencode + undef/UTF8
15              
16             our $VERSION = '1.003_1';
17             our $max_depth;
18             our @CARP_NOT = (__PACKAGE__);
19              
20             sub _croak {
21 71   33 73   139 my $type = shift // croak 'usage: _error($TYPE, [$msg])';
22 73         796 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             DecodeFloat => 'malformed FLOAT data at',
30             DecodeFloatTrunc => 'unexpected FLOAT 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::BYTES ref is undefined',
40             EncodeFloat => undef,
41             EncodeFloatUndef => 'Bifcode::FLOAT ref is undefined',
42             EncodeInteger => undef,
43             EncodeIntegerUndef => 'Bifcode::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::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         129 my $err = 'Bifcode::Error::' . $type;
55 73   66     231 my $msg = shift // $messages{$type} // '(no message)';
      50        
56 71         5751 my $short = shortmess('');
57              
58 71   100     454 $msg =~ s! at$!' at input byte '. ( pos() // 0 )!e;
  53         181  
59              
60 4     6   24 eval 'package ' . $err . qq[;
  6     3   20  
  6     1   30  
  2     1   12  
  1     1   4785  
  1     1   11  
  73     1   7971  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   2  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   5  
  1     1   2  
  1     1   24  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   1  
  1     1   19  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   5  
  1     1   2  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   2  
  1     1   6  
  0     1   0  
  0     1   0  
  0     1   0  
  1     1   6  
  1     1   1  
  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         2  
  1         6  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  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         7  
  1         2  
  1         5  
  0         0  
  0         0  
  0         0  
  1         7  
  1         1  
  1         6  
  0         0  
  0         0  
  0         0  
  1         16  
  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         1  
  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         11  
  0         0  
  0         0  
  0         0  
  1         6  
  1         1  
  1         7  
  0         0  
  0         0  
  0         0  
  1         6  
  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         2  
  1         7  
  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         2  
  1         15  
  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         2  
  1         7  
  0            
  0            
  0            
61             use overload
62             bool => sub { 1 },
63             '""' => sub { \${ \$_[0] } . ' (' . ( ref \$_[0] ) . ')$short' },
64             fallback => 1;
65             1; ];
66              
67 73         679 die bless \$msg, $err;
68             }
69              
70             my $match = qr/ \G (?|
71             (~)
72             | (0)
73             | (1)
74             | (B|U) (?: ( 0 | [1-9] [0-9]* ) : )?
75             | (I) (?: ( 0 | -? [1-9] [0-9]* ) , )?
76             | (F) (?: ( 0 | -? [1-9] [0-9]* ) \. ( 0 | [0-9]* [1-9] ) e
77             ( (?: 0 | -? [1-9] ) [0-9]* ) , )?
78             | (\[)
79             | (\{)
80             ) /x;
81              
82             sub _decode_bifcode_chunk {
83 184 100   184   246 local $max_depth = $max_depth - 1 if defined $max_depth;
84              
85 182 100       921 unless (m/$match/gc) {
86 5 100       17 _croak m/ \G \z /xgc ? 'DecodeTrunc' : 'Decode';
87             }
88              
89 177 100       711 if ( $1 eq '~' ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
90 10         30 return undef;
91             }
92             elsif ( $1 eq '0' ) {
93 5         9 return boolean::false;
94             }
95             elsif ( $1 eq '1' ) {
96 5         27 return boolean::true;
97             }
98             elsif ( $1 eq 'B' ) {
99 7   66     40 my $len = $2 // _croak 'DecodeBytes';
100 5 100       15 _croak 'DecodeBytesTrunc' if $len > length() - pos();
101              
102 4         7 my $data = substr $_, pos(), $len;
103 6         21 pos() = pos() + $len;
104              
105 6 100       19 _croak 'DecodeBytesTerm' unless m/ \G , /xgc;
106 5         20 return $data;
107             }
108             elsif ( $1 eq 'U' ) {
109 68   100     133 my $len = $2 // _croak 'DecodeUTF8';
110 63 100       131 _croak 'DecodeUTF8Trunc' if $len > length() - pos();
111              
112 61         123 utf8::decode( my $str = substr $_, pos(), $len );
113 63         104 pos() = pos() + $len;
114              
115 63 100       150 _croak 'DecodeUTF8Term' unless m/ \G , /xgc;
116 61         154 return $str;
117             }
118             elsif ( $1 eq 'I' ) {
119 24 100       69 return $2 if defined $2;
120 8 100       18 _croak 'DecodeIntegerTrunc' if m/ \G \z /xgc;
121 7         10 _croak 'DecodeInteger';
122             }
123             elsif ( $1 eq 'F' ) {
124 13 100       33 if ( !defined $2 ) {
125 10 100       23 _croak 'DecodeFloatTrunc' if m/ \G \z /xgc;
126 9         23 _croak 'DecodeFloat';
127             }
128 3 100 66     17 _croak 'DecodeFloat'
      100        
129             if $2 eq '0' # mantissa 0.
130             and $3 eq '0' # mantissa 0.0
131             and $4 ne '0'; # sign or exponent 0.0e0
132              
133 2         7 return $2 . '.' . $3 . 'e' . $4;
134             }
135             elsif ( $1 eq '[' ) {
136 26 100 100     67 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
137              
138 23         27 my @list;
139 23         38 until (m/ \G \] /xgc) {
140 36         60 push @list, _decode_bifcode_chunk();
141             }
142 16         35 return \@list;
143             }
144             elsif ( $1 eq '{' ) {
145 27 100 100     64 _croak 'DecodeDepth' if defined $max_depth and $max_depth < 0;
146              
147 24         27 my $last_key;
148             my %hash;
149 25         51 until (m/ \G \} /xgc) {
150 34 100       65 _croak 'DecodeTrunc' if m/ \G \z /xgc;
151 32 100       71 _croak 'DecodeKeyType' unless m/ \G (B|U) /xgc;
152              
153 29         50 pos() = pos() - 1;
154 29         47 my $key = _decode_bifcode_chunk();
155              
156 29 100       48 _croak 'DecodeKeyDuplicate' if exists $hash{$key};
157 29 100 100     63 _croak 'DecodeKeyOrder'
158             if defined $last_key and $key lt $last_key;
159 28 100       39 _croak 'DecodeKeyValue' if m/ \G \} /xgc;
160              
161 27         29 $last_key = $key;
162 26         34 $hash{$key} = _decode_bifcode_chunk();
163             }
164 13         28 return \%hash;
165             }
166             }
167              
168             sub decode_bifcode {
169 96     97 1 52989 local $_ = shift;
170 97         108 local $max_depth = shift;
171              
172 97 100       195 _croak 'DecodeUsage', 'decode_bifcode: too many arguments' if @_;
173 96 100       158 _croak 'DecodeUsage', 'decode_bifcode: input undefined'
174             unless defined $_;
175 93 100       179 _croak 'DecodeUsage', 'decode_bifcode: only accepts bytes'
176             if utf8::is_utf8($_);
177              
178 92         120 my $deserialised_data = _decode_bifcode_chunk();
179 43 100       103 _croak 'DecodeTrailing' if $_ !~ m/ \G \z /xgc;
180 40         178 return $deserialised_data;
181             }
182              
183             my $number_qr = qr/\A ( 0 | -? [1-9] [0-9]* )
184             ( \. ( [0-9]+? ) 0* )?
185             ( e ( -? [0-9]+ ) )? \z/xi;
186              
187             sub _encode_bifcode {
188             map {
189 82 100 100 82   139 if ( !defined $_ ) {
  89 100       301  
    100          
    100          
    100          
    100          
    100          
    100          
    100          
190 4         12 '~';
191             }
192             elsif ( ( my $ref = ref $_ ) eq '' ) {
193 46 100       329 if ( $_ =~ $number_qr ) {
194 35 100 100     111 if ( defined $3 or defined $5 ) {
195              
196             # normalize to BIFCODE_FLOAT standards
197 26   100     144 my $x = 'F' . ( 0 + $1 ) # remove leading zeros
      100        
198             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
199 26         57 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
200 26         79 $x;
201             }
202             else {
203 10         34 'I' . $_ . ',';
204             }
205             }
206             else {
207 11         22 utf8::encode( my $str = $_ );
208 11         40 'U' . length($str) . ':' . $str . ',';
209             }
210             }
211             elsif ( $ref eq 'ARRAY' ) {
212 6         26 '[' . join( '', map _encode_bifcode($_), @$_ ) . ']';
213             }
214             elsif ( $ref eq 'HASH' ) {
215             '{' . join(
216             '',
217 7         9 do {
218 7         29 my @k = sort keys %$_;
219             map {
220 13         13 my $k = shift @k;
221              
222             # if ( is valid utf8($k) ) {
223 13         20 utf8::encode($k);
224 14         40 ( 'U' . length($k) . ':' . $k . ',', $_ );
225              
226             # }
227             # else {
228             # ('B' . length($k) . ':' . $k .',', $_);
229             # }
230 6         13 } _encode_bifcode( @$_{@k} );
231             }
232             ) . '}';
233             }
234             elsif ( $ref eq 'SCALAR' or $ref eq 'Bifcode::BYTES' ) {
235 7   66     16 $$_ // _croak 'EncodeBytesUndef';
236 5         20 'B' . length($$_) . ':' . $$_ . ',';
237             }
238             elsif ( boolean::isBoolean($_) ) {
239 4         98 $_;
240             }
241             elsif ( $ref eq 'Bifcode::INTEGER' ) {
242 4   66     63 $$_ // _croak 'EncodeIntegerUndef';
243 3 100       19 _croak 'EncodeInteger', 'invalid integer: ' . $$_
244             unless $$_ =~ m/\A (?: 0 | -? [1-9] [0-9]* ) \z/x;
245 2         14 sprintf 'I%s,', $$_;
246             }
247             elsif ( $ref eq 'Bifcode::FLOAT' ) {
248 10   66     154 $$_ // _croak 'EncodeFloatUndef';
249 9 100       70 _croak 'EncodeFloat', 'invalid float: ' . $$_
250             unless $$_ =~ $number_qr;
251              
252 7   100     44 my $x = 'F' . ( 0 + $1 ) # remove leading zeros
      100        
253             . '.' . ( $3 // 0 ) . 'e' . ( 0 + ( $5 // 0 ) ) . ',';
254 7         13 $x =~ s/ ([1-9]) (0+ e)/.${1}e/x; # remove trailing zeros
255 7         23 $x;
256             }
257             elsif ( $ref eq 'Bifcode::UTF8' ) {
258 4   66     57 my $str = $$_ // _croak 'EncodeUTF8Undef';
259 3         6 utf8::encode($str); #, sub { croak 'invalid Bifcode::UTF8' } );
260 3         16 'U' . length($str) . ':' . $str . ',';
261             }
262             else {
263 1         19 _croak 'EncodeUnhandled', 'unhandled data type: ' . $ref;
264             }
265             } @_;
266             }
267              
268             sub encode_bifcode {
269 68 100   69 1 31862 _croak 'EncodeUsage' if @_ != 1;
270 65         93 (&_encode_bifcode)[0];
271             }
272              
273             sub force_bifcode {
274 17     17 1 4132 my $ref = shift;
275 17         22 my $type = shift;
276              
277 17 50 33     60 _croak 'ForceUsage' unless defined $ref and defined $type;
278 16         89 bless \$ref, 'Bifcode::' . uc($type);
279             }
280              
281             sub _expand_bifcode {
282 122     123   113 my $bifcode = shift;
283 122         627 $bifcode =~ s/ (
284             [~\[\]\{\}]
285             | (U|B) [0-9]+ :
286             | F -? [0-9]+ \. [0-9]+ e -? [0-9]+ ,
287             | I [0-9]+ ,
288             ) /\n$1/gmx;
289 123         279 $bifcode =~ s/ \A \n //mx;
290 123         243 $bifcode . "\n";
291             }
292              
293             sub diff_bifcode {
294 64 100 100 64 1 8275 _croak 'DiffUsage' unless @_ >= 2 and @_ <= 3;
295 61         82 my $b1 = shift;
296 61         57 my $b2 = shift;
297 61   50     172 my $diff_args = shift || { STYLE => 'Unified' };
298              
299 62         1102 require Text::Diff;
300              
301 62         13897 $b1 = _expand_bifcode($b1);
302 62         91 $b2 = _expand_bifcode($b2);
303 61         136 return Text::Diff::diff( \$b1, \$b2, $diff_args );
304             }
305              
306             decode_bifcode('I1,');
307              
308             __END__