File Coverage

blib/lib/File/KDBX/Util.pm
Criterion Covered Total %
statement 360 488 73.7
branch 151 268 56.3
condition 87 152 57.2
subroutine 88 104 84.6
pod 45 45 100.0
total 731 1057 69.1


line stmt bran cond sub pod time code
1             package File::KDBX::Util;
2             # ABSTRACT: Utility functions for working with KDBX files
3              
4 27     27   368 use 5.010;
  27         84  
5 27     27   119 use warnings;
  27         45  
  27         634  
6 27     27   207 use strict;
  27         67  
  27         763  
7              
8 27     27   10025 use Crypt::PRNG qw(random_bytes random_string);
  27         114741  
  27         1811  
9 27     27   13355 use Encode qw(decode encode);
  27         227365  
  27         1912  
10 27     27   176 use Exporter qw(import);
  27         45  
  27         582  
11 27     27   10374 use File::KDBX::Error;
  27         71  
  27         1970  
12 27     27   300 use List::Util 1.33 qw(any all);
  27         534  
  27         2410  
13 27     27   11368 use Module::Load;
  27         25887  
  27         147  
14 27     27   11542 use Ref::Util qw(is_arrayref is_coderef is_hashref is_ref is_refref is_scalarref);
  27         36902  
  27         2055  
15 27     27   184 use Scalar::Util qw(blessed looks_like_number readonly);
  27         48  
  27         1442  
16 27     27   12412 use Time::Piece 1.33;
  27         278491  
  27         147  
17 27     27   11732 use boolean;
  27         26008  
  27         106  
18 27     27   1859 use namespace::clean -except => 'import';
  27         47  
  27         191  
19              
20             our $VERSION = '0.905'; # VERSION
21              
22             our %EXPORT_TAGS = (
23             assert => [qw(DEBUG assert)],
24             class => [qw(extends has list_attributes)],
25             clone => [qw(clone clone_nomagic)],
26             coercion => [qw(to_bool to_number to_string to_time to_tristate to_uuid)],
27             crypt => [qw(pad_pkcs7)],
28             debug => [qw(DEBUG dumper)],
29             fork => [qw(can_fork)],
30             function => [qw(memoize recurse_limit)],
31             empty => [qw(empty nonempty)],
32             erase => [qw(erase erase_scoped)],
33             gzip => [qw(gzip gunzip)],
34             int => [qw(int64 pack_ql pack_Ql unpack_ql unpack_Ql)],
35             io => [qw(read_all)],
36             load => [qw(load_optional load_xs try_load_optional)],
37             search => [qw(query query_any search simple_expression_query)],
38             text => [qw(snakify trim)],
39             uuid => [qw(format_uuid generate_uuid is_uuid uuid UUID_NULL)],
40             uri => [qw(split_url uri_escape_utf8 uri_unescape_utf8)],
41             );
42              
43             $EXPORT_TAGS{all} = [map { @$_ } values %EXPORT_TAGS];
44             our @EXPORT_OK = @{$EXPORT_TAGS{all}};
45              
46             BEGIN {
47 27     27   17665 my $debug = $ENV{DEBUG};
48 27 50       207 $debug = looks_like_number($debug) ? (0 + $debug) : ($debug ? 1 : 0);
    50          
49             *DEBUG = $debug == 1 ? sub() { 1 } :
50             $debug == 2 ? sub() { 2 } :
51             $debug == 3 ? sub() { 3 } :
52 27 50       34675 $debug == 4 ? sub() { 4 } : sub() { 0 };
    50          
    50          
    50          
53             }
54              
55             my %OPS = (
56             'eq' => 2, # binary
57             'ne' => 2,
58             'lt' => 2,
59             'gt' => 2,
60             'le' => 2,
61             'ge' => 2,
62             '==' => 2,
63             '!=' => 2,
64             '<' => 2,
65             '>' => 2,
66             '<=' => 2,
67             '>=' => 2,
68             '=~' => 2,
69             '!~' => 2,
70             '!' => 1, # unary
71             '!!' => 1,
72             '-not' => 1, # special
73             '-false' => 1,
74             '-true' => 1,
75             '-defined' => 1,
76             '-undef' => 1,
77             '-empty' => 1,
78             '-nonempty' => 1,
79             '-or' => -1,
80             '-and' => -1,
81             );
82             my %OP_NEG = (
83             'eq' => 'ne',
84             'ne' => 'eq',
85             'lt' => 'ge',
86             'gt' => 'le',
87             'le' => 'gt',
88             'ge' => 'lt',
89             '==' => '!=',
90             '!=' => '==',
91             '<' => '>=',
92             '>' => '<=',
93             '<=' => '>',
94             '>=' => '<',
95             '=~' => '!~',
96             '!~' => '=~',
97             );
98             my %ATTRIBUTES;
99              
100              
101             my $XS_LOADED;
102             sub load_xs {
103 38     38 1 94 my $version = shift;
104              
105 38 100       159 goto IS_LOADED if defined $XS_LOADED;
106              
107 27 100 66     306 if ($ENV{PERL_ONLY} || (exists $ENV{PERL_FILE_KDBX_XS} && !$ENV{PERL_FILE_KDBX_XS})) {
      66        
108 1         5 return $XS_LOADED = !1;
109             }
110              
111 26         55 $XS_LOADED = !!eval { require File::KDBX::XS; 1 };
  26         10820  
  26         9230  
112              
113             IS_LOADED:
114             {
115 37         77 local $@;
  37         65  
116 37 50       197 return $XS_LOADED if !$version;
117 0         0 return !!eval { File::KDBX::XS->VERSION($version); 1 };
  0         0  
  0         0  
118             }
119             }
120              
121              
122             sub assert(&) { ## no critic (ProhibitSubroutinePrototypes)
123 2320     2320 1 3017 return if !DEBUG;
124 0         0 my $code = shift;
125 0 0       0 return if $code->();
126              
127 0         0 (undef, my $file, my $line) = caller;
128 0         0 $file =~ s!([^/\\]+)$!$1!;
129 0         0 my $assertion = '';
130 0 0       0 if (try_load_optional('B::Deparse')) {
131 0         0 my $deparse = B::Deparse->new(qw{-P -x9});
132 0         0 $assertion = $deparse->coderef2text($code);
133 0         0 $assertion =~ s/^\{(?:\s*(?:package[^;]+|use[^;]+);)*\s*(.*?);\s*\}$/$1/s;
134 0         0 $assertion =~ s/\s+/ /gs;
135 0         0 $assertion = ": $assertion";
136             }
137 0         0 die "$0: $file:$line: Assertion failed$assertion\n";
138             }
139              
140              
141             sub can_fork {
142 15     15 1 105 require Config;
143 15 50       806 return 1 if $Config::Config{d_fork};
144 0 0 0     0 return 0 if $^O ne 'MSWin32' && $^O ne 'NetWare';
145 0 0       0 return 0 if !$Config::Config{useithreads};
146 0 0       0 return 0 if $Config::Config{ccflags} !~ /-DPERL_IMPLICIT_SYS/;
147 0 0       0 return 0 if $] < 5.008001;
148 0 0 0     0 if ($] == 5.010000 && $Config::Config{ccname} eq 'gcc' && $Config::Config{gccversion}) {
      0        
149 0 0       0 return 0 if $Config::Config{gccversion} !~ m/^(\d+)\.(\d+)/;
150 0         0 my @parts = split(/[\.\s]+/, $Config::Config{gccversion});
151 0 0 0     0 return 0 if $parts[0] > 4 || ($parts[0] == 4 && $parts[1] >= 8);
      0        
152             }
153 0 0       0 return 0 if $INC{'Devel/Cover.pm'};
154 0         0 return 1;
155             }
156              
157              
158             sub clone {
159 0     0 1 0 require Storable;
160 0         0 goto &Storable::dclone;
161             }
162              
163              
164             sub clone_nomagic {
165 0     0 1 0 my $thing = shift;
166 0 0       0 if (is_arrayref($thing)) {
    0          
    0          
167 0         0 my @arr = map { clone_nomagic($_) } @$thing;
  0         0  
168 0         0 return \@arr;
169             }
170             elsif (is_hashref($thing)) {
171 0         0 my %hash;
172 0         0 $hash{$_} = clone_nomagic($thing->{$_}) for keys %$thing;
173 0         0 return \%hash;
174             }
175             elsif (is_ref($thing)) {
176 0         0 return clone($thing);
177             }
178 0         0 return $thing;
179             }
180              
181              
182             sub dumper {
183 0     0 1 0 require Data::Dumper;
184             # avoid "once" warnings
185 0         0 local $Data::Dumper::Deepcopy = $Data::Dumper::Deepcopy = 1;
186 0         0 local $Data::Dumper::Deparse = $Data::Dumper::Deparse = 1;
187 0         0 local $Data::Dumper::Indent = 1;
188 0         0 local $Data::Dumper::Quotekeys = 0;
189 0         0 local $Data::Dumper::Sortkeys = 1;
190 0         0 local $Data::Dumper::Terse = 1;
191 0         0 local $Data::Dumper::Trailingcomma = 1;
192 0         0 local $Data::Dumper::Useqq = 1;
193              
194 0         0 my @dumps;
195 0         0 for my $struct (@_) {
196 0         0 my $str = Data::Dumper::Dumper($struct);
197              
198             # boolean
199 0         0 $str =~ s/bless\( do\{\\\(my \$o = ([01])\)\}, 'boolean' \)/boolean($1)/gs;
200             # Time::Piece
201 0         0 $str =~ s/bless\([^\)]+?(\d+)'?,\s+\d+,?\s+\], 'Time::Piece' \),/
202 0         0 "scalar gmtime($1), # " . scalar gmtime($1)->datetime/ges;
203              
204 0 0       0 print STDERR $str if !defined wantarray;
205 0         0 push @dumps, $str;
206 0         0 return $str;
207             }
208 0         0 return join("\n", @dumps);
209             }
210              
211              
212 583     583 1 8705 sub empty { _empty(@_) }
213 786     786 1 1334 sub nonempty { !_empty(@_) }
214              
215             sub _empty {
216 1397 100   1397   2429 return 1 if @_ == 0;
217 1393         1928 local $_ = shift;
218 1393   66     11658 return !defined $_
219             || $_ eq ''
220             || (is_arrayref($_) && @$_ == 0)
221             || (is_hashref($_) && keys %$_ == 0)
222             || (is_scalarref($_) && (!defined $$_ || $$_ eq ''))
223             || (is_refref($_) && _empty($$_));
224             }
225              
226              
227             BEGIN {
228 27 100   27   150 if (load_xs) {
    50          
229 26         2476 *_CowREFCNT = \&File::KDBX::XS::CowREFCNT;
230             }
231 1         899 elsif (eval { require B::COW; 1 }) {
  0         0  
232 0         0 *_CowREFCNT = \&B::COW::cowrefcnt;
233             }
234             else {
235 1     0   117 *_CowREFCNT = sub { undef };
  0         0  
236             }
237             }
238              
239             sub erase {
240             # Only bother zeroing out memory if we have the last SvPV COW reference, otherwise we'll end up just
241             # creating a copy and erasing the copy.
242             # TODO - Is this worth doing? Need some benchmarking.
243 1966     1966 1 4720 for (@_) {
244 2053 100       4447 if (!is_ref($_)) {
    100          
    100          
    50          
245 348 100 66     1180 next if !defined $_ || readonly $_;
246 235         462 my $cowrefcnt = _CowREFCNT($_);
247 235 50 33     685 goto FREE_NONREF if defined $cowrefcnt && 1 < $cowrefcnt;
248             # if (__PACKAGE__->can('erase_xs')) {
249             # erase_xs($_);
250             # }
251             # else {
252 235         587 substr($_, 0, length($_), "\0" x length($_));
253             # }
254             FREE_NONREF: {
255 27     27   179 no warnings 'uninitialized';
  27         64  
  27         3216  
  235         289  
256 235         436 undef $_;
257             }
258             }
259             elsif (is_scalarref($_)) {
260 1639 100 66     7321 next if !defined $$_ || readonly $$_;
261 1224         2381 my $cowrefcnt = _CowREFCNT($$_);
262 1224 100 100     4063 goto FREE_REF if defined $cowrefcnt && 1 < $cowrefcnt;
263             # if (__PACKAGE__->can('erase_xs')) {
264             # erase_xs($$_);
265             # }
266             # else {
267 962         2630 substr($$_, 0, length($$_), "\0" x length($$_));
268             # }
269             FREE_REF: {
270 27     27   166 no warnings 'uninitialized';
  27         47  
  27         9153  
  1224         1456  
271 1224         7178 undef $$_;
272             }
273             }
274             elsif (is_arrayref($_)) {
275 65         180 erase(@$_);
276 65         182 @$_ = ();
277             }
278             elsif (is_hashref($_)) {
279 1         6 erase(values %$_);
280 1         2 %$_ = ();
281             }
282             else {
283 0         0 throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
284             }
285             }
286             }
287              
288              
289             sub erase_scoped {
290 828 50   828 1 4694 throw 'Programmer error: Cannot call erase_scoped in void context' if !defined wantarray;
291 828         1006 my @args;
292 828         1508 for (@_) {
293 890 50 100     2181 !is_ref($_) || is_arrayref($_) || is_hashref($_) || is_scalarref($_)
      66        
      66        
294             or throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
295 890 100       2075 push @args, is_ref($_) ? $_ : \$_;
296             }
297 828         9248 require Scope::Guard;
298 828     828   9377 return Scope::Guard->new(sub { erase(@args) });
  828         6221  
299             }
300              
301              
302             sub extends {
303 118     118 1 309 my $parent = shift;
304 118         286 my $caller = caller;
305 118         483 load $parent;
306 27     27   169 no strict 'refs'; ## no critic (ProhibitNoStrict)
  27         63  
  27         126007  
307 118         19478 @{"${caller}::ISA"} = $parent;
  118         2561  
308             }
309              
310              
311             sub has {
312 1063     1063 1 2212 my $name = shift;
313 1063 100       3907 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
314              
315 1063         3115 my ($package, $file, $line) = caller;
316              
317 1063         1859 my $d = $args{default};
318 1063 100   495   2606 my $default = is_arrayref($d) ? sub { [@$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
  144 100       667  
  736         3312  
319 1063         1340 my $coerce = $args{coerce};
320 1063   100     3030 my $is = $args{is} || 'rw';
321              
322 1063         1299 my $store = $args{store};
323 1063 100       3361 ($store, $name) = split(/\./, $name, 2) if $name =~ /\./;
324              
325 1063   100     3308 my @path = split(/\./, $args{path} || '');
326 1063         1516 my $last = pop @path;
327 1063 100       2885 my $path = $last ? join('', map { qq{->$_} } @path) . qq{->{'$last'}}
  30 100       129  
328             : $store ? qq{->$store\->{'$name'}} : qq{->{'$name'}};
329 1063         1530 my $member = qq{\$_[0]$path};
330              
331              
332 1063 100       2029 my $default_code = is_coderef $default ? q{scalar $default->($_[0])}
    100          
333             : defined $default ? q{$default}
334             : q{undef};
335 1063         1565 my $get = qq{$member //= $default_code;};
336              
337 1063         1221 my $set = '';
338 1063 100       1913 if ($is eq 'rw') {
339 966 50       1946 $set = is_coderef $coerce ? qq{$member = scalar \$coerce->(\@_[1..\$#_]) if \$#_;}
    100          
340             : defined $coerce ? qq{$member = do { local @_ = (\@_[1..\$#_]); $coerce } if \$#_;}
341             : qq{$member = \$_[1] if \$#_;};
342             }
343              
344 1063   100     1278 push @{$ATTRIBUTES{$package} //= []}, $name;
  1063         2862  
345 1063         1460 $line -= 4;
346 1063         2787 my $code = <
347             # line $line "$file"
348             sub ${package}::${name} {
349             return $default_code if !Scalar::Util::blessed(\$_[0]);
350             $set
351             $get
352             }
353             END
354 1063         107230 eval $code; ## no critic (ProhibitStringyEval)
355             }
356              
357              
358             sub format_uuid {
359 346   50 346 1 3060 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
360 346   100     918 my $delim = shift // '';
361 346 50       737 length($_) == 16 or throw 'Must provide a 16-bytes UUID', size => length($_), str => $_;
362 346         1997 return uc(join($delim, unpack('H8 H4 H4 H4 H12', $_)));
363             }
364              
365              
366             sub generate_uuid {
367 191 100   191 1 1704 my $set = @_ % 2 == 1 ? shift : undef;
368 191         298 my %args = @_;
369 191   66     638 my $test = $set //= $args{test};
370 191 100   1   443 $test = sub { !$set->{$_} } if is_hashref($test);
  1         137  
371 191   100 189   1138 $test //= sub { 1 };
  189         2582  
372 191   100     606 my $printable = $args{printable} // $args{print};
373 191         319 local $_ = '';
374 191         265 do {
375 191 100       709 $_ = $printable ? random_string(16) : random_bytes(16);
376             } while (!$test->($_));
377 191         958 return $_;
378             }
379              
380              
381             sub gunzip {
382 0     0 1 0 load_optional('Compress::Raw::Zlib');
383 0         0 local $_ = shift;
384 0         0 my ($i, $status) = Compress::Raw::Zlib::Inflate->new(-WindowBits => 31);
385 0 0       0 $status == Compress::Raw::Zlib::Z_OK()
386             or throw 'Failed to initialize compression library', status => $status;
387 0         0 $status = $i->inflate($_, my $out);
388 0 0       0 $status == Compress::Raw::Zlib::Z_STREAM_END()
389             or throw 'Failed to decompress data', status => $status;
390 0         0 return $out;
391             }
392              
393              
394             sub gzip {
395 2     2 1 7 load_optional('Compress::Raw::Zlib');
396 2         5 local $_ = shift;
397 2         15 my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
398 2 50       1252 $status == Compress::Raw::Zlib::Z_OK()
399             or throw 'Failed to initialize compression library', status => $status;
400 2         35 $status = $d->deflate($_, my $out);
401 2 50       7 $status == Compress::Raw::Zlib::Z_OK()
402             or throw 'Failed to compress data', status => $status;
403 2         48 $status = $d->flush($out);
404 2 50       8 $status == Compress::Raw::Zlib::Z_OK()
405             or throw 'Failed to compress data', status => $status;
406 2         91 return $out;
407             }
408              
409              
410             sub int64 {
411 30     30 1 203 require Config;
412 30 50       2323 if ($Config::Config{ivsize} < 8) {
413 0         0 require Math::BigInt;
414 0         0 return Math::BigInt->new(@_);
415             }
416 30         2134 return 0 + shift;
417             }
418              
419              
420             sub pack_Ql {
421 317     317 1 16411 my $num = shift;
422 317         1526 require Config;
423 317 50       2395 if ($Config::Config{ivsize} < 8) {
424 0 0 0     0 if (blessed $num && $num->can('as_hex')) {
425 0         0 require Math::BigInt;
426 0 0       0 return "\xff\xff\xff\xff\xff\xff\xff\xff" if Math::BigInt->new('18446744073709551615') <= $num;
427 0 0       0 return "\x00\x00\x00\x00\x00\x00\x00\x80" if $num <= Math::BigInt->new('-9223372036854775808');
428 0         0 my $neg;
429 0 0       0 if ($num < 0) {
430 0         0 $neg = 1;
431 0         0 $num = -$num;
432             }
433 0         0 my $hex = $num->as_hex;
434 0         0 $hex =~ s/^0x/000000000000000/;
435 0         0 my $bytes = reverse pack('H16', substr($hex, -16));
436 0 0       0 $bytes .= "\0" x (8 - length $bytes) if length $bytes < 8;
437 0 0       0 if ($neg) {
438             # two's compliment
439 0         0 $bytes = join('', map { chr(~ord($_) & 0xff) } split(//, $bytes));
  0         0  
440 0         0 substr($bytes, 0, 1, chr(ord(substr($bytes, 0, 1)) + 1));
441             }
442 0         0 return $bytes;
443             }
444             else {
445 0 0       0 my $pad = $num < 0 ? "\xff" : "\0";
446 0         0 return pack('L<', $num) . ($pad x 4);
447             };
448             }
449 317         1338 return pack('Q<', $num);
450             }
451              
452              
453 0     0 1 0 sub pack_ql { goto &pack_Ql }
454              
455              
456             sub unpack_Ql {
457 174     174 1 5555 my $bytes = shift;
458 174         648 require Config;
459 174 50       1193 if ($Config::Config{ivsize} < 8) {
460 0         0 require Math::BigInt;
461 0         0 return Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
462             }
463 174         620 return unpack('Q<', $bytes);
464             }
465              
466              
467             sub unpack_ql {
468 8     8 1 5263 my $bytes = shift;
469 8         35 require Config;
470 8 50       56 if ($Config::Config{ivsize} < 8) {
471 0         0 require Math::BigInt;
472 0 0       0 if (ord(substr($bytes, -1, 1)) & 128) {
473 0 0       0 return Math::BigInt->new('-9223372036854775808') if $bytes eq "\x00\x00\x00\x00\x00\x00\x00\x80";
474             # two's compliment
475 0         0 substr($bytes, 0, 1, chr(ord(substr($bytes, 0, 1)) - 1));
476 0         0 $bytes = join('', map { chr(~ord($_) & 0xff) } split(//, $bytes));
  0         0  
477 0         0 return -Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
478             }
479             else {
480 0         0 return Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
481             }
482             }
483 8         29 return unpack('q<', $bytes);
484             }
485              
486              
487 0 0 0 0 1 0 sub is_uuid { defined $_[0] && !is_ref($_[0]) && length($_[0]) == 16 }
488              
489              
490             sub list_attributes {
491 223     223 1 339 my $package = shift;
492 223   50     274 return @{$ATTRIBUTES{$package} // []};
  223         2088  
493             }
494              
495              
496             sub load_optional {
497 96     96 1 179 for my $module (@_) {
498 96         169 eval { load $module };
  96         228  
499 96 50       228134 if (my $err = $@) {
500 0         0 throw "Missing dependency: Please install $module to use this feature.\n",
501             module => $module,
502             error => $err;
503             }
504             }
505 96 50       234 return wantarray ? @_ : $_[0];
506             }
507              
508              
509             sub memoize {
510 346     346 1 483 my $func = shift;
511 346         594 my @args = @_;
512 346         408 my %cache;
513 346   100 88   1233 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
  88         137  
  218         668  
514             }
515              
516              
517             sub pad_pkcs7 {
518 16   66 16 1 3987 my $data = shift // throw 'Must provide a string to pad';
519 15 100       66 my $size = shift or throw 'Must provide block size';
520              
521 13 50 33     95 0 <= $size && $size < 256
522             or throw 'Cannot add PKCS7 padding to a large block size', size => $size;
523              
524 13         41 my $pad_len = $size - length($data) % $size;
525 13         146 $data .= chr($pad_len) x $pad_len;
526             }
527              
528              
529 65     65 1 1528 sub query { _query(undef, '-or', \@_) }
530              
531              
532             sub query_any {
533 395     395 1 566 my $code = shift;
534              
535 395 100 66     905 if (is_coderef($code) || overload::Method($code, '&{}')) {
    100          
536 359         678 return $code;
537             }
538             elsif (is_scalarref($code)) {
539 2         41 return simple_expression_query($$code, @_);
540             }
541             else {
542 34         923 return query($code, @_);
543             }
544             }
545              
546              
547             sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
548 1306 50   1306 1 330260 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
549             : read($_[0], $_[1], $_[2], $_[3]);
550 1306 50       3509 return if !defined $result;
551 1306 100       1896 return if $result != $_[2];
552 1305         2454 return $result;
553             }
554              
555              
556             sub recurse_limit {
557 23     23 1 32 my $func = shift;
558 23   50     46 my $max_depth = shift // 200;
559 23   50 0   41 my $error = shift // sub {};
560 23         30 my $depth = 0;
561 23 100   42   87 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
  42         85  
  41         126  
562             };
563              
564              
565             sub search {
566 36     36 1 36514 my $list = shift;
567 36         77 my $query = query_any(@_);
568              
569 36         58 my @match;
570 36         51 for my $item (@$list) {
571 144 100       213 push @match, $item if $query->($item);
572             }
573 36         170 return \@match;
574             }
575              
576              
577             sub simple_expression_query {
578 21     21 1 17389 my $expr = shift;
579 21 100 66     143 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
580              
581 21         41 my $neg_op = $OP_NEG{$op};
582 21   66     55 my $is_re = $op eq '=~' || $op eq '!~';
583              
584 21         1623 require Text::ParseWords;
585 21         3746 my @terms = Text::ParseWords::shellwords($expr);
586              
587 21         1356 my @query = qw(-and);
588              
589 21         33 for my $term (@terms) {
590 28         42 my @subquery = qw(-or);
591              
592 28         51 my $neg = $term =~ s/^-//;
593 28 100       245 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
    100          
594              
595 28         88 for my $field (@_) {
596 32         57 push @subquery, $field => $condition;
597             }
598              
599 28         53 push @query, \@subquery;
600             }
601              
602 21         46 return query(\@query);
603             }
604              
605              
606             sub snakify {
607 3450     3450 1 7817 local $_ = shift;
608 3450         5184 s/UserName/Username/g;
609 3450         18935 s/([a-z])([A-Z0-9])/${1}_${2}/g;
610 3450         8787 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
611 3450         8702 return lc($_);
612             }
613              
614              
615             sub split_url {
616 0     0 1 0 local $_ = shift;
617 0         0 my ($scheme, $auth, $host, $port, $path, $query, $hash) =~ m!
618             ^([^:/\?\#]+) ://
619             (?:([^\@]+)\@)
620             ([^:/\?\#]*)
621             (?::(\d+))?
622             ([^\?\#]*)
623             (\?[^\#]*)?
624             (\#(.*))?
625             !x;
626              
627 0         0 $scheme = lc($scheme);
628              
629 0   0     0 $host ||= 'localhost';
630 0         0 $host = lc($host);
631              
632 0 0       0 $path = "/$path" if $path !~ m!^/!;
633              
634 0 0 0     0 $port ||= $scheme eq 'http' ? 80 : $scheme eq 'https' ? 433 : undef;
    0          
635              
636 0         0 my ($username, $password) = split($auth, ':', 2);
637              
638 0         0 return ($scheme, $auth, $host, $port, $path, $query, $hash, $username, $password);
639             }
640              
641              
642 66   50 66 1 223 sub to_bool { $_[0] // return; boolean($_[0]) }
  66         400  
643 98   50 98 1 192 sub to_number { $_[0] // return; 0+$_[0] }
  98         198  
644 203   100 203 1 391 sub to_string { $_[0] // return; "$_[0]" }
  200         527  
645             sub to_time {
646 32   50 32 1 88 $_[0] // return;
647 32 50       461 return scalar gmtime($_[0]) if looks_like_number($_[0]);
648 32 100       373 return scalar gmtime if $_[0] eq 'now';
649 31 100       493 return Time::Piece->strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed $_[0];
650 30         79 return $_[0];
651             }
652 2   50 2 1 5 sub to_tristate { $_[0] // return; boolean($_[0]) }
  2         12  
653             sub to_uuid {
654 10   100 10 1 21 my $str = to_string(@_) // return;
655 7 50       19 return sprintf('%016s', $str) if length($str) < 16;
656 7 50       16 return substr($str, 0, 16) if 16 < length($str);
657 7         17 return $str;
658             }
659              
660              
661             sub trim($) { ## no critic (ProhibitSubroutinePrototypes)
662 1790   100 1790 1 3375 local $_ = shift // return;
663 1712         5272 s/^\s*//;
664 1712         10522 s/\s*$//;
665 1712         3487 return $_;
666             }
667              
668              
669             sub try_load_optional {
670 2     2 1 7 for my $module (@_) {
671 2         3 eval { load $module };
  2         18  
672 2 50       18349 if (my $err = $@) {
673 0         0 warn $err if 3 <= DEBUG;
674 0         0 return;
675             }
676             }
677 2         7 return @_;
678             }
679              
680              
681             my %ESC = map { chr($_) => sprintf('%%%02X', $_) } 0..255;
682             sub uri_escape_utf8 {
683 148   100 148 1 252 local $_ = shift // return;
684 146         211 $_ = encode('UTF-8', $_);
685             # RFC 3986 section 2.3 unreserved characters
686 146         4618 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
  4         18  
687 146         362 return $_;
688             }
689              
690              
691             sub uri_unescape_utf8 {
692 24   50 24 1 47 local $_ = shift // return;
693 24         39 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
694 24         58 return decode('UTF-8', $_);
695             }
696              
697              
698             sub uuid {
699 12   50 12 1 4885 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
700 12         32 s/-//g;
701 12 50       53 /^[A-Fa-f0-9]{32}$/ or throw 'Must provide a formatted 128-bit UUID';
702 12         70 return pack('H32', $_);
703              
704             }
705              
706              
707             sub UUID_NULL() { "\0" x 16 }
708              
709             ### --------------------------------------------------------------------------
710              
711             # Determine if an array looks like keypairs from a hash.
712             sub _looks_like_keypairs {
713 216     216   250 my $arr = shift;
714 216 100       392 return 0 if @$arr % 2 == 1;
715 149         275 for (my $i = 0; $i < @$arr; $i += 2) {
716 161 100       367 return 0 if is_ref($arr->[$i]);
717             }
718 142         222 return 1;
719             }
720              
721             sub _is_operand_plain {
722 298     298   345 local $_ = shift;
723 298   100     939 return !(is_hashref($_) || is_arrayref($_));
724             }
725              
726             sub _query {
727             # dumper \@_;
728 287     287   335 my $subject = shift;
729 287   33     442 my $op = shift // throw 'Must specify a query operator';
730 287         350 my $operand = shift;
731              
732 287 50 66     862 return _query_simple($op, $subject) if defined $subject && !is_ref($op) && ($OPS{$subject} || 2) < 2;
      50        
      66        
733 287 100       390 return _query_simple($subject, $op, $operand) if _is_operand_plain($operand);
734 212 100 66     592 return _query_inverse(_query($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
735 210 100       342 return _query($subject, '-and', [%$operand]) if is_hashref($operand);
736              
737 197         205 my @queries;
738              
739 197         313 my @atoms = @$operand;
740 197         306 while (@atoms) {
741 216 100       338 if (_looks_like_keypairs(\@atoms)) {
742 142         262 my ($atom, $operand) = splice @atoms, 0, 2;
743 142 100       365 if (my $op_type = $OPS{$atom}) {
    50          
744 67 100 100     148 if ($op_type == 1 && _is_operand_plain($operand)) { # unary
745 9         20 push @queries, _query_simple($operand, $atom);
746             }
747             else {
748 58         101 push @queries, _query($subject, $atom, $operand);
749             }
750             }
751             elsif (!is_ref($atom)) {
752 75         147 push @queries, _query($atom, 'eq', $operand);
753             }
754             }
755             else {
756 74         94 my $atom = shift @atoms;
757 74 100       185 if ($OPS{$atom}) { # apply new operator over the rest
758 35         62 push @queries, _query($subject, $atom, \@atoms);
759 35         69 last;
760             }
761             else { # apply original operator over this one
762 39         82 push @queries, _query($subject, $op, $atom);
763             }
764             }
765             }
766              
767 197 100       368 if (@queries == 1) {
    100          
    50          
768 178         515 return $queries[0];
769             }
770             elsif ($op eq '-and') {
771 12         28 return _query_all(@queries);
772             }
773             elsif ($op eq '-or') {
774 7         15 return _query_any(@queries);
775             }
776 0         0 throw 'Malformed query';
777             }
778              
779             sub _query_simple {
780 84     84   116 my $subject = shift;
781 84   50     149 my $op = shift // 'eq';
782 84         101 my $operand = shift;
783              
784             # these special operators can also act as simple operators
785 84 100       145 $op = '!!' if $op eq '-true';
786 84 100       139 $op = '!' if $op eq '-false';
787 84 50       122 $op = '!' if $op eq '-not';
788              
789 84 50       136 defined $subject or throw 'Subject is not set in query';
790 84 50       185 $OPS{$op} >= 0 or throw 'Cannot use a non-simple operator in a simple query';
791 84 100       165 if (empty($operand)) {
792 11 100 66     29 if ($OPS{$op} < 2) {
    100 33        
    50          
793             # no operand needed
794             }
795             # Allow field => undef and field => {'ne' => undef} to do the (arguably) right thing.
796             elsif ($op eq 'eq' || $op eq '==') {
797 1         2 $op = '-empty';
798             }
799             elsif ($op eq 'ne' || $op eq '!=') {
800 1         2 $op = '-nonempty';
801             }
802             else {
803 0         0 throw 'Operand is required';
804             }
805             }
806              
807 84 100 66 226   304 my $field = sub { blessed $_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
  226         860  
808              
809             my %map = (
810 80 100   80   133 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
  80         349  
811 2 50   2   4 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
  2         17  
812 0 0   0   0 'lt' => sub { local $_ = $field->(@_); defined && $_ lt $operand },
  0         0  
813 0 0   0   0 'gt' => sub { local $_ = $field->(@_); defined && $_ gt $operand },
  0         0  
814 0 0   0   0 'le' => sub { local $_ = $field->(@_); defined && $_ le $operand },
  0         0  
815 0 0   0   0 'ge' => sub { local $_ = $field->(@_); defined && $_ ge $operand },
  0         0  
816 4 50   4   5 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
  4         16  
817 4 50   4   8 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
  4         16  
818 0 0   0   0 '<' => sub { local $_ = $field->(@_); defined && $_ < $operand },
  0         0  
819 0 0   0   0 '>' => sub { local $_ = $field->(@_); defined && $_ > $operand },
  0         0  
820 0 0   0   0 '<=' => sub { local $_ = $field->(@_); defined && $_ <= $operand },
  0         0  
821 4 50   4   7 '>=' => sub { local $_ = $field->(@_); defined && $_ >= $operand },
  4         14  
822 81 50   81   115 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
  81         517  
823 7 50   7   9 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
  7         42  
824 11     11   14 '!' => sub { local $_ = $field->(@_); ! $_ },
  11         28  
825 9     9   17 '!!' => sub { local $_ = $field->(@_); !!$_ },
  9         38  
826 4     4   7 '-defined' => sub { local $_ = $field->(@_); defined $_ },
  4         10  
827 4     4   6 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
  4         9  
828 8     8   10 '-nonempty' => sub { local $_ = $field->(@_); nonempty $_ },
  8         13  
829 8     8   10 '-empty' => sub { local $_ = $field->(@_); empty $_ },
  8         13  
830 84         1975 );
831              
832 84   33     1473 return $map{$op} // throw "Unexpected operator in query: $op",
833             subject => $subject,
834             operator => $op,
835             operand => $operand;
836             }
837              
838             sub _query_inverse {
839 2     2   3 my $query = shift;
840 2     7   9 return sub { !$query->(@_) };
  7         10  
841             }
842              
843             sub _query_all {
844 12     12   28 my @queries = @_;
845             return sub {
846 44     44   46 my $val = shift;
847 44         125 all { $_->($val) } @queries;
  59         91  
848 12         48 };
849             }
850              
851             sub _query_any {
852 7     7   18 my @queries = @_;
853             return sub {
854 26     26   28 my $val = shift;
855 26         81 any { $_->($val) } @queries;
  43         60  
856 7         27 };
857             }
858              
859             1;
860              
861             __END__