File Coverage

blib/lib/CBOR/PP/Decode.pm
Criterion Covered Total %
statement 120 162 74.0
branch 85 120 70.8
condition 4 6 66.6
subroutine 7 7 100.0
pod 1 1 100.0
total 217 296 73.3


line stmt bran cond sub pod time code
1             package CBOR::PP::Decode;
2              
3 6     6   40 use strict;
  6         14  
  6         172  
4 6     6   29 use warnings;
  6         11  
  6         473  
5              
6             =encoding utf-8
7              
8             =head1 NAME
9              
10             CBOR::PP::Decode
11              
12             =head1 SYNOPSIS
13              
14             my $perlvar = CBOR::PP::Decode::decode($binary);
15              
16             =head1 DESCRIPTION
17              
18             This implements a basic CBOR decoder in pure Perl.
19              
20             =head1 MAPPING CBOR TO PERL
21              
22             =over
23              
24             =item * All tags are ignored. (This could be iterated on later.)
25              
26             =item * Indefinite-length objects are supported, but streamed parsing
27             is not; the data structure must be complete to be decoded.
28              
29             =item * CBOR text strings are decoded to UTF8-flagged strings, while
30             binary strings are decoded to non-UTF8-flagged strings. In practical
31             terms, this means that a decoded CBOR binary string will have no code point
32             above 255, while a decoded CBOR text string can contain any valid Unicode
33             code point.
34              
35             =item * null, undefined, true, and false become undef, undef,
36             Types::Serialiser::true(), and Types::Serialiser::false(), respectively.
37             (NB: undefined is deserialized as an error object in L,
38             which doesn’t seem to make sense.)
39              
40             =back
41              
42             =head1 TODO
43              
44             =over
45              
46             =item * Add tag decode support via callbacks.
47              
48             =item * Make it faster by removing some of the internal buffer copying.
49              
50             =back
51              
52             =head1 AUTHOR
53              
54             L (FELIPE)
55              
56             =head1 LICENSE
57              
58             This code is licensed under the same license as Perl itself.
59              
60             =cut
61              
62             #----------------------------------------------------------------------
63              
64             =head1 METHODS
65              
66             =head2 $value = decode( $CBOR_BYTESTRING )
67              
68             Returns a Perl value that represents the serialized CBOR string.
69              
70             =cut
71              
72             my ($byte1, $offset, $lead3bits);
73              
74             my $len;
75              
76             # This ensures that pieces of indefinite-length strings
77             # are all of the same type.
78             our $_lead3_must_be;
79              
80             use constant {
81 6         9778 _LEAD3_UINT => 0,
82             _LEAD3_NEGINT => 1 << 5,
83             _LEAD3_BINSTR => 2 << 5,
84             _LEAD3_UTF8STR => 3 << 5,
85             _LEAD3_ARRAY => 4 << 5,
86             _LEAD3_HASH => 5 << 5,
87             _LEAD3_TAG => 6 << 5,
88 6     6   35 };
  6         9  
89              
90             # CBOR is much simpler to create than it is to parse!
91              
92             # TODO: Optimize by removing the buffer duplication.
93              
94             sub decode {
95 404     404 1 30532 $offset = 0;
96              
97 404         711 for ($_[0]) {
98 404         776 $byte1 = ord( substr( $_, $offset, 1 ) );
99 404         633 $lead3bits = 0xe0 & $byte1;
100              
101 404 50 66     803 die "Improper lead3 ($lead3bits) within streamed $_lead3_must_be!" if $_lead3_must_be && $lead3bits != $_lead3_must_be;
102              
103             #use Text::Control;
104             #print Text::Control::to_hex($_) . $/;
105              
106 404 100       1005 if ($lead3bits == _LEAD3_UINT()) {
    100          
    100          
    100          
    100          
    100          
    100          
107 203 100       856 return (1, ord) if $byte1 < 0x18;
108              
109 27 100       96 return (2, unpack('x C', $_)) if $byte1 == 0x18;
110              
111 13 100       40 return (3, unpack('x n', $_)) if $byte1 == 0x19;
112              
113 11 100       44 return (5, unpack('x N', $_)) if $byte1 == 0x1a;
114              
115 6         41 return (9, unpack('x Q>', $_));
116             }
117              
118             elsif ($lead3bits == _LEAD3_NEGINT()) {
119 5 100       18 return (1, 0x1f - ord()) if $byte1 < 0x38;
120              
121 2 100       11 return (2, -unpack( 'x C', $_) - 1) if $byte1 == 0x38;
122              
123 1 50       7 return (3, -unpack( 'x n', $_) - 1) if $byte1 == 0x39;
124              
125 0 0       0 return (5, -unpack( 'x N', $_) - 1) if $byte1 == 0x3a;
126              
127 0         0 return (9, -unpack( 'x Q>', $_) - 1);
128             }
129              
130             elsif ($lead3bits == _LEAD3_BINSTR()) {
131 59         79 my $hdrlen;
132              
133 59 100       126 if ($byte1 < 0x58) {
    50          
    50          
    50          
    50          
    50          
134 58         76 $len = $byte1 - 0x40;
135 58         88 $hdrlen = 1;
136             }
137             elsif ($byte1 == 0x58) {
138 0         0 $len = unpack 'x C', $_;
139 0         0 $hdrlen = 2;
140             }
141             elsif ($byte1 == 0x59) {
142 0         0 $len = unpack 'x n', $_;
143 0         0 $hdrlen = 3;
144             }
145             elsif ($byte1 == 0x5a) {
146 0         0 $len = unpack 'x N', $_;
147 0         0 $hdrlen = 5;
148             }
149             elsif ($byte1 == 0x5b) {
150 0         0 $len = unpack 'x Q>', $_;
151 0         0 $hdrlen = 9;
152             }
153             elsif ($byte1 == 0x5f) {
154 1         25 return _stringstream();
155             }
156             else {
157 0         0 die "Invalid lead byte: $byte1";
158             }
159              
160 58         249 return ($hdrlen + $len, substr( $_, $hdrlen, $len ));
161             }
162              
163             elsif ($lead3bits == _LEAD3_UTF8STR()) {
164 38         55 my $hdrlen;
165              
166 38 100       80 if ($byte1 < 0x78) {
    50          
    50          
    50          
    50          
    50          
167 37         50 $len = $byte1 - 0x60;
168 37         44 $hdrlen = 1;
169             }
170             elsif ($byte1 == 0x78) {
171 0         0 $len = unpack 'x C', $_;
172 0         0 $hdrlen = 2;
173             }
174             elsif ($byte1 == 0x79) {
175 0         0 $len = unpack 'x n', $_;
176 0         0 $hdrlen = 3;
177             }
178             elsif ($byte1 == 0x7a) {
179 0         0 $len = unpack 'x N', $_;
180 0         0 $hdrlen = 5;
181             }
182             elsif ($byte1 == 0x7b) {
183 0         0 $len = unpack 'x Q>', $_;
184 0         0 $hdrlen = 9;
185             }
186             elsif ($byte1 == 0x7f) {
187 1         7 return _stringstream();
188             }
189             else {
190 0         0 die "Invalid lead byte: $byte1";
191             }
192              
193 37         68 my $v = substr( $_, $hdrlen, $len );
194 37         108 utf8::decode($v);
195              
196             # A no-op if $v is already UTF8-flagged, but if it’s not,
197             # then this will apply the flag. We thus preserve the ability
198             # to round-trip a character string through Perl.
199 37         74 utf8::upgrade($v);
200              
201 37         118 return ($hdrlen + $len, $v);
202             }
203              
204             elsif ($lead3bits == _LEAD3_ARRAY()) {
205 48         63 my $total;
206              
207 48 100       100 if ($byte1 < 0x98) {
    100          
    50          
    50          
    50          
    50          
208 37         51 $len = $byte1 - 0x80;
209 37         48 $total = 1;
210             }
211             elsif ($byte1 == 0x98) {
212 3         8 $len = unpack 'x C', $_;
213 3         5 $total = 2;
214             }
215             elsif ($byte1 == 0x99) {
216 0         0 $len = unpack 'x n', $_;
217 0         0 $total = 3;
218             }
219             elsif ($byte1 == 0x9a) {
220 0         0 $len = unpack 'x N', $_;
221 0         0 $total = 5;
222             }
223             elsif ($byte1 == 0x9b) {
224 0         0 $len = unpack 'x Q>', $_;
225 0         0 $total = 9;
226             }
227             elsif ($byte1 == 0x9f) {
228 8         17 return _arraystream();
229             }
230             else {
231 0         0 die "Invalid lead byte: $byte1";
232             }
233              
234             # pre-fill the array
235 40         104 my @val = (undef) x $len;
236              
237 40         63 my $cur_len;
238              
239 40         83 for my $i ( 0 .. ($len - 1) ) {
240 153         302 ($cur_len, $val[$i]) = decode( substr( $_, $total ) );
241 153         282 $total += $cur_len;
242             }
243              
244 40         149 return( $total, \@val );
245             }
246              
247             elsif ($lead3bits == _LEAD3_HASH()) {
248 16         25 my ($len, $total);
249              
250 16 100       40 if ($byte1 < 0xb8) {
    50          
    50          
    50          
    50          
    50          
251 13         22 $len = $byte1 - 0xa0;
252 13         17 $total = 1;
253             }
254             elsif ($byte1 == 0xb8) {
255 0         0 $len = unpack 'x C', $_;
256 0         0 $total = 2;
257             }
258             elsif ($byte1 == 0xb9) {
259 0         0 $len = unpack 'x n', $_;
260 0         0 $total = 3;
261             }
262             elsif ($byte1 == 0xba) {
263 0         0 $len = unpack 'x N', $_;
264 0         0 $total = 5;
265             }
266             elsif ($byte1 == 0xbb) {
267 0         0 $len = unpack 'x Q>', $_;
268 0         0 $total = 9;
269             }
270             elsif ($byte1 == 0xbf) {
271 3         7 return _hashstream();
272             }
273             else {
274 0         0 die "Invalid lead byte: $byte1";
275             }
276              
277 13         16 my %val;
278              
279             my $cur_len;
280              
281 13         44 while ( $len > 0 ) {
282 25         51 ($cur_len, my $key) = decode( substr( $_, $total ) );
283 25         51 $total += $cur_len;
284              
285 25         51 ( $cur_len, $val{$key} ) = decode( substr( $_, $total ) );
286 25         52 $total += $cur_len;
287              
288 25         61 $len--;
289             }
290              
291 13         59 return( $total, \%val );
292             }
293              
294             # tags … just ignore for now
295             elsif ($lead3bits == _LEAD3_TAG()) {
296 6         9 my $taglen;
297              
298 6 100       14 if ($byte1 < 0xd8) {
    50          
    0          
    0          
    0          
299 4         5 $taglen = 1;
300             }
301             elsif ($byte1 == 0xd8) {
302 2         3 $taglen = 2;
303             }
304             elsif ($byte1 == 0xd9) {
305 0         0 $taglen = 3;
306             }
307             elsif ($byte1 == 0xda) {
308 0         0 $taglen = 5;
309             }
310             elsif ($byte1 == 0xdb) {
311 0         0 $taglen = 9;
312             }
313             else {
314 0         0 die "Invalid lead byte: $byte1";
315             }
316              
317 6         18 my @ret = decode( substr( $_, $taglen ) );
318 6         23 return( $taglen + $ret[0], $ret[1] );
319             }
320              
321             # floats, true, false, null, undefined
322             else {
323 29 100 66     148 if ($byte1 == 0xf4) {
    100          
    100          
    100          
    100          
    50          
324 2         13 require Types::Serialiser;
325 2         5 return ( 1, Types::Serialiser::false() );
326             }
327             elsif ($byte1 == 0xf5) {
328 4         25 require Types::Serialiser;
329 4         12 return ( 1, Types::Serialiser::true() );
330             }
331             elsif ($byte1 == 0xf6 || $byte1 == 0xf7) {
332 2         11 return (1, undef);
333             }
334             elsif ($byte1 == 0xf9) {
335              
336             # Adapted from the Python code in RFC 7049 appendix D:
337 11         34 my $half = unpack 'x n', $_;
338 11         25 my $valu = (($half & 0x7fff) << 13) | (($half & 0x8000) << 16);
339              
340 11 100       24 if (($half & 0x7c00) != 0x7c00) {
341 8         84 return( unpack('f>', pack('N', $valu)) * (2**112) );
342             }
343              
344 3         27 return ( 3, unpack('f>', pack('N', $valu | 0x7f800000)) );
345             }
346             elsif ($byte1 == 0xfa) {
347 4         16 return ( 5, unpack( 'x f>', $_ ) );
348             }
349             elsif ($byte1 == 0xfb) {
350 6         25 return ( 5, unpack( 'x d>', $_ ) );
351             }
352              
353 0         0 die sprintf('can’t decode special value: %v.02x', $_);
354             }
355             }
356             }
357              
358             sub _stringstream {
359 2     2   7 my $full = q<>;
360              
361 2         3 my $i = 1;
362              
363 2         4 local $_lead3_must_be = $lead3bits;
364              
365 2         3 while (1) {
366 6 50       14 die 'Incomplete indefinite-length string!' if $i >= length();
367              
368 6 100       27 if ("\xff" eq substr( $_, $i, 1 )) {
369 2         4 $i++;
370 2         4 last;
371             }
372              
373 4         11 my ($len, $chunk) = decode( substr( $_, $i ) );
374              
375 4         9 $full .= $chunk;
376 4         7 $i += $len;
377             }
378              
379 2 100       14 utf8::decode($full) if $lead3bits == _LEAD3_UTF8STR();
380              
381 2         10 return ($i, $full);
382             }
383              
384             sub _arraystream {
385 8     8   13 my @full;
386              
387 8         10 my $i = 1;
388              
389 8         10 while (1) {
390 47 50       83 die 'Incomplete indefinite-length array!' if $i >= length();
391              
392 47 100       122 if ("\xff" eq substr( $_, $i, 1 )) {
393 8         11 $i++;
394 8         11 last;
395             }
396              
397 39         124 my ($len, $chunk) = decode( substr( $_, $i ) );
398              
399 39         72 push @full, $chunk;
400 39         54 $i += $len;
401             }
402              
403 8         26 return ($i, \@full);
404             }
405              
406             sub _hashstream {
407 3     3   5 my %full;
408              
409 3         4 my $i = 1;
410              
411 3         5 while (1) {
412 8 50       18 die 'Incomplete indefinite-length map!' if $i >= length();
413              
414 8 100       17 if ("\xff" eq substr( $_, $i, 1 )) {
415 3         4 $i++;
416 3         4 last;
417             }
418              
419 5         12 my ($len, $key) = decode( substr( $_, $i ) );
420 5         11 $i += $len;
421              
422 5 50       13 if ( "\xff" eq substr( $_, $i, 1 ) ) {
423 0         0 die "Odd number of elements in map! (Last key: “$key”)";
424             }
425              
426 5         10 ($len, $full{$key}) = decode( substr( $_, $i ) );
427 5         14 $i += $len;
428             }
429              
430 3         9 return ($i, \%full);
431             }
432              
433             1;