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   185142 use utf8;
  11         36  
  11         44  
2 11     11   251 use strict;
  11         14  
  11         145  
3 11     11   33 use warnings;
  11         11  
  11         307  
4              
5             package DR::Tnt::Msgpack;
6 11     11   46 use base qw(Exporter);
  11         22  
  11         1127  
7             our @EXPORT = qw(msgpack msgunpack msgunpack_check msgunpack_utf8);
8 11     11   55 use Scalar::Util ();
  11         14  
  11         214  
9 11     11   51 use Carp;
  11         14  
  11         637  
10             $Carp::Internal{ (__PACKAGE__) }++;
11 11     11   59 use feature 'state';
  11         12  
  11         1189  
12 11     11   2900 use DR::Tnt::Msgpack::Types ':all';
  11         19  
  11         26207  
13              
14             sub _retstr($$) {
15 74     74   129 my ($str, $utf8) = @_;
16 74 100       411 utf8::decode $str if $utf8;
17 74         170 return $str;
18             }
19              
20             sub _msgunpack($$);
21             sub _extract_hash_elements($$$$) {
22 39     39   42 my ($str, $len, $size, $utf8) = @_;
23              
24 39         34 my %o;
25 39         45 for (my $i = 0; $i < $size; $i++) {
26 122         189 my ($k, $klen) = _msgunpack(substr($str, $len), $utf8);
27 122 50       202 return unless defined $klen;
28 122         90 $len += $klen;
29              
30 122         159 my ($v, $vlen) = _msgunpack(substr($str, $len), $utf8);
31 122 50       178 return unless defined $vlen;
32 122         90 $len += $vlen;
33              
34 122         217 $o{$k} = $v;
35             }
36 39         62 return \%o, $len;
37             }
38              
39             sub _extract_array_elements($$$$) {
40 25     25   30 my ($str, $len, $size, $utf8) = @_;
41              
42 25         23 my @o;
43 25         35 for (my $i = 0; $i < $size; $i++) {
44 105         158 my ($e, $elen) = _msgunpack(substr($str, $len), $utf8);
45 105 50       150 return unless defined $elen;
46 105         82 $len += $elen;
47 105         158 push @o => $e;
48             }
49 25         45 return \@o, $len;
50             }
51              
52              
53             sub _msgunpack($$) {
54 817     817   1674 my ($str, $utf8) = @_;
55              
56 817 100 66     1584 return unless defined $str and length $str;
57              
58 810         854 my $tag = unpack 'C', $str;
59              
60             # fix uint
61 810 100       1089 return ($tag, 1) if $tag <= 0x7F;
62            
63             # fix negative
64 223 100       251 return (unpack('c', $str), 1) if $tag >= 0xE0;
65              
66             # fix str
67 219 100       280 if (($tag & ~0x1F) == 0xA0) {
68 67         57 my $len = $tag & 0x1F;
69 67 50       84 return unless length($str) >= 1 + $len;
70 67 100       85 return '', 1 unless $len;
71 63         156 return (_retstr(unpack("x[C]a$len", $str), $utf8), 1 + $len);
72             }
73              
74             # fix map
75 152 100       167 if (($tag & ~0x0F) == 0x80) {
76 38         24 my $size = $tag & 0x0F;
77 38         46 return _extract_hash_elements($str, 1, $size, $utf8);
78             }
79              
80             # fix array
81 114 100       136 if (($tag & ~0x0F) == 0x90) {
82 25         25 my $size = $tag & 0x0F;
83 25         35 return _extract_array_elements($str, 1, $size, $utf8);
84             }
85              
86              
87             state $variant = {
88             (0xD0) => sub { # int8
89 1     1   2 my ($str) = @_;
90 1 50       4 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       15 return unless length($str) >= 3;
96 7         18 return (unpack('x[C]s>', $str), 3);
97             },
98             (0xD2) => sub { # int32
99 9     9   14 my ($str) = @_;
100 9 50       17 return unless length($str) >= 5;
101 9         25 return (unpack('x[C]l>', $str), 5);
102             },
103             (0xD3) => sub { # int64
104 7     7   12 my ($str) = @_;
105 7 50       14 return unless length($str) >= 9;
106 7         20 return (unpack('x[C]q>', $str), 9);
107             },
108              
109              
110             (0xCC) => sub { # uint8
111 4     4   7 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 9     9   12 my ($str) = @_;
117 9 50       18 return unless length($str) >= 3;
118 9         22 return (unpack('x[C]S>', $str), 3);
119             },
120             (0xCE) => sub { # uint32
121 22     22   30 my ($str) = @_;
122 22 50       53 return unless length($str) >= 5;
123 22         67 return (unpack('x[C]L>', $str), 5);
124             },
125             (0xCF) => sub { # uint64
126 9     9   15 my ($str) = @_;
127 9 50       15 return unless length($str) >= 9;
128 9         21 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   3 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   5 my ($str) = @_;
144 3 50       5 return unless length($str) >= 2;
145 3         13 my $len = unpack('x[C]C', $str);
146 3 50       8 return unless length($str) >= 2 + $len;
147 3         10 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         14 return (unpack("x[C]S>/a", $str), 3 + $len);
155             },
156             (0xC6) => sub { # bin32
157 1     1   3 my ($str) = @_;
158 1 50       3 return unless length($str) >= 5;
159 1         3 my $len = unpack('x[C]L>', $str);
160 1 50       4 return unless length($str) >= 5 + $len;
161 1         793 return (unpack("x[C]L>/a", $str), 5 + $len);
162             },
163              
164              
165             (0xD9) => sub { # str8
166 4     4   9 my ($str, $utf8) = @_;
167 4 50       10 return unless length($str) >= 2;
168 4         10 my ($len) = unpack('x[C]C', $str);
169 4 50       10 return unless length($str) >= 2 + $len;
170 4         12 return (_retstr(unpack("x[C]C/a", $str), $utf8), 2 + $len);
171             },
172             (0xDA) => sub { # str16
173 4     4   10 my ($str, $utf8) = @_;
174 4 50       12 return unless length($str) >= 3;
175 4         13 my $len = unpack('x[C]S>', $str);
176 4 50       8 return unless length($str) >= 3 + $len;
177 4         162 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       10 return unless length($str) >= 5 + $len;
185 3         974 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       4 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   3 my ($str, $utf8) = @_;
222 1 50       4 return unless length($str) >= 9;
223 1         4 return (unpack('x[C]d>', $str), 9);
224             },
225 89         234 };
226              
227 89 50       225 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 265     265 1 273 my ($str) = @_;
240 265         267 my ($o, $len) = _msgunpack($str, 0);
241 265 50       340 croak 'Input buffer does not contain valid msgpack' unless defined $len;
242 265         459 return $o;
243             }
244              
245             sub msgunpack_utf8($) {
246 8     8 1 13 my ($str) = @_;
247 8         15 my ($o, $len) = _msgunpack($str, 1);
248 8 50       19 croak 'Input buffer does not contain valid msgpack' unless defined $len;
249 8         27 return $o;
250             }
251              
252             sub msgunpack_check($) {
253 195     195 0 167 my ($str) = @_;
254 195         166 my ($o, $len) = _msgunpack($str, 1);
255 195   100     340 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 100166 my ($v) = @_;
270 131576 100       143940 return 0 unless Scalar::Util::looks_like_number($v);
271              
272              
273 131480         86414 state $MAX_INT = unpack('J', pack('j', -1));
274              
275 131480 100       124967 return 0 if $v == 'Infinity';
276 131479 50       124109 return 0 if $v == '-Infinity';
277              
278 131479 100       123408 if ($v == int $v) {
279 131475 100       127893 return 0 unless $v <= $MAX_INT;
280             }
281 131478         124198 return 1;
282             }
283              
284             sub msgpack($);
285             sub msgpack($) {
286 131699     131699 1 189111 my ($v) = @_;
287              
288 131699 100       113242 if (ref $v) {
289 123 100       335 if ('ARRAY' eq ref $v) {
    100          
    100          
    50          
290 24         27 my $size = @$v;
291 24         22 my $res;
292              
293 24 100       43 if ($size <= 0xF) {
    100          
294 21         39 $res = pack 'C', 0x90 | $size;
295             } elsif ($size <= 0xFFFF) {
296 2         7 $res = pack 'CS>', 0xDC, $size;
297             } else {
298 1         7 $res = pack 'CL>', 0xDD, $size;
299             }
300              
301 24         66 $res .= msgpack($_) for @$v;
302 24         1816 return $res;
303              
304             } elsif ('HASH' eq ref $v) {
305 28         41 my $size = scalar keys %$v;
306            
307 28         24 my $res;
308              
309 28 100       41 if ($size <= 0xF) {
    50          
310 26         50 $res = pack 'C', 0x80 | $size;
311             } elsif ($size <= 0xFFFF) {
312 2         9 $res = pack 'CS>', 0xDE, $size;
313             } else {
314 0         0 $res = pack 'CL>', 0xDF, $size;
315             }
316              
317 28         66 while (my ($k, $v) = each %$v) {
318 137         171 $res .= msgpack($k);
319 137         166 $res .= msgpack($v);
320             }
321 28         94 return $res;
322             } elsif ('SCALAR' eq ref $v) {
323 9 100       24 return pack 'C', 0xC3 if $$v;
324 4         14 return pack 'C', 0xC2;
325             } elsif (Scalar::Util::blessed $v) {
326 62 100       227 return $v->TO_MSGPACK if $v->can('TO_MSGPACK');
327              
328 12         20 my @l = ($v);
329 12 50       29 if ($v->can('TO_JSON')) {
330 0         0 push @l => $v->TO_JSON;
331             }
332            
333 12         16 for (@l) {
334 12 50       21 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       18 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       17 if ('JSON::PP::Boolean' eq ref $_) {
343 12 100       64 return pack 'C', 0xC3 if $_;
344 6         65 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       105057 if (looks_like_number $v) {
357 131478 100       118146 if ($v == int $v) {
358 131474 100       120953 if ($v >= 0) {
359 131442 100       104184 if ($v <= 0x7F) {
    100          
    100          
    100          
360 131394         218905 return pack 'C', $v;
361             } elsif ($v <= 0xFF) {
362 7         38 return pack 'CC', 0xCC, $v;
363             } elsif ($v <= 0xFFFF) {
364 11         65 return pack 'CS>', 0xCD, $v;
365             } elsif ($v <= 0xFFFF_FFFF) {
366 18         84 return pack 'CL>', 0xCE, $v;
367             } else {
368 12         72 return pack 'CQ>', 0xCF, $v;
369             }
370             }
371 32 100       87 if ($v >= - 0x20) {
    100          
    100          
    100          
372 7         57 return pack 'c', $v;
373             } elsif ($v >= -0x7F - 1) {
374 4         43 return pack 'Cc', 0xD0, $v;
375             } elsif ($v >= -0x7F_FF - 1) {
376 6         37 return pack 'Cs>', 0xD1, $v;
377             } elsif ($v >= -0x7FFF_FFFF - 1) {
378 8         45 return pack 'Cl>', 0xD2, $v;
379             } else {
380 7         41 return pack 'Cq>', 0xD3, $v;
381             }
382             } else {
383 4         44 return pack 'Cd>', 0xCB, $v;
384             }
385              
386             } else {
387 98 100       165 unless (defined $v) { # undef
388 2         10 return pack 'C', 0xC0;
389             }
390 96 100       202 if (utf8::is_utf8 $v) {
391 10         20 utf8::encode $v;
392             }
393             # strings
394 96 100       180 if (length($v) <= 0x1F) {
    100          
    100          
395 60         268 return pack 'Ca*',
396             (0xA0 | length $v),
397             $v;
398             } elsif (length($v) <= 0xFF) {
399 14         101 return pack 'CCa*',
400             0xD9,
401             length $v,
402             $v;
403             } elsif (length($v) <= 0xFFFF) {
404 13         604 return pack 'CS>a*',
405             0xDA,
406             length $v,
407             $v;
408             } else {
409 9         437 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;