File Coverage

lib/DR/Tnt/Msgpack.pm
Criterion Covered Total %
statement 203 233 87.1
branch 112 162 69.1
condition 4 5 80.0
subroutine 36 42 85.7
pod 5 7 71.4
total 360 449 80.1


line stmt bran cond sub pod time code
1 11     11   297196 use utf8;
  11         49  
  11         66  
2 11     11   354 use strict;
  11         24  
  11         250  
3 11     11   59 use warnings;
  11         24  
  11         493  
4              
5             package DR::Tnt::Msgpack;
6 11     11   83 use base qw(Exporter);
  11         30  
  11         1608  
7             our @EXPORT = qw(msgpack msgunpack msgunpack_check msgunpack_utf8);
8 11     11   86 use Scalar::Util ();
  11         24  
  11         316  
9 11     11   84 use Carp;
  11         31  
  11         953  
10             $Carp::Internal{ (__PACKAGE__) }++;
11 11     11   94 use feature 'state';
  11         22  
  11         1599  
12 11     11   4294 use DR::Tnt::Msgpack::Types ':all';
  11         32  
  11         42803  
13              
14             sub _retstr($$) {
15 74     74   185 my ($str, $utf8) = @_;
16 74 100       594 utf8::decode $str if $utf8;
17 74         296 return $str;
18             }
19              
20             sub _msgunpack($$);
21             sub _extract_hash_elements($$$$) {
22 39     39   82 my ($str, $len, $size, $utf8) = @_;
23              
24 39         56 my %o;
25 39         91 for (my $i = 0; $i < $size; $i++) {
26 122         352 my ($k, $klen) = _msgunpack(substr($str, $len), $utf8);
27 122 50       335 return unless defined $klen;
28 122         212 $len += $klen;
29              
30 122         283 my ($v, $vlen) = _msgunpack(substr($str, $len), $utf8);
31 122 50       355 return unless defined $vlen;
32 122         174 $len += $vlen;
33              
34 122         407 $o{$k} = $v;
35             }
36 39         137 return \%o, $len;
37             }
38              
39             sub _extract_array_elements($$$$) {
40 25     25   69 my ($str, $len, $size, $utf8) = @_;
41              
42 25         40 my @o;
43 25         63 for (my $i = 0; $i < $size; $i++) {
44 105         281 my ($e, $elen) = _msgunpack(substr($str, $len), $utf8);
45 105 50       272 return unless defined $elen;
46 105         150 $len += $elen;
47 105         314 push @o => $e;
48             }
49 25         82 return \@o, $len;
50             }
51              
52              
53             sub _msgunpack($$) {
54 813     813   2564 my ($str, $utf8) = @_;
55              
56 813 100 66     3058 return unless defined $str and length $str;
57              
58 806         1719 my $tag = unpack 'C', $str;
59              
60             # fix uint
61 806 100       2304 return ($tag, 1) if $tag <= 0x7F;
62            
63             # fix negative
64 223 100       450 return (unpack('c', $str), 1) if $tag >= 0xE0;
65              
66             # fix str
67 219 100       467 if (($tag & ~0x1F) == 0xA0) {
68 67         117 my $len = $tag & 0x1F;
69 67 50       158 return unless length($str) >= 1 + $len;
70 67 100       138 return '', 1 unless $len;
71 63         255 return (_retstr(unpack("x[C]a$len", $str), $utf8), 1 + $len);
72             }
73              
74             # fix map
75 152 100       318 if (($tag & ~0x0F) == 0x80) {
76 38         59 my $size = $tag & 0x0F;
77 38         90 return _extract_hash_elements($str, 1, $size, $utf8);
78             }
79              
80             # fix array
81 114 100       238 if (($tag & ~0x0F) == 0x90) {
82 25         38 my $size = $tag & 0x0F;
83 25         65 return _extract_array_elements($str, 1, $size, $utf8);
84             }
85              
86              
87             state $variant = {
88             (0xD0) => sub { # int8
89 1     1   4 my ($str) = @_;
90 1 50       4 return unless length($str) >= 2;
91 1         4 return (unpack('x[C]c', $str), 2);
92             },
93             (0xD1) => sub { # int16
94 7     7   16 my ($str) = @_;
95 7 50       21 return unless length($str) >= 3;
96 7         28 return (unpack('x[C]s>', $str), 3);
97             },
98             (0xD2) => sub { # int32
99 9     9   17 my ($str) = @_;
100 9 50       29 return unless length($str) >= 5;
101 9         37 return (unpack('x[C]l>', $str), 5);
102             },
103             (0xD3) => sub { # int64
104 7     7   17 my ($str) = @_;
105 7 50       19 return unless length($str) >= 9;
106 7         27 return (unpack('x[C]q>', $str), 9);
107             },
108              
109              
110             (0xCC) => sub { # uint8
111 4     4   10 my ($str) = @_;
112 4 50       11 return unless length($str) >= 2;
113 4         14 return (unpack('x[C]C', $str), 2);
114             },
115             (0xCD) => sub { # uint16
116 11     11   27 my ($str) = @_;
117 11 50       39 return unless length($str) >= 3;
118 11         44 return (unpack('x[C]S>', $str), 3);
119             },
120             (0xCE) => sub { # uint32
121 20     20   45 my ($str) = @_;
122 20 50       84 return unless length($str) >= 5;
123 20         106 return (unpack('x[C]L>', $str), 5);
124             },
125             (0xCF) => sub { # uint64
126 9     9   20 my ($str) = @_;
127 9 50       21 return unless length($str) >= 9;
128 9         30 return (unpack('x[C]Q>', $str), 9);
129             },
130              
131             (0xC0) => sub { # null
132 1     1   4 return (undef, 1);
133             },
134              
135             (0xC2) => sub {
136 1     1   4 return (mp_false, 1); # false
137             },
138             (0xC3) => sub {
139 1     1   4 return (mp_true, 1); # true
140             },
141              
142             (0xC4) => sub { # bin8
143 3     3   7 my ($str) = @_;
144 3 50       8 return unless length($str) >= 2;
145 3         8 my $len = unpack('x[C]C', $str);
146 3 50       9 return unless length($str) >= 2 + $len;
147 3         26 return (unpack("x[C]C/a", $str), 2 + $len);
148             },
149             (0xC5) => sub { # bin16
150 1     1   3 my ($str) = @_;
151 1 50       4 return unless length($str) >= 3;
152 1         5 my $len = unpack('x[C]S>', $str);
153 1 50       4 return unless length($str) >= 3 + $len;
154 1         29 return (unpack("x[C]S>/a", $str), 3 + $len);
155             },
156             (0xC6) => sub { # bin32
157 1     1   3 my ($str) = @_;
158 1 50       5 return unless length($str) >= 5;
159 1         4 my $len = unpack('x[C]L>', $str);
160 1 50       4 return unless length($str) >= 5 + $len;
161 1         1242 return (unpack("x[C]L>/a", $str), 5 + $len);
162             },
163              
164              
165             (0xD9) => sub { # str8
166 4     4   12 my ($str, $utf8) = @_;
167 4 50       15 return unless length($str) >= 2;
168 4         13 my ($len) = unpack('x[C]C', $str);
169 4 50       17 return unless length($str) >= 2 + $len;
170 4         19 return (_retstr(unpack("x[C]C/a", $str), $utf8), 2 + $len);
171             },
172             (0xDA) => sub { # str16
173 4     4   12 my ($str, $utf8) = @_;
174 4 50       15 return unless length($str) >= 3;
175 4         13 my $len = unpack('x[C]S>', $str);
176 4 50       17 return unless length($str) >= 3 + $len;
177 4         224 return (_retstr(unpack("x[C]S>/a", $str), $utf8), 3 + $len);
178             },
179              
180             (0xDB) => sub { # str32
181 3     3   11 my ($str, $utf8) = @_;
182 3 50       13 return unless length($str) >= 5;
183 3         11 my $len = unpack('x[C]L>', $str);
184 3 50       12 return unless length($str) >= 5 + $len;
185 3         1433 return (_retstr(unpack("x[C]L>/a", $str), $utf8), 5 + $len);
186             },
187              
188              
189             (0xDC) => sub { #array16
190 0     0   0 my ($str, $utf8) = @_;
191 0 0       0 return unless length($str) >= 3;
192 0         0 my $size = unpack('x[C]S>', $str);
193 0         0 return _extract_array_elements($str, 3, $size, $utf8);
194             },
195             (0xDD) => sub { #array32
196 0     0   0 my ($str, $utf8) = @_;
197 0 0       0 return unless length($str) >= 5;
198 0         0 my $size = unpack('x[C]L>', $str);
199 0         0 return _extract_array_elements($str, 5, $size, $utf8);
200             },
201            
202             (0xDE) => sub { #map16
203 1     1   3 my ($str, $utf8) = @_;
204 1 50       6 return unless length($str) >= 3;
205 1         4 my $size = unpack('x[C]S>', $str);
206 1         4 return _extract_hash_elements($str, 3, $size, $utf8);
207             },
208             (0xDF) => sub { #map32
209 0     0   0 my ($str, $utf8) = @_;
210 0 0       0 return unless length($str) >= 5;
211 0         0 my $size = unpack('x[C]L>', $str);
212 0         0 return _extract_hash_elements($str, 5, $size, $utf8);
213             },
214              
215             (0xCA) => sub { # float32
216 0     0   0 my ($str, $utf8) = @_;
217 0 0       0 return unless length($str) >= 5;
218 0         0 return (unpack('x[C]f>', $str), 5);
219             },
220             (0xCB) => sub { # float64
221 1     1   4 my ($str, $utf8) = @_;
222 1 50       4 return unless length($str) >= 9;
223 1         6 return (unpack('x[C]d>', $str), 9);
224             },
225 89         376 };
226              
227 89 50       366 return $variant->{$tag}($str, $utf8) if exists $variant->{$tag};
228            
229              
230 0         0 warn sprintf "%02X", $tag;
231 0         0 return;
232              
233              
234              
235              
236             }
237              
238             sub msgunpack($) {
239 263     263 1 557 my ($str) = @_;
240 263         523 my ($o, $len) = _msgunpack($str, 0);
241 263 50       636 croak 'Input buffer does not contain valid msgpack' unless defined $len;
242 263         828 return $o;
243             }
244              
245             sub msgunpack_utf8($) {
246 8     8 1 21 my ($str) = @_;
247 8         21 my ($o, $len) = _msgunpack($str, 1);
248 8 50       22 croak 'Input buffer does not contain valid msgpack' unless defined $len;
249 8         38 return $o;
250             }
251              
252             sub msgunpack_check($) {
253 193     193 0 431 my ($str) = @_;
254 193         359 my ($o, $len) = _msgunpack($str, 1);
255 193   100     754 return $len // 0;
256             }
257              
258             sub msgunpack_safely($) {
259 0     0 1 0 push @_ => 0;
260 0         0 goto \&_msgunpack;
261             }
262              
263             sub msgunpack_safely_utf8($) {
264 0     0 1 0 push @_ => 1;
265 0         0 goto \&_msgunpack;
266             }
267              
268             sub looks_like_number($) {
269 131576     131576 0 157262 my ($v) = @_;
270 131576 100       233137 return 0 unless Scalar::Util::looks_like_number($v);
271              
272              
273 131480         133595 state $MAX_INT = unpack('J', pack('j', -1));
274              
275 131480 100       194469 return 0 if $v == 'Infinity';
276 131479 50       178900 return 0 if $v == '-Infinity';
277              
278 131479 100       183764 if ($v == int $v) {
279 131475 100       187514 return 0 unless $v <= $MAX_INT;
280             }
281 131478         190331 return 1;
282             }
283              
284             sub msgpack($);
285             sub msgpack($) {
286 131699     131699 1 295977 my ($v) = @_;
287              
288 131699 100       174364 if (ref $v) {
289 123 100       573 if ('ARRAY' eq ref $v) {
    100          
    100          
    50          
290 24         44 my $size = @$v;
291 24         32 my $res;
292              
293 24 100       58 if ($size <= 0xF) {
    100          
294 21         65 $res = pack 'C', 0x90 | $size;
295             } elsif ($size <= 0xFFFF) {
296 2         11 $res = pack 'CS>', 0xDC, $size;
297             } else {
298 1         5 $res = pack 'CL>', 0xDD, $size;
299             }
300              
301 24         97 $res .= msgpack($_) for @$v;
302 24         2368 return $res;
303              
304             } elsif ('HASH' eq ref $v) {
305 28         78 my $size = scalar keys %$v;
306            
307 28         51 my $res;
308              
309 28 100       64 if ($size <= 0xF) {
    50          
310 26         86 $res = pack 'C', 0x80 | $size;
311             } elsif ($size <= 0xFFFF) {
312 2         11 $res = pack 'CS>', 0xDE, $size;
313             } else {
314 0         0 $res = pack 'CL>', 0xDF, $size;
315             }
316              
317 28         116 while (my ($k, $v) = each %$v) {
318 137         319 $res .= msgpack($k);
319 137         323 $res .= msgpack($v);
320             }
321 28         263 return $res;
322             } elsif ('SCALAR' eq ref $v) {
323 9 100       48 return pack 'C', 0xC3 if $$v;
324 4         20 return pack 'C', 0xC2;
325             } elsif (Scalar::Util::blessed $v) {
326 62 100       384 return $v->TO_MSGPACK if $v->can('TO_MSGPACK');
327              
328 12         30 my @l = ($v);
329 12 50       41 if ($v->can('TO_JSON')) {
330 0         0 push @l => $v->TO_JSON;
331             }
332            
333 12         28 for (@l) {
334 12 50       31 if ('JSON::XS::Boolean' eq ref $_) {
335 0 0       0 return pack 'C', 0xC3 if $_;
336 0         0 return pack 'C', 0xC2;
337             }
338 12 50       25 if ('Types::Serialiser::Boolean' eq ref $_) {
339 0 0       0 return pack 'C', 0xC3 if $_;
340 0         0 return pack 'C', 0xC2;
341             }
342 12 50       29 if ('JSON::PP::Boolean' eq ref $_) {
343 12 100       82 return pack 'C', 0xC3 if $_;
344 6         76 return pack 'C', 0xC2;
345             }
346             }
347             # TO_JSON return pure perl object
348 0 0       0 return msgpack($l[1]) if @l == 2;
349              
350 0         0 croak "Can't msgpack blessed value " . ref $v;
351             } else {
352 0         0 croak "Can't msgpack value " . ref $v;
353             }
354             } else {
355             # numbers
356 131576 100       155141 if (looks_like_number $v) {
357 131478 100       173858 if ($v == int $v) {
358 131474 100       180031 if ($v >= 0) {
359 131442 100       161831 if ($v <= 0x7F) {
    100          
    100          
    100          
360 131394         381575 return pack 'C', $v;
361             } elsif ($v <= 0xFF) {
362 7         52 return pack 'CC', 0xCC, $v;
363             } elsif ($v <= 0xFFFF) {
364 12         87 return pack 'CS>', 0xCD, $v;
365             } elsif ($v <= 0xFFFF_FFFF) {
366 17         124 return pack 'CL>', 0xCE, $v;
367             } else {
368 12         81 return pack 'CQ>', 0xCF, $v;
369             }
370             }
371 32 100       97 if ($v >= - 0x20) {
    100          
    100          
    100          
372 7         71 return pack 'c', $v;
373             } elsif ($v >= -0x7F - 1) {
374 4         52 return pack 'Cc', 0xD0, $v;
375             } elsif ($v >= -0x7F_FF - 1) {
376 6         39 return pack 'Cs>', 0xD1, $v;
377             } elsif ($v >= -0x7FFF_FFFF - 1) {
378 8         56 return pack 'Cl>', 0xD2, $v;
379             } else {
380 7         48 return pack 'Cq>', 0xD3, $v;
381             }
382             } else {
383 4         47 return pack 'Cd>', 0xCB, $v;
384             }
385              
386             } else {
387 98 100       246 unless (defined $v) { # undef
388 2         15 return pack 'C', 0xC0;
389             }
390 96 100       259 if (utf8::is_utf8 $v) {
391 10         28 utf8::encode $v;
392             }
393             # strings
394 96 100       257 if (length($v) <= 0x1F) {
    100          
    100          
395 60         391 return pack 'Ca*',
396             (0xA0 | length $v),
397             $v;
398             } elsif (length($v) <= 0xFF) {
399 14         123 return pack 'CCa*',
400             0xD9,
401             length $v,
402             $v;
403             } elsif (length($v) <= 0xFFFF) {
404 13         804 return pack 'CS>a*',
405             0xDA,
406             length $v,
407             $v;
408             } else {
409 9         498 return pack 'CL>a*',
410             0xDB,
411             length $v,
412             $v;
413             }
414              
415             }
416             }
417             }
418              
419             =head1 NAME
420              
421             DR::Tnt::Msgpack - msgpack encoder/decoder.
422              
423             =head1 SYNOPSIS
424              
425             use DR::Tnt::Msgpack;
426             use DR::Tnt::Msgpack::Types ':all'; # mp_*
427              
428            
429             my $blob = msgpack { a => 'b', c => 123, d => [ 3, 4, 5 ] };
430            
431             my $object = msgunpack $blob;
432             my $object = msgunpack_utf8 $blob;
433            
434            
435             my ($object, $len) = msgunpack_safely $blob;
436             my ($object, $len) = msgunpack_safely_utf8 $blob;
437              
438             if (defined $len) {
439             substr $blob, 0, $len, '';
440             ...
441             }
442              
443             if (my $len = msgunpack_check $blob) {
444             # $blob contains msgpack with len=$len
445             }
446              
447             =head1 METHODS
448              
449             =head2 msgpack
450              
451             my $blob = msgpack $scalar;
452             my $blob = msgpack \%hash;
453             my $blob = msgpack \@array;
454              
455             Pack any perl object to msgpack. Blessed objects have to have C
456             methods.
457              
458             =head2 msgunpack
459              
460             Unpack msgpack'ed string to perl object. Throws exception if buffer is invalid.
461             Booleans are extracted to L,
462             see also L.
463              
464             =head2 msgunpack_utf8
465              
466             The same as L. Decode utf8 for each string.
467              
468             =head2 msgunpack_safely, msgunpack_safely_utf8
469              
470             Unpack msgpack'ed string to perl object.
471             Don't throw exception if buffer is invalid.
472              
473             Return unpacked object and length of unpacked object. If length is C,
474             buffer is invalid.
475              
476             =cut
477              
478             1;