File Coverage

blib/lib/Pcore/Util/Data.pm
Criterion Covered Total %
statement 47 289 16.2
branch 15 140 10.7
condition 3 29 10.3
subroutine 10 37 27.0
pod 0 24 0.0
total 75 519 14.4


line stmt bran cond sub pod time code
1             package Pcore::Util::Data;
2              
3 5         104 use Pcore -const, -export,
4             { ALL => [qw[encode_data decode_data]],
5             PERL => [qw[to_perl from_perl]],
6             JSON => [qw[to_json from_json]],
7             CBOR => [qw[to_cbor from_cbor]],
8             YAML => [qw[to_yaml from_yaml]],
9             XML => [qw[to_xml from_xml]],
10             INI => [qw[to_ini from_ini]],
11             B64 => [qw[to_b64 to_b64_url from_b64 from_b64_url]],
12             B85 => [qw[to_b85 from_b85]],
13             URI => [qw[to_uri from_uri from_uri_query]],
14             XOR => [qw[to_xor from_xor]],
15             CONST => [qw[$DATA_ENC_B64 $DATA_ENC_HEX $DATA_ENC_B85 $DATA_COMPRESS_ZLIB $DATA_CIPHER_DES]],
16             TYPE => [qw[$DATA_TYPE_PERL $DATA_TYPE_JSON $DATA_TYPE_CBOR $DATA_TYPE_YAML $DATA_TYPE_XML $DATA_TYPE_INI]],
17 5     5   38 };
  5         13  
18 5     5   33 use Pcore::Util::Text qw[decode_utf8 encode_utf8 escape_scalar];
  5         11  
  5         32  
19 5     5   1586 use Pcore::Util::List qw[pairs];
  5         13  
  5         37  
20 5     5   35 use Sort::Naturally qw[nsort];
  5         11  
  5         235  
21 5     5   29 use Pcore::Util::Scalar qw[is_blessed_ref is_plain_arrayref];
  5         10  
  5         31  
22 5     5   32 use URI::Escape::XS qw[]; ## no critic qw[Modules::ProhibitEvilModules]
  5         9  
  5         2398  
23              
24             const our $DATA_TYPE_PERL => 1;
25             const our $DATA_TYPE_JSON => 2;
26             const our $DATA_TYPE_CBOR => 3;
27             const our $DATA_TYPE_YAML => 4;
28             const our $DATA_TYPE_XML => 5;
29             const our $DATA_TYPE_INI => 6;
30              
31             const our $DATA_ENC_B64 => 1;
32             const our $DATA_ENC_HEX => 2;
33             const our $DATA_ENC_B85 => 3;
34              
35             const our $DATA_COMPRESS_ZLIB => 1;
36              
37             const our $DATA_CIPHER_DES => 1;
38              
39             const our $CIPHER_NAME => { #
40             $DATA_CIPHER_DES => 'DES',
41             };
42              
43             our $JSON_CACHE;
44              
45             # JSON is used by default
46             # JSON can't serialize ScalarRefs
47             # objects should have TO_JSON method, otherwise object will be serialized as null
48             # base64 encoder is used by default, it generates more compressed data
49 0     0 0 0 sub encode_data ( $type, $data, @ ) {
  0         0  
  0         0  
  0         0  
50 0         0 my %args = (
51             readable => undef, # make serialized data readable for humans
52             compress => undef, # use compression
53             secret => undef, # crypt data if defined, can be ArrayRef
54             secret_index => 0, # index of secret to use in secret array, if secret is ArrayRef
55             encode => undef, # 0 - disable
56             token => undef, # attach informational token
57             compress_threshold => 100, # min data length in bytes to perform compression, only if compress = 1
58             cipher => $DATA_CIPHER_DES, # cipher to use
59             json => undef, # HashRef with additional params for Cpanel::JSON::XS
60             splice @_, 2,
61             );
62              
63 0 0 0     0 if ( $args{readable} && $type != $DATA_TYPE_CBOR ) {
64 0         0 $args{compress} = undef;
65 0         0 $args{secret} = undef;
66 0         0 $args{encode} = undef;
67 0         0 $args{token} = undef;
68             }
69              
70 0         0 my $res;
71              
72             # encode
73 0 0       0 if ( $type == $DATA_TYPE_PERL ) {
    0          
    0          
    0          
    0          
    0          
74 0         0 state $init = !!require Data::Dumper;
75              
76             state $sort_keys = sub {
77 0     0   0 return [ nsort keys $_[0]->%* ];
78 0         0 };
79              
80 0         0 local $Data::Dumper::Indent = 0;
81 0         0 local $Data::Dumper::Purity = 1;
82 0         0 local $Data::Dumper::Pad = q[];
83 0         0 local $Data::Dumper::Terse = 1;
84 0         0 local $Data::Dumper::Deepcopy = 0;
85 0         0 local $Data::Dumper::Quotekeys = 0;
86 0         0 local $Data::Dumper::Pair = '=>';
87 0         0 local $Data::Dumper::Maxdepth = 0;
88 0         0 local $Data::Dumper::Deparse = 0;
89 0         0 local $Data::Dumper::Sparseseen = 1;
90 0         0 local $Data::Dumper::Useperl = 1;
91 0         0 local $Data::Dumper::Useqq = 1;
92 0 0       0 local $Data::Dumper::Sortkeys = $args{readable} ? $sort_keys : 0;
93              
94 0 0       0 if ( !defined $data ) {
95 0         0 $res = \'undef';
96             }
97             else {
98 5     5   34 no warnings qw[redefine];
  5         15  
  5         20087  
99              
100             local *Data::Dumper::qquote = sub {
101 0     0   0 return q["] . encode_utf8( escape_scalar $_[0] ) . q["];
102 0         0 };
103              
104 0         0 $res = \Data::Dumper->Dump( [$data] );
105             }
106              
107 0 0       0 if ( $args{readable} ) {
108 0         0 state $init1 = !!require Pcore::Src::File;
109              
110 0         0 $res = Pcore::Src::File->new(
111             { action => $Pcore::Src::SRC_DECOMPRESS,
112             path => 'config.perl', # mark file as perl config
113             is_realpath => 0,
114             in_buffer => $res,
115             filter_args => {
116             perl_tidy => '--comma-arrow-breakpoints=0',
117             perl_critic => 0,
118             },
119             }
120             )->run->out_buffer;
121             }
122             }
123             elsif ( $type == $DATA_TYPE_JSON ) {
124 0 0       0 if ( $args{json} ) {
    0          
125 0         0 my $json = _get_json_obj( $args{json}->%* );
126              
127 0         0 $res = \$json->encode($data);
128             }
129             elsif ( $args{readable} ) {
130 0         0 state $json = _get_json_obj( ascii => 0, latin1 => 0, utf8 => 1, canonical => 1, indent => 1, space_before => 0, space_after => 1 );
131              
132 0         0 $res = \$json->encode($data);
133             }
134             else {
135 0         0 state $json = _get_json_obj( ascii => 1, latin1 => 0, utf8 => 1, pretty => 0 );
136              
137 0         0 $res = \$json->encode($data);
138             }
139             }
140             elsif ( $type == $DATA_TYPE_CBOR ) {
141 0         0 state $cbor = _get_cbor_obj();
142              
143 0         0 $res = \$cbor->encode($data);
144             }
145             elsif ( $type == $DATA_TYPE_YAML ) {
146 0         0 state $init = !!require YAML::XS;
147              
148 0         0 local $YAML::XS::UseCode = 0;
149 0         0 local $YAML::XS::DumpCode = 0;
150 0         0 local $YAML::XS::LoadCode = 0;
151              
152 0         0 $res = \YAML::XS::Dump($data);
153             }
154             elsif ( $type == $DATA_TYPE_XML ) {
155 0         0 state $init = !!require XML::Hash::XS;
156              
157 0         0 state $xml_args = {
158             root => 'root',
159             version => '1.0',
160             encode => 'UTF-8',
161             output => undef,
162             canonical => 0, # sort hash keys
163             use_attr => 1,
164             content => 'content', # if defined that the key name for the text content(used only if use_attr=1)
165             xml_decl => 1,
166             trim => 1,
167             utf8 => 0,
168             buf_size => 4096,
169             method => 'NATIVE',
170             };
171              
172 0         0 state $xml_obj = XML::Hash::XS->new( $xml_args->%* );
173              
174 0         0 my $root = [ keys $data->%* ]->[0];
175              
176 0 0       0 $res = \$xml_obj->hash2xml( $data->{$root}, root => $root, indent => $args{readable} ? 4 : 0 );
177             }
178             elsif ( $type == $DATA_TYPE_INI ) {
179 0         0 state $init = !!require Pcore::Util::Config::INI;
180              
181 0         0 $res = Pcore::Util::Config::INI::to_ini($data);
182             }
183             else {
184 0         0 die qq[Unknown serializer "$type"];
185             }
186              
187             # compress
188 0 0       0 if ( $args{compress} ) {
189 0 0       0 if ( bytes::length $res->$* >= $args{compress_threshold} ) {
190 0 0       0 if ( $args{compress} == $DATA_COMPRESS_ZLIB ) {
191 0         0 state $init = !!require Compress::Zlib;
192              
193 0         0 $res = \Compress::Zlib::compress( $res->$* );
194             }
195             else {
196 0         0 die qq[Unknown compressor type "$args{compress}"];
197             }
198             }
199             else {
200 0         0 $args{compress} = 0;
201             }
202             }
203              
204             # encrypt
205 0 0       0 if ( defined $args{secret} ) {
206 0         0 my $secret;
207              
208 0 0       0 if ( is_plain_arrayref $args{secret} ) {
209 0         0 $secret = $args{secret}->[ $args{secret_index} ];
210             }
211             else {
212 0         0 $secret = $args{secret};
213             }
214              
215 0 0       0 if ( defined $secret ) {
216 0         0 state $init = !!require Crypt::CBC;
217              
218             $res = \Crypt::CBC->new(
219             -key => $secret,
220             -cipher => $CIPHER_NAME->{ $args{cipher} },
221 0         0 )->encrypt( $res->$* );
222             }
223             else {
224 0         0 $args{secret} = undef;
225             }
226             }
227              
228             # encode
229 0 0       0 if ( $args{encode} ) {
230 0 0       0 if ( $args{encode} == $DATA_ENC_B64 ) {
    0          
    0          
231 0         0 $res = \to_b64_url( $res->$* );
232             }
233             elsif ( $args{encode} == $DATA_ENC_HEX ) {
234 0         0 $res = \unpack 'H*', $res->$*;
235             }
236             elsif ( $args{encode} == $DATA_ENC_B85 ) {
237 0         0 $res = \to_b85( $res->$* );
238             }
239             else {
240 0         0 die qq[Unknown encoder "$args{encode}"];
241             }
242             }
243              
244             # add token
245 0 0       0 if ( $args{token} ) {
246 0 0 0     0 $res->$* .= sprintf( '#%x', ( $args{compress} // 0 ) . ( defined $args{secret} ? $args{cipher} : 0 ) . ( $args{secret_index} // 0 ) . ( $args{encode} // 0 ) . $type ) . sprintf '#%x', bytes::length $res->$*;
      0        
      0        
247             }
248              
249 0         0 return $res;
250             }
251              
252             # JSON data should be without UTF8 flag
253             # objects isn't deserialized automatically from JSON
254 8     8 0 20 sub decode_data ( $type, @ ) {
  8         18  
  8         14  
255 8 50       39 my $data_ref = ref $_[1] ? $_[1] : \$_[1];
256              
257 8         94 my %args = (
258             compress => undef,
259             secret => undef, # can be ArrayRef
260             secret_index => 0,
261             cipher => $DATA_CIPHER_DES,
262             encode => undef, # 0, 1 = 'hex', 'hex', 'b64'
263             perl_ns => undef, # for PERL only, namespace for data evaluation
264             json => undef, # HashRef with additional params for Cpanel::JSON::XS
265             return_token => 0, # return token
266             splice( @_, 2 ),
267             type => $type,
268             );
269              
270             # parse token
271 8 50       54 if ( $data_ref->$* =~ /#([[:xdigit:]]{1,8})#([[:xdigit:]]{1,16})\z/sm ) {
272 0         0 my $token_len = 2 + length($1) + length $2;
273              
274 0 0       0 if ( bytes::length( $data_ref->$* ) - $token_len == hex $2 ) {
275 0         0 $args{has_token} = 1;
276              
277 0         0 substr $data_ref->$*, -$token_len, $token_len, q[];
278              
279 0         0 ( $args{compress}, $args{cipher}, $args{secret_index}, $args{encode}, $type ) = split //sm, sprintf '%05s', hex $1;
280              
281 0         0 $args{type} = $type;
282             }
283             }
284              
285             # decode
286 8 50       32 if ( $args{encode} ) {
287 0 0       0 if ( $args{encode} == $DATA_ENC_B64 ) {
    0          
    0          
288 0         0 $data_ref = \from_b64_url( $data_ref->$* );
289             }
290             elsif ( $args{encode} == $DATA_ENC_HEX ) {
291 0         0 $data_ref = \pack 'H*', $data_ref->$*;
292             }
293             elsif ( $args{encode} == $DATA_ENC_B85 ) {
294 0         0 $data_ref = \from_b85( $data_ref->$* );
295             }
296             else {
297 0         0 die qq[Unknown encoder "$args{encode}"];
298             }
299             }
300              
301             # decrypt
302 8 50 33     68 if ( $args{cipher} && defined $args{secret} ) {
303 0         0 my $secret;
304              
305 0 0       0 if ( is_plain_arrayref $args{secret} ) {
306 0         0 $secret = $args{secret}->[ $args{secret_index} ];
307             }
308             else {
309 0         0 $secret = $args{secret};
310             }
311              
312 0 0       0 if ( defined $secret ) {
313 0         0 state $init = !!require Crypt::CBC;
314              
315             $data_ref = \Crypt::CBC->new(
316             -key => $secret,
317             -cipher => $CIPHER_NAME->{ $args{cipher} },
318 0         0 )->decrypt( $data_ref->$* );
319              
320             }
321             }
322              
323             # decompress
324 8 50       32 if ( $args{compress} ) {
325 0 0       0 if ( $args{compress} == $DATA_COMPRESS_ZLIB ) {
326 0         0 state $init = !!require Compress::Zlib;
327              
328 0         0 $data_ref = \Compress::Zlib::uncompress($data_ref);
329              
330 0 0       0 die if !defined $data_ref->$*;
331             }
332             else {
333 0         0 die qq[Unknown compressor "$args{compressor}"];
334             }
335             }
336              
337             # decode
338 8         19 my $res;
339              
340 8 100       56 if ( $type == $DATA_TYPE_PERL ) {
    50          
    50          
    50          
    50          
    50          
341 5   50     30 my $ns = $args{perl_ns} || '_Pcore::CONFIG::SANDBOX';
342              
343 5         158 decode_utf8 $data_ref->$*;
344              
345             ## no critic qw[BuiltinFunctions::ProhibitStringyEval]
346 5     5   40 $res = eval <<"CODE";
  5         12  
  5         38  
  5         588  
347             package $ns;
348              
349             use Pcore -config;
350              
351             $data_ref->$*
352             CODE
353 5 50       43 die $@ if $@;
354              
355 5 50       22 die q[Config must return value] unless $res;
356             }
357             elsif ( $type == $DATA_TYPE_JSON ) {
358 0 0       0 if ( $args{json} ) {
359 0         0 my $json = _get_json_obj( $args{json}->%* );
360              
361 0         0 $res = $json->decode( $data_ref->$* );
362             }
363             else {
364 0         0 state $json = _get_json_obj( utf8 => 1 );
365              
366             # $res = $json->decode_prefix( $data_ref->$* );
367              
368 0         0 $res = $json->decode( $data_ref->$* );
369             }
370             }
371             elsif ( $type == $DATA_TYPE_CBOR ) {
372 0         0 state $cbor = _get_cbor_obj();
373              
374 0         0 $res = $cbor->decode( $data_ref->$* );
375             }
376             elsif ( $type == $DATA_TYPE_YAML ) {
377 0         0 state $init = !!require YAML::XS;
378              
379 0         0 local $YAML::XS::UseCode = 0;
380 0         0 local $YAML::XS::DumpCode = 0;
381 0         0 local $YAML::XS::LoadCode = 0;
382              
383 0         0 $res = YAML::XS::Load( $data_ref->$* );
384             }
385             elsif ( $type == $DATA_TYPE_XML ) {
386 0         0 state $init = !!require XML::Hash::XS;
387              
388 0         0 state $xml_args = {
389             encoding => 'UTF-8',
390             utf8 => 1,
391             max_depth => 1024,
392             buf_size => 4096,
393             force_array => 1,
394             force_content => 1,
395             merge_text => 1,
396             keep_root => 1,
397             };
398              
399 0         0 state $xml_obj = XML::Hash::XS->new( $xml_args->%* );
400              
401 0         0 $res = $xml_obj->xml2hash($data_ref);
402             }
403             elsif ( $type == $DATA_TYPE_INI ) {
404 3         1204 state $init = !!require Pcore::Util::Config::INI;
405              
406 3         16 $res = Pcore::Util::Config::INI::from_ini( $data_ref->$* );
407             }
408             else {
409 0         0 die qq[Unknown serializer "$type"];
410             }
411              
412 8 50 33     37 if ( wantarray && $args{return_token} ) {
413 0         0 return $res, \%args;
414             }
415             else {
416 8         168 return $res;
417             }
418             }
419              
420             # PERL
421             sub to_perl {
422 0     0 0   return encode_data( $DATA_TYPE_PERL, @_ );
423             }
424              
425             sub from_perl {
426 0     0 0   return decode_data( $DATA_TYPE_PERL, @_ );
427             }
428              
429             # JSON
430             sub _get_json_obj {
431 0     0     my %args = (
432              
433             # COMMON
434             utf8 => 1,
435             allow_nonref => 1, # allow scalars
436             allow_tags => 0, # use FREEZE / THAW, we don't use this, because non-standard JSON will be generated, use CBOR instead to serialize objects
437              
438             # shrink => 0,
439             # max_depth => 512,
440              
441             # DECODE
442             relaxed => 1, # allows commas and # - style comments
443              
444             # filter_json_object => undef,
445             # filter_json_single_key_object => undef,
446             # max_size => 0,
447              
448             # ENCODE
449             ascii => 1,
450             latin1 => 0,
451              
452             # pretty => 0, # set indent, space_before, space_after
453             canonical => 0, # sort hash keys, slow
454             indent => 0,
455             space_before => 0, # put a space before the ":" separating key from values
456             space_after => 0, # put a space after the ":" separating key from values, and after "," separating key-value pairs
457              
458             allow_unknown => 0, # throw exception if can't encode item
459             allow_blessed => 1, # allow blessed objects
460             convert_blessed => 1, # use TO_JSON method of blessed objects
461              
462             @_,
463             );
464              
465 0           state $init = !!require Cpanel::JSON::XS;
466              
467 0           my $json = Cpanel::JSON::XS->new;
468              
469 0           for ( keys %args ) {
470 0           $json->$_( $args{$_} );
471             }
472              
473 0           return $json;
474             }
475              
476 0     0 0   sub to_json ( $data, @ ) {
  0            
  0            
477 0           return encode_data( $DATA_TYPE_JSON, @_ );
478             }
479              
480 0     0 0   sub from_json ( $data, @ ) {
  0            
  0            
481 0           return decode_data( $DATA_TYPE_JSON, @_ );
482             }
483              
484             # CBOR
485             sub _get_cbor_obj {
486 0     0     state $init = !!require CBOR::XS;
487              
488 0           my $cbor = CBOR::XS->new;
489              
490 0           $cbor->max_depth(512);
491 0           $cbor->max_size(0); # max. string size is unlimited
492 0           $cbor->allow_unknown(0);
493 0           $cbor->allow_sharing(1);
494 0           $cbor->allow_cycles(1);
495 0           $cbor->pack_strings(0); # set to 1 affect speed, but makes size smaller
496 0           $cbor->validate_utf8(0);
497 0           $cbor->filter(undef);
498              
499 0           return $cbor;
500             }
501              
502             sub to_cbor {
503 0     0 0   return encode_data( $DATA_TYPE_CBOR, @_ );
504             }
505              
506             sub from_cbor {
507 0     0 0   return decode_data( $DATA_TYPE_CBOR, @_ );
508             }
509              
510             # YAML
511             sub to_yaml {
512 0     0 0   return encode_data( $DATA_TYPE_YAML, @_ );
513             }
514              
515             sub from_yaml {
516 0     0 0   return decode_data( $DATA_TYPE_YAML, @_ );
517             }
518              
519             # XML
520             sub to_xml {
521 0     0 0   return encode_data( $DATA_TYPE_XML, @_ );
522             }
523              
524             sub from_xml {
525 0     0 0   return decode_data( $DATA_TYPE_XML, @_ );
526             }
527              
528             # INI
529             sub to_ini {
530 0     0 0   return encode_data( $DATA_TYPE_INI, @_ );
531             }
532              
533             sub from_ini {
534 0     0 0   return decode_data( $DATA_TYPE_INI, @_ );
535             }
536              
537             # BASE64
538             sub to_b64 {
539 0     0 0   state $init = !!require MIME::Base64;
540              
541 0           return &MIME::Base64::encode_base64; ## no critic qw[Subroutines::ProhibitAmpersandSigils]
542             }
543              
544             sub to_b64_url {
545 0     0 0   state $init = !!require MIME::Base64;
546              
547 0           return &MIME::Base64::encode_base64url; ## no critic qw[Subroutines::ProhibitAmpersandSigils]
548             }
549              
550             sub from_b64 {
551 0     0 0   state $init = !!require MIME::Base64;
552              
553 0           return &MIME::Base64::decode_base64; ## no critic qw[Subroutines::ProhibitAmpersandSigils]
554             }
555              
556             sub from_b64_url {
557 0     0 0   state $init = !!require MIME::Base64;
558              
559 0           return &MIME::Base64::decode_base64url; ## no critic qw[Subroutines::ProhibitAmpersandSigils]
560             }
561              
562             # BASE85
563             sub to_b85 {
564 0     0 0   state $init = !!require Convert::Ascii85;
565              
566 0           state $args = { compress_zero => 1, compress_space => 1 };
567              
568 0           return Convert::Ascii85::ascii85_encode( $_[0], $args );
569             }
570              
571             sub from_b85 {
572 0     0 0   state $init = !!require Convert::Ascii85;
573              
574 0           return &Convert::Ascii85::ascii85_decode; ## no critic qw[Subroutines::ProhibitAmpersandSigils]
575             }
576              
577             # URI
578             sub to_uri {
579 0 0   0 0   if ( ref $_[0] ) {
580 0 0 0       my $data = is_blessed_ref $_[0] && $_[0]->isa('Pcore::Util::Hash::Multivalue') ? $_[0]->get_hash : $_[0];
581              
582 0           my @res;
583              
584 0 0         if ( is_plain_arrayref $data ) {
585 0           for ( my $i = 0; $i <= $data->$#*; $i += 2 ) {
586 0 0         push @res, join q[=], defined $data->[$i] ? URI::Escape::XS::encodeURIComponent( $data->[$i] ) : q[], defined $data->[ $i + 1 ] ? URI::Escape::XS::encodeURIComponent( $data->[ $i + 1 ] ) : ();
    0          
587             }
588             }
589             else {
590 0           while ( my ( $k, $v ) = each $data->%* ) {
591 0           $k = URI::Escape::XS::encodeURIComponent($k);
592              
593 0 0         if ( ref $v ) {
594              
595             # value is ArrayRef
596 0           for my $v1 ( $v->@* ) {
597 0 0         push @res, join q[=], $k, defined $v1 ? URI::Escape::XS::encodeURIComponent($v1) : ();
598             }
599             }
600             else {
601 0 0         push @res, join q[=], $k, defined $v ? URI::Escape::XS::encodeURIComponent($v) : ();
602             }
603             }
604             }
605              
606 0           return join q[&], @res;
607             }
608             else {
609 0           return URI::Escape::XS::encodeURIComponent( $_[0] );
610             }
611             }
612              
613             # always return scalar string
614             sub from_uri {
615 0     0 0   my %args = (
616             encoding => 'UTF-8',
617             splice @_, 1,
618             );
619              
620 0           my $u = URI::Escape::XS::decodeURIComponent( $_[0] );
621              
622 0 0         if ( $args{encoding} ) {
623 0           state $encoding = {};
624              
625 0   0       $encoding->{ $args{encoding} } //= Encode::find_encoding( $args{encoding} );
626              
627 0 0         eval { $u = $encoding->{ $args{encoding} }->decode( $u, Encode::FB_CROAK | Encode::LEAVE_SRC ); 1; } or do {
  0            
  0            
628 0 0         utf8::upgrade($u) if $@;
629             };
630             }
631              
632 0 0         if ( defined wantarray ) {
633 0           return $u;
634             }
635             else {
636 0           $_[0] = $u;
637              
638 0           return;
639             }
640             }
641              
642             # always return HashMultivalue
643             sub from_uri_query {
644 0     0 0   my %args = (
645             encoding => 'UTF-8',
646             splice @_, 1,
647             );
648              
649 0           my $enc;
650              
651 0 0         if ( $args{encoding} ) {
652 0           state $encoding = {};
653              
654 0   0       $encoding->{ $args{encoding} } //= Encode::find_encoding( $args{encoding} );
655              
656 0           $enc = $encoding->{ $args{encoding} };
657             }
658              
659 0           my $res = P->hash->multivalue;
660              
661 0           my $hash = $res->get_hash;
662              
663 0           for my $key ( split /&/sm, $_[0] ) {
664 0           my $val;
665              
666 0 0         if ( ( my $idx = index $key, q[=] ) != -1 ) {
667 0           $val = substr $key, $idx, length $key, q[];
668              
669 0           substr $val, 0, 1, q[];
670              
671 0           $val = URI::Escape::XS::decodeURIComponent($val);
672             }
673              
674 0           $key = URI::Escape::XS::decodeURIComponent($key);
675              
676 0 0         if ($enc) {
677              
678             # decode key
679 0 0         eval { $key = $enc->decode( $key, Encode::FB_CROAK | Encode::LEAVE_SRC ); 1; } or do {
  0            
  0            
680 0 0         utf8::upgrade($key) if $@;
681             };
682              
683             # decode value
684 0 0         if ( defined $val ) {
685 0 0         eval { $val = $enc->decode( $val, Encode::FB_CROAK | Encode::LEAVE_SRC ); 1; } or do {
  0            
  0            
686 0 0         utf8::upgrade($val) if $@;
687             };
688             }
689             }
690              
691 0           push $hash->{$key}->@*, $val;
692             }
693              
694 0 0         if ( defined wantarray ) {
695 0           return $res;
696             }
697             else {
698 0           $_[0] = $res;
699              
700 0           return;
701             }
702             }
703              
704             # XOR
705 0     0 0   sub to_xor ( $buf, $mask ) {
  0            
  0            
  0            
706 5     5   52 no feature qw[bitwise];
  5         11  
  5         1089  
707              
708 0           my $mlen = length $mask;
709              
710             # select mask length, max. mask length is 1K
711 0           state $max_mlen = 1024;
712              
713 0 0 0       if ( length $buf > $max_mlen && $mlen < $max_mlen ) {
714 0           $mask = $mask x int $max_mlen / $mlen;
715              
716 0           $mlen = length $mask;
717             }
718              
719 0           my $tmp_buf = my $out = q[];
720              
721 0           $out .= $tmp_buf ^ $mask while length( $tmp_buf = substr $buf, 0, $mlen, q[] ) == $mlen;
722              
723 0           $out .= $tmp_buf ^ substr $mask, 0, length $tmp_buf;
724              
725 0           return $out;
726             }
727              
728             *from_xor = \&to_xor;
729              
730             1;
731             ## -----SOURCE FILTER LOG BEGIN-----
732             ##
733             ## PerlCritic profile "pcore-script" policy violations:
734             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
735             ## | Sev. | Lines | Policy |
736             ## |======+======================+================================================================================================================|
737             ## | 3 | | Subroutines::ProhibitExcessComplexity |
738             ## | | 49 | * Subroutine "encode_data" with high complexity score (35) |
739             ## | | 254 | * Subroutine "decode_data" with high complexity score (33) |
740             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
741             ## | 2 | 585 | ControlStructures::ProhibitCStyleForLoops - C-style "for" loop used |
742             ## |------+----------------------+----------------------------------------------------------------------------------------------------------------|
743             ## | 2 | 721 | ControlStructures::ProhibitPostfixControls - Postfix control "while" used |
744             ## +------+----------------------+----------------------------------------------------------------------------------------------------------------+
745             ##
746             ## -----SOURCE FILTER LOG END-----
747             __END__
748             =pod
749              
750             =encoding utf8
751              
752             =head1 NAME
753              
754             Pcore::Util::Data
755              
756             =head1 SYNOPSIS
757              
758             =head1 DESCRIPTION
759              
760             JSON SERIALIZE
761              
762             ascii(1):
763             - qq[\xA3] -> \u00A3, upgrded and encoded to UTF-8 character;
764             - qq[£] -> \u00A3, UTF-8 character;
765             - qq[á¾¥] -> \u1FA5, UTF-8 character;
766              
767             latin1(1):
768             - qq[\xA3] -> qq[\xA3], encoded as bytes;
769             - qq[£] -> qq[\xA3], downgraded and encoded as bytes;
770             - qq[á¾¥] -> \u1FA5, downgrade impossible, encoded as UTF-8 character;
771              
772             utf8 - used only when ascii(0) and latin1(0);
773             utf8(0) - upgrade scalar, UTF8 on, DO NOT USE, SERIALIZED DATA SHOULD ALWAYS BY WITHOUT UTF8 FLAG!!!!!!!!!!!!!!!!!!;
774             - qq[\xA3] -> "£" (UTF8, multi-byte, len = 1, bytes::len = 2);
775             - qq[£] -> "£" (UTF8, multi-byte, len = 1, bytes::len = 2);
776             - qq[á¾¥] -> "á¾¥" (UTF8, multi-byte, len = 1, bytes::len = 3);
777              
778             utf8(1) - upgrade, encode scalar, UTF8 off;
779             - qq[\xA3] -> "\xC2\xA3" (latin1, bytes::len = 2);
780             - qq[£] -> "\xC2\xA3" (latin1, bytes::len = 2);
781             - qq[á¾¥] -> "\xE1\xBE\xA5" (latin1, bytes::len = 3);
782              
783             So,
784             - don't use latin1(1);
785             - don't use utf8(0);
786              
787             JSON DESERIALIZE
788              
789             utf8(0):
790             - qq[\xA3] -> "£", upgrade;
791             - qq[£] -> "£", as is;
792             - qq[\xC2\xA3] -> "£", upgrade each byte, invalid;
793             - qq[á¾¥] -> error;
794              
795             utf8(1):
796             - qq[\xA3] -> "£", error, can't decode utf8;
797             - qq[£] -> "£", error, can't decode utf8;
798             - qq[\xC2\xA3] -> "£", decode utf8;
799             - qq[á¾¥] -> error, can't decode utf8;
800              
801             So,
802             - if data was encoded with utf8(0) - use utf8(0) to decode;
803             - if data was encoded with utf8(1) - use utf8(1) to decode;
804              
805             =cut