]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Util.pm
9fe9a9eb318faf99efe195d2928b875b79ef4185
[chaz/p5-File-KDBX] / lib / File / KDBX / Util.pm
1 package File::KDBX::Util;
2 # ABSTRACT: Utility functions for working with KDBX files
3
4 use warnings;
5 use strict;
6
7 use Crypt::PRNG qw(random_bytes random_string);
8 use Encode qw(decode encode);
9 use Exporter qw(import);
10 use File::KDBX::Constants qw(:bool);
11 use File::KDBX::Error;
12 use List::Util 1.33 qw(any all);
13 use Module::Load;
14 use Ref::Util qw(is_arrayref is_coderef is_hashref is_ref is_refref is_scalarref);
15 use Scalar::Util qw(blessed looks_like_number readonly);
16 use Time::Piece;
17 use boolean;
18 use namespace::clean -except => 'import';
19
20 our $VERSION = '999.999'; # VERSION
21
22 our %EXPORT_TAGS = (
23 assert => [qw(assert_64bit)],
24 class => [qw(extends has)],
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(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 io => [qw(is_readable is_writable read_all)],
35 load => [qw(load_optional load_xs try_load_optional)],
36 search => [qw(query search search_limited simple_expression_query)],
37 text => [qw(snakify trim)],
38 uuid => [qw(format_uuid generate_uuid is_uuid uuid)],
39 uri => [qw(split_url uri_escape_utf8 uri_unescape_utf8)],
40 );
41
42 $EXPORT_TAGS{all} = [map { @$_ } values %EXPORT_TAGS];
43 our @EXPORT_OK = @{$EXPORT_TAGS{all}};
44
45 my %OPS = (
46 'eq' => 2, # binary
47 'ne' => 2,
48 'lt' => 2,
49 'gt' => 2,
50 'le' => 2,
51 'ge' => 2,
52 '==' => 2,
53 '!=' => 2,
54 '<' => 2,
55 '>' => 2,
56 '<=' => 2,
57 '>=' => 2,
58 '=~' => 2,
59 '!~' => 2,
60 '!' => 1, # unary
61 '!!' => 1,
62 '-not' => 1, # special
63 '-false' => 1,
64 '-true' => 1,
65 '-defined' => 1,
66 '-undef' => 1,
67 '-empty' => 1,
68 '-nonempty' => 1,
69 '-or' => -1,
70 '-and' => -1,
71 );
72 my %OP_NEG = (
73 'eq' => 'ne',
74 'ne' => 'eq',
75 'lt' => 'ge',
76 'gt' => 'le',
77 'le' => 'gt',
78 'ge' => 'lt',
79 '==' => '!=',
80 '!=' => '==',
81 '<' => '>=',
82 '>' => '<=',
83 '<=' => '>',
84 '>=' => '<',
85 '=~' => '!~',
86 '!~' => '=~',
87 );
88
89 =func load_xs
90
91 $bool = load_xs();
92 $bool = load_xs($version);
93
94 Attempt to load L<File::KDBX::XS>. Return truthy if C<XS> is loaded. If C<$version> is given, it will check
95 that at least the given version is loaded.
96
97 =cut
98
99 my $XS_LOADED;
100 sub load_xs {
101 my $version = shift;
102
103 goto IS_LOADED if defined $XS_LOADED;
104
105 if ($ENV{PERL_ONLY} || (exists $ENV{PERL_FILE_KDBX_XS} && !$ENV{PERL_FILE_KDBX_XS})) {
106 return $XS_LOADED = FALSE;
107 }
108
109 $XS_LOADED = !!eval { require File::KDBX::XS; 1 };
110
111 IS_LOADED:
112 {
113 local $@;
114 return $XS_LOADED if !$version;
115 return !!eval { File::KDBX::XS->VERSION($version); 1 };
116 }
117 }
118
119 =func assert_64bit
120
121 assert_64bit();
122
123 Throw if perl doesn't support 64-bit IVs.
124
125 =cut
126
127 sub assert_64bit() {
128 require Config;
129 $Config::Config{ivsize} < 8
130 and throw "64-bit perl is required to use this feature.\n", ivsize => $Config::Config{ivsize};
131 }
132
133 =func can_fork
134
135 $bool = can_fork;
136
137 Determine if perl can fork, with logic lifted from L<Test2::Util/CAN_FORK>.
138
139 =cut
140
141 sub can_fork {
142 require Config;
143 return 1 if $Config::Config{d_fork};
144 return 0 if $^O ne 'MSWin32' && $^O ne 'NetWare';
145 return 0 if !$Config::Config{useithreads};
146 return 0 if $Config::Config{ccflags} !~ /-DPERL_IMPLICIT_SYS/;
147 return 0 if $] < 5.008001;
148 if ($] == 5.010000 && $Config::Config{ccname} eq 'gcc' && $Config::Config{gccversion}) {
149 return 0 if $Config::Config{gccversion} !~ m/^(\d+)\.(\d+)/;
150 my @parts = split(/[\.\s]+/, $Config::Config{gccversion});
151 return 0 if $parts[0] > 4 || ($parts[0] == 4 && $parts[1] >= 8);
152 }
153 return 0 if $INC{'Devel/Cover.pm'};
154 return 1;
155 }
156
157 =func clone
158
159 $clone = clone($thing);
160
161 Clone deeply. This is an unadorned alias to L<Storable> C<dclone>.
162
163 =cut
164
165 sub clone {
166 require Storable;
167 goto &Storable::dclone;
168 }
169
170 =func clone_nomagic
171
172 $clone = clone_nomagic($thing);
173
174 Clone deeply without keeping [most of] the magic.
175
176 B<WARNING:> At the moment the implementation is naïve and won't respond well to nontrivial data or recursive
177 structures.
178
179 =cut
180
181 sub clone_nomagic {
182 my $thing = shift;
183 if (is_arrayref($thing)) {
184 my @arr = map { clone_nomagic($_) } @$thing;
185 return \@arr;
186 }
187 elsif (is_hashref($thing)) {
188 my %hash;
189 $hash{$_} = clone_nomagic($thing->{$_}) for keys %$thing;
190 return \%hash;
191 }
192 elsif (is_ref($thing)) {
193 return clone($thing);
194 }
195 return $thing;
196 }
197
198 =func dumper
199
200 $str = dumper $thing;
201 dumper $thing; # in void context, prints to STDERR
202
203 Like L<Data::Dumper> but slightly terser in some cases relevent to L<File::KDBX>.
204
205 =cut
206
207 sub dumper {
208 require Data::Dumper;
209 # avoid "once" warnings
210 local $Data::Dumper::Deepcopy = $Data::Dumper::Deepcopy = 1;
211 local $Data::Dumper::Deparse = $Data::Dumper::Deparse = 1;
212 local $Data::Dumper::Indent = 1;
213 local $Data::Dumper::Quotekeys = 0;
214 local $Data::Dumper::Sortkeys = 1;
215 local $Data::Dumper::Terse = 1;
216 local $Data::Dumper::Trailingcomma = 1;
217 local $Data::Dumper::Useqq = 1;
218
219 my @dumps;
220 for my $struct (@_) {
221 my $str = Data::Dumper::Dumper($struct);
222
223 # boolean
224 $str =~ s/bless\( do\{\\\(my \$o = ([01])\)\}, 'boolean' \)/boolean($1)/gs;
225 # Time::Piece
226 $str =~ s/bless\([^\)]+?(\d+)'?,\s+\d+,?\s+\], 'Time::Piece' \)/Time::Piece->new($1)/gs;
227
228 print STDERR $str if !defined wantarray;
229 push @dumps, $str;
230 return $str;
231 }
232 return join("\n", @dumps);
233 }
234
235 =func empty
236
237 =func nonempty
238
239 $bool = empty $thing;
240
241 $bool = nonempty $thing;
242
243 Test whether a thing is empty (or nonempty). An empty thing is one of these:
244
245 =for :list
246 * nonexistent
247 * C<undef>
248 * zero-length string
249 * zero-length array
250 * hash with zero keys
251 * reference to an empty thing (recursive)
252
253 Note in particular that zero C<0> is not considered empty because it is an actual value.
254
255 =cut
256
257 sub empty { _empty(@_) }
258 sub nonempty { !_empty(@_) }
259
260 sub _empty {
261 return 1 if @_ == 0;
262 local $_ = shift;
263 return !defined $_
264 || $_ eq ''
265 || (is_arrayref($_) && @$_ == 0)
266 || (is_hashref($_) && keys %$_ == 0)
267 || (is_scalarref($_) && (!defined $$_ || $$_ eq ''))
268 || (is_refref($_) && _empty($$_));
269 }
270
271 =func erase
272
273 erase($string, ...);
274 erase(\$string, ...);
275
276 Overwrite the memory used by one or more string.
277
278 =cut
279
280 BEGIN {
281 if (load_xs) {
282 *_CowREFCNT = \&File::KDBX::XS::CowREFCNT;
283 }
284 elsif (eval { require B::COW; 1 }) {
285 *_CowREFCNT = \&B::COW::cowrefcnt;
286 }
287 else {
288 *_CowREFCNT = sub { undef };
289 }
290 }
291
292 sub erase {
293 # Only bother zeroing out memory if we have the last SvPV COW reference, otherwise we'll end up just
294 # creating a copy and erasing the copy.
295 # TODO - Is this worth doing? Need some benchmarking.
296 for (@_) {
297 if (!is_ref($_)) {
298 next if !defined $_ || readonly $_;
299 my $cowrefcnt = _CowREFCNT($_);
300 goto FREE_NONREF if defined $cowrefcnt && 1 < $cowrefcnt;
301 # if (__PACKAGE__->can('erase_xs')) {
302 # erase_xs($_);
303 # }
304 # else {
305 substr($_, 0, length($_), "\0" x length($_));
306 # }
307 FREE_NONREF: {
308 no warnings 'uninitialized';
309 undef $_;
310 }
311 }
312 elsif (is_scalarref($_)) {
313 next if !defined $$_ || readonly $$_;
314 my $cowrefcnt = _CowREFCNT($$_);
315 goto FREE_REF if defined $cowrefcnt && 1 < $cowrefcnt;
316 # if (__PACKAGE__->can('erase_xs')) {
317 # erase_xs($$_);
318 # }
319 # else {
320 substr($$_, 0, length($$_), "\0" x length($$_));
321 # }
322 FREE_REF: {
323 no warnings 'uninitialized';
324 undef $$_;
325 }
326 }
327 elsif (is_arrayref($_)) {
328 erase(@$_);
329 @$_ = ();
330 }
331 elsif (is_hashref($_)) {
332 erase(values %$_);
333 %$_ = ();
334 }
335 else {
336 throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
337 }
338 }
339 }
340
341 =func erase_scoped
342
343 $scope_guard = erase_scoped($string, ...);
344 $scope_guard = erase_scoped(\$string, ...);
345 undef $scope_guard; # erase happens here
346
347 Get a scope guard that will cause scalars to be erased later (i.e. when the scope ends). This is useful if you
348 want to make sure a string gets erased after you're done with it, even if the scope ends abnormally.
349
350 See L</erase>.
351
352 =cut
353
354 sub erase_scoped {
355 throw 'Programmer error: Cannot call erase_scoped in void context' if !defined wantarray;
356 my @args;
357 for (@_) {
358 !is_ref($_) || is_arrayref($_) || is_hashref($_) || is_scalarref($_)
359 or throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
360 push @args, is_ref($_) ? $_ : \$_;
361 }
362 require Scope::Guard;
363 return Scope::Guard->new(sub { erase(@args) });
364 }
365
366 =func extends
367
368 extends $class;
369
370 Set up the current module to inheret from another module.
371
372 =cut
373
374 sub extends {
375 my $parent = shift;
376 my $caller = caller;
377 load $parent;
378 no strict 'refs'; ## no critic (ProhibitNoStrict)
379 @{"${caller}::ISA"} = $parent;
380 }
381
382 =func has
383
384 has $name => %options;
385
386 Create an attribute getter/setter. Possible options:
387
388 =for :list
389 * C<is> - Either "rw" (default) or "ro"
390 * C<default> - Default value
391 * C<coerce> - Coercive function
392
393 =cut
394
395 sub has {
396 my $name = shift;
397 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
398
399 my $d = $args{default};
400 my $default = is_arrayref($d) ? sub { [%$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
401 my $coerce = $args{coerce};
402 my $is = $args{is} || 'rw';
403
404 my $has_default = is_coderef $default;
405 my $has_coerce = is_coderef $coerce;
406
407 my $caller = caller;
408 no strict 'refs'; ## no critic (ProhibitNoStrict)
409 if (my $store = $args{store}) {
410 *{"${caller}::${name}"} = $is eq 'ro' && $has_default ? sub {
411 $_[0]->$store->{$name} //= scalar $default->($_[0]);
412 } : $is eq 'ro' ? sub {
413 $_[0]->$store->{$name} //= $default;
414 } : $has_default && $has_coerce ? sub {
415 $#_ ? $_[0]->$store->{$name} = scalar $coerce->($_[1])
416 : $_[0]->$store->{$name} //= scalar $default->($_[0]);
417 } : $has_default ? sub {
418 $#_ ? $_[0]->$store->{$name} = $_[1]
419 : $_[0]->$store->{$name} //= scalar $default->($_[0]);
420 } : $has_coerce ? sub {
421 $#_ ? $_[0]->$store->{$name} = scalar $coerce->($_[1])
422 : $_[0]->$store->{$name} //= $default;
423 } : sub {
424 $#_ ? $_[0]->$store->{$name} = $_[1]
425 : $_[0]->$store->{$name} //= $default;
426 };
427 }
428 else {
429 *{"${caller}::${name}"} = $is eq 'ro' && $has_default ? sub {
430 $_[0]->{$name} //= scalar $default->($_[0]);
431 } : $is eq 'ro' ? sub {
432 $_[0]->{$name} //= $default;
433 } : $has_default && $has_coerce ? sub {
434 $#_ ? $_[0]->{$name} = scalar $coerce->($_[1])
435 : $_[0]->{$name} //= scalar $default->($_[0]);
436 } : $has_default ? sub {
437 $#_ ? $_[0]->{$name} = $_[1]
438 : $_[0]->{$name} //= scalar $default->($_[0]);
439 } : $has_coerce ? sub {
440 $#_ ? $_[0]->{$name} = scalar $coerce->($_[1])
441 : $_[0]->{$name} //= $default;
442 } : sub {
443 $#_ ? $_[0]->{$name} = $_[1]
444 : ($_[0]->{$name} //= $default);
445 };
446 }
447 }
448
449 =func format_uuid
450
451 $string_uuid = format_uuid($raw_uuid);
452 $string_uuid = format_uuid($raw_uuid, $delimiter);
453
454 Format a 128-bit UUID (given as a string of 16 octets) into a hexidecimal string, optionally with a delimiter
455 to break up the UUID visually into five parts. Examples:
456
457 my $uuid = uuid('01234567-89AB-CDEF-0123-456789ABCDEF');
458 say format_uuid($uuid); # -> 0123456789ABCDEF0123456789ABCDEF
459 say format_uuid($uuid, '-'); # -> 01234567-89AB-CDEF-0123-456789ABCDEF
460
461 This is the inverse of L</uuid>.
462
463 =cut
464
465 sub format_uuid {
466 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
467 my $delim = shift // '';
468 length($_) == 16 or throw 'Must provide a 16-bytes UUID', size => length($_), str => $_;
469 return uc(join($delim, unpack('H8 H4 H4 H4 H12', $_)));
470 }
471
472 =func generate_uuid
473
474 $uuid = generate_uuid;
475 $uuid = generate_uuid(\%set);
476 $uuid = generate_uuid(\&test_uuid);
477
478 Generate a new random UUID. It's pretty unlikely that this will generate a repeat, but if you're worried about
479 that you can provide either a set of existing UUIDs (as a hashref where the keys are the elements of a set) or
480 a function to check for existing UUIDs, and this will be sure to not return a UUID already in provided set.
481 Perhaps an example will make it clear:
482
483 my %uuid_set = (
484 uuid('12345678-9ABC-DEFG-1234-56789ABCDEFG') => 'whatever',
485 );
486 $uuid = generate_uuid(\%uuid_set);
487 # OR
488 $uuid = generate_uuid(sub { !$uuid_set{$_} });
489
490 Here, C<$uuid> can't be "12345678-9ABC-DEFG-1234-56789ABCDEFG". This example uses L</uuid> to easily pack
491 a 16-byte UUID from a literal, but it otherwise is not a consequential part of the example.
492
493 =cut
494
495 sub generate_uuid {
496 my $set = @_ % 2 == 1 ? shift : undef;
497 my %args = @_;
498 my $test = $set //= $args{test};
499 $test = sub { !$set->{$_} } if is_hashref($test);
500 $test //= sub { 1 };
501 my $printable = $args{printable} // $args{print};
502 local $_ = '';
503 do {
504 $_ = $printable ? random_string(16) : random_bytes(16);
505 } while (!$test->($_));
506 return $_;
507 }
508
509 =func gunzip
510
511 $unzipped = gunzip($string);
512
513 Decompress an octet stream.
514
515 =cut
516
517 sub gunzip {
518 load_optional('Compress::Raw::Zlib');
519 local $_ = shift;
520 my ($i, $status) = Compress::Raw::Zlib::Inflate->new(-WindowBits => 31);
521 $status == Compress::Raw::Zlib::Z_OK()
522 or throw 'Failed to initialize compression library', status => $status;
523 $status = $i->inflate($_, my $out);
524 $status == Compress::Raw::Zlib::Z_STREAM_END()
525 or throw 'Failed to decompress data', status => $status;
526 return $out;
527 }
528
529 =func gzip
530
531 $zipped = gzip($string);
532
533 Compress an octet stream.
534
535 =cut
536
537 sub gzip {
538 load_optional('Compress::Raw::Zlib');
539 local $_ = shift;
540 my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
541 $status == Compress::Raw::Zlib::Z_OK()
542 or throw 'Failed to initialize compression library', status => $status;
543 $status = $d->deflate($_, my $out);
544 $status == Compress::Raw::Zlib::Z_OK()
545 or throw 'Failed to compress data', status => $status;
546 $status = $d->flush($out);
547 $status == Compress::Raw::Zlib::Z_OK()
548 or throw 'Failed to compress data', status => $status;
549 return $out;
550 }
551
552 =func is_readable
553
554 =func is_writable
555
556 $bool = is_readable($mode);
557 $bool = is_writable($mode);
558
559 Determine of an C<fopen>-style mode is readable, writable or both.
560
561 =cut
562
563 sub is_readable { $_[0] !~ /^[aw]b?$/ }
564 sub is_writable { $_[0] !~ /^rb?$/ }
565
566 =func is_uuid
567
568 $bool = is_uuid($thing);
569
570 Check if a thing is a UUID (i.e. scalar string of length 16).
571
572 =cut
573
574 sub is_uuid { defined $_[0] && !is_ref($_[0]) && length($_[0]) == 16 }
575
576 =func load_optional
577
578 $package = load_optional($package);
579
580 Load a module that isn't required but can provide extra functionality. Throw if the module is not available.
581
582 =cut
583
584 sub load_optional {
585 for my $module (@_) {
586 eval { load $module };
587 if (my $err = $@) {
588 warn $err if $ENV{DEBUG};
589 throw "Missing dependency: Please install $module to use this feature.\n", module => $module;
590 }
591 }
592 return wantarray ? @_ : $_[0];
593 }
594
595 =func memoize
596
597 \&memoized_code = memoize(\&code, ...);
598
599 Memoize a function. Extra arguments are passed through to C<&code> when it is called.
600
601 =cut
602
603 sub memoize {
604 my $func = shift;
605 my @args = @_;
606 my %cache;
607 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
608 }
609
610 =func pad_pkcs7
611
612 $padded_string = pad_pkcs7($string, $block_size),
613
614 Pad a block using the PKCS#7 method.
615
616 =cut
617
618 sub pad_pkcs7 {
619 my $data = shift // throw 'Must provide a string to pad';
620 my $size = shift or throw 'Must provide block size';
621
622 0 <= $size && $size < 256
623 or throw 'Cannot add PKCS7 padding to a large block size', size => $size;
624
625 my $pad_len = $size - length($data) % $size;
626 $data .= chr($pad_len) x $pad_len;
627 }
628
629 =func query
630
631 $query = query(@where);
632 $query->(\%data);
633
634 Generate a function that will run a series of tests on a passed hashref and return true or false depending on
635 if the data record in the hash matched the specified logic.
636
637 The logic can be specified in a manner similar to L<SQL::Abstract/"WHERE CLAUSES"> which was the inspiration
638 for this function, but this code is distinct, supporting an overlapping but not identical feature set and
639 having its own bugs.
640
641 See L<File::KDBX/QUERY> for examples.
642
643 =cut
644
645 sub query { _query(undef, '-or', \@_) }
646
647 =func read_all
648
649 $size = read_all($fh, my $buffer, $size);
650 $size = read_all($fh, my $buffer, $size, $offset);
651
652 Like L<functions/read> but returns C<undef> if not all C<$size> bytes are read. This is considered an error,
653 distinguishable from other errors by C<$!> not being set.
654
655 =cut
656
657 sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
658 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
659 : read($_[0], $_[1], $_[2], $_[3]);
660 return if !defined $result;
661 return if $result != $_[2];
662 return $result;
663 }
664
665 =func recurse_limit
666
667 \&limited_code = recurse_limit(\&code);
668 \&limited_code = recurse_limit(\&code, $max_depth);
669 \&limited_code = recurse_limit(\&code, $max_depth, \&error_handler);
670
671 Wrap a function with a guard to prevent deep recursion.
672
673 =cut
674
675 sub recurse_limit {
676 my $func = shift;
677 my $max_depth = shift // 200;
678 my $error = shift // sub {};
679 my $depth = 0;
680 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
681 };
682
683 =func search
684
685 # Generate a query on-the-fly:
686 \@matches = search(\@records, @where);
687
688 # Use a pre-compiled query:
689 $query = query(@where);
690 \@matches = search(\@records, $query);
691
692 # Use a simple expression:
693 \@matches = search(\@records, \'query terms', @fields);
694 \@matches = search(\@records, \'query terms', $operator, @fields);
695
696 # Use your own subroutine:
697 \@matches = search(\@records, \&query);
698 \@matches = search(\@records, sub { $record = shift; ... });
699
700 Execute a linear search over an array of records using a L</query>. A "record" is usually a hash.
701
702 This is the search engine described with many examples at L<File::KDBX/QUERY>.
703
704 =cut
705
706 sub search {
707 my $list = shift;
708 my $query = shift;
709
710 if (is_coderef($query) && !@_) {
711 # already a query
712 }
713 elsif (is_scalarref($query)) {
714 $query = simple_expression_query($$query, @_);
715 }
716 else {
717 $query = query($query, @_);
718 }
719
720 my @match;
721 for my $item (@$list) {
722 push @match, $item if $query->($item);
723 }
724 return \@match;
725 }
726
727 =for Pod::Coverage search_limited
728
729 =cut
730
731 sub search_limited {
732 my $list = shift;
733 my $query = shift;
734 my $limit = shift // 1;
735
736 if (is_coderef($query) && !@_) {
737 # already a query
738 }
739 elsif (is_scalarref($query)) {
740 $query = simple_expression_query($$query, @_);
741 }
742 else {
743 $query = query($query, @_);
744 }
745
746 my @match;
747 for my $item (@$list) {
748 push @match, $item if $query->($item);
749 last if $limit <= @match;
750 }
751 return \@match;
752 }
753
754 =func simple_expression_query
755
756 $query = simple_expression_query($expression, @fields);
757
758 Generate a query, like L</query>, to be used with L</search> but built from a "simple expression" as
759 L<described here|https://keepass.info/help/base/search.html#mode_se>.
760
761 An expression is a string with one or more space-separated terms. Terms with spaces can be enclosed in double
762 quotes. Terms are negated if they are prefixed with a minus sign. A record must match every term on at least
763 one of the given fields.
764
765 =cut
766
767 sub simple_expression_query {
768 my $expr = shift;
769 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
770
771 my $neg_op = $OP_NEG{$op};
772 my $is_re = $op eq '=~' || $op eq '!~';
773
774 require Text::ParseWords;
775 my @terms = Text::ParseWords::shellwords($expr);
776
777 my @query = qw(-and);
778
779 for my $term (@terms) {
780 my @subquery = qw(-or);
781
782 my $neg = $term =~ s/^-//;
783 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
784
785 for my $field (@_) {
786 push @subquery, $field => $condition;
787 }
788
789 push @query, \@subquery;
790 }
791
792 return query(\@query);
793 }
794
795 =func snakify
796
797 $string = snakify($string);
798
799 Turn a CamelCase string into snake_case.
800
801 =cut
802
803 sub snakify {
804 local $_ = shift;
805 s/UserName/Username/g;
806 s/([a-z])([A-Z0-9])/${1}_${2}/g;
807 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
808 return lc($_);
809 }
810
811 =func split_url
812
813 ($scheme, $auth, $host, $port, $path, $query, $hash, $usename, $password) = split_url($url);
814
815 Split a URL into its parts.
816
817 For example, C<http://user:pass@localhost:4000/path?query#hash> gets split like:
818
819 =for :list
820 * C<http>
821 * C<user:pass>
822 * C<host>
823 * C<4000>
824 * C</path>
825 * C<?query>
826 * C<#hash>
827 * C<user>
828 * C<pass>
829
830 =cut
831
832 sub split_url {
833 local $_ = shift;
834 my ($scheme, $auth, $host, $port, $path, $query, $hash) =~ m!
835 ^([^:/\?\#]+) ://
836 (?:([^\@]+)\@)
837 ([^:/\?\#]*)
838 (?::(\d+))?
839 ([^\?\#]*)
840 (\?[^\#]*)?
841 (\#(.*))?
842 !x;
843
844 $scheme = lc($scheme);
845
846 $host ||= 'localhost';
847 $host = lc($host);
848
849 $path = "/$path" if $path !~ m!^/!;
850
851 $port ||= $scheme eq 'http' ? 80 : $scheme eq 'https' ? 433 : undef;
852
853 my ($username, $password) = split($auth, ':', 2);
854
855 return ($scheme, $auth, $host, $port, $path, $query, $hash, $username, $password);
856 }
857
858 =func to_bool
859
860 =func to_number
861
862 =func to_string
863
864 =func to_time
865
866 =func to_tristate
867
868 =func to_uuid
869
870 Various typecasting / coercive functions.
871
872 =cut
873
874 sub to_bool { $_[0] // return; boolean($_[0]) }
875 sub to_number { $_[0] // return; 0+$_[0] }
876 sub to_string { $_[0] // return; "$_[0]" }
877 sub to_time {
878 $_[0] // return;
879 return gmtime($_[0]) if looks_like_number($_[0]);
880 return Time::Piece->strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed $_[0];
881 return $_[0];
882 }
883 sub to_tristate { $_[0] // return; boolean($_[0]) }
884 sub to_uuid {
885 my $str = to_string(@_) // return;
886 return sprintf('%016s', $str) if length($str) < 16;
887 return substr($str, 0, 16) if 16 < length($str);
888 return $str;
889 }
890
891 =func trim
892
893 $string = trim($string);
894
895 The ubiquitous C<trim> function. Removes all whitespace from both ends of a string.
896
897 =cut
898
899 sub trim($) { ## no critic (ProhibitSubroutinePrototypes)
900 local $_ = shift // return;
901 s/^\s*//;
902 s/\s*$//;
903 return $_;
904 }
905
906 =func try_load_optional
907
908 $package = try_load_optional($package);
909
910 Try to load a module that isn't required but can provide extra functionality, and return true if successful.
911
912 =cut
913
914 sub try_load_optional {
915 for my $module (@_) {
916 eval { load $module };
917 if (my $err = $@) {
918 warn $err if $ENV{DEBUG};
919 return;
920 }
921 }
922 return @_;
923 }
924
925 =func uri_escape_utf8
926
927 $string = uri_escape_utf8($string);
928
929 Percent-encode arbitrary text strings, like for a URI.
930
931 =cut
932
933 my %ESC = map { chr($_) => sprintf('%%%02X', $_) } 0..255;
934 sub uri_escape_utf8 {
935 local $_ = shift // return;
936 $_ = encode('UTF-8', $_);
937 # RFC 3986 section 2.3 unreserved characters
938 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
939 return $_;
940 }
941
942 =func uri_unescape_utf8
943
944 $string = uri_unescape_utf8($string);
945
946 Inverse of L</uri_escape_utf8>.
947
948 =cut
949
950 sub uri_unescape_utf8 {
951 local $_ = shift // return;
952 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
953 return decode('UTF-8', $_);
954 }
955
956 =func uuid
957
958 $raw_uuid = uuid($string_uuid);
959
960 Pack a 128-bit UUID (given as a hexidecimal string with optional C<->'s, like
961 C<12345678-9ABC-DEFG-1234-56789ABCDEFG>) into a string of exactly 16 octets.
962
963 This is the inverse of L</format_uuid>.
964
965 =cut
966
967 sub uuid {
968 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
969 s/-//g;
970 /^[A-Fa-f0-9]{32}$/ or throw 'Must provide a formatted 128-bit UUID';
971 return pack('H32', $_);
972
973 }
974
975 ### --------------------------------------------------------------------------
976
977 # Determine if an array looks like keypairs from a hash.
978 sub _looks_like_keypairs {
979 my $arr = shift;
980 return 0 if @$arr % 2 == 1;
981 for (my $i = 0; $i < @$arr; $i += 2) {
982 return 0 if is_ref($arr->[$i]);
983 }
984 return 1;
985 }
986
987 sub _is_operand_plain {
988 local $_ = shift;
989 return !(is_hashref($_) || is_arrayref($_));
990 }
991
992 sub _query {
993 # dumper \@_;
994 my $subject = shift;
995 my $op = shift // throw 'Must specify a query operator';
996 my $operand = shift;
997
998 return _query_simple($op, $subject) if defined $subject && !is_ref($op) && ($OPS{$subject} || 2) < 2;
999 return _query_simple($subject, $op, $operand) if _is_operand_plain($operand);
1000 return _query_inverse(_query($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
1001 return _query($subject, '-and', [%$operand]) if is_hashref($operand);
1002
1003 my @queries;
1004
1005 my @atoms = @$operand;
1006 while (@atoms) {
1007 if (_looks_like_keypairs(\@atoms)) {
1008 my ($atom, $operand) = splice @atoms, 0, 2;
1009 if (my $op_type = $OPS{$atom}) {
1010 if ($op_type == 1 && _is_operand_plain($operand)) { # unary
1011 push @queries, _query_simple($operand, $atom);
1012 }
1013 else {
1014 push @queries, _query($subject, $atom, $operand);
1015 }
1016 }
1017 elsif (!is_ref($atom)) {
1018 push @queries, _query($atom, 'eq', $operand);
1019 }
1020 }
1021 else {
1022 my $atom = shift @atoms;
1023 if ($OPS{$atom}) { # apply new operator over the rest
1024 push @queries, _query($subject, $atom, \@atoms);
1025 last;
1026 }
1027 else { # apply original operator over this one
1028 push @queries, _query($subject, $op, $atom);
1029 }
1030 }
1031 }
1032
1033 if (@queries == 1) {
1034 return $queries[0];
1035 }
1036 elsif ($op eq '-and') {
1037 return _query_all(@queries);
1038 }
1039 elsif ($op eq '-or') {
1040 return _query_any(@queries);
1041 }
1042 throw 'Malformed query';
1043 }
1044
1045 sub _query_simple {
1046 my $subject = shift;
1047 my $op = shift // 'eq';
1048 my $operand = shift;
1049
1050 # these special operators can also act as simple operators
1051 $op = '!!' if $op eq '-true';
1052 $op = '!' if $op eq '-false';
1053 $op = '!' if $op eq '-not';
1054
1055 defined $subject or throw 'Subject is not set in query';
1056 $OPS{$op} >= 0 or throw 'Cannot use a non-simple operator in a simple query';
1057 if (empty($operand)) {
1058 if ($OPS{$op} < 2) {
1059 # no operand needed
1060 }
1061 # Allow field => undef and field => {'ne' => undef} to do the (arguably) right thing.
1062 elsif ($op eq 'eq' || $op eq '==') {
1063 $op = '-empty';
1064 }
1065 elsif ($op eq 'ne' || $op eq '!=') {
1066 $op = '-nonempty';
1067 }
1068 else {
1069 throw 'Operand is required';
1070 }
1071 }
1072
1073 my $field = sub { blessed $_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
1074
1075 my %map = (
1076 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
1077 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
1078 'lt' => sub { local $_ = $field->(@_); defined && $_ lt $operand },
1079 'gt' => sub { local $_ = $field->(@_); defined && $_ gt $operand },
1080 'le' => sub { local $_ = $field->(@_); defined && $_ le $operand },
1081 'ge' => sub { local $_ = $field->(@_); defined && $_ ge $operand },
1082 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
1083 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
1084 '<' => sub { local $_ = $field->(@_); defined && $_ < $operand },
1085 '>' => sub { local $_ = $field->(@_); defined && $_ > $operand },
1086 '<=' => sub { local $_ = $field->(@_); defined && $_ <= $operand },
1087 '>=' => sub { local $_ = $field->(@_); defined && $_ >= $operand },
1088 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
1089 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
1090 '!' => sub { local $_ = $field->(@_); ! $_ },
1091 '!!' => sub { local $_ = $field->(@_); !!$_ },
1092 '-defined' => sub { local $_ = $field->(@_); defined $_ },
1093 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
1094 '-nonempty' => sub { local $_ = $field->(@_); nonempty $_ },
1095 '-empty' => sub { local $_ = $field->(@_); empty $_ },
1096 );
1097
1098 return $map{$op} // throw "Unexpected operator in query: $op",
1099 subject => $subject,
1100 operator => $op,
1101 operand => $operand;
1102 }
1103
1104 sub _query_inverse {
1105 my $query = shift;
1106 return sub { !$query->(@_) };
1107 }
1108
1109 sub _query_all {
1110 my @queries = @_;
1111 return sub {
1112 my $val = shift;
1113 all { $_->($val) } @queries;
1114 };
1115 }
1116
1117 sub _query_any {
1118 my @queries = @_;
1119 return sub {
1120 my $val = shift;
1121 any { $_->($val) } @queries;
1122 };
1123 }
1124
1125 1;
This page took 0.095628 seconds and 3 git commands to generate.