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   407 use 5.010;
  27         88  
5 27     27   116 use warnings;
  27         41  
  27         635  
6 27     27   134 use strict;
  27         62  
  27         731  
7              
8 27     27   9911 use Crypt::PRNG qw(random_bytes random_string);
  27         110953  
  27         1700  
9 27     27   12681 use Encode qw(decode encode);
  27         216145  
  27         1757  
10 27     27   172 use Exporter qw(import);
  27         45  
  27         566  
11 27     27   10008 use File::KDBX::Error;
  27         70  
  27         1893  
12 27     27   293 use List::Util 1.33 qw(any all);
  27         444  
  27         2335  
13 27     27   10861 use Module::Load;
  27         25679  
  27         143  
14 27     27   11026 use Ref::Util qw(is_arrayref is_coderef is_hashref is_ref is_refref is_scalarref);
  27         35288  
  27         2009  
15 27     27   195 use Scalar::Util qw(blessed looks_like_number readonly);
  27         45  
  27         1195  
16 27     27   11828 use Time::Piece 1.33;
  27         273763  
  27         148  
17 27     27   11065 use boolean;
  27         25026  
  27         113  
18 27     27   1848 use namespace::clean -except => 'import';
  27         54  
  27         171  
19              
20             our $VERSION = '0.904'; # 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   17415 my $debug = $ENV{DEBUG};
48 27 50       206 $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       34439 $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 77 my $version = shift;
104              
105 38 100       156 goto IS_LOADED if defined $XS_LOADED;
106              
107 27 100 66     312 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         52 $XS_LOADED = !!eval { require File::KDBX::XS; 1 };
  26         10138  
  26         8493  
112              
113             IS_LOADED:
114             {
115 37         73 local $@;
  37         61  
116 37 50       186 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 3108 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 103 require Config;
143 15 50       799 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 10313 sub empty { _empty(@_) }
213 786     786 1 1361 sub nonempty { !_empty(@_) }
214              
215             sub _empty {
216 1397 100   1397   2552 return 1 if @_ == 0;
217 1393         1989 local $_ = shift;
218 1393   66     12548 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   136 if (load_xs) {
    50          
229 26         2419 *_CowREFCNT = \&File::KDBX::XS::CowREFCNT;
230             }
231 1         172 elsif (eval { require B::COW; 1 }) {
  0         0  
232 0         0 *_CowREFCNT = \&B::COW::cowrefcnt;
233             }
234             else {
235 1     0   111 *_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 4692 for (@_) {
244 2053 100       4540 if (!is_ref($_)) {
    100          
    100          
    50          
245 348 100 66     1282 next if !defined $_ || readonly $_;
246 235         472 my $cowrefcnt = _CowREFCNT($_);
247 235 50 33     800 goto FREE_NONREF if defined $cowrefcnt && 1 < $cowrefcnt;
248             # if (__PACKAGE__->can('erase_xs')) {
249             # erase_xs($_);
250             # }
251             # else {
252 235         671 substr($_, 0, length($_), "\0" x length($_));
253             # }
254             FREE_NONREF: {
255 27     27   178 no warnings 'uninitialized';
  27         36  
  27         3210  
  235         312  
256 235         456 undef $_;
257             }
258             }
259             elsif (is_scalarref($_)) {
260 1639 100 66     7310 next if !defined $$_ || readonly $$_;
261 1224         2381 my $cowrefcnt = _CowREFCNT($$_);
262 1224 100 100     4152 goto FREE_REF if defined $cowrefcnt && 1 < $cowrefcnt;
263             # if (__PACKAGE__->can('erase_xs')) {
264             # erase_xs($$_);
265             # }
266             # else {
267 963         2486 substr($$_, 0, length($$_), "\0" x length($$_));
268             # }
269             FREE_REF: {
270 27     27   165 no warnings 'uninitialized';
  27         41  
  27         8994  
  1224         1534  
271 1224         7224 undef $$_;
272             }
273             }
274             elsif (is_arrayref($_)) {
275 65         187 erase(@$_);
276 65         201 @$_ = ();
277             }
278             elsif (is_hashref($_)) {
279 1         5 erase(values %$_);
280 1         3 %$_ = ();
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 4804 throw 'Programmer error: Cannot call erase_scoped in void context' if !defined wantarray;
291 828         1023 my @args;
292 828         1462 for (@_) {
293 890 50 100     2212 !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       2113 push @args, is_ref($_) ? $_ : \$_;
296             }
297 828         8869 require Scope::Guard;
298 828     828   9366 return Scope::Guard->new(sub { erase(@args) });
  828         6060  
299             }
300              
301              
302             sub extends {
303 118     118 1 309 my $parent = shift;
304 118         287 my $caller = caller;
305 118         809 load $parent;
306 27     27   170 no strict 'refs'; ## no critic (ProhibitNoStrict)
  27         45  
  27         123656  
307 118         19270 @{"${caller}::ISA"} = $parent;
  118         2533  
308             }
309              
310              
311             sub has {
312 1063     1063 1 2231 my $name = shift;
313 1063 100       3948 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
314              
315 1063         3401 my ($package, $file, $line) = caller;
316              
317 1063         1870 my $d = $args{default};
318 1063 100   244   2701 my $default = is_arrayref($d) ? sub { [@$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
  144 100       634  
  736         3710  
319 1063         1327 my $coerce = $args{coerce};
320 1063   100     2935 my $is = $args{is} || 'rw';
321              
322 1063         1324 my $store = $args{store};
323 1063 100       3437 ($store, $name) = split(/\./, $name, 2) if $name =~ /\./;
324              
325 1063   100     3347 my @path = split(/\./, $args{path} || '');
326 1063         1473 my $last = pop @path;
327 1063 100       2844 my $path = $last ? join('', map { qq{->$_} } @path) . qq{->{'$last'}}
  30 100       127  
328             : $store ? qq{->$store\->{'$name'}} : qq{->{'$name'}};
329 1063         1591 my $member = qq{\$_[0]$path};
330              
331              
332 1063 100       2074 my $default_code = is_coderef $default ? q{scalar $default->($_[0])}
    100          
333             : defined $default ? q{$default}
334             : q{undef};
335 1063         1531 my $get = qq{$member //= $default_code;};
336              
337 1063         1254 my $set = '';
338 1063 100       1954 if ($is eq 'rw') {
339 966 50       1928 $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     1206 push @{$ATTRIBUTES{$package} //= []}, $name;
  1063         3065  
345 1063         1542 $line -= 4;
346 1063         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 1063         105612 eval $code; ## no critic (ProhibitStringyEval)
355             }
356              
357              
358             sub format_uuid {
359 346   50 346 1 2945 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
360 346   100     1034 my $delim = shift // '';
361 346 50       730 length($_) == 16 or throw 'Must provide a 16-bytes UUID', size => length($_), str => $_;
362 346         2255 return uc(join($delim, unpack('H8 H4 H4 H4 H12', $_)));
363             }
364              
365              
366             sub generate_uuid {
367 191 100   191 1 1976 my $set = @_ % 2 == 1 ? shift : undef;
368 191         315 my %args = @_;
369 191   66     614 my $test = $set //= $args{test};
370 191 100   1   402 $test = sub { !$set->{$_} } if is_hashref($test);
  1         135  
371 191   100 189   1174 $test //= sub { 1 };
  189         2843  
372 191   100     609 my $printable = $args{printable} // $args{print};
373 191         319 local $_ = '';
374 191         267 do {
375 191 100       673 $_ = $printable ? random_string(16) : random_bytes(16);
376             } while (!$test->($_));
377 191         971 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 5 load_optional('Compress::Raw::Zlib');
396 2         3 local $_ = shift;
397 2         10 my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
398 2 50       1131 $status == Compress::Raw::Zlib::Z_OK()
399             or throw 'Failed to initialize compression library', status => $status;
400 2         37 $status = $d->deflate($_, my $out);
401 2 50       6 $status == Compress::Raw::Zlib::Z_OK()
402             or throw 'Failed to compress data', status => $status;
403 2         44 $status = $d->flush($out);
404 2 50       9 $status == Compress::Raw::Zlib::Z_OK()
405             or throw 'Failed to compress data', status => $status;
406 2         60 return $out;
407             }
408              
409              
410             sub int64 {
411 30     30 1 197 require Config;
412 30 50       2277 if ($Config::Config{ivsize} < 8) {
413 0         0 require Math::BigInt;
414 0         0 return Math::BigInt->new(@_);
415             }
416 30         2025 return 0 + shift;
417             }
418              
419              
420             sub pack_Ql {
421 317     317 1 17182 my $num = shift;
422 317         1314 require Config;
423 317 50       2226 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         1256 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 5838 my $bytes = shift;
458 174         667 require Config;
459 174 50       1272 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         660 return unpack('Q<', $bytes);
464             }
465              
466              
467             sub unpack_ql {
468 8     8 1 5753 my $bytes = shift;
469 8         35 require Config;
470 8 50       48 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 223     223 1 381 my $package = shift;
492 223   50     308 return @{$ATTRIBUTES{$package} // []};
  223         2646  
493             }
494              
495              
496             sub load_optional {
497 96     96 1 208 for my $module (@_) {
498 96         145 eval { load $module };
  96         281  
499 96 50       222721 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       271 return wantarray ? @_ : $_[0];
506             }
507              
508              
509             sub memoize {
510 346     346 1 444 my $func = shift;
511 346         576 my @args = @_;
512 346         457 my %cache;
513 346   100 88   1209 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
  88         130  
  218         687  
514             }
515              
516              
517             sub pad_pkcs7 {
518 16   66 16 1 4099 my $data = shift // throw 'Must provide a string to pad';
519 15 100       48 my $size = shift or throw 'Must provide block size';
520              
521 13 50 33     84 0 <= $size && $size < 256
522             or throw 'Cannot add PKCS7 padding to a large block size', size => $size;
523              
524 13         45 my $pad_len = $size - length($data) % $size;
525 13         147 $data .= chr($pad_len) x $pad_len;
526             }
527              
528              
529 65     65 1 1372 sub query { _query(undef, '-or', \@_) }
530              
531              
532             sub query_any {
533 395     395 1 532 my $code = shift;
534              
535 395 100 66     959 if (is_coderef($code) || overload::Method($code, '&{}')) {
    100          
536 359         715 return $code;
537             }
538             elsif (is_scalarref($code)) {
539 2         41 return simple_expression_query($$code, @_);
540             }
541             else {
542 34         943 return query($code, @_);
543             }
544             }
545              
546              
547             sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
548 1306 50   1306 1 313808 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
549             : read($_[0], $_[1], $_[2], $_[3]);
550 1306 50       4801 return if !defined $result;
551 1306 100       1925 return if $result != $_[2];
552 1305         2638 return $result;
553             }
554              
555              
556             sub recurse_limit {
557 23     23 1 32 my $func = shift;
558 23   50     55 my $max_depth = shift // 200;
559 23   50 0   38 my $error = shift // sub {};
560 23         26 my $depth = 0;
561 23 100   42   72 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
  42         92  
  41         92  
562             };
563              
564              
565             sub search {
566 36     36 1 35872 my $list = shift;
567 36         59 my $query = query_any(@_);
568              
569 36         46 my @match;
570 36         51 for my $item (@$list) {
571 144 100       204 push @match, $item if $query->($item);
572             }
573 36         175 return \@match;
574             }
575              
576              
577             sub simple_expression_query {
578 21     21 1 17055 my $expr = shift;
579 21 100 66     132 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
580              
581 21         45 my $neg_op = $OP_NEG{$op};
582 21   66     47 my $is_re = $op eq '=~' || $op eq '!~';
583              
584 21         1545 require Text::ParseWords;
585 21         3512 my @terms = Text::ParseWords::shellwords($expr);
586              
587 21         1366 my @query = qw(-and);
588              
589 21         37 for my $term (@terms) {
590 28         44 my @subquery = qw(-or);
591              
592 28         58 my $neg = $term =~ s/^-//;
593 28 100       263 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
    100          
594              
595 28         60 for my $field (@_) {
596 32         54 push @subquery, $field => $condition;
597             }
598              
599 28         48 push @query, \@subquery;
600             }
601              
602 21         46 return query(\@query);
603             }
604              
605              
606             sub snakify {
607 3450     3450 1 7808 local $_ = shift;
608 3450         4927 s/UserName/Username/g;
609 3450         18295 s/([a-z])([A-Z0-9])/${1}_${2}/g;
610 3450         9267 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
611 3450         8842 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 262 sub to_bool { $_[0] // return; boolean($_[0]) }
  66         461  
643 98   50 98 1 209 sub to_number { $_[0] // return; 0+$_[0] }
  98         222  
644 203   100 203 1 410 sub to_string { $_[0] // return; "$_[0]" }
  200         648  
645             sub to_time {
646 32   50 32 1 83 $_[0] // return;
647 32 50       493 return scalar gmtime($_[0]) if looks_like_number($_[0]);
648 32 100       360 return scalar gmtime if $_[0] eq 'now';
649 31 100       495 return Time::Piece->strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed $_[0];
650 30         82 return $_[0];
651             }
652 2   50 2 1 5 sub to_tristate { $_[0] // return; boolean($_[0]) }
  2         11  
653             sub to_uuid {
654 10   100 10 1 24 my $str = to_string(@_) // return;
655 7 50       23 return sprintf('%016s', $str) if length($str) < 16;
656 7 50       18 return substr($str, 0, 16) if 16 < length($str);
657 7         20 return $str;
658             }
659              
660              
661             sub trim($) { ## no critic (ProhibitSubroutinePrototypes)
662 1790   100 1790 1 3298 local $_ = shift // return;
663 1712         5166 s/^\s*//;
664 1712         6237 s/\s*$//;
665 1712         3393 return $_;
666             }
667              
668              
669             sub try_load_optional {
670 2     2 1 4 for my $module (@_) {
671 2         4 eval { load $module };
  2         6  
672 2 50       16801 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 273 local $_ = shift // return;
684 146         268 $_ = encode('UTF-8', $_);
685             # RFC 3986 section 2.3 unreserved characters
686 146         4705 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
  4         16  
687 146         348 return $_;
688             }
689              
690              
691             sub uri_unescape_utf8 {
692 24   50 24 1 55 local $_ = shift // return;
693 24         49 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
694 24         106 return decode('UTF-8', $_);
695             }
696              
697              
698             sub uuid {
699 12   50 12 1 4994 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
700 12         29 s/-//g;
701 12 50       45 /^[A-Fa-f0-9]{32}$/ or throw 'Must provide a formatted 128-bit UUID';
702 12         85 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   242 my $arr = shift;
714 216 100       407 return 0 if @$arr % 2 == 1;
715 149         260 for (my $i = 0; $i < @$arr; $i += 2) {
716 161 100       377 return 0 if is_ref($arr->[$i]);
717             }
718 142         246 return 1;
719             }
720              
721             sub _is_operand_plain {
722 298     298   348 local $_ = shift;
723 298   100     918 return !(is_hashref($_) || is_arrayref($_));
724             }
725              
726             sub _query {
727             # dumper \@_;
728 287     287   324 my $subject = shift;
729 287   33     439 my $op = shift // throw 'Must specify a query operator';
730 287         326 my $operand = shift;
731              
732 287 50 66     886 return _query_simple($op, $subject) if defined $subject && !is_ref($op) && ($OPS{$subject} || 2) < 2;
      50        
      66        
733 287 100       386 return _query_simple($subject, $op, $operand) if _is_operand_plain($operand);
734 212 100 66     573 return _query_inverse(_query($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
735 210 100       363 return _query($subject, '-and', [%$operand]) if is_hashref($operand);
736              
737 197         212 my @queries;
738              
739 197         313 my @atoms = @$operand;
740 197         316 while (@atoms) {
741 216 100       311 if (_looks_like_keypairs(\@atoms)) {
742 142         263 my ($atom, $operand) = splice @atoms, 0, 2;
743 142 100       351 if (my $op_type = $OPS{$atom}) {
    50          
744 67 100 100     165 if ($op_type == 1 && _is_operand_plain($operand)) { # unary
745 9         17 push @queries, _query_simple($operand, $atom);
746             }
747             else {
748 58         107 push @queries, _query($subject, $atom, $operand);
749             }
750             }
751             elsif (!is_ref($atom)) {
752 75         177 push @queries, _query($atom, 'eq', $operand);
753             }
754             }
755             else {
756 74         102 my $atom = shift @atoms;
757 74 100       144 if ($OPS{$atom}) { # apply new operator over the rest
758 35         70 push @queries, _query($subject, $atom, \@atoms);
759 35         57 last;
760             }
761             else { # apply original operator over this one
762 39         74 push @queries, _query($subject, $op, $atom);
763             }
764             }
765             }
766              
767 197 100       334 if (@queries == 1) {
    100          
    50          
768 178         516 return $queries[0];
769             }
770             elsif ($op eq '-and') {
771 12         27 return _query_all(@queries);
772             }
773             elsif ($op eq '-or') {
774 7         14 return _query_any(@queries);
775             }
776 0         0 throw 'Malformed query';
777             }
778              
779             sub _query_simple {
780 84     84   124 my $subject = shift;
781 84   50     139 my $op = shift // 'eq';
782 84         100 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       137 $op = '!' if $op eq '-false';
787 84 50       132 $op = '!' if $op eq '-not';
788              
789 84 50       139 defined $subject or throw 'Subject is not set in query';
790 84 50       169 $OPS{$op} >= 0 or throw 'Cannot use a non-simple operator in a simple query';
791 84 100       157 if (empty($operand)) {
792 11 100 66     32 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 224   323 my $field = sub { blessed $_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
  224         779  
808              
809             my %map = (
810 78 100   78   114 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
  78         367  
811 2 50   2   3 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
  2         13  
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   8 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
  4         15  
817 4 50   4   5 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
  4         14  
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   121 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
  81         515  
823 7 50   7   11 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
  7         40  
824 11     11   14 '!' => sub { local $_ = $field->(@_); ! $_ },
  11         36  
825 9     9   14 '!!' => sub { local $_ = $field->(@_); !!$_ },
  9         33  
826 4     4   6 '-defined' => sub { local $_ = $field->(@_); defined $_ },
  4         10  
827 4     4   6 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
  4         9  
828 8     8   11 '-nonempty' => sub { local $_ = $field->(@_); nonempty $_ },
  8         14  
829 8     8   12 '-empty' => sub { local $_ = $field->(@_); empty $_ },
  8         25  
830 84         1852 );
831              
832 84   33     1779 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   2 my $query = shift;
840 2     7   10 return sub { !$query->(@_) };
  7         10  
841             }
842              
843             sub _query_all {
844 12     12   22 my @queries = @_;
845             return sub {
846 44     44   49 my $val = shift;
847 44         122 all { $_->($val) } @queries;
  57         93  
848 12         48 };
849             }
850              
851             sub _query_any {
852 7     7   12 my @queries = @_;
853             return sub {
854 26     26   29 my $val = shift;
855 26         62 any { $_->($val) } @queries;
  43         65  
856 7         25 };
857             }
858              
859             1;
860              
861             __END__