File Coverage

lib/DR/Tnt/Msgpack.pm
Criterion Covered Total %
statement 210 240 87.5
branch 122 174 70.1
condition 4 5 80.0
subroutine 36 42 85.7
pod 5 7 71.4
total 377 468 80.5


line stmt bran cond sub pod time code
1 11     11   207817 use utf8;
  11         37  
  11         45  
2 11     11   293 use strict;
  11         16  
  11         167  
3 11     11   38 use warnings;
  11         14  
  11         332  
4              
5             package DR::Tnt::Msgpack;
6 11     11   53 use base qw(Exporter);
  11         21  
  11         1160  
7             our @EXPORT = qw(msgpack msgunpack msgunpack_check msgunpack_utf8);
8 11     11   69 use Scalar::Util ();
  11         11  
  11         222  
9 11     11   51 use Carp;
  11         18  
  11         706  
10             $Carp::Internal{ (__PACKAGE__) }++;
11 11     11   59 use feature 'state';
  11         14  
  11         1319  
12 11     11   3276 use DR::Tnt::Msgpack::Types ':all';
  11         17  
  11         32565  
13              
14             sub _retstr($$) {
15 74     74   111 my ($str, $utf8) = @_;
16 74 100       418 utf8::decode $str if $utf8;
17 74         162 return $str;
18             }
19              
20             sub _msgunpack($$);
21             sub _extract_hash_elements($$$$) {
22 39     39   49 my ($str, $len, $size, $utf8) = @_;
23              
24 39         38 my %o;
25 39         57 for (my $i = 0; $i < $size; $i++) {
26 122         257 my ($k, $klen) = _msgunpack(substr($str, $len), $utf8);
27 122 50       1042 return unless defined $klen;
28 122         112 $len += $klen;
29              
30 122         171 my ($v, $vlen) = _msgunpack(substr($str, $len), $utf8);
31 122 50       232 return unless defined $vlen;
32 122         97 $len += $vlen;
33              
34 122         232 $o{$k} = $v;
35             }
36 39         88 return \%o, $len;
37             }
38              
39             sub _extract_array_elements($$$$) {
40 25     25   30 my ($str, $len, $size, $utf8) = @_;
41              
42 25         25 my @o;
43 25         39 for (my $i = 0; $i < $size; $i++) {
44 105         161 my ($e, $elen) = _msgunpack(substr($str, $len), $utf8);
45 105 50       163 return unless defined $elen;
46 105         77 $len += $elen;
47 105         187 push @o => $e;
48             }
49 25         47 return \@o, $len;
50             }
51              
52              
53             sub _msgunpack($$) {
54 813     813   2525 my ($str, $utf8) = @_;
55              
56 813 100 66     2414 return unless defined $str and length $str;
57              
58 806         950 my $tag = unpack 'C', $str;
59              
60             # fix uint
61 806 100       1331 return ($tag, 1) if $tag <= 0x7F;
62            
63             # fix negative
64 223 100       258 return (unpack('c', $str), 1) if $tag >= 0xE0;
65              
66             # fix str
67 219 100       275 if (($tag & ~0x1F) == 0xA0) {
68 67         67 my $len = $tag & 0x1F;
69 67 50       84 return unless length($str) >= 1 + $len;
70 67 100       98 return '', 1 unless $len;
71 63         154 return (_retstr(unpack("x[C]a$len", $str), $utf8), 1 + $len);
72             }
73              
74             # fix map
75 152 100       191 if (($tag & ~0x0F) == 0x80) {
76 38         34 my $size = $tag & 0x0F;
77 38         64 return _extract_hash_elements($str, 1, $size, $utf8);
78             }
79              
80             # fix array
81 114 100       141 if (($tag & ~0x0F) == 0x90) {
82 25         23 my $size = $tag & 0x0F;
83 25         41 return _extract_array_elements($str, 1, $size, $utf8);
84             }
85              
86              
87             state $variant = {
88             (0xD0) => sub { # int8
89 1     1   3 my ($str) = @_;
90 1 50       3 return unless length($str) >= 2;
91 1         3 return (unpack('x[C]c', $str), 2);
92             },
93             (0xD1) => sub { # int16
94 7     7   10 my ($str) = @_;
95 7 50       12 return unless length($str) >= 3;
96 7         21 return (unpack('x[C]s>', $str), 3);
97             },
98             (0xD2) => sub { # int32
99 9     9   15 my ($str) = @_;
100 9 50       15 return unless length($str) >= 5;
101 9         26 return (unpack('x[C]l>', $str), 5);
102             },
103             (0xD3) => sub { # int64
104 7     7   11 my ($str) = @_;
105 7 50       16 return unless length($str) >= 9;
106 7         18 return (unpack('x[C]q>', $str), 9);
107             },
108              
109              
110             (0xCC) => sub { # uint8
111 4     4   6 my ($str) = @_;
112 4 50       8 return unless length($str) >= 2;
113 4         10 return (unpack('x[C]C', $str), 2);
114             },
115             (0xCD) => sub { # uint16
116 11     11   30 my ($str) = @_;
117 11 50       39 return unless length($str) >= 3;
118 11         32 return (unpack('x[C]S>', $str), 3);
119             },
120             (0xCE) => sub { # uint32
121 20     20   29 my ($str) = @_;
122 20 50       33 return unless length($str) >= 5;
123 20         49 return (unpack('x[C]L>', $str), 5);
124             },
125             (0xCF) => sub { # uint64
126 9     9   13 my ($str) = @_;
127 9 50       13 return unless length($str) >= 9;
128 9         23 return (unpack('x[C]Q>', $str), 9);
129             },
130              
131             (0xC0) => sub { # null
132 1     1   3 return (undef, 1);
133             },
134              
135             (0xC2) => sub {
136 1     1   4 return (mp_false, 1); # false
137             },
138             (0xC3) => sub {
139 1     1   3 return (mp_true, 1); # true
140             },
141              
142             (0xC4) => sub { # bin8
143 3     3   6 my ($str) = @_;
144 3 50       4 return unless length($str) >= 2;
145 3         6 my $len = unpack('x[C]C', $str);
146 3 50       6 return unless length($str) >= 2 + $len;
147 3         11 return (unpack("x[C]C/a", $str), 2 + $len);
148             },
149             (0xC5) => sub { # bin16
150 1     1   2 my ($str) = @_;
151 1 50       3 return unless length($str) >= 3;
152 1         3 my $len = unpack('x[C]S>', $str);
153 1 50       3 return unless length($str) >= 3 + $len;
154 1         19 return (unpack("x[C]S>/a", $str), 3 + $len);
155             },
156             (0xC6) => sub { # bin32
157 1     1   2 my ($str) = @_;
158 1 50       4 return unless length($str) >= 5;
159 1         3 my $len = unpack('x[C]L>', $str);
160 1 50       3 return unless length($str) >= 5 + $len;
161 1         876 return (unpack("x[C]L>/a", $str), 5 + $len);
162             },
163              
164              
165             (0xD9) => sub { # str8
166 4     4   10 my ($str, $utf8) = @_;
167 4 50       10 return unless length($str) >= 2;
168 4         8 my ($len) = unpack('x[C]C', $str);
169 4 50       10 return unless length($str) >= 2 + $len;
170 4         11 return (_retstr(unpack("x[C]C/a", $str), $utf8), 2 + $len);
171             },
172             (0xDA) => sub { # str16
173 4     4   8 my ($str, $utf8) = @_;
174 4 50       9 return unless length($str) >= 3;
175 4         10 my $len = unpack('x[C]S>', $str);
176 4 50       10 return unless length($str) >= 3 + $len;
177 4         157 return (_retstr(unpack("x[C]S>/a", $str), $utf8), 3 + $len);
178             },
179              
180             (0xDB) => sub { # str32
181 3     3   8 my ($str, $utf8) = @_;
182 3 50       8 return unless length($str) >= 5;
183 3         8 my $len = unpack('x[C]L>', $str);
184 3 50       9 return unless length($str) >= 5 + $len;
185 3         990 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   2 my ($str, $utf8) = @_;
204 1 50       3 return unless length($str) >= 3;
205 1         3 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   2 my ($str, $utf8) = @_;
222 1 50       2 return unless length($str) >= 9;
223 1         4 return (unpack('x[C]d>', $str), 9);
224             },
225 89         265 };
226              
227 89 50       234 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 308 my ($str) = @_;
240 263         1653 my ($o, $len) = _msgunpack($str, 0);
241 263 50       341 croak 'Input buffer does not contain valid msgpack' unless defined $len;
242 263         483 return $o;
243             }
244              
245             sub msgunpack_utf8($) {
246 8     8 1 16 my ($str) = @_;
247 8         13 my ($o, $len) = _msgunpack($str, 1);
248 8 50       16 croak 'Input buffer does not contain valid msgpack' unless defined $len;
249 8         23 return $o;
250             }
251              
252             sub msgunpack_check($) {
253 193     193 0 239 my ($str) = @_;
254 193         217 my ($o, $len) = _msgunpack($str, 1);
255 193   100     397 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 131597     131597 0 118066 my ($v) = @_;
270            
271 131597         106210 state $MAX_INT = unpack('J', pack('j', -1));
272              
273 131597         112416 for ($v) {
274 131597 100       141943 return 0 unless defined $_;
275              
276 131595 100       163424 return 0 unless Scalar::Util::looks_like_number($_);
277             # phones
278 131501 50       173363 return 0 if /\s/;
279 131501 100       157810 return 0 if /^\s*\+\s*/;
280            
281              
282 131500 100       146889 return 0 if $_ == 'Infinity';
283 131499 50       138513 return 0 if $_ == '-Infinity';
284              
285 131499 100       140779 if ($_ == int $_) {
286 131491 100       145787 return 0 unless $_ <= $MAX_INT;
287             }
288            
289 131498 100       161774 unless ($v eq (0 + $v)) {
290 10 100       51 return 1 if /-?(?:\d+\.\d*|\.\d+)$/;
291 8 50       28 return 0 unless /^-?(\d+(?:\.\d*)?|\.\d+)e-?\d+$/;
292             }
293              
294 131496         159104 return 1;
295             }
296             }
297              
298             sub msgpack($);
299             sub msgpack($) {
300 131720     131720 1 226107 my ($v) = @_;
301              
302 131720 100       131367 if (ref $v) {
303 123 100       379 if ('ARRAY' eq ref $v) {
    100          
    100          
    50          
304 24         29 my $size = @$v;
305 24         27 my $res;
306              
307 24 100       42 if ($size <= 0xF) {
    100          
308 21         43 $res = pack 'C', 0x90 | $size;
309             } elsif ($size <= 0xFFFF) {
310 2         6 $res = pack 'CS>', 0xDC, $size;
311             } else {
312 1         6 $res = pack 'CL>', 0xDD, $size;
313             }
314              
315 24         73 $res .= msgpack($_) for @$v;
316 24         1827 return $res;
317              
318             } elsif ('HASH' eq ref $v) {
319 28         47 my $size = scalar keys %$v;
320            
321 28         26 my $res;
322              
323 28 100       45 if ($size <= 0xF) {
    50          
324 26         65 $res = pack 'C', 0x80 | $size;
325             } elsif ($size <= 0xFFFF) {
326 2         7 $res = pack 'CS>', 0xDE, $size;
327             } else {
328 0         0 $res = pack 'CL>', 0xDF, $size;
329             }
330              
331 28         75 while (my ($k, $v) = each %$v) {
332 137         193 $res .= msgpack($k);
333 137         184 $res .= msgpack($v);
334             }
335 28         111 return $res;
336             } elsif ('SCALAR' eq ref $v) {
337 9 100       37 return pack 'C', 0xC3 if $$v;
338 4         15 return pack 'C', 0xC2;
339             } elsif (Scalar::Util::blessed $v) {
340 62 100       238 return $v->TO_MSGPACK if $v->can('TO_MSGPACK');
341              
342 12         21 my @l = ($v);
343 12 50       27 if ($v->can('TO_JSON')) {
344 0         0 push @l => $v->TO_JSON;
345             }
346            
347 12         14 for (@l) {
348 12 50       21 if ('JSON::XS::Boolean' eq ref $_) {
349 0 0       0 return pack 'C', 0xC3 if $_;
350 0         0 return pack 'C', 0xC2;
351             }
352 12 50       19 if ('Types::Serialiser::Boolean' eq ref $_) {
353 0 0       0 return pack 'C', 0xC3 if $_;
354 0         0 return pack 'C', 0xC2;
355             }
356 12 50       17 if ('JSON::PP::Boolean' eq ref $_) {
357 12 100       69 return pack 'C', 0xC3 if $_;
358 6         66 return pack 'C', 0xC2;
359             }
360             }
361             # TO_JSON return pure perl object
362 0 0       0 return msgpack($l[1]) if @l == 2;
363              
364 0         0 croak "Can't msgpack blessed value " . ref $v;
365             } else {
366 0         0 croak "Can't msgpack value " . ref $v;
367             }
368             } else {
369             # numbers
370 131597 100       118770 if (looks_like_number $v) {
371 131498 100       129649 if ($v == int $v) {
372 131490 100       136127 if ($v >= 0) {
373 131450 100       119025 if ($v <= 0x7F) {
    100          
    100          
    100          
374 131398         281693 return pack 'C', $v;
375             } elsif ($v <= 0xFF) {
376 7         47 return pack 'CC', 0xCC, $v;
377             } elsif ($v <= 0xFFFF) {
378 16         89 return pack 'CS>', 0xCD, $v;
379             } elsif ($v <= 0xFFFF_FFFF) {
380 17         78 return pack 'CL>', 0xCE, $v;
381             } else {
382 12         50 return pack 'CQ>', 0xCF, $v;
383             }
384             }
385 40 100       83 if ($v >= - 0x20) {
    100          
    100          
    100          
386 11         49 return pack 'c', $v;
387             } elsif ($v >= -0x7F - 1) {
388 4         38 return pack 'Cc', 0xD0, $v;
389             } elsif ($v >= -0x7F_FF - 1) {
390 10         47 return pack 'Cs>', 0xD1, $v;
391             } elsif ($v >= -0x7FFF_FFFF - 1) {
392 8         38 return pack 'Cl>', 0xD2, $v;
393             } else {
394 7         40 return pack 'Cq>', 0xD3, $v;
395             }
396             } else {
397 8         57 return pack 'Cd>', 0xCB, $v;
398             }
399              
400             } else {
401 99 100       148 unless (defined $v) { # undef
402 2         15 return pack 'C', 0xC0;
403             }
404 97 100       171 if (utf8::is_utf8 $v) {
405 10         17 utf8::encode $v;
406             }
407             # strings
408 97 100       185 if (length($v) <= 0x1F) {
    100          
    100          
409 61         240 return pack 'Ca*',
410             (0xA0 | length $v),
411             $v;
412             } elsif (length($v) <= 0xFF) {
413 14         76 return pack 'CCa*',
414             0xD9,
415             length $v,
416             $v;
417             } elsif (length($v) <= 0xFFFF) {
418 13         580 return pack 'CS>a*',
419             0xDA,
420             length $v,
421             $v;
422             } else {
423 9         416 return pack 'CL>a*',
424             0xDB,
425             length $v,
426             $v;
427             }
428              
429             }
430             }
431             }
432              
433             =head1 NAME
434              
435             DR::Tnt::Msgpack - msgpack encoder/decoder.
436              
437             =head1 SYNOPSIS
438              
439             use DR::Tnt::Msgpack;
440             use DR::Tnt::Msgpack::Types ':all'; # mp_*
441              
442            
443             my $blob = msgpack { a => 'b', c => 123, d => [ 3, 4, 5 ] };
444            
445             my $object = msgunpack $blob;
446             my $object = msgunpack_utf8 $blob;
447            
448            
449             my ($object, $len) = msgunpack_safely $blob;
450             my ($object, $len) = msgunpack_safely_utf8 $blob;
451              
452             if (defined $len) {
453             substr $blob, 0, $len, '';
454             ...
455             }
456              
457             if (my $len = msgunpack_check $blob) {
458             # $blob contains msgpack with len=$len
459             }
460              
461             =head1 METHODS
462              
463             =head2 msgpack
464              
465             my $blob = msgpack $scalar;
466             my $blob = msgpack \%hash;
467             my $blob = msgpack \@array;
468              
469             Pack any perl object to msgpack. Blessed objects have to have C
470             methods.
471              
472             =head2 msgunpack
473              
474             Unpack msgpack'ed string to perl object. Throws exception if buffer is invalid.
475             Booleans are extracted to L,
476             see also L.
477              
478             =head2 msgunpack_utf8
479              
480             The same as L. Decode utf8 for each string.
481              
482             =head2 msgunpack_safely, msgunpack_safely_utf8
483              
484             Unpack msgpack'ed string to perl object.
485             Don't throw exception if buffer is invalid.
486              
487             Return unpacked object and length of unpacked object. If length is C,
488             buffer is invalid.
489              
490             =cut
491              
492             1;