File Coverage

blib/lib/Data/Smile/PP.pm
Criterion Covered Total %
statement 386 573 67.3
branch 207 394 52.5
condition 69 160 43.1
subroutine 34 44 77.2
pod 4 4 100.0
total 700 1175 59.5


line stmt bran cond sub pod time code
1 9     9   2223600 use 5.008008;
  9         40  
2 9     9   63 use strict;
  9         74  
  9         275  
3 9     9   44 use warnings;
  9         17  
  9         689  
4              
5             package Data::Smile::PP;
6              
7 9     9   56 use Scalar::Util qw( blessed dualvar looks_like_number );
  9         31  
  9         882  
8 9     9   63 use constant HEADER => "\x3A\x29\x0A\x03";
  9         20  
  9         770  
9 9     9   54 use constant MAX_SAFE_INT_DOUBLE => 9_007_199_254_740_992;
  9         16  
  9         587  
10 9     9   52 use constant MIN_SIGNED_64BIT => -9_223_372_036_854_775_808;
  9         88  
  9         490  
11 9     9   61 use constant MAX_UNSIGNED_64BIT => 18_446_744_073_709_551_615;
  9         26  
  9         801  
12              
13             our $AUTHORITY = 'cpan:TOBYINK';
14             our $VERSION = '0.001000';
15              
16 9     9   5187 use Exporter::Tiny;
  9         55633  
  9         83  
17             our @ISA = qw( Exporter::Tiny );
18             our @EXPORT_OK = qw( encode_smile decode_smile dump_smile load_smile );
19              
20             BEGIN {
21 9     9   484 my @HELPERS = (
22             _is_arrayref => [
23             [ 'Ref::Util::XS', 'is_plain_arrayref' ],
24             [ 'Type::Tiny::XS', 'ArrayRef' ],
25             q{
26             my ( $v ) = @_;
27             ref( $v ) eq 'ARRAY';
28             },
29             ],
30             _is_hashref => [
31             [ 'Ref::Util::XS', 'is_plain_hashref' ],
32             [ 'Type::Tiny::XS', 'HashRef' ],
33             q{
34             my ( $v ) = @_;
35             ref( $v ) eq 'HASH';
36             },
37             ],
38             _is_dual => [
39             [ 'Scalar::Util', 'isdual' ],
40             q{
41             use B ();
42             my $f = B::svref_2object(\$_[0])->FLAGS;
43             my $SVp_POK = eval { B::SVp_POK() } || 0;
44             my $SVp_IOK = eval { B::SVp_IOK() } || 0;
45             my $SVp_NOK = eval { B::SVp_NOK() } || 0;
46             my $pok = $f & ( B::SVf_POK | $SVp_POK );
47             my $niok = $f & ( B::SVf_IOK | B::SVf_NOK | $SVp_IOK | $SVp_NOK );
48             !!( $pok and $niok );
49             },
50             ],
51             _is_bool => [
52             [ 'builtin', 'is_bool' ],
53             q{
54             my $value = shift;
55             return !!0 unless defined $value;
56             return !!0 if ref $value;
57             return !!0 unless _is_dual( $value );
58             return !!1 if $value and "$value" eq '1' and $value + 0 == 1;
59             return !!1 if not $value and "$value" eq q'' and $value + 0 == 0;
60             return !!0;
61             },
62             ],
63             _created_as_number => [
64             [ 'builtin', 'created_as_number' ],
65             q{
66             use B ();
67             my $value = shift;
68             return !!0 unless defined $value;
69             return !!0 if ref $value;
70             return !!0 if utf8::is_utf8( $value );
71             my $b_obj = B::svref_2object(\$value);
72             my $flags = $b_obj->FLAGS;
73             return !!1 if $flags & ( B::SVp_IOK() | B::SVp_NOK() ) and not( $flags & B::SVp_POK() );
74             return !!0;
75             },
76             ],
77             _created_as_string => [
78             [ 'builtin', 'created_as_string' ],
79             q{
80             my $value = shift;
81             defined $value
82             and not ref $value
83             and not _is_bool( $value )
84             and not _created_as_number( $value );
85             },
86             ],
87             );
88              
89 9         53 HELPER: while ( @HELPERS ) {
90 54         207 my ( $name, $implementation_list ) = splice @HELPERS, 0, 2;
91 54         142 IMPLEMENTATION: for my $i ( @$implementation_list ) {
92 54 50       180 if ( ref $i ) {
93 9     9   3152 no strict 'refs';
  9         17  
  9         6521  
94 54         118 my ( $module, $external_function_name ) = @$i;
95 54 100       140 if ( $module eq 'builtin' ) {
96 27 50       86 $] >= 5.038 or next IMPLEMENTATION;
97 27         4963 require experimental;
98 27         43480 experimental->import( 'builtin' );
99 27 50       1040 if ( defined &{"builtin::$external_function_name"} ) {
  27         148  
100 27         44 *$name = \&{"builtin::$external_function_name"};
  27         183  
101 27         78661 next HELPER;
102             }
103             }
104             else {
105 27 50       2461 eval "require $module; 1" or next IMPLEMENTATION;
106 27 50       422 my $f = $module->can( $external_function_name ) or next IMPLEMENTATION;
107 27         147 *$name = $f;
108 27         167 next HELPER;
109             }
110             }
111             else {
112 0 0       0 next HELPER if eval "sub $name { $i }; 1";
113             }
114             }
115             }
116             }
117              
118             sub _check_opts {
119 46     46   134 my ( $name, $opts, $allowed ) = @_;
120 46 100       182 return {} if not defined $opts;
121 13 50       48 die 'Options must be a hash reference' if not _is_hashref( $opts );
122 13         48 for my $k ( keys %$opts ) {
123 14 100       96 die "Unknown option for $name: $k" if not $allowed->{$k};
124             }
125 10         29 return $opts;
126             }
127              
128 0     0   0 sub _pack_u32 { pack('N', $_[0]) }
129             sub _unpack_u32 {
130 0     0   0 my ( $buf, $pos ) = @_;
131 0 0       0 die 'Unexpected EOF' if $$pos + 4 > length $$buf;
132 0         0 my $v = unpack('N', substr($$buf, $$pos, 4));
133 0         0 $$pos += 4;
134 0         0 return $v;
135             }
136              
137             sub _pack_str {
138 0     0   0 my ( $s ) = @_;
139 0 0       0 my $flag = utf8::is_utf8( $s ) ? 1 : 0;
140 0         0 my $bytes = $s;
141 0 0       0 utf8::encode( $bytes ) if $flag;
142 0         0 return chr( $flag ) . _pack_u32( length( $bytes ) ) . $bytes;
143             }
144              
145             sub _unpack_str {
146 0     0   0 my ( $buf, $pos ) = @_;
147 0 0       0 die 'Unexpected EOF' if $$pos >= length $$buf;
148 0         0 my $flag = ord substr( $$buf, $$pos++, 1 );
149 0         0 my $len = _unpack_u32( $buf, $pos );
150 0 0       0 die 'Unexpected EOF' if $$pos + $len > length $$buf;
151 0         0 my $s = substr( $$buf, $$pos, $len );
152 0         0 $$pos += $len;
153 0 0       0 utf8::decode( $s ) if $flag;
154 0         0 return $s;
155             }
156              
157             sub _maybe_to_json {
158 49     49   95 my ( $v ) = @_;
159 49 100       125 return $v if not ref $v;
160 19 100       68 return $v if not blessed $v;
161 1 50       12 return $v if not $v->can( 'TO_JSON' );
162 1         6 return $v->TO_JSON;
163             }
164              
165             sub _encode_value {
166 0     0   0 my ( $v ) = @_;
167 0         0 $v = _maybe_to_json( $v );
168              
169 0 0       0 if ( not defined $v ) {
170 0         0 return 'U';
171             }
172              
173 0 0       0 if ( _is_arrayref( $v ) ) {
174 0         0 my $out = 'A' . _pack_u32(scalar @$v);
175 0         0 $out .= _encode_value( $_ ) for @$v;
176 0         0 return $out;
177             }
178              
179 0 0       0 if ( _is_hashref( $v ) ) {
180 0         0 my @keys = keys %$v;
181 0         0 my $out = 'H' . _pack_u32(scalar @keys);
182 0         0 for my $k (@keys) {
183 0         0 $out .= _pack_str( $k );
184 0         0 $out .= _encode_value( $v->{$k} );
185             }
186 0         0 return $out;
187             }
188              
189 0 0       0 die 'Unsupported reference in encode_smile' if ref $v;
190              
191 0 0       0 if ( _is_bool( $v ) ) {
192 0 0       0 return 'b' . ($v ? "\x01" : "\x00");
193             }
194              
195 0 0       0 if ( _created_as_string( $v ) ) {
196 0         0 return 'S' . _pack_str( $v );
197             }
198              
199 0 0 0     0 if ( _created_as_number( $v ) or looks_like_number( $v ) ) {
200 0 0 0     0 if ( $v =~ /\A-?[0-9]+\z/ and $v >= -9_223_372_036_854_775_808 and $v <= 9_223_372_036_854_775_807 ) {
      0        
201 0         0 return 'I' . pack('q>', 0 + $v);
202             }
203 0         0 my $n = "$v";
204 0         0 return 'N' . _pack_str( $n );
205             }
206              
207 0         0 return 'S' . _pack_str( "$v" );
208             }
209              
210             my ( $jppT, $jppF );
211              
212             sub _decode_value {
213 0     0   0 my ( $buf, $pos, $json_bool, $use_bigint ) = @_;
214 0 0       0 die 'Unexpected EOF' if $$pos >= length $$buf;
215 0         0 my $tag = substr $$buf, $$pos++, 1;
216              
217 0 0       0 return undef if $tag eq 'U';
218 0 0       0 if ( $tag eq 'b' ) {
219 0 0       0 die 'Unexpected EOF' if $$pos >= length $$buf;
220 0 0       0 if ( $json_bool ) {
221 0 0       0 if ( not defined $jppT ) {
222 0         0 require JSON::PP;
223 0         0 $jppT = JSON::PP::true();
224 0         0 $jppF = JSON::PP::false();
225             }
226 0 0       0 return ord substr( $$buf, $$pos++, 1 ) ? $jppT : $jppF;
227             }
228 0 0       0 return ord substr( $$buf, $$pos++, 1 ) ? !!1 : !!0;
229             }
230 0 0       0 if ( $tag eq 'S' ) {
231 0         0 return _unpack_str( $buf, $pos );
232             }
233 0 0       0 if ( $tag eq 'I' ) {
234 0 0       0 die 'Unexpected EOF' if $$pos + 8 > length $$buf;
235 0         0 my $v = unpack( 'q>', substr( $$buf, $$pos, 8 ) );
236 0         0 $$pos += 8;
237 0         0 return 0 + $v;
238             }
239 0 0       0 if ( $tag eq 'N' ) {
240 0         0 my $s = _unpack_str( $buf, $pos );
241              
242 0 0 0     0 if ( $use_bigint and $s =~ /\A-?[0-9]{19,}\z/ ) {
243 0         0 require Math::BigInt;
244 0         0 return Math::BigInt->new( $s );
245             }
246              
247 0         0 return 0 + $s;
248             }
249 0 0       0 if ( $tag eq 'A' ) {
250 0         0 my $n = _unpack_u32( $buf, $pos );
251 0         0 my @a;
252 0         0 push @a, _decode_value( $buf, $pos, $json_bool, $use_bigint ) for 1..$n;
253 0         0 return \@a;
254             }
255 0 0       0 if ( $tag eq 'H' ) {
256 0         0 my $n = _unpack_u32( $buf, $pos );
257 0         0 my %h;
258 0         0 for (1..$n) {
259 0         0 my $k = _unpack_str( $buf, $pos );
260 0         0 $h{$k} = _decode_value( $buf, $pos, $json_bool, $use_bigint );
261             }
262 0         0 return \%h;
263             }
264              
265 0         0 die "Unknown type tag in payload";
266             }
267              
268              
269             sub _normalize_smile_float {
270 5133     5133   10646 my ( $v ) = @_;
271 5133         25776 my $text = "$v";
272 5133         9835 my $num = 0 + $v;
273 5133         18675 my $integer_text = sprintf '%.0f', $num;
274              
275 5133 100 100     29580 if (
      100        
      100        
276             $num == 0 + $integer_text
277             and abs( $num ) >= MAX_SAFE_INT_DOUBLE
278             and $num >= MIN_SIGNED_64BIT
279             and $num <= MAX_UNSIGNED_64BIT
280             ) {
281 198         1189 return dualvar( $num, $integer_text );
282             }
283              
284 4935         24969 return dualvar( $num, $text );
285             }
286              
287             sub _decode_vint {
288 8596     8596   16019 my ( $buf, $pos ) = @_;
289 8596         16743 my $v = 0;
290              
291 8596         18459 while (1) {
292 21929 50       45771 die 'Unexpected EOF' if $$pos >= length $$buf;
293 21929         38800 my $byte = ord substr( $$buf, $$pos++, 1 );
294 21929 100       65424 if ( $byte & 0x80 ) {
295 8596         15843 $v = ( $v << 6 ) | ( $byte & 0x3F );
296 8596         15984 last;
297             }
298 13333         24235 $v = ( $v << 7 ) | ( $byte & 0x7F );
299             }
300              
301 8596         20086 return $v;
302             }
303              
304             sub _zigzag_decode {
305 11865     11865   22531 my ( $v ) = @_;
306 11865 100       43018 return ( $v & 1 ) ? -( ( $v + 1 ) >> 1 ) : ( $v >> 1 );
307             }
308              
309              
310             sub _decode_fixed_7bit_bytes {
311 5133     5133   11475 my ( $buf, $pos, $groups, $bits_needed ) = @_;
312 5133         8923 my $bits = q{};
313              
314 5133         13225 for ( 1 .. $groups ) {
315 51330 50       121218 die 'Unexpected EOF' if $$pos >= length $$buf;
316 51330         94585 my $byte = ord substr( $$buf, $$pos++, 1 );
317 51330         125733 $bits .= sprintf '%07b', $byte & 0x7F;
318             }
319              
320 5133 50 33     22964 if ( defined $bits_needed and length( $bits ) > $bits_needed ) {
321 5133         11108 $bits = substr( $bits, -$bits_needed );
322             }
323              
324 5133         13642 my $out = q{};
325 5133         14129 for ( my $i = 0; $i < length $bits; $i += 8 ) {
326 41064         140141 $out .= chr oct( '0b' . substr( $bits, $i, 8 ) );
327             }
328              
329 5133         19711 return $out;
330             }
331              
332             sub _decode_7bit_binary {
333 4     4   10 my ( $buf, $pos, $len ) = @_;
334 4         10 my $bits = q{};
335              
336 4         15 while ( length $bits < $len * 8 ) {
337 60 50       116 die 'Unexpected EOF' if $$pos >= length $$buf;
338 60         99 my $byte = ord substr( $$buf, $$pos++, 1 );
339 60         185 $bits .= sprintf '%07b', $byte & 0x7F;
340             }
341              
342 4         11 $bits = substr( $bits, 0, $len * 8 );
343 4         9 my $out = q{};
344 4         34 for ( my $i = 0; $i < length $bits; $i += 8 ) {
345 52         139 $out .= chr oct( '0b' . substr( $bits, $i, 8 ) );
346             }
347              
348 4         13 return $out;
349             }
350              
351              
352             sub _raw_bytes_to_native_number {
353 2     2   31 my ( $raw ) = @_;
354 2         5 my $v = 0.0;
355              
356 2         9 for my $byte ( unpack 'C*', $raw ) {
357 26         82 $v = ( $v * 256 ) + $byte;
358             }
359              
360 2         13 return 0 + $v;
361             }
362              
363             sub _decode_long_text {
364 472     472   1039 my ( $buf, $pos, $utf8 ) = @_;
365 472         847 my $s = q{};
366              
367 472         782 while (1) {
368 178546 50       371744 die 'Unexpected EOF' if $$pos >= length $$buf;
369 178546         311853 my $byte = ord substr( $$buf, $$pos++, 1 );
370 178546 100       364493 last if $byte == 0xFC;
371 178074         293695 $s .= chr $byte;
372             }
373              
374 472 100       1303 utf8::decode( $s ) if $utf8;
375 472         4557 return $s;
376             }
377              
378             sub _shared_push {
379 4046     4046   13578 my ( $ary, $v ) = @_;
380 4046 50       10200 return if not defined $v;
381              
382 4046 100       8441 if ( @$ary >= 1024 ) {
383 1         475 @$ary = ();
384             }
385              
386 4046         10584 push @$ary, $v;
387             }
388              
389             sub _decode_value_smile {
390 53161     53161   112844 my ( $buf, $pos, $ctx, $json_bool, $use_bigint ) = @_;
391 53161 50       108820 die 'Unexpected EOF' if $$pos >= length $$buf;
392 53161         109768 my $t = ord substr( $$buf, $$pos++, 1 );
393              
394 53161 100       114278 if ( $t <= 0x1F ) {
395 11414 50 33     46725 die 'Invalid shared value reference' if $t == 0 or not defined $ctx->{shared_values}[ $t - 1 ];
396 11414         44893 return $ctx->{shared_values}[ $t - 1 ];
397             }
398 41747 100       90579 if ( $t == 0x20 ) {
399 3 50       14 _shared_push( $ctx->{shared_values}, q{} ) if $ctx->{enable_shared_values};
400 3         12 return q{};
401             }
402 41744 100       85130 if ( $t == 0x21 ) {
403 6         32 return undef;
404             }
405 41738 100 100     143187 if ( $t == 0x22 or $t == 0x23 ) {
406 5         13 my $is_true = $t == 0x23;
407 5 50       15 if ( $json_bool ) {
408 5 100       13 if ( not defined $jppT ) {
409 1         34 require JSON::PP;
410 1         8 $jppT = JSON::PP::true();
411 1         8 $jppF = JSON::PP::false();
412             }
413 5 100       36 return $is_true ? $jppT : $jppF;
414             }
415 0 0       0 return $is_true ? !!1 : !!0;
416             }
417 41733 100 66     129354 if ( $t == 0x24 or $t == 0x25 ) {
418 8592         18949 my $v = _zigzag_decode( _decode_vint( $buf, $pos ) );
419 8592         25739 return $v;
420             }
421 33141 100       80776 if ( $t == 0x26 ) {
422 4         27 my $len = _decode_vint( $buf, $pos );
423 4         16 my $raw = _decode_7bit_binary( $buf, $pos, $len );
424 4         26 my $hex = unpack 'H*', $raw;
425              
426 4 100       12 if ( $use_bigint ) {
427 2         2488 require Math::BigInt;
428 2   50     55158 return Math::BigInt->from_hex( '0x' . ( $hex or '0' ) );
429             }
430              
431 2         10 return _raw_bytes_to_native_number( $raw );
432             }
433 33137 50       70818 if ( $t == 0x2A ) {
434 0         0 my $scale = _zigzag_decode( _decode_vint( $buf, $pos ) );
435 0         0 my $len = _decode_vint( $buf, $pos );
436 0         0 my $raw = _decode_7bit_binary( $buf, $pos, $len );
437 0         0 require Math::BigInt;
438 0         0 require Math::BigFloat;
439 0         0 my $hex = unpack 'H*', $raw;
440 0   0     0 my $unscaled = Math::BigInt->from_hex( '0x' . ( $hex or '0' ) );
441 0         0 my $v = Math::BigFloat->new( $unscaled );
442 0 0       0 if ( $scale > 0 ) {
    0          
443 0         0 $v->bdiv( Math::BigFloat->new( 10 )->bpow( $scale ) );
444             }
445             elsif ( $scale < 0 ) {
446 0         0 $v->bmul( Math::BigFloat->new( 10 )->bpow( -$scale ) );
447             }
448 0 0       0 return $use_bigint ? $v : 0 + $v;
449             }
450 33137 50       70052 if ( $t == 0x28 ) {
451 0         0 my $raw = _decode_fixed_7bit_bytes( $buf, $pos, 5, 32 );
452 0         0 my $v = unpack( 'f>', $raw );
453 0         0 return _normalize_smile_float( $v );
454             }
455 33137 100       67775 if ( $t == 0x29 ) {
456 5133         13283 my $raw = _decode_fixed_7bit_bytes( $buf, $pos, 10, 64 );
457 5133         14298 my $v = unpack( 'd>', $raw );
458 5133         12533 return _normalize_smile_float( $v );
459             }
460 28004 100 66     102280 if ( $t >= 0x40 and $t <= 0x7F ) {
461 3316 100       8130 my $len = ( $t <= 0x5F ) ? ( ( $t & 0x1F ) + 1 ) : ( ( $t & 0x1F ) + 33 );
462 3316 50       7684 die 'Unexpected EOF' if $$pos + $len > length $$buf;
463 3316         7816 my $s = substr( $$buf, $$pos, $len );
464 3316         5128 $$pos += $len;
465 3316 100       16657 _shared_push( $ctx->{shared_values}, $s ) if $ctx->{enable_shared_values};
466 3316         13535 return $s;
467             }
468 24688 100 66     88635 if ( $t >= 0x80 and $t <= 0xBF ) {
469 16 100       53 my $len = ( $t <= 0x9F ) ? ( ( $t & 0x1F ) + 2 ) : ( ( $t & 0x1F ) + 34 );
470 16 50       43 die 'Unexpected EOF' if $$pos + $len > length $$buf;
471 16         45 my $s = substr( $$buf, $$pos, $len );
472 16         26 $$pos += $len;
473 16         67 utf8::decode( $s );
474 16 100       62 _shared_push( $ctx->{shared_values}, $s ) if $ctx->{enable_shared_values};
475 16         113 return $s;
476             }
477 24672 100 66     80576 if ( $t >= 0xC0 and $t <= 0xDF ) {
478 3273         8256 return _zigzag_decode( $t & 0x1F );
479             }
480 21399 100 66     87434 if ( $t >= 0xE0 and $t <= 0xE3 ) {
481 454         1270 return _decode_long_text( $buf, $pos, 0 );
482             }
483 20945 100 66     64259 if ( $t >= 0xE4 and $t <= 0xE7 ) {
484 18         60 return _decode_long_text( $buf, $pos, 1 );
485             }
486 20927 50       42512 if ( $t == 0xE8 ) {
487 0         0 my $len = _decode_vint( $buf, $pos );
488 0         0 return _decode_7bit_binary( $buf, $pos, $len );
489             }
490 20927 100 66     72326 if ( $t >= 0xEC and $t <= 0xEF ) {
491 9606 50       24700 die 'Unexpected EOF' if $$pos >= length $$buf;
492 9606         22762 my $lsb = ord substr( $$buf, $$pos++, 1 );
493 9606         18038 my $idx = ( ( $t & 0x03 ) << 8 ) | $lsb;
494 9606 50 33     35779 die 'Invalid shared value reference' if $idx < 31 or not defined $ctx->{shared_values}[$idx];
495 9606         42695 return $ctx->{shared_values}[$idx];
496             }
497 11321 100       22452 if ( $t == 0xF8 ) {
498 734         1159 my @a;
499 734         1069 while (1) {
500 20260 50       48613 die 'Unexpected EOF' if $$pos >= length $$buf;
501 20260 100       67954 last if ord substr( $$buf, $$pos, 1 ) == 0xF9;
502 19526         43482 push @a, _decode_value_smile( $buf, $pos, $ctx, $json_bool, $use_bigint );
503             }
504 734         1279 $$pos++;
505 734         2834 return \@a;
506             }
507 10587 50       19725 if ( $t == 0xFA ) {
508 10587         23006 my %h;
509 10587         17940 while (1) {
510 44192         108348 my $k = _decode_key_smile( $buf, $pos, $ctx );
511 44192 100       91270 last if not defined $k;
512 33605         82345 $h{$k} = _decode_value_smile( $buf, $pos, $ctx, $json_bool, $use_bigint );
513             }
514 10587         36888 return \%h;
515             }
516              
517 0         0 die "Unknown type tag in payload";
518             }
519              
520             sub _decode_key_smile {
521 44192     44192   118424 my ( $buf, $pos, $ctx ) = @_;
522 44192 50       97392 die 'Unexpected EOF' if $$pos >= length $$buf;
523 44192         77184 my $t = ord substr( $$buf, $$pos++, 1 );
524              
525 44192 100       112927 return undef if $t == 0xFB;
526              
527 33605 50       64630 if ( $t == 0x20 ) {
528 0 0       0 _shared_push( $ctx->{shared_names}, q{} ) if $ctx->{enable_shared_names};
529 0         0 return q{};
530             }
531 33605 100 66     109006 if ( $t >= 0x30 and $t <= 0x33 ) {
532 9884 50       23423 die 'Unexpected EOF' if $$pos >= length $$buf;
533 9884         22479 my $lsb = ord substr( $$buf, $$pos++, 1 );
534 9884         20931 my $idx = ( ( $t & 0x03 ) << 8 ) | $lsb;
535 9884 50 33     41246 die 'Invalid shared name reference' if $idx < 64 or not defined $ctx->{shared_names}[$idx];
536 9884         31588 return $ctx->{shared_names}[$idx];
537             }
538 23721 100 66     79824 if ( $t >= 0x40 and $t <= 0x7F ) {
539 23005         50130 my $idx = $t & 0x3F;
540 23005 50       54913 die 'Invalid shared name reference' if not defined $ctx->{shared_names}[$idx];
541 23005         63028 return $ctx->{shared_names}[$idx];
542             }
543 716 100 66     2331 if ( $t >= 0x80 and $t <= 0xBF ) {
544 715         1390 my $len = ( $t & 0x3F ) + 1;
545 715 50       1602 die 'Unexpected EOF' if $$pos + $len > length $$buf;
546 715         1474 my $k = substr( $$buf, $$pos, $len );
547 715         1141 $$pos += $len;
548 715 50       2805 _shared_push( $ctx->{shared_names}, $k ) if $ctx->{enable_shared_names};
549 715         5609 return $k;
550             }
551 1 50 33     8 if ( $t >= 0xC0 and $t <= 0xF7 ) {
552 1         3 my $len = ( $t & 0x3F ) + 2;
553 1 50       6 die 'Unexpected EOF' if $$pos + $len > length $$buf;
554 1         21 my $k = substr( $$buf, $$pos, $len );
555 1         3 $$pos += $len;
556 1         6 utf8::decode( $k );
557 1 50       9 _shared_push( $ctx->{shared_names}, $k ) if $ctx->{enable_shared_names};
558 1         4 return $k;
559             }
560 0 0       0 if ( $t == 0x34 ) {
561 0         0 my $k = _decode_long_text( $buf, $pos, 1 );
562 0 0       0 _shared_push( $ctx->{shared_names}, $k ) if $ctx->{enable_shared_names};
563 0         0 return $k;
564             }
565              
566 0         0 die 'Invalid object key token';
567             }
568              
569             sub _contains_shared_name_pattern {
570 0     0   0 my ( $v ) = @_;
571 0 0       0 return 0 if not _is_arrayref( $v );
572 0         0 my %seen;
573 0         0 for my $e ( @$v ) {
574 0 0       0 next if not _is_hashref( $e );
575 0         0 for my $k ( keys %$e ) {
576 0 0       0 return 1 if ++$seen{$k} > 1;
577             }
578             }
579 0         0 return 0;
580             }
581              
582             sub _contains_shared_short_string_value {
583 0     0   0 my ( $v ) = @_;
584 0 0       0 return 0 if not _is_arrayref( $v );
585 0         0 my %seen;
586 0         0 for my $e ( @$v ) {
587 0 0       0 next if ref $e;
588 0 0 0     0 next if not defined( $e ) or utf8::is_utf8( $e ) or length( $e ) > 64;
      0        
589 0 0       0 return 1 if ++$seen{$e} > 1;
590             }
591 0         0 return 0;
592             }
593              
594             sub _encode_vint {
595 0     0   0 my ( $v ) = @_;
596 0         0 my @groups;
597              
598 0         0 do {
599 0         0 unshift @groups, $v & 0x7F;
600 0         0 $v >>= 7;
601             } while ( $v > 0 );
602              
603 0         0 $groups[-1] |= 0x80;
604 0         0 return pack 'C*', @groups;
605             }
606              
607             sub _zigzag_encode {
608 15     15   33 my ( $v ) = @_;
609 15 50       105 return ( $v < 0 ) ? ( ( -$v ) * 2 - 1 ) : ( $v * 2 );
610             }
611              
612             sub _encode_7bit_binary {
613 0     0   0 my ( $raw ) = @_;
614 0         0 my $bits = join q{}, map { sprintf '%08b', $_ } unpack 'C*', $raw;
  0         0  
615 0         0 my $pad = ( 7 - ( length( $bits ) % 7 ) ) % 7;
616 0         0 $bits .= '0' x $pad;
617              
618 0         0 my @bytes;
619 0         0 for ( my $i = 0; $i < length $bits; $i += 7 ) {
620 0         0 push @bytes, oct '0b' . substr( $bits, $i, 7 );
621             }
622              
623 0         0 return pack 'C*', @bytes;
624             }
625              
626             sub _encode_short_or_long_text {
627 9     9   18 my ( $bytes, $is_utf8 ) = @_;
628 9         19 my $len = length $bytes;
629              
630 9 100 66     74 if ( not $is_utf8 and $len >= 1 and $len <= 32 ) {
      66        
631 8         45 return chr( 0x40 + $len - 1 ) . $bytes;
632             }
633 1 50 33     17 if ( not $is_utf8 and $len >= 33 and $len <= 64 ) {
      33        
634 0         0 return chr( 0x60 + $len - 33 ) . $bytes;
635             }
636 1 0 33     11 if ( $is_utf8 and $len >= 2 and $len <= 33 ) {
      33        
637 0         0 return chr( 0x80 + $len - 2 ) . $bytes;
638             }
639 1 0 33     5 if ( $is_utf8 and $len >= 34 and $len <= 65 ) {
      33        
640 0         0 return chr( 0xA0 + $len - 34 ) . $bytes;
641             }
642              
643 1 50       3 my $marker = $is_utf8 ? 0xE4 : 0xE0;
644 1         8 return chr( $marker ) . $bytes . "\xFC";
645             }
646              
647             sub _encode_key_smile {
648 23     23   47 my ( $key, $ctx ) = @_;
649              
650 23 100       101 if ( defined $ctx->{name_to_idx}{$key} ) {
651 1         2 my $idx = $ctx->{name_to_idx}{$key};
652 1 50       4 if ( $idx <= 63 ) {
653 1         4 return chr( 0x40 + $idx );
654             }
655 0 0       0 if ( $idx <= 1023 ) {
656 0         0 my $msb = ( $idx >> 8 ) & 0x03;
657 0         0 my $lsb = $idx & 0xFF;
658 0         0 return chr( 0x30 + $msb ) . chr( $lsb );
659             }
660             }
661              
662 22 100 66     85 if ( $ctx->{enable_shared_names} and @{$ctx->{shared_names}} < 1024 ) {
  19         83  
663 19 50       29 if ( @{$ctx->{shared_names}} >= 1024 ) {
  19         51  
664 0         0 $ctx->{shared_names} = [];
665 0         0 $ctx->{name_to_idx} = {};
666             }
667              
668 19         54 my $idx = scalar @{$ctx->{shared_names}};
  19         36  
669 19         46 $ctx->{name_to_idx}{$key} = $idx;
670 19         26 push @{$ctx->{shared_names}}, $key;
  19         64  
671             }
672              
673 22 50       61 return "\x20" if $key eq q{};
674              
675 22         37 my $bytes = $key;
676 22         74 my $is_utf8 = utf8::is_utf8 $bytes;
677 22 50       52 utf8::encode( $bytes ) if $is_utf8;
678 22         69 my $len = length $bytes;
679              
680 22 50 33     227 if ( not $is_utf8 and $len >= 1 and $len <= 64 ) {
      33        
681 22         121 return chr( 0x80 + $len - 1 ) . $bytes;
682             }
683 0 0 0     0 if ( $is_utf8 and $len >= 2 and $len <= 65 ) {
      0        
684 0         0 return chr( 0xC0 + $len - 2 ) . $bytes;
685             }
686              
687 0         0 return "\x34" . $bytes . "\xFC";
688             }
689              
690             sub _encode_value_smile {
691 49     49   121 my ( $v, $ctx ) = @_;
692 49         175 $v = _maybe_to_json( $v );
693              
694 49 100       114 if ( not defined $v ) {
695 1         5 return "\x21";
696             }
697              
698 48 100       125 if ( _is_arrayref( $v ) ) {
699 6         18 my $out = "\xF8";
700 6         36 $out .= _encode_value_smile( $_, $ctx ) for @$v;
701 6         12 $out .= "\xF9";
702 6         15 return $out;
703             }
704              
705 42 100       97 if ( _is_hashref( $v ) ) {
706 13         50 my @keys = keys %$v;
707 13 100       47 @keys = sort @keys if $ctx->{canonical};
708              
709 13         37 my $out = "\xFA";
710 13         43 for my $k ( @keys ) {
711 23         70 $out .= _encode_key_smile( $k, $ctx );
712 23         186 $out .= _encode_value_smile( $v->{$k}, $ctx );
713             }
714 13         25 $out .= "\xFB";
715 13         46 return $out;
716             }
717              
718 29 50       66 die 'Unsupported reference in encode_smile' if ref $v;
719              
720 29 50       68 if ( _is_bool( $v ) ) {
721 0 0       0 return $v ? "\x23" : "\x22";
722             }
723              
724 29 100 66     190 if ( _created_as_number( $v ) or looks_like_number( $v ) ) {
725 15 50       101 if ( $v =~ /\A-?[0-9]+\z/ ) {
726 15         30 my $n = 0 + $v;
727 15 50 33     95 if ( $n >= -16 and $n <= 15 ) {
728 15         54 return chr( 0xC0 + _zigzag_encode( $n ) );
729             }
730 0         0 return "\x24" . _encode_vint( _zigzag_encode( $n ) );
731             }
732              
733 0         0 my $raw = pack 'd>', 0 + $v;
734 0         0 return "\x29" . _encode_7bit_binary( $raw );
735             }
736              
737 14         60 my $text = "$v";
738 14 50       49 my $cache_key = utf8::is_utf8( $text ) ? "u\0$text" : "b\0$text";
739              
740 14 100 100     85 if ( $ctx->{enable_shared_values} and defined $ctx->{value_to_idx}{$cache_key} ) {
741 5         9 my $idx = $ctx->{value_to_idx}{$cache_key};
742 5 50       11 if ( $idx <= 30 ) {
743 5         17 return chr( $idx + 1 );
744             }
745 0 0       0 if ( $idx <= 1023 ) {
746 0         0 my $msb = ( $idx >> 8 ) & 0x03;
747 0         0 my $lsb = $idx & 0xFF;
748 0         0 return chr( 0xEC + $msb ) . chr( $lsb );
749             }
750             }
751              
752 9 100       50 if ( $ctx->{enable_shared_values} ) {
753 7 50       12 if ( @{$ctx->{shared_values}} >= 1024 ) {
  7         24  
754 0         0 $ctx->{shared_values} = [];
755 0         0 $ctx->{value_to_idx} = {};
756             }
757              
758 7         24 my $idx = scalar @{$ctx->{shared_values}};
  7         15  
759 7         20 $ctx->{value_to_idx}{$cache_key} = $idx;
760 7         10 push @{$ctx->{shared_values}}, $text;
  7         25  
761             }
762              
763 9         19 my $bytes = $text;
764 9         21 my $is_utf8 = utf8::is_utf8 $bytes;
765 9 50       24 utf8::encode( $bytes ) if $is_utf8;
766 9         33 return _encode_short_or_long_text( $bytes, $is_utf8 );
767             }
768              
769             sub encode_smile {
770 14     14 1 1334176 my ( $data, $opts ) = @_;
771 14 50       72 die 'encode_smile expects at most 2 arguments' if @_ > 2;
772 14         111 $opts = _check_opts( 'encode_smile', $opts, { write_header => 1, shared_values => 1, shared_names => 1, canonical => 1 } );
773              
774 12 100       84 my $write_header = exists $opts->{write_header} ? !!$opts->{write_header} : 1;
775 12 100       41 my $shared_values = exists $opts->{shared_values} ? !!$opts->{shared_values} : 1;
776 12 100       37 my $shared_names = exists $opts->{shared_names} ? !!$opts->{shared_names} : 1;
777 12 100       40 my $canonical = exists $opts->{canonical} ? !!$opts->{canonical} : 0;
778              
779 12         92 my %ctx = (
780             shared_names => [],
781             name_to_idx => {},
782             shared_values => [],
783             value_to_idx => {},
784             enable_shared_names => !!$shared_names,
785             enable_shared_values => !!$shared_values,
786             canonical => $canonical,
787             );
788              
789 12         56 my $payload = _encode_value_smile( $data, \%ctx );
790 12 100       79 my $out = $write_header
791             ? substr( HEADER, 0, 3 ) . chr( $shared_names | ( $shared_values << 1 ) )
792             : '';
793 12         36 $out .= $payload;
794 12         83 return $out;
795             }
796              
797             sub decode_smile {
798 32     32 1 341257 my ( $bytes, $opts ) = @_;
799 32 50       150 die 'decode_smile expects at most 2 arguments' if @_ > 2;
800 32         274 $opts = _check_opts( 'decode_smile', $opts, { use_bigint => 1, require_header => 1, json_bool => 1 } );
801 31 100       168 my $use_bigint = exists $opts->{use_bigint} ? !!$opts->{use_bigint} : 1;
802 31 100       104 my $require_header = exists $opts->{require_header} ? !!$opts->{require_header} : 0;
803 31 50       112 my $json_bool = exists $opts->{json_bool} ? !!$opts->{json_bool} : 1;
804              
805 31         59 my $pos = 0;
806 31         55 my $has_header = 0;
807 31 100       178 if ( substr( $bytes, 0, 3 ) eq "\x3A\x29\x0A" ) {
    100          
808 27         52 $pos = 4;
809 27         57 $has_header = 1;
810             }
811             elsif ( $require_header ) {
812 1         11 die 'Smile header required';
813             }
814              
815 30 50 33     336 if ( $pos < length $bytes and substr( $bytes, $pos, 1 ) eq "\x7A" ) {
816 0         0 $pos++;
817 0 0       0 die 'Unexpected EOF' if $pos >= length $bytes;
818 0         0 my $flags = ord substr( $bytes, $pos++, 1 );
819 0 0       0 $pos++ if $flags & 0x01;
820 0 0       0 $pos++ if $flags & 0x02;
821              
822 0         0 my $len = _unpack_u32( \$bytes, \$pos );
823 0 0       0 die 'Unexpected EOF' if $pos + $len > length $bytes;
824 0         0 my $payload = substr( $bytes, $pos, $len );
825 0         0 $pos += $len;
826              
827 0         0 my $inner = 0;
828 0         0 my $value = _decode_value( \$payload, \$inner, $json_bool, $use_bigint );
829 0         0 return $value;
830             }
831              
832 30 100       134 my $flags = $has_header ? ord substr( $bytes, 3, 1 ) : 0x01;
833 30 100       326 my %ctx = (
    100          
834             shared_names => [],
835             shared_values => [],
836             enable_shared_names => $has_header ? !!($flags & 0x01) : !!1,
837             enable_shared_values => $has_header ? !!($flags & 0x02) : !!0,
838             );
839              
840 30         196 my $value = _decode_value_smile( \$bytes, \$pos, \%ctx, $json_bool, $use_bigint );
841 30         37002 while ( $pos < length $bytes ) {
842 2         7 my $rest = ord substr( $bytes, $pos, 1 );
843 2 50       8 last if $rest == 0xFF;
844 2 0 33     10 if ( $rest == 0x0A or $rest == 0x0D or $rest == 0x09 or $rest == 0x20 ) {
      33        
      0        
845 2         4 $pos++;
846 2         5 next;
847             }
848 0         0 die 'Invalid payload';
849             }
850 30         1343 return $value;
851             }
852              
853             sub _is_pathish_filename {
854 24     24   63 my ( $x ) = @_;
855 24 100       102 return 0 if not ref $x;
856 19 100 66     44 return 1 if eval { $x->isa('Path::Tiny') } or eval { $x->isa('Path::Class::File') };
  19         224  
  1         7  
857 1         3 return 0;
858             }
859              
860             sub _open_for_read {
861 19     19   65 my ( $file ) = @_;
862              
863 19 100       85 if ( _is_pathish_filename( $file ) ) {
864 17         149 $file = "$file";
865             }
866              
867 19 50       222 if ( not ref $file ) {
868 19 50       1770 open my $fh, '<:raw', $file or die "open($file) for read: $!";
869 19         179 return ( $fh, 1 );
870             }
871              
872 0 0 0     0 if ( eval { $file->can('read') } or eval { $file->can('getline') } or ref( $file ) eq 'GLOB' ) {
  0   0     0  
  0         0  
873 0         0 binmode( $file, ':raw' );
874 0         0 return ( $file, 0 );
875             }
876              
877 0         0 die "Unsupported file argument for read";
878             }
879              
880             sub _open_for_write {
881 5     5   12 my ( $file ) = @_;
882              
883 5 100       16 if ( _is_pathish_filename( $file ) ) {
884 1         20 $file = "$file";
885             }
886              
887 5 100       19 if ( not ref $file ) {
888 4 50       523 open my $fh, '>:raw', $file or die "open($file) for write: $!";
889 4         27 return ( $fh, 1 );
890             }
891              
892 1 0 33     1 if ( eval { $file->can('print') } or eval { $file->can('write') } or ref( $file ) eq 'GLOB' ) {
  1   33     9  
  0         0  
893 1         4 binmode( $file, ':raw' );
894 1         3 return ( $file, 0 );
895             }
896              
897 0         0 die "Unsupported file argument for write";
898             }
899              
900             sub dump_smile {
901 5     5 1 250802 my ( $file, $data, $opts ) = @_;
902 5 50       25 die 'dump_smile expects at most 3 arguments' if @_ > 3;
903              
904 5 100 66     24 if ( @_ >= 3 and defined $opts ) {
905 2 50       9 die 'Options must be a hash reference' if not _is_hashref( $opts );
906             }
907              
908 5         31 my ( $fh, $close ) = _open_for_write( $file );
909              
910 5         18 my $bytes = encode_smile( $data, $opts );
911 4 50       10 my $ok = eval {
912 4 50       6 print {$fh} $bytes or die "write: $!";
  4         66  
913 4         16 1;
914             } ? 1 : 0;
915              
916 4 100       10 if ( $close ) {
917 3 50       182 close $fh or $ok = 0;
918             }
919              
920 4 50       59 return $ok ? 1 : 0;
921             }
922              
923             sub load_smile {
924 19     19 1 25693279 my ( $file, $opts ) = @_;
925 19 50       128 die 'load_smile expects at most 2 arguments' if @_ > 2;
926              
927 19 100 66     104 if ( @_ >= 2 and defined $opts ) {
928 1 50       4 die 'Options must be a hash reference' if not _is_hashref( $opts );
929             }
930              
931 19         94 my ( $fh, $close ) = _open_for_read( $file );
932              
933 19         48 my $buf = do { local $/; <$fh> };
  19         111  
  19         6721  
934 19 50       101 die "read failed" if not defined $buf;
935              
936 19 50       58 if ( $close ) {
937 19 50       333 close $fh or die "close: $!";
938             }
939              
940 19         112 return decode_smile( $buf, $opts );
941             }
942              
943             1;
944              
945             __END__