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   380 use 5.010;
  27         75  
5 27     27   115 use warnings;
  27         70  
  27         634  
6 27     27   141 use strict;
  27         60  
  27         776  
7              
8 27     27   10210 use Crypt::PRNG qw(random_bytes random_string);
  27         115586  
  27         1855  
9 27     27   13272 use Encode qw(decode encode);
  27         224523  
  27         1869  
10 27     27   181 use Exporter qw(import);
  27         46  
  27         591  
11 27     27   9913 use File::KDBX::Error;
  27         76  
  27         2065  
12 27     27   315 use List::Util 1.33 qw(any all);
  27         492  
  27         2394  
13 27     27   12255 use Module::Load;
  27         26084  
  27         147  
14 27     27   11824 use Ref::Util qw(is_arrayref is_coderef is_hashref is_ref is_refref is_scalarref);
  27         36727  
  27         2035  
15 27     27   173 use Scalar::Util qw(blessed looks_like_number readonly);
  27         48  
  27         1258  
16 27     27   12484 use Time::Piece 1.33;
  27         280127  
  27         144  
17 27     27   11845 use boolean;
  27         25694  
  27         107  
18 27     27   1821 use namespace::clean -except => 'import';
  27         52  
  27         176  
19              
20             our $VERSION = '0.906'; # 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   16945 my $debug = $ENV{DEBUG};
48 27 50       234 $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       34586 $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 97 my $version = shift;
104              
105 38 100       175 goto IS_LOADED if defined $XS_LOADED;
106              
107 27 100 66     297 if ($ENV{PERL_ONLY} || (exists $ENV{PERL_FILE_KDBX_XS} && !$ENV{PERL_FILE_KDBX_XS})) {
      66        
108 1         4 return $XS_LOADED = !1;
109             }
110              
111 26         60 $XS_LOADED = !!eval { require File::KDBX::XS; 1 };
  26         11121  
  26         9129  
112              
113             IS_LOADED:
114             {
115 37         84 local $@;
  37         75  
116 37 50       198 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 2361     2361 1 3163 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 122 require Config;
143 15 50       928 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 523     523 1 8377 sub empty { _empty(@_) }
213 796     796 1 1342 sub nonempty { !_empty(@_) }
214              
215             sub _empty {
216 1347 100   1347   2116 return 1 if @_ == 0;
217 1343         1827 local $_ = shift;
218 1343   66     11335 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   160 if (load_xs) {
    50          
229 26         2525 *_CowREFCNT = \&File::KDBX::XS::CowREFCNT;
230             }
231 1         1891 elsif (eval { require B::COW; 1 }) {
  0         0  
232 0         0 *_CowREFCNT = \&B::COW::cowrefcnt;
233             }
234             else {
235 1     0   181 *_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 1989     1989 1 4753 for (@_) {
244 2076 100       4536 if (!is_ref($_)) {
    100          
    100          
    50          
245 357 100 66     1197 next if !defined $_ || readonly $_;
246 241         462 my $cowrefcnt = _CowREFCNT($_);
247 241 50 33     717 goto FREE_NONREF if defined $cowrefcnt && 1 < $cowrefcnt;
248             # if (__PACKAGE__->can('erase_xs')) {
249             # erase_xs($_);
250             # }
251             # else {
252 241         611 substr($_, 0, length($_), "\0" x length($_));
253             # }
254             FREE_NONREF: {
255 27     27   173 no warnings 'uninitialized';
  27         51  
  27         3285  
  241         281  
256 241         459 undef $_;
257             }
258             }
259             elsif (is_scalarref($_)) {
260 1653 100 66     7686 next if !defined $$_ || readonly $$_;
261 1238         2453 my $cowrefcnt = _CowREFCNT($$_);
262 1238 100 100     4218 goto FREE_REF if defined $cowrefcnt && 1 < $cowrefcnt;
263             # if (__PACKAGE__->can('erase_xs')) {
264             # erase_xs($$_);
265             # }
266             # else {
267 977         2384 substr($$_, 0, length($$_), "\0" x length($$_));
268             # }
269             FREE_REF: {
270 27     27   172 no warnings 'uninitialized';
  27         44  
  27         9198  
  1238         1512  
271 1238         7333 undef $$_;
272             }
273             }
274             elsif (is_arrayref($_)) {
275 65         179 erase(@$_);
276 65         200 @$_ = ();
277             }
278             elsif (is_hashref($_)) {
279 1         7 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 831 50   831 1 5137 throw 'Programmer error: Cannot call erase_scoped in void context' if !defined wantarray;
291 831         1025 my @args;
292 831         1448 for (@_) {
293 893 50 100     2251 !is_ref($_) || is_arrayref($_) || is_hashref($_) || is_scalarref($_)
      66        
      66        
294             or throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
295 893 100       2303 push @args, is_ref($_) ? $_ : \$_;
296             }
297 831         9082 require Scope::Guard;
298 831     831   9426 return Scope::Guard->new(sub { erase(@args) });
  831         6390  
299             }
300              
301              
302             sub extends {
303 122     122 1 309 my $parent = shift;
304 122         278 my $caller = caller;
305 122         474 load $parent;
306 27     27   188 no strict 'refs'; ## no critic (ProhibitNoStrict)
  27         60  
  27         126493  
307 122         21613 @{"${caller}::ISA"} = $parent;
  122         2706  
308             }
309              
310              
311             sub has {
312 1072     1072 1 2239 my $name = shift;
313 1072 100       4032 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
314              
315 1072         3203 my ($package, $file, $line) = caller;
316              
317 1072         1970 my $d = $args{default};
318 1072 100   447   2712 my $default = is_arrayref($d) ? sub { [@$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
  148 100       651  
  757         3662  
319 1072         1403 my $coerce = $args{coerce};
320 1072   100     3109 my $is = $args{is} || 'rw';
321              
322 1072         1404 my $store = $args{store};
323 1072 100       3503 ($store, $name) = split(/\./, $name, 2) if $name =~ /\./;
324              
325 1072   100     3268 my @path = split(/\./, $args{path} || '');
326 1072         1461 my $last = pop @path;
327 1072 100       2998 my $path = $last ? join('', map { qq{->$_} } @path) . qq{->{'$last'}}
  30 100       127  
328             : $store ? qq{->$store\->{'$name'}} : qq{->{'$name'}};
329 1072         1544 my $member = qq{\$_[0]$path};
330              
331              
332 1072 100       1997 my $default_code = is_coderef $default ? q{scalar $default->($_[0])}
    100          
333             : defined $default ? q{$default}
334             : q{undef};
335 1072         1596 my $get = qq{$member //= $default_code;};
336              
337 1072         1190 my $set = '';
338 1072 100       1930 if ($is eq 'rw') {
339 969 50       1916 $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 1072   100     1214 push @{$ATTRIBUTES{$package} //= []}, $name;
  1072         2904  
345 1072         1503 $line -= 4;
346 1072         2731 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 1072         109018 eval $code; ## no critic (ProhibitStringyEval)
355             }
356              
357              
358             sub format_uuid {
359 362   50 362 1 2716 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
360 362   100     977 my $delim = shift // '';
361 362 50       702 length($_) == 16 or throw 'Must provide a 16-bytes UUID', size => length($_), str => $_;
362 362         2208 return uc(join($delim, unpack('H8 H4 H4 H4 H12', $_)));
363             }
364              
365              
366             sub generate_uuid {
367 195 100   195 1 1687 my $set = @_ % 2 == 1 ? shift : undef;
368 195         334 my %args = @_;
369 195   66     617 my $test = $set //= $args{test};
370 195 100   1   415 $test = sub { !$set->{$_} } if is_hashref($test);
  1         115  
371 195   100 193   1114 $test //= sub { 1 };
  193         2978  
372 195   100     636 my $printable = $args{printable} // $args{print};
373 195         358 local $_ = '';
374 195         247 do {
375 195 100       750 $_ = $printable ? random_string(16) : random_bytes(16);
376             } while (!$test->($_));
377 195         1035 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 6 load_optional('Compress::Raw::Zlib');
396 2         3 local $_ = shift;
397 2         12 my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
398 2 50       1210 $status == Compress::Raw::Zlib::Z_OK()
399             or throw 'Failed to initialize compression library', status => $status;
400 2         40 $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         54 $status = $d->flush($out);
404 2 50       7 $status == Compress::Raw::Zlib::Z_OK()
405             or throw 'Failed to compress data', status => $status;
406 2         39 return $out;
407             }
408              
409              
410             sub int64 {
411 30     30 1 232 require Config;
412 30 50       2496 if ($Config::Config{ivsize} < 8) {
413 0         0 require Math::BigInt;
414 0         0 return Math::BigInt->new(@_);
415             }
416 30         2116 return 0 + shift;
417             }
418              
419              
420             sub pack_Ql {
421 317     317 1 16071 my $num = shift;
422 317         1844 require Config;
423 317 50       2551 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         1403 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 5604 my $bytes = shift;
458 174         705 require Config;
459 174 50       1125 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         630 return unpack('Q<', $bytes);
464             }
465              
466              
467             sub unpack_ql {
468 8     8 1 5310 my $bytes = shift;
469 8         35 require Config;
470 8 50       52 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         28 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 230     230 1 365 my $package = shift;
492 230   50     306 return @{$ATTRIBUTES{$package} // []};
  230         2522  
493             }
494              
495              
496             sub load_optional {
497 96     96 1 206 for my $module (@_) {
498 96         143 eval { load $module };
  96         376  
499 96 50       229647 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       342 return wantarray ? @_ : $_[0];
506             }
507              
508              
509             sub memoize {
510 346     346 1 450 my $func = shift;
511 346         662 my @args = @_;
512 346         479 my %cache;
513 346   100 88   1282 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
  88         130  
  218         724  
514             }
515              
516              
517             sub pad_pkcs7 {
518 16   66 16 1 3803 my $data = shift // throw 'Must provide a string to pad';
519 15 100       74 my $size = shift or throw 'Must provide block size';
520              
521 13 50 33     88 0 <= $size && $size < 256
522             or throw 'Cannot add PKCS7 padding to a large block size', size => $size;
523              
524 13         36 my $pad_len = $size - length($data) % $size;
525 13         153 $data .= chr($pad_len) x $pad_len;
526             }
527              
528              
529 65     65 1 1418 sub query { _query(undef, '-or', \@_) }
530              
531              
532             sub query_any {
533 403     403 1 519 my $code = shift;
534              
535 403 100 66     1052 if (is_coderef($code) || overload::Method($code, '&{}')) {
    100          
536 367         729 return $code;
537             }
538             elsif (is_scalarref($code)) {
539 2         39 return simple_expression_query($$code, @_);
540             }
541             else {
542 34         972 return query($code, @_);
543             }
544             }
545              
546              
547             sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
548 1306 50   1306 1 331190 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
549             : read($_[0], $_[1], $_[2], $_[3]);
550 1306 50       3737 return if !defined $result;
551 1306 100       2022 return if $result != $_[2];
552 1305         2766 return $result;
553             }
554              
555              
556             sub recurse_limit {
557 23     23 1 35 my $func = shift;
558 23   50     44 my $max_depth = shift // 200;
559 23   50 0   44 my $error = shift // sub {};
560 23         27 my $depth = 0;
561 23 100   42   71 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
  42         83  
  41         99  
562             };
563              
564              
565             sub search {
566 36     36 1 36601 my $list = shift;
567 36         77 my $query = query_any(@_);
568              
569 36         45 my @match;
570 36         56 for my $item (@$list) {
571 144 100       227 push @match, $item if $query->($item);
572             }
573 36         216 return \@match;
574             }
575              
576              
577             sub simple_expression_query {
578 21     21 1 21089 my $expr = shift;
579 21 100 66     188 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
580              
581 21         50 my $neg_op = $OP_NEG{$op};
582 21   66     62 my $is_re = $op eq '=~' || $op eq '!~';
583              
584 21         1335 require Text::ParseWords;
585 21         3413 my @terms = Text::ParseWords::shellwords($expr);
586              
587 21         1517 my @query = qw(-and);
588              
589 21         41 for my $term (@terms) {
590 28         56 my @subquery = qw(-or);
591              
592 28         65 my $neg = $term =~ s/^-//;
593 28 100       279 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
    100          
594              
595 28         62 for my $field (@_) {
596 32         60 push @subquery, $field => $condition;
597             }
598              
599 28         53 push @query, \@subquery;
600             }
601              
602 21         56 return query(\@query);
603             }
604              
605              
606             sub snakify {
607 3450     3450 1 7653 local $_ = shift;
608 3450         5075 s/UserName/Username/g;
609 3450         18699 s/([a-z])([A-Z0-9])/${1}_${2}/g;
610 3450         8576 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
611 3450         8875 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 69   50 69 1 234 sub to_bool { $_[0] // return; boolean($_[0]) }
  69         443  
643 99   50 99 1 180 sub to_number { $_[0] // return; 0+$_[0] }
  99         204  
644 209   100 209 1 419 sub to_string { $_[0] // return; "$_[0]" }
  206         572  
645             sub to_time {
646 32   50 32 1 88 $_[0] // return;
647 32 50       431 return scalar gmtime($_[0]) if looks_like_number($_[0]);
648 32 100       364 return scalar gmtime if $_[0] eq 'now';
649 31 100       507 return Time::Piece->strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed $_[0];
650 30         75 return $_[0];
651             }
652 2   50 2 1 5 sub to_tristate { $_[0] // return; boolean($_[0]) }
  2         15  
653             sub to_uuid {
654 10   100 10 1 23 my $str = to_string(@_) // return;
655 7 50       22 return sprintf('%016s', $str) if length($str) < 16;
656 7 50       15 return substr($str, 0, 16) if 16 < length($str);
657 7         19 return $str;
658             }
659              
660              
661             sub trim($) { ## no critic (ProhibitSubroutinePrototypes)
662 1790   100 1790 1 3352 local $_ = shift // return;
663 1712         5255 s/^\s*//;
664 1712         6255 s/\s*$//;
665 1712         3486 return $_;
666             }
667              
668              
669             sub try_load_optional {
670 2     2 1 6 for my $module (@_) {
671 2         3 eval { load $module };
  2         11  
672 2 50       18581 if (my $err = $@) {
673 0         0 warn $err if 3 <= DEBUG;
674 0         0 return;
675             }
676             }
677 2         8 return @_;
678             }
679              
680              
681             my %ESC = map { chr($_) => sprintf('%%%02X', $_) } 0..255;
682             sub uri_escape_utf8 {
683 148   100 148 1 290 local $_ = shift // return;
684 146         278 $_ = encode('UTF-8', $_);
685             # RFC 3986 section 2.3 unreserved characters
686 146         5195 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
  4         22  
687 146         370 return $_;
688             }
689              
690              
691             sub uri_unescape_utf8 {
692 24   50 24 1 72 local $_ = shift // return;
693 24         61 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
694 24         172 return decode('UTF-8', $_);
695             }
696              
697              
698             sub uuid {
699 12   50 12 1 4563 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
700 12         30 s/-//g;
701 12 50       40 /^[A-Fa-f0-9]{32}$/ or throw 'Must provide a formatted 128-bit UUID';
702 12         71 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   243 my $arr = shift;
714 216 100       445 return 0 if @$arr % 2 == 1;
715 149         265 for (my $i = 0; $i < @$arr; $i += 2) {
716 161 100       374 return 0 if is_ref($arr->[$i]);
717             }
718 142         249 return 1;
719             }
720              
721             sub _is_operand_plain {
722 298     298   351 local $_ = shift;
723 298   100     1002 return !(is_hashref($_) || is_arrayref($_));
724             }
725              
726             sub _query {
727             # dumper \@_;
728 287     287   359 my $subject = shift;
729 287   33     457 my $op = shift // throw 'Must specify a query operator';
730 287         307 my $operand = shift;
731              
732 287 50 66     907 return _query_simple($op, $subject) if defined $subject && !is_ref($op) && ($OPS{$subject} || 2) < 2;
      50        
      66        
733 287 100       391 return _query_simple($subject, $op, $operand) if _is_operand_plain($operand);
734 212 100 66     571 return _query_inverse(_query($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
735 210 100       345 return _query($subject, '-and', [%$operand]) if is_hashref($operand);
736              
737 197         281 my @queries;
738              
739 197         323 my @atoms = @$operand;
740 197         319 while (@atoms) {
741 216 100       340 if (_looks_like_keypairs(\@atoms)) {
742 142         259 my ($atom, $operand) = splice @atoms, 0, 2;
743 142 100       346 if (my $op_type = $OPS{$atom}) {
    50          
744 67 100 100     150 if ($op_type == 1 && _is_operand_plain($operand)) { # unary
745 9         20 push @queries, _query_simple($operand, $atom);
746             }
747             else {
748 58         96 push @queries, _query($subject, $atom, $operand);
749             }
750             }
751             elsif (!is_ref($atom)) {
752 75         166 push @queries, _query($atom, 'eq', $operand);
753             }
754             }
755             else {
756 74         98 my $atom = shift @atoms;
757 74 100       184 if ($OPS{$atom}) { # apply new operator over the rest
758 35         73 push @queries, _query($subject, $atom, \@atoms);
759 35         58 last;
760             }
761             else { # apply original operator over this one
762 39         129 push @queries, _query($subject, $op, $atom);
763             }
764             }
765             }
766              
767 197 100       362 if (@queries == 1) {
    100          
    50          
768 178         612 return $queries[0];
769             }
770             elsif ($op eq '-and') {
771 12         29 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   119 my $subject = shift;
781 84   50     160 my $op = shift // 'eq';
782 84         108 my $operand = shift;
783              
784             # these special operators can also act as simple operators
785 84 100       152 $op = '!!' if $op eq '-true';
786 84 100       134 $op = '!' if $op eq '-false';
787 84 50       169 $op = '!' if $op eq '-not';
788              
789 84 50       149 defined $subject or throw 'Subject is not set in query';
790 84 50       171 $OPS{$op} >= 0 or throw 'Cannot use a non-simple operator in a simple query';
791 84 100       160 if (empty($operand)) {
792 11 100 66     46 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         3 $op = '-nonempty';
801             }
802             else {
803 0         0 throw 'Operand is required';
804             }
805             }
806              
807 84 100 66 224   321 my $field = sub { blessed $_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
  224         816  
808              
809             my %map = (
810 78 100   78   114 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
  78         342  
811 2 50   2   4 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
  2         14  
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   7 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
  4         20  
817 4 50   4   7 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
  4         18  
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   6 '>=' => sub { local $_ = $field->(@_); defined && $_ >= $operand },
  4         17  
822 81 50   81   113 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
  81         523  
823 7 50   7   12 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
  7         44  
824 11     11   15 '!' => sub { local $_ = $field->(@_); ! $_ },
  11         29  
825 9     9   17 '!!' => sub { local $_ = $field->(@_); !!$_ },
  9         31  
826 4     4   6 '-defined' => sub { local $_ = $field->(@_); defined $_ },
  4         10  
827 4     4   6 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
  4         8  
828 8     8   11 '-nonempty' => sub { local $_ = $field->(@_); nonempty $_ },
  8         13  
829 8     8   13 '-empty' => sub { local $_ = $field->(@_); empty $_ },
  8         11  
830 84         2111 );
831              
832 84   33     1606 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   4 my $query = shift;
840 2     7   9 return sub { !$query->(@_) };
  7         10  
841             }
842              
843             sub _query_all {
844 12     12   26 my @queries = @_;
845             return sub {
846 44     44   52 my $val = shift;
847 44         128 all { $_->($val) } @queries;
  57         93  
848 12         50 };
849             }
850              
851             sub _query_any {
852 7     7   15 my @queries = @_;
853             return sub {
854 26     26   30 my $val = shift;
855 26         67 any { $_->($val) } @queries;
  43         60  
856 7         27 };
857             }
858              
859             1;
860              
861             __END__