File Coverage

blib/lib/Bifcode/V2.pm
Criterion Covered Total %
statement 234 330 70.9
branch 103 106 97.1
condition 52 65 80.0
subroutine 55 55 100.0
pod 4 4 100.0
total 448 560 80.0


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