1 package File
::KDBX
::Util
;
2 # ABSTRACT: Utility functions for working with KDBX files
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);
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);
18 use namespace
::clean
-except
=> 'import';
20 our $VERSION = '999.999'; # VERSION
23 assert
=> [qw(assert_64bit)],
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(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)],
42 $EXPORT_TAGS{all
} = [map { @$_ } values %EXPORT_TAGS];
43 our @EXPORT_OK = @{$EXPORT_TAGS{all
}};
62 '-not' => 1, # special
93 $bool = load_xs
($version);
95 Attempt to load L
<File
::KDBX
::XS
>. Return truthy
if C
<XS
> is loaded
. If C
<$version> is given, it will check
96 that at least the
given version
is loaded
.
104 goto IS_LOADED
if defined $XS_LOADED;
106 if ($ENV{PERL_ONLY
} || (exists $ENV{PERL_FILE_KDBX_XS
} && !$ENV{PERL_FILE_KDBX_XS
})) {
107 return $XS_LOADED = FALSE
;
110 $XS_LOADED = !!eval { require File
::KDBX
::XS
; 1 };
115 return $XS_LOADED if !$version;
116 return !!eval { File
::KDBX
::XS-
>VERSION($version); 1 };
124 Throw
if perl doesn
't support 64-bit IVs.
130 $Config::Config{ivsize} < 8
131 and throw "64-bit perl is required to use this feature.\n", ivsize => $Config::Config{ivsize};
138 Determine if perl can fork, with logic lifted from L<Test2::Util/CAN_FORK>.
144 return 1 if $Config::Config{d_fork};
145 return 0 if $^O ne 'MSWin32
' && $^O ne 'NetWare
';
146 return 0 if !$Config::Config{useithreads};
147 return 0 if $Config::Config{ccflags} !~ /-DPERL_IMPLICIT_SYS/;
148 return 0 if $] < 5.008001;
149 if ($] == 5.010000 && $Config::Config{ccname} eq 'gcc
' && $Config::Config{gccversion}) {
150 return 0 if $Config::Config{gccversion} !~ m/^(\d+)\.(\d+)/;
151 my @parts = split(/[\.\s]+/, $Config::Config{gccversion});
152 return 0 if $parts[0] > 4 || ($parts[0] == 4 && $parts[1] >= 8);
154 return 0 if $INC{'Devel
/Cover
.pm
'};
160 $clone = clone($thing);
162 Clone deeply. This is an unadorned alias to L<Storable> C<dclone>.
168 goto &Storable::dclone;
173 $clone = clone_nomagic($thing);
175 Clone deeply without keeping [most of] the magic.
177 B<WARNING:> At the moment the implementation is naïve and won't respond well to nontrivial data
or recursive
184 if (is_arrayref
($thing)) {
185 my @arr = map { clone_nomagic
($_) } @$thing;
188 elsif (is_hashref
($thing)) {
190 $hash{$_} = clone_nomagic
($thing->{$_}) for keys %$thing;
193 elsif (is_ref
($thing)) {
194 return clone
($thing);
201 $str = dumper
$thing;
202 dumper
$thing; # in void context, prints to STDERR
204 Like L
<Data
::Dumper
> but slightly terser
in some cases relevent to L
<File
::KDBX
>.
209 require Data
::Dumper
;
210 # avoid "once" warnings
211 local $Data::Dumper
::Deepcopy
= $Data::Dumper
::Deepcopy
= 1;
212 local $Data::Dumper
::Deparse
= $Data::Dumper
::Deparse
= 1;
213 local $Data::Dumper
::Indent
= 1;
214 local $Data::Dumper
::Quotekeys
= 0;
215 local $Data::Dumper
::Sortkeys
= 1;
216 local $Data::Dumper
::Terse
= 1;
217 local $Data::Dumper
::Trailingcomma
= 1;
218 local $Data::Dumper
::Useqq
= 1;
221 for my $struct (@_) {
222 my $str = Data
::Dumper
::Dumper
($struct);
225 $str =~ s/bless\( do\{\\\(my \$o = ([01])\)\}, 'boolean' \)/boolean($1)/gs;
227 $str =~ s
/bless\([^\)]+?(\d+)'?,\s+\d+,?\s+\], 'Time::Piece' \),/
228 "scalar gmtime($1), # " . scalar gmtime($1)->datetime/ges
;
230 print STDERR
$str if !defined wantarray;
234 return join("\n", @dumps);
241 $bool = empty
$thing;
243 $bool = nonempty
$thing;
245 Test whether a thing
is empty
(or nonempty
). An empty thing
is one of these
:
252 * hash with zero keys
253 * reference to an empty thing (recursive)
255 Note in particular that zero C<0> is not considered empty because it is an actual value.
259 sub empty
{ _empty
(@_) }
260 sub nonempty
{ !_empty
(@_) }
267 || (is_arrayref
($_) && @$_ == 0)
268 || (is_hashref
($_) && keys %$_ == 0)
269 || (is_scalarref
($_) && (!defined $$_ || $$_ eq ''))
270 || (is_refref
($_) && _empty
($$_));
276 erase
(\
$string, ...);
278 Overwrite the memory used by one
or more string
.
284 *_CowREFCNT
= \
&File
::KDBX
::XS
::CowREFCNT
;
286 elsif (eval { require B
::COW
; 1 }) {
287 *_CowREFCNT
= \
&B
::COW
::cowrefcnt
;
290 *_CowREFCNT
= sub { undef };
295 # Only bother zeroing out memory if we have the last SvPV COW reference, otherwise we'll end up just
296 # creating a copy and erasing the copy.
297 # TODO - Is this worth doing? Need some benchmarking.
300 next if !defined $_ || readonly
$_;
301 my $cowrefcnt = _CowREFCNT
($_);
302 goto FREE_NONREF
if defined $cowrefcnt && 1 < $cowrefcnt;
303 # if (__PACKAGE__->can('erase_xs')) {
307 substr($_, 0, length($_), "\0" x
length($_));
310 no warnings
'uninitialized';
314 elsif (is_scalarref
($_)) {
315 next if !defined $$_ || readonly
$$_;
316 my $cowrefcnt = _CowREFCNT
($$_);
317 goto FREE_REF
if defined $cowrefcnt && 1 < $cowrefcnt;
318 # if (__PACKAGE__->can('erase_xs')) {
322 substr($$_, 0, length($$_), "\0" x
length($$_));
325 no warnings
'uninitialized';
329 elsif (is_arrayref
($_)) {
333 elsif (is_hashref
($_)) {
338 throw
'Cannot erase this type of scalar', type
=> ref $_, what
=> $_;
345 $scope_guard = erase_scoped
($string, ...);
346 $scope_guard = erase_scoped
(\
$string, ...);
347 undef $scope_guard; # erase happens here
349 Get a scope guard that will cause scalars to be erased later
(i
.e
. when the scope ends
). This
is useful
if you
350 want to make sure a string gets erased after you
're done with it, even if the scope ends abnormally.
357 throw 'Programmer error
: Cannot call erase_scoped
in void context
' if !defined wantarray;
360 !is_ref($_) || is_arrayref($_) || is_hashref($_) || is_scalarref($_)
361 or throw 'Cannot erase this type of
scalar', type => ref $_, what => $_;
362 push @args, is_ref($_) ? $_ : \$_;
364 require Scope::Guard;
365 return Scope::Guard->new(sub { erase(@args) });
372 Set up the current module to inheret from another module.
380 no strict 'refs
'; ## no critic (ProhibitNoStrict)
381 @{"${caller}::ISA"} = $parent;
386 has $name => %options;
388 Create an attribute getter/setter. Possible options:
391 * C<is> - Either "rw" (default) or "ro"
392 * C<default> - Default value
393 * C<coerce> - Coercive function
399 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
401 my $d = $args{default};
402 my $default = is_arrayref($d) ? sub { [%$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
403 my $coerce = $args{coerce};
404 my $is = $args{is} || 'rw';
406 my $has_default = is_coderef $default;
407 my $has_coerce = is_coderef $coerce;
409 my $store = $args{store};
410 ($store, $name) = split(/\./, $name, 2) if $name =~ /\./;
413 push @{$ATTRIBUTES{$caller} //= []}, $name;
415 no strict 'refs
'; ## no critic (ProhibitNoStrict)
417 *{"${caller}::${name}"} = $is eq 'ro
' && $has_default ? sub {
418 $_[0]->$store->{$name} //= scalar $default->($_[0]);
419 } : $is eq 'ro
' ? sub {
420 $_[0]->$store->{$name} //= $default;
421 } : $has_default && $has_coerce ? sub {
422 $#_ ? $_[0]->$store->{$name} = scalar $coerce->($_[1])
423 : $_[0]->$store->{$name} //= scalar $default->($_[0]);
424 } : $has_default ? sub {
425 $#_ ? $_[0]->$store->{$name} = $_[1]
426 : $_[0]->$store->{$name} //= scalar $default->($_[0]);
427 } : $has_coerce ? sub {
428 $#_ ? $_[0]->$store->{$name} = scalar $coerce->($_[1])
429 : $_[0]->$store->{$name} //= $default;
431 $#_ ? $_[0]->$store->{$name} = $_[1]
432 : $_[0]->$store->{$name} //= $default;
436 *{"${caller}::${name}"} = $is eq 'ro
' && $has_default ? sub {
437 $_[0]->{$name} //= scalar $default->($_[0]);
438 } : $is eq 'ro
' ? sub {
439 $_[0]->{$name} //= $default;
440 } : $has_default && $has_coerce ? sub {
441 $#_ ? $_[0]->{$name} = scalar $coerce->($_[1])
442 : $_[0]->{$name} //= scalar $default->($_[0]);
443 } : $has_default ? sub {
444 $#_ ? $_[0]->{$name} = $_[1]
445 : $_[0]->{$name} //= scalar $default->($_[0]);
446 } : $has_coerce ? sub {
447 $#_ ? $_[0]->{$name} = scalar $coerce->($_[1])
448 : $_[0]->{$name} //= $default;
450 $#_ ? $_[0]->{$name} = $_[1]
451 : ($_[0]->{$name} //= $default);
458 $string_uuid = format_uuid($raw_uuid);
459 $string_uuid = format_uuid($raw_uuid, $delimiter);
461 Format a 128-bit UUID (given as a string of 16 octets) into a hexidecimal string, optionally with a delimiter
462 to break up the UUID visually into five parts. Examples:
464 my $uuid = uuid('01234567-89AB-CDEF-0123-456789ABCDEF
');
465 say format_uuid($uuid); # -> 0123456789ABCDEF0123456789ABCDEF
466 say format_uuid($uuid, '-'); # -> 01234567-89AB-CDEF-0123-456789ABCDEF
468 This is the inverse of L</uuid>.
473 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
474 my $delim = shift // '';
475 length($_) == 16 or throw 'Must provide a
16-bytes UUID
', size => length($_), str => $_;
476 return uc(join($delim, unpack('H8 H4 H4 H4 H12
', $_)));
481 $uuid = generate_uuid;
482 $uuid = generate_uuid(\%set);
483 $uuid = generate_uuid(\&test_uuid);
485 Generate a new random UUID. It's pretty unlikely that this will generate a repeat
, but
if you
're worried about
486 that you can provide either a set of existing UUIDs (as a hashref where the keys are the elements of a set) or
487 a function to check for existing UUIDs, and this will be sure to not return a UUID already in provided set.
488 Perhaps an example will make it clear:
491 uuid('12345678-9ABC-DEFG-1234-56789ABCDEFG
') => 'whatever
',
493 $uuid = generate_uuid(\%uuid_set);
495 $uuid = generate_uuid(sub { !$uuid_set{$_} });
497 Here, C<$uuid> can't be
"12345678-9ABC-DEFG-1234-56789ABCDEFG". This example uses L
</uuid
> to easily
pack
498 a
16-byte UUID from a literal
, but it otherwise
is not a consequential part of the example
.
503 my $set = @_ % 2 == 1 ? shift : undef;
505 my $test = $set //= $args{test
};
506 $test = sub { !$set->{$_} } if is_hashref
($test);
508 my $printable = $args{printable
} // $args{print};
511 $_ = $printable ? random_string
(16) : random_bytes
(16);
512 } while (!$test->($_));
518 $unzipped = gunzip
($string);
520 Decompress an octet stream
.
525 load_optional
('Compress::Raw::Zlib');
527 my ($i, $status) = Compress
::Raw
::Zlib
::Inflate-
>new(-WindowBits
=> 31);
528 $status == Compress
::Raw
::Zlib
::Z_OK
()
529 or throw
'Failed to initialize compression library', status
=> $status;
530 $status = $i->inflate($_, my $out);
531 $status == Compress
::Raw
::Zlib
::Z_STREAM_END
()
532 or throw
'Failed to decompress data', status
=> $status;
538 $zipped = gzip
($string);
540 Compress an octet stream
.
545 load_optional
('Compress::Raw::Zlib');
547 my ($d, $status) = Compress
::Raw
::Zlib
::Deflate-
>new(-WindowBits
=> 31, -AppendOutput
=> 1);
548 $status == Compress
::Raw
::Zlib
::Z_OK
()
549 or throw
'Failed to initialize compression library', status
=> $status;
550 $status = $d->deflate($_, my $out);
551 $status == Compress
::Raw
::Zlib
::Z_OK
()
552 or throw
'Failed to compress data', status
=> $status;
553 $status = $d->flush($out);
554 $status == Compress
::Raw
::Zlib
::Z_OK
()
555 or throw
'Failed to compress data', status
=> $status;
563 $bool = is_readable
($mode);
564 $bool = is_writable
($mode);
566 Determine of an C
<fopen
>-style mode
is readable
, writable
or both
.
570 sub is_readable
{ $_[0] !~ /^[aw]b?$/ }
571 sub is_writable
{ $_[0] !~ /^rb?$/ }
575 $bool = is_uuid
($thing);
577 Check
if a thing
is a UUID
(i
.e
. scalar string of
length 16).
581 sub is_uuid
{ defined $_[0] && !is_ref
($_[0]) && length($_[0]) == 16 }
583 =func list_attributes
585 @attributes = list_attributes
($package);
587 Get a list of attributes
for a
class.
591 sub list_attributes
{
593 return @{$ATTRIBUTES{$package} // []};
598 $package = load_optional
($package);
600 Load a module that isn
't required but can provide extra functionality. Throw if the module is not available.
605 for my $module (@_) {
606 eval { load $module };
608 warn $err if $ENV{DEBUG};
609 throw "Missing dependency: Please install $module to use this feature.\n", module => $module;
612 return wantarray ? @_ : $_[0];
617 \&memoized_code = memoize(\&code, ...);
619 Memoize a function. Extra arguments are passed through to C<&code> when it is called.
627 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
632 $padded_string = pad_pkcs7($string, $block_size),
634 Pad a block using the PKCS#7 method.
639 my $data = shift // throw 'Must provide a string to pad
';
640 my $size = shift or throw 'Must provide block size
';
642 0 <= $size && $size < 256
643 or throw 'Cannot add PKCS7 padding to a large block size
', size => $size;
645 my $pad_len = $size - length($data) % $size;
646 $data .= chr($pad_len) x $pad_len;
651 $query = query(@where);
654 Generate a function that will run a series of tests on a passed hashref and return true or false depending on
655 if the data record in the hash matched the specified logic.
657 The logic can be specified in a manner similar to L<SQL::Abstract/"WHERE CLAUSES"> which was the inspiration
658 for this function, but this code is distinct, supporting an overlapping but not identical feature set and
661 See L<File::KDBX/QUERY> for examples.
665 sub query { _query(undef, '-or', \@_) }
669 $size = read_all($fh, my $buffer, $size);
670 $size = read_all($fh, my $buffer, $size, $offset);
672 Like L<functions/read> but returns C<undef> if not all C<$size> bytes are read. This is considered an error,
673 distinguishable from other errors by C<$!> not being set.
677 sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
678 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
679 : read($_[0], $_[1], $_[2], $_[3]);
680 return if !defined $result;
681 return if $result != $_[2];
687 \&limited_code = recurse_limit(\&code);
688 \&limited_code = recurse_limit(\&code, $max_depth);
689 \&limited_code = recurse_limit(\&code, $max_depth, \&error_handler);
691 Wrap a function with a guard to prevent deep recursion.
697 my $max_depth = shift // 200;
698 my $error = shift // sub {};
700 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
705 # Generate a query on-the-fly:
706 \@matches = search(\@records, @where);
708 # Use a pre-compiled query:
709 $query = query(@where);
710 \@matches = search(\@records, $query);
712 # Use a simple expression:
713 \@matches = search(\@records, \'query terms', @fields);
714 \
@matches = search
(\
@records, \'query terms
', $operator, @fields);
716 # Use your own subroutine:
717 \@matches = search(\@records, \&query);
718 \@matches = search(\@records, sub { $record = shift; ... });
720 Execute a linear search over an array of records using a L</query>. A "record" is usually a hash.
722 This is the search engine described with many examples at L<File::KDBX/QUERY>.
730 if (is_coderef($query) && !@_) {
733 elsif (is_scalarref($query)) {
734 $query = simple_expression_query($$query, @_);
737 $query = query($query, @_);
741 for my $item (@$list) {
742 push @match, $item if $query->($item);
747 =for Pod::Coverage search_limited
754 my $limit = shift // 1;
756 if (is_coderef($query) && !@_) {
759 elsif (is_scalarref($query)) {
760 $query = simple_expression_query($$query, @_);
763 $query = query($query, @_);
767 for my $item (@$list) {
768 push @match, $item if $query->($item);
769 last if $limit <= @match;
774 =func simple_expression_query
776 $query = simple_expression_query($expression, @fields);
778 Generate a query, like L</query>, to be used with L</search> but built from a "simple expression" as
779 L<described here|https://keepass.info/help/base/search.html#mode_se>.
781 An expression is a string with one or more space-separated terms. Terms with spaces can be enclosed in double
782 quotes. Terms are negated if they are prefixed with a minus sign. A record must match every term on at least
783 one of the given fields.
787 sub simple_expression_query {
789 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
791 my $neg_op = $OP_NEG{$op};
792 my $is_re = $op eq '=~' || $op eq '!~';
794 require Text::ParseWords;
795 my @terms = Text::ParseWords::shellwords($expr);
797 my @query = qw(-and);
799 for my $term (@terms) {
800 my @subquery = qw(-or);
802 my $neg = $term =~ s/^-//;
803 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
806 push @subquery, $field => $condition;
809 push @query, \
@subquery;
812 return query
(\
@query);
817 $string = snakify
($string);
819 Turn a CamelCase string into snake_case
.
825 s/UserName/Username/g;
826 s/([a-z])([A-Z0-9])/${1}_${2}/g;
827 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
833 ($scheme, $auth, $host, $port, $path, $query, $hash, $usename, $password) = split_url
($url);
835 Split a URL into its parts
.
837 For example
, C
<http
://user
:pass
@localhost:4000/path
?query
#hash> gets split like:
854 my ($scheme, $auth, $host, $port, $path, $query, $hash) =~ m
!
864 $scheme = lc($scheme);
866 $host ||= 'localhost';
869 $path = "/$path" if $path !~ m
!^/!;
871 $port ||= $scheme eq 'http' ? 80 : $scheme eq 'https' ? 433 : undef;
873 my ($username, $password) = split($auth, ':', 2);
875 return ($scheme, $auth, $host, $port, $path, $query, $hash, $username, $password);
890 Various typecasting
/ coercive functions
.
894 sub to_bool
{ $_[0] // return; boolean
($_[0]) }
895 sub to_number
{ $_[0] // return; 0+$_[0] }
896 sub to_string
{ $_[0] // return; "$_[0]" }
899 return gmtime($_[0]) if looks_like_number
($_[0]);
900 return Time
::Piece-
>strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed
$_[0];
903 sub to_tristate
{ $_[0] // return; boolean
($_[0]) }
905 my $str = to_string
(@_) // return;
906 return sprintf('%016s', $str) if length($str) < 16;
907 return substr($str, 0, 16) if 16 < length($str);
913 $string = trim
($string);
915 The ubiquitous C
<trim
> function
. Removes all whitespace from both ends of a string
.
919 sub trim
($) { ## no critic (ProhibitSubroutinePrototypes)
920 local $_ = shift // return;
926 =func try_load_optional
928 $package = try_load_optional
($package);
930 Try to load a module that isn
't required but can provide extra functionality, and return true if successful.
934 sub try_load_optional {
935 for my $module (@_) {
936 eval { load $module };
938 warn $err if $ENV{DEBUG};
945 =func uri_escape_utf8
947 $string = uri_escape_utf8($string);
949 Percent-encode arbitrary text strings, like for a URI.
953 my %ESC = map { chr($_) => sprintf('%%%02X', $_) } 0..255;
954 sub uri_escape_utf8 {
955 local $_ = shift // return;
956 $_ = encode('UTF-8
', $_);
957 # RFC 3986 section 2.3 unreserved characters
958 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
962 =func uri_unescape_utf8
964 $string = uri_unescape_utf8($string);
966 Inverse of L</uri_escape_utf8>.
970 sub uri_unescape_utf8 {
971 local $_ = shift // return;
972 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
973 return decode('UTF-8
', $_);
978 $raw_uuid = uuid($string_uuid);
980 Pack a 128-bit UUID (given as a hexidecimal string with optional C<->'s
, like
981 C
<12345678-9ABC-DEFG-1234-56789ABCDEFG
>) into a string of exactly
16 octets
.
983 This
is the inverse of L
</format_uuid
>.
988 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
990 /^[A-Fa-f0-9]{32}$/ or throw
'Must provide a formatted 128-bit UUID';
991 return pack('H32', $_);
995 ### --------------------------------------------------------------------------
997 # Determine if an array looks like keypairs from a hash.
998 sub _looks_like_keypairs
{
1000 return 0 if @$arr % 2 == 1;
1001 for (my $i = 0; $i < @$arr; $i += 2) {
1002 return 0 if is_ref
($arr->[$i]);
1007 sub _is_operand_plain
{
1009 return !(is_hashref
($_) || is_arrayref
($_));
1014 my $subject = shift;
1015 my $op = shift // throw
'Must specify a query operator';
1016 my $operand = shift;
1018 return _query_simple
($op, $subject) if defined $subject && !is_ref
($op) && ($OPS{$subject} || 2) < 2;
1019 return _query_simple
($subject, $op, $operand) if _is_operand_plain
($operand);
1020 return _query_inverse
(_query
($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
1021 return _query
($subject, '-and', [%$operand]) if is_hashref
($operand);
1025 my @atoms = @$operand;
1027 if (_looks_like_keypairs
(\
@atoms)) {
1028 my ($atom, $operand) = splice @atoms, 0, 2;
1029 if (my $op_type = $OPS{$atom}) {
1030 if ($op_type == 1 && _is_operand_plain
($operand)) { # unary
1031 push @queries, _query_simple
($operand, $atom);
1034 push @queries, _query
($subject, $atom, $operand);
1037 elsif (!is_ref
($atom)) {
1038 push @queries, _query
($atom, 'eq', $operand);
1042 my $atom = shift @atoms;
1043 if ($OPS{$atom}) { # apply new operator over the rest
1044 push @queries, _query
($subject, $atom, \
@atoms);
1047 else { # apply original operator over this one
1048 push @queries, _query
($subject, $op, $atom);
1053 if (@queries == 1) {
1056 elsif ($op eq '-and') {
1057 return _query_all
(@queries);
1059 elsif ($op eq '-or') {
1060 return _query_any
(@queries);
1062 throw
'Malformed query';
1066 my $subject = shift;
1067 my $op = shift // 'eq';
1068 my $operand = shift;
1070 # these special operators can also act as simple operators
1071 $op = '!!' if $op eq '-true';
1072 $op = '!' if $op eq '-false';
1073 $op = '!' if $op eq '-not';
1075 defined $subject or throw
'Subject is not set in query';
1076 $OPS{$op} >= 0 or throw
'Cannot use a non-simple operator in a simple query';
1077 if (empty
($operand)) {
1078 if ($OPS{$op} < 2) {
1081 # Allow field => undef and field => {'ne' => undef} to do the (arguably) right thing.
1082 elsif ($op eq 'eq' || $op eq '==') {
1085 elsif ($op eq 'ne' || $op eq '!=') {
1089 throw
'Operand is required';
1093 my $field = sub { blessed
$_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
1096 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
1097 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
1098 'lt' => sub { local $_ = $field->(@_); defined && $_ lt $operand },
1099 'gt' => sub { local $_ = $field->(@_); defined && $_ gt $operand },
1100 'le' => sub { local $_ = $field->(@_); defined && $_ le $operand },
1101 'ge' => sub { local $_ = $field->(@_); defined && $_ ge $operand },
1102 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
1103 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
1104 '<' => sub { local $_ = $field->(@_); defined && $_ < $operand },
1105 '>' => sub { local $_ = $field->(@_); defined && $_ > $operand },
1106 '<=' => sub { local $_ = $field->(@_); defined && $_ <= $operand },
1107 '>=' => sub { local $_ = $field->(@_); defined && $_ >= $operand },
1108 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
1109 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
1110 '!' => sub { local $_ = $field->(@_); ! $_ },
1111 '!!' => sub { local $_ = $field->(@_); !!$_ },
1112 '-defined' => sub { local $_ = $field->(@_); defined $_ },
1113 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
1114 '-nonempty' => sub { local $_ = $field->(@_); nonempty
$_ },
1115 '-empty' => sub { local $_ = $field->(@_); empty
$_ },
1118 return $map{$op} // throw
"Unexpected operator in query: $op",
1119 subject
=> $subject,
1121 operand
=> $operand;
1124 sub _query_inverse
{
1126 return sub { !$query->(@_) };
1133 all
{ $_->($val) } @queries;
1141 any
{ $_->($val) } @queries;