]> Dogcows Code - chaz/p5-File-KDBX/blob - lib/File/KDBX/Util.pm
57e0e18f46da82faeed32301b78872538aad711f
[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 5.010;
5 use warnings;
6 use strict;
7
8 use Crypt::PRNG qw(random_bytes random_string);
9 use Encode qw(decode encode);
10 use Exporter qw(import);
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 = '0.902'; # 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 my $debug = $ENV{DEBUG};
48 $debug = looks_like_number($debug) ? (0 + $debug) : ($debug ? 1 : 0);
49 *DEBUG = $debug == 1 ? sub() { 1 } :
50 $debug == 2 ? sub() { 2 } :
51 $debug == 3 ? sub() { 3 } :
52 $debug == 4 ? sub() { 4 } : sub() { 0 };
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 my $version = shift;
104
105 goto IS_LOADED if defined $XS_LOADED;
106
107 if ($ENV{PERL_ONLY} || (exists $ENV{PERL_FILE_KDBX_XS} && !$ENV{PERL_FILE_KDBX_XS})) {
108 return $XS_LOADED = !1;
109 }
110
111 $XS_LOADED = !!eval { require File::KDBX::XS; 1 };
112
113 IS_LOADED:
114 {
115 local $@;
116 return $XS_LOADED if !$version;
117 return !!eval { File::KDBX::XS->VERSION($version); 1 };
118 }
119 }
120
121
122 sub assert(&) { ## no critic (ProhibitSubroutinePrototypes)
123 return if !DEBUG;
124 my $code = shift;
125 return if $code->();
126
127 (undef, my $file, my $line) = caller;
128 $file =~ s!([^/\\]+)$!$1!;
129 my $assertion = '';
130 if (try_load_optional('B::Deparse')) {
131 my $deparse = B::Deparse->new(qw{-P -x9});
132 $assertion = $deparse->coderef2text($code);
133 $assertion =~ s/^\{(?:\s*(?:package[^;]+|use[^;]+);)*\s*(.*?);\s*\}$/$1/s;
134 $assertion =~ s/\s+/ /gs;
135 $assertion = ": $assertion";
136 }
137 die "$0: $file:$line: Assertion failed$assertion\n";
138 }
139
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
158 sub clone {
159 require Storable;
160 goto &Storable::dclone;
161 }
162
163
164 sub clone_nomagic {
165 my $thing = shift;
166 if (is_arrayref($thing)) {
167 my @arr = map { clone_nomagic($_) } @$thing;
168 return \@arr;
169 }
170 elsif (is_hashref($thing)) {
171 my %hash;
172 $hash{$_} = clone_nomagic($thing->{$_}) for keys %$thing;
173 return \%hash;
174 }
175 elsif (is_ref($thing)) {
176 return clone($thing);
177 }
178 return $thing;
179 }
180
181
182 sub dumper {
183 require Data::Dumper;
184 # avoid "once" warnings
185 local $Data::Dumper::Deepcopy = $Data::Dumper::Deepcopy = 1;
186 local $Data::Dumper::Deparse = $Data::Dumper::Deparse = 1;
187 local $Data::Dumper::Indent = 1;
188 local $Data::Dumper::Quotekeys = 0;
189 local $Data::Dumper::Sortkeys = 1;
190 local $Data::Dumper::Terse = 1;
191 local $Data::Dumper::Trailingcomma = 1;
192 local $Data::Dumper::Useqq = 1;
193
194 my @dumps;
195 for my $struct (@_) {
196 my $str = Data::Dumper::Dumper($struct);
197
198 # boolean
199 $str =~ s/bless\( do\{\\\(my \$o = ([01])\)\}, 'boolean' \)/boolean($1)/gs;
200 # Time::Piece
201 $str =~ s/bless\([^\)]+?(\d+)'?,\s+\d+,?\s+\], 'Time::Piece' \),/
202 "scalar gmtime($1), # " . scalar gmtime($1)->datetime/ges;
203
204 print STDERR $str if !defined wantarray;
205 push @dumps, $str;
206 return $str;
207 }
208 return join("\n", @dumps);
209 }
210
211
212 sub empty { _empty(@_) }
213 sub nonempty { !_empty(@_) }
214
215 sub _empty {
216 return 1 if @_ == 0;
217 local $_ = shift;
218 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 if (load_xs) {
229 *_CowREFCNT = \&File::KDBX::XS::CowREFCNT;
230 }
231 elsif (eval { require B::COW; 1 }) {
232 *_CowREFCNT = \&B::COW::cowrefcnt;
233 }
234 else {
235 *_CowREFCNT = sub { undef };
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 for (@_) {
244 if (!is_ref($_)) {
245 next if !defined $_ || readonly $_;
246 my $cowrefcnt = _CowREFCNT($_);
247 goto FREE_NONREF if defined $cowrefcnt && 1 < $cowrefcnt;
248 # if (__PACKAGE__->can('erase_xs')) {
249 # erase_xs($_);
250 # }
251 # else {
252 substr($_, 0, length($_), "\0" x length($_));
253 # }
254 FREE_NONREF: {
255 no warnings 'uninitialized';
256 undef $_;
257 }
258 }
259 elsif (is_scalarref($_)) {
260 next if !defined $$_ || readonly $$_;
261 my $cowrefcnt = _CowREFCNT($$_);
262 goto FREE_REF if defined $cowrefcnt && 1 < $cowrefcnt;
263 # if (__PACKAGE__->can('erase_xs')) {
264 # erase_xs($$_);
265 # }
266 # else {
267 substr($$_, 0, length($$_), "\0" x length($$_));
268 # }
269 FREE_REF: {
270 no warnings 'uninitialized';
271 undef $$_;
272 }
273 }
274 elsif (is_arrayref($_)) {
275 erase(@$_);
276 @$_ = ();
277 }
278 elsif (is_hashref($_)) {
279 erase(values %$_);
280 %$_ = ();
281 }
282 else {
283 throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
284 }
285 }
286 }
287
288
289 sub erase_scoped {
290 throw 'Programmer error: Cannot call erase_scoped in void context' if !defined wantarray;
291 my @args;
292 for (@_) {
293 !is_ref($_) || is_arrayref($_) || is_hashref($_) || is_scalarref($_)
294 or throw 'Cannot erase this type of scalar', type => ref $_, what => $_;
295 push @args, is_ref($_) ? $_ : \$_;
296 }
297 require Scope::Guard;
298 return Scope::Guard->new(sub { erase(@args) });
299 }
300
301
302 sub extends {
303 my $parent = shift;
304 my $caller = caller;
305 load $parent;
306 no strict 'refs'; ## no critic (ProhibitNoStrict)
307 @{"${caller}::ISA"} = $parent;
308 }
309
310
311 sub has {
312 my $name = shift;
313 my %args = @_ % 2 == 1 ? (default => shift, @_) : @_;
314
315 my ($package, $file, $line) = caller;
316
317 my $d = $args{default};
318 my $default = is_arrayref($d) ? sub { [@$d] } : is_hashref($d) ? sub { +{%$d} } : $d;
319 my $coerce = $args{coerce};
320 my $is = $args{is} || 'rw';
321
322 my $store = $args{store};
323 ($store, $name) = split(/\./, $name, 2) if $name =~ /\./;
324
325 my @path = split(/\./, $args{path} || '');
326 my $last = pop @path;
327 my $path = $last ? join('', map { qq{->$_} } @path) . qq{->{'$last'}}
328 : $store ? qq{->$store\->{'$name'}} : qq{->{'$name'}};
329 my $member = qq{\$_[0]$path};
330
331
332 my $default_code = is_coderef $default ? q{scalar $default->($_[0])}
333 : defined $default ? q{$default}
334 : q{undef};
335 my $get = qq{$member //= $default_code;};
336
337 my $set = '';
338 if ($is eq 'rw') {
339 $set = is_coderef $coerce ? qq{$member = scalar \$coerce->(\@_[1..\$#_]) if \$#_;}
340 : defined $coerce ? qq{$member = do { local @_ = (\@_[1..\$#_]); $coerce } if \$#_;}
341 : qq{$member = \$_[1] if \$#_;};
342 }
343
344 push @{$ATTRIBUTES{$package} //= []}, $name;
345 $line -= 4;
346 my $code = <<END;
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 eval $code; ## no critic (ProhibitStringyEval)
355 }
356
357
358 sub format_uuid {
359 local $_ = shift // "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
360 my $delim = shift // '';
361 length($_) == 16 or throw 'Must provide a 16-bytes UUID', size => length($_), str => $_;
362 return uc(join($delim, unpack('H8 H4 H4 H4 H12', $_)));
363 }
364
365
366 sub generate_uuid {
367 my $set = @_ % 2 == 1 ? shift : undef;
368 my %args = @_;
369 my $test = $set //= $args{test};
370 $test = sub { !$set->{$_} } if is_hashref($test);
371 $test //= sub { 1 };
372 my $printable = $args{printable} // $args{print};
373 local $_ = '';
374 do {
375 $_ = $printable ? random_string(16) : random_bytes(16);
376 } while (!$test->($_));
377 return $_;
378 }
379
380
381 sub gunzip {
382 load_optional('Compress::Raw::Zlib');
383 local $_ = shift;
384 my ($i, $status) = Compress::Raw::Zlib::Inflate->new(-WindowBits => 31);
385 $status == Compress::Raw::Zlib::Z_OK()
386 or throw 'Failed to initialize compression library', status => $status;
387 $status = $i->inflate($_, my $out);
388 $status == Compress::Raw::Zlib::Z_STREAM_END()
389 or throw 'Failed to decompress data', status => $status;
390 return $out;
391 }
392
393
394 sub gzip {
395 load_optional('Compress::Raw::Zlib');
396 local $_ = shift;
397 my ($d, $status) = Compress::Raw::Zlib::Deflate->new(-WindowBits => 31, -AppendOutput => 1);
398 $status == Compress::Raw::Zlib::Z_OK()
399 or throw 'Failed to initialize compression library', status => $status;
400 $status = $d->deflate($_, my $out);
401 $status == Compress::Raw::Zlib::Z_OK()
402 or throw 'Failed to compress data', status => $status;
403 $status = $d->flush($out);
404 $status == Compress::Raw::Zlib::Z_OK()
405 or throw 'Failed to compress data', status => $status;
406 return $out;
407 }
408
409
410 sub int64 {
411 require Config;
412 if ($Config::Config{ivsize} < 8) {
413 require Math::BigInt;
414 return Math::BigInt->new(@_);
415 }
416 return 0 + shift;
417 }
418
419
420 sub pack_Ql {
421 my $num = shift;
422 require Config;
423 if ($Config::Config{ivsize} < 8) {
424 if (blessed $num && $num->can('as_hex')) {
425 return "\xff\xff\xff\xff\xff\xff\xff\xff" if Math::BigInt->new('18446744073709551615') <= $num;
426 return "\x00\x00\x00\x00\x00\x00\x00\x80" if $num <= Math::BigInt->new('-9223372036854775808');
427 my $neg;
428 if ($num < 0) {
429 $neg = 1;
430 $num = -$num;
431 }
432 my $hex = $num->as_hex;
433 $hex =~ s/^0x/000000000000000/;
434 my $bytes = reverse pack('H16', substr($hex, -16));
435 $bytes .= "\0" x (8 - length $bytes) if length $bytes < 8;
436 if ($neg) {
437 # two's compliment
438 $bytes = join('', map { chr(~ord($_) & 0xff) } split(//, $bytes));
439 substr($bytes, 0, 1, chr(ord(substr($bytes, 0, 1)) + 1));
440 }
441 return $bytes;
442 }
443 else {
444 my $pad = $num < 0 ? "\xff" : "\0";
445 return pack('L<', $num) . ($pad x 4);
446 };
447 }
448 return pack('Q<', $num);
449 }
450
451
452 sub pack_ql { goto &pack_Ql }
453
454
455 sub unpack_Ql {
456 my $bytes = shift;
457 require Config;
458 if ($Config::Config{ivsize} < 8) {
459 require Math::BigInt;
460 return Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
461 }
462 return unpack('Q<', $bytes);
463 }
464
465
466 sub unpack_ql {
467 my $bytes = shift;
468 require Config;
469 if ($Config::Config{ivsize} < 8) {
470 require Math::BigInt;
471 if (ord(substr($bytes, -1, 1)) & 128) {
472 return Math::BigInt->new('-9223372036854775808') if $bytes eq "\x00\x00\x00\x00\x00\x00\x00\x80";
473 # two's compliment
474 substr($bytes, 0, 1, chr(ord(substr($bytes, 0, 1)) - 1));
475 $bytes = join('', map { chr(~ord($_) & 0xff) } split(//, $bytes));
476 return -Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
477 }
478 else {
479 return Math::BigInt->new('0x' . unpack('H*', scalar reverse $bytes));
480 }
481 }
482 return unpack('q<', $bytes);
483 }
484
485
486 sub is_uuid { defined $_[0] && !is_ref($_[0]) && length($_[0]) == 16 }
487
488
489 sub list_attributes {
490 my $package = shift;
491 return @{$ATTRIBUTES{$package} // []};
492 }
493
494
495 sub load_optional {
496 for my $module (@_) {
497 eval { load $module };
498 if (my $err = $@) {
499 throw "Missing dependency: Please install $module to use this feature.\n",
500 module => $module,
501 error => $err;
502 }
503 }
504 return wantarray ? @_ : $_[0];
505 }
506
507
508 sub memoize {
509 my $func = shift;
510 my @args = @_;
511 my %cache;
512 return sub { $cache{join("\0", grep { defined } @_)} //= $func->(@args, @_) };
513 }
514
515
516 sub pad_pkcs7 {
517 my $data = shift // throw 'Must provide a string to pad';
518 my $size = shift or throw 'Must provide block size';
519
520 0 <= $size && $size < 256
521 or throw 'Cannot add PKCS7 padding to a large block size', size => $size;
522
523 my $pad_len = $size - length($data) % $size;
524 $data .= chr($pad_len) x $pad_len;
525 }
526
527
528 sub query { _query(undef, '-or', \@_) }
529
530
531 sub query_any {
532 my $code = shift;
533
534 if (is_coderef($code) || overload::Method($code, '&{}')) {
535 return $code;
536 }
537 elsif (is_scalarref($code)) {
538 return simple_expression_query($$code, @_);
539 }
540 else {
541 return query($code, @_);
542 }
543 }
544
545
546 sub read_all($$$;$) { ## no critic (ProhibitSubroutinePrototypes)
547 my $result = @_ == 3 ? read($_[0], $_[1], $_[2])
548 : read($_[0], $_[1], $_[2], $_[3]);
549 return if !defined $result;
550 return if $result != $_[2];
551 return $result;
552 }
553
554
555 sub recurse_limit {
556 my $func = shift;
557 my $max_depth = shift // 200;
558 my $error = shift // sub {};
559 my $depth = 0;
560 return sub { return $error->(@_) if $max_depth < ++$depth; $func->(@_) };
561 };
562
563
564 sub search {
565 my $list = shift;
566 my $query = query_any(@_);
567
568 my @match;
569 for my $item (@$list) {
570 push @match, $item if $query->($item);
571 }
572 return \@match;
573 }
574
575
576 sub simple_expression_query {
577 my $expr = shift;
578 my $op = @_ && ($OPS{$_[0] || ''} || 0) == 2 ? shift : '=~';
579
580 my $neg_op = $OP_NEG{$op};
581 my $is_re = $op eq '=~' || $op eq '!~';
582
583 require Text::ParseWords;
584 my @terms = Text::ParseWords::shellwords($expr);
585
586 my @query = qw(-and);
587
588 for my $term (@terms) {
589 my @subquery = qw(-or);
590
591 my $neg = $term =~ s/^-//;
592 my $condition = [($neg ? $neg_op : $op) => ($is_re ? qr/\Q$term\E/i : $term)];
593
594 for my $field (@_) {
595 push @subquery, $field => $condition;
596 }
597
598 push @query, \@subquery;
599 }
600
601 return query(\@query);
602 }
603
604
605 sub snakify {
606 local $_ = shift;
607 s/UserName/Username/g;
608 s/([a-z])([A-Z0-9])/${1}_${2}/g;
609 s/([A-Z0-9]+)([A-Z0-9])(?![A-Z0-9]|$)/${1}_${2}/g;
610 return lc($_);
611 }
612
613
614 sub split_url {
615 local $_ = shift;
616 my ($scheme, $auth, $host, $port, $path, $query, $hash) =~ m!
617 ^([^:/\?\#]+) ://
618 (?:([^\@]+)\@)
619 ([^:/\?\#]*)
620 (?::(\d+))?
621 ([^\?\#]*)
622 (\?[^\#]*)?
623 (\#(.*))?
624 !x;
625
626 $scheme = lc($scheme);
627
628 $host ||= 'localhost';
629 $host = lc($host);
630
631 $path = "/$path" if $path !~ m!^/!;
632
633 $port ||= $scheme eq 'http' ? 80 : $scheme eq 'https' ? 433 : undef;
634
635 my ($username, $password) = split($auth, ':', 2);
636
637 return ($scheme, $auth, $host, $port, $path, $query, $hash, $username, $password);
638 }
639
640
641 sub to_bool { $_[0] // return; boolean($_[0]) }
642 sub to_number { $_[0] // return; 0+$_[0] }
643 sub to_string { $_[0] // return; "$_[0]" }
644 sub to_time {
645 $_[0] // return;
646 return scalar gmtime($_[0]) if looks_like_number($_[0]);
647 return scalar gmtime if $_[0] eq 'now';
648 return Time::Piece->strptime($_[0], '%Y-%m-%d %H:%M:%S') if !blessed $_[0];
649 return $_[0];
650 }
651 sub to_tristate { $_[0] // return; boolean($_[0]) }
652 sub to_uuid {
653 my $str = to_string(@_) // return;
654 return sprintf('%016s', $str) if length($str) < 16;
655 return substr($str, 0, 16) if 16 < length($str);
656 return $str;
657 }
658
659
660 sub trim($) { ## no critic (ProhibitSubroutinePrototypes)
661 local $_ = shift // return;
662 s/^\s*//;
663 s/\s*$//;
664 return $_;
665 }
666
667
668 sub try_load_optional {
669 for my $module (@_) {
670 eval { load $module };
671 if (my $err = $@) {
672 warn $err if 3 <= DEBUG;
673 return;
674 }
675 }
676 return @_;
677 }
678
679
680 my %ESC = map { chr($_) => sprintf('%%%02X', $_) } 0..255;
681 sub uri_escape_utf8 {
682 local $_ = shift // return;
683 $_ = encode('UTF-8', $_);
684 # RFC 3986 section 2.3 unreserved characters
685 s/([^A-Za-z0-9\-\._~])/$ESC{$1}/ge;
686 return $_;
687 }
688
689
690 sub uri_unescape_utf8 {
691 local $_ = shift // return;
692 s/\%([A-Fa-f0-9]{2})/chr(hex($1))/;
693 return decode('UTF-8', $_);
694 }
695
696
697 sub uuid {
698 local $_ = shift // return "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
699 s/-//g;
700 /^[A-Fa-f0-9]{32}$/ or throw 'Must provide a formatted 128-bit UUID';
701 return pack('H32', $_);
702
703 }
704
705
706 sub UUID_NULL() { "\0" x 16 }
707
708 ### --------------------------------------------------------------------------
709
710 # Determine if an array looks like keypairs from a hash.
711 sub _looks_like_keypairs {
712 my $arr = shift;
713 return 0 if @$arr % 2 == 1;
714 for (my $i = 0; $i < @$arr; $i += 2) {
715 return 0 if is_ref($arr->[$i]);
716 }
717 return 1;
718 }
719
720 sub _is_operand_plain {
721 local $_ = shift;
722 return !(is_hashref($_) || is_arrayref($_));
723 }
724
725 sub _query {
726 # dumper \@_;
727 my $subject = shift;
728 my $op = shift // throw 'Must specify a query operator';
729 my $operand = shift;
730
731 return _query_simple($op, $subject) if defined $subject && !is_ref($op) && ($OPS{$subject} || 2) < 2;
732 return _query_simple($subject, $op, $operand) if _is_operand_plain($operand);
733 return _query_inverse(_query($subject, '-or', $operand)) if $op eq '-not' || $op eq '-false';
734 return _query($subject, '-and', [%$operand]) if is_hashref($operand);
735
736 my @queries;
737
738 my @atoms = @$operand;
739 while (@atoms) {
740 if (_looks_like_keypairs(\@atoms)) {
741 my ($atom, $operand) = splice @atoms, 0, 2;
742 if (my $op_type = $OPS{$atom}) {
743 if ($op_type == 1 && _is_operand_plain($operand)) { # unary
744 push @queries, _query_simple($operand, $atom);
745 }
746 else {
747 push @queries, _query($subject, $atom, $operand);
748 }
749 }
750 elsif (!is_ref($atom)) {
751 push @queries, _query($atom, 'eq', $operand);
752 }
753 }
754 else {
755 my $atom = shift @atoms;
756 if ($OPS{$atom}) { # apply new operator over the rest
757 push @queries, _query($subject, $atom, \@atoms);
758 last;
759 }
760 else { # apply original operator over this one
761 push @queries, _query($subject, $op, $atom);
762 }
763 }
764 }
765
766 if (@queries == 1) {
767 return $queries[0];
768 }
769 elsif ($op eq '-and') {
770 return _query_all(@queries);
771 }
772 elsif ($op eq '-or') {
773 return _query_any(@queries);
774 }
775 throw 'Malformed query';
776 }
777
778 sub _query_simple {
779 my $subject = shift;
780 my $op = shift // 'eq';
781 my $operand = shift;
782
783 # these special operators can also act as simple operators
784 $op = '!!' if $op eq '-true';
785 $op = '!' if $op eq '-false';
786 $op = '!' if $op eq '-not';
787
788 defined $subject or throw 'Subject is not set in query';
789 $OPS{$op} >= 0 or throw 'Cannot use a non-simple operator in a simple query';
790 if (empty($operand)) {
791 if ($OPS{$op} < 2) {
792 # no operand needed
793 }
794 # Allow field => undef and field => {'ne' => undef} to do the (arguably) right thing.
795 elsif ($op eq 'eq' || $op eq '==') {
796 $op = '-empty';
797 }
798 elsif ($op eq 'ne' || $op eq '!=') {
799 $op = '-nonempty';
800 }
801 else {
802 throw 'Operand is required';
803 }
804 }
805
806 my $field = sub { blessed $_[0] && $_[0]->can($subject) ? $_[0]->$subject : $_[0]->{$subject} };
807
808 my %map = (
809 'eq' => sub { local $_ = $field->(@_); defined && $_ eq $operand },
810 'ne' => sub { local $_ = $field->(@_); defined && $_ ne $operand },
811 'lt' => sub { local $_ = $field->(@_); defined && $_ lt $operand },
812 'gt' => sub { local $_ = $field->(@_); defined && $_ gt $operand },
813 'le' => sub { local $_ = $field->(@_); defined && $_ le $operand },
814 'ge' => sub { local $_ = $field->(@_); defined && $_ ge $operand },
815 '==' => sub { local $_ = $field->(@_); defined && $_ == $operand },
816 '!=' => sub { local $_ = $field->(@_); defined && $_ != $operand },
817 '<' => sub { local $_ = $field->(@_); defined && $_ < $operand },
818 '>' => sub { local $_ = $field->(@_); defined && $_ > $operand },
819 '<=' => sub { local $_ = $field->(@_); defined && $_ <= $operand },
820 '>=' => sub { local $_ = $field->(@_); defined && $_ >= $operand },
821 '=~' => sub { local $_ = $field->(@_); defined && $_ =~ $operand },
822 '!~' => sub { local $_ = $field->(@_); defined && $_ !~ $operand },
823 '!' => sub { local $_ = $field->(@_); ! $_ },
824 '!!' => sub { local $_ = $field->(@_); !!$_ },
825 '-defined' => sub { local $_ = $field->(@_); defined $_ },
826 '-undef' => sub { local $_ = $field->(@_); !defined $_ },
827 '-nonempty' => sub { local $_ = $field->(@_); nonempty $_ },
828 '-empty' => sub { local $_ = $field->(@_); empty $_ },
829 );
830
831 return $map{$op} // throw "Unexpected operator in query: $op",
832 subject => $subject,
833 operator => $op,
834 operand => $operand;
835 }
836
837 sub _query_inverse {
838 my $query = shift;
839 return sub { !$query->(@_) };
840 }
841
842 sub _query_all {
843 my @queries = @_;
844 return sub {
845 my $val = shift;
846 all { $_->($val) } @queries;
847 };
848 }
849
850 sub _query_any {
851 my @queries = @_;
852 return sub {
853 my $val = shift;
854 any { $_->($val) } @queries;
855 };
856 }
857
858 1;
859
860 __END__
861
862 =pod
863
864 =encoding UTF-8
865
866 =head1 NAME
867
868 File::KDBX::Util - Utility functions for working with KDBX files
869
870 =head1 VERSION
871
872 version 0.902
873
874 =head1 FUNCTIONS
875
876 =head2 load_xs
877
878 $bool = load_xs();
879 $bool = load_xs($version);
880
881 Attempt to load L<File::KDBX::XS>. Return truthy if it is loaded. If C<$version> is given, it will check that
882 at least the given version is loaded.
883
884 =head2 assert
885
886 assert { ... };
887
888 Write an executable comment. Only executed if C<DEBUG> is set in the environment.
889
890 =head2 can_fork
891
892 $bool = can_fork;
893
894 Determine if perl can fork, with logic lifted from L<Test2::Util/CAN_FORK>.
895
896 =head2 clone
897
898 $clone = clone($thing);
899
900 Clone deeply. This is an unadorned alias to L<Storable> C<dclone>.
901
902 =head2 clone_nomagic
903
904 $clone = clone_nomagic($thing);
905
906 Clone deeply without keeping [most of] the magic.
907
908 B<WARNING:> At the moment the implementation is naïve and won't respond well to nontrivial data or recursive
909 structures.
910
911 =head2 DEBUG
912
913 Constant number indicating the level of debuggingness.
914
915 =head2 dumper
916
917 $str = dumper $thing;
918 dumper $thing; # in void context, prints to STDERR
919
920 Like L<Data::Dumper> but slightly terser in some cases relevent to L<File::KDBX>.
921
922 =head2 empty
923
924 =head2 nonempty
925
926 $bool = empty $thing;
927
928 $bool = nonempty $thing;
929
930 Test whether a thing is empty (or nonempty). An empty thing is one of these:
931
932 =over 4
933
934 =item *
935
936 nonexistent
937
938 =item *
939
940 C<undef>
941
942 =item *
943
944 zero-length string
945
946 =item *
947
948 zero-length array
949
950 =item *
951
952 hash with zero keys
953
954 =item *
955
956 reference to an empty thing (recursive)
957
958 =back
959
960 Note in particular that zero C<0> is not considered empty because it is an actual value.
961
962 =head2 erase
963
964 erase($string, ...);
965 erase(\$string, ...);
966
967 Overwrite the memory used by one or more string.
968
969 =head2 erase_scoped
970
971 $scope_guard = erase_scoped($string, ...);
972 $scope_guard = erase_scoped(\$string, ...);
973 undef $scope_guard; # erase happens here
974
975 Get a scope guard that will cause scalars to be erased later (i.e. when the scope ends). This is useful if you
976 want to make sure a string gets erased after you're done with it, even if the scope ends abnormally.
977
978 See L</erase>.
979
980 =head2 extends
981
982 extends $class;
983
984 Set up the current module to inheret from another module.
985
986 =head2 has
987
988 has $name => %options;
989
990 Create an attribute getter/setter. Possible options:
991
992 =over 4
993
994 =item *
995
996 C<is> - Either "rw" (default) or "ro"
997
998 =item *
999
1000 C<default> - Default value
1001
1002 =item *
1003
1004 C<coerce> - Coercive function
1005
1006 =back
1007
1008 =head2 format_uuid
1009
1010 $string_uuid = format_uuid($raw_uuid);
1011 $string_uuid = format_uuid($raw_uuid, $delimiter);
1012
1013 Format a 128-bit UUID (given as a string of 16 octets) into a hexidecimal string, optionally with a delimiter
1014 to break up the UUID visually into five parts. Examples:
1015
1016 my $uuid = uuid('01234567-89AB-CDEF-0123-456789ABCDEF');
1017 say format_uuid($uuid); # -> 0123456789ABCDEF0123456789ABCDEF
1018 say format_uuid($uuid, '-'); # -> 01234567-89AB-CDEF-0123-456789ABCDEF
1019
1020 This is the inverse of L</uuid>.
1021
1022 =head2 generate_uuid
1023
1024 $uuid = generate_uuid;
1025 $uuid = generate_uuid(\%set);
1026 $uuid = generate_uuid(\&test_uuid);
1027
1028 Generate a new random UUID. It's pretty unlikely that this will generate a repeat, but if you're worried about
1029 that you can provide either a set of existing UUIDs (as a hashref where the keys are the elements of a set) or
1030 a function to check for existing UUIDs, and this will be sure to not return a UUID already in provided set.
1031 Perhaps an example will make it clear:
1032
1033 my %uuid_set = (
1034 uuid('12345678-9ABC-DEFG-1234-56789ABCDEFG') => 'whatever',
1035 );
1036 $uuid = generate_uuid(\%uuid_set);
1037 # OR
1038 $uuid = generate_uuid(sub { !$uuid_set{$_} });
1039
1040 Here, C<$uuid> can't be "12345678-9ABC-DEFG-1234-56789ABCDEFG". This example uses L</uuid> to easily pack
1041 a 16-byte UUID from a literal, but it otherwise is not a consequential part of the example.
1042
1043 =head2 gunzip
1044
1045 $unzipped = gunzip($string);
1046
1047 Decompress an octet stream.
1048
1049 =head2 gzip
1050
1051 $zipped = gzip($string);
1052
1053 Compress an octet stream.
1054
1055 =head2 int64
1056
1057 $int = int64($string);
1058
1059 Get a scalar integer capable of holding 64-bit values, initialized with a given default value. On a 64-bit
1060 perl, it will return a regular SvIV. On a 32-bit perl it will return a L<Math::BigInt>.
1061
1062 =head2 pack_Ql
1063
1064 $bytes = pack_Ql($int);
1065
1066 Like C<pack('QE<lt>', $int)>, but also works on 32-bit perls.
1067
1068 =head2 pack_ql
1069
1070 $bytes = pack_ql($int);
1071
1072 Like C<pack('qE<lt>', $int)>, but also works on 32-bit perls.
1073
1074 =head2 unpack_Ql
1075
1076 $int = unpack_Ql($bytes);
1077
1078 Like C<unpack('QE<lt>', $bytes)>, but also works on 32-bit perls.
1079
1080 =head2 unpack_ql
1081
1082 $int = unpack_ql($bytes);
1083
1084 Like C<unpack('qE<lt>', $bytes)>, but also works on 32-bit perls.
1085
1086 =head2 is_uuid
1087
1088 $bool = is_uuid($thing);
1089
1090 Check if a thing is a UUID (i.e. scalar string of length 16).
1091
1092 =head2 list_attributes
1093
1094 @attributes = list_attributes($package);
1095
1096 Get a list of attributes for a class.
1097
1098 =head2 load_optional
1099
1100 $package = load_optional($package);
1101
1102 Load a module that isn't required but can provide extra functionality. Throw if the module is not available.
1103
1104 =head2 memoize
1105
1106 \&memoized_code = memoize(\&code, ...);
1107
1108 Memoize a function. Extra arguments are passed through to C<&code> when it is called.
1109
1110 =head2 pad_pkcs7
1111
1112 $padded_string = pad_pkcs7($string, $block_size),
1113
1114 Pad a block using the PKCS#7 method.
1115
1116 =head2 query
1117
1118 $query = query(@where);
1119 $query->(\%data);
1120
1121 Generate a function that will run a series of tests on a passed hashref and return true or false depending on
1122 if the data record in the hash matched the specified logic.
1123
1124 The logic can be specified in a manner similar to L<SQL::Abstract/"WHERE CLAUSES"> which was the inspiration
1125 for this function, but this code is distinct, supporting an overlapping but not identical feature set and
1126 having its own bugs.
1127
1128 See L<File::KDBX/"Declarative Syntax"> for examples.
1129
1130 =head2 query_any
1131
1132 Get either a L</query> or L</simple_expression_query>, depending on the arguments.
1133
1134 =head2 read_all
1135
1136 $size = read_all($fh, my $buffer, $size);
1137 $size = read_all($fh, my $buffer, $size, $offset);
1138
1139 Like L<perlfunc/"read FILEHANDLE,SCALAR,LENGTH,OFFSET"> but returns C<undef> if not all C<$size> bytes are
1140 read. This is considered an error, distinguishable from other errors by C<$!> not being set.
1141
1142 =head2 recurse_limit
1143
1144 \&limited_code = recurse_limit(\&code);
1145 \&limited_code = recurse_limit(\&code, $max_depth);
1146 \&limited_code = recurse_limit(\&code, $max_depth, \&error_handler);
1147
1148 Wrap a function with a guard to prevent deep recursion.
1149
1150 =head2 search
1151
1152 # Generate a query on-the-fly:
1153 \@matches = search(\@records, @where);
1154
1155 # Use a pre-compiled query:
1156 $query = query(@where);
1157 \@matches = search(\@records, $query);
1158
1159 # Use a simple expression:
1160 \@matches = search(\@records, \'query terms', @fields);
1161 \@matches = search(\@records, \'query terms', $operator, @fields);
1162
1163 # Use your own subroutine:
1164 \@matches = search(\@records, \&query);
1165 \@matches = search(\@records, sub { $record = shift; ... });
1166
1167 Execute a linear search over an array of records using a L</query>. A "record" is usually a hash.
1168
1169 =head2 simple_expression_query
1170
1171 $query = simple_expression_query($expression, @fields);
1172 $query = simple_expression_query($expression, $operator, @fields);
1173
1174 Generate a query, like L</query>, to be used with L</search> but built from a "simple expression" as
1175 L<described here|https://keepass.info/help/base/search.html#mode_se>.
1176
1177 An expression is a string with one or more space-separated terms. Terms with spaces can be enclosed in double
1178 quotes. Terms are negated if they are prefixed with a minus sign. A record must match every term on at least
1179 one of the given fields.
1180
1181 =head2 snakify
1182
1183 $string = snakify($string);
1184
1185 Turn a CamelCase string into snake_case.
1186
1187 =head2 split_url
1188
1189 ($scheme, $auth, $host, $port, $path, $query, $hash, $usename, $password) = split_url($url);
1190
1191 Split a URL into its parts.
1192
1193 For example, C<http://user:pass@localhost:4000/path?query#hash> gets split like:
1194
1195 =over 4
1196
1197 =item *
1198
1199 C<http>
1200
1201 =item *
1202
1203 C<user:pass>
1204
1205 =item *
1206
1207 C<host>
1208
1209 =item *
1210
1211 C<4000>
1212
1213 =item *
1214
1215 C</path>
1216
1217 =item *
1218
1219 C<?query>
1220
1221 =item *
1222
1223 C<#hash>
1224
1225 =item *
1226
1227 C<user>
1228
1229 =item *
1230
1231 C<pass>
1232
1233 =back
1234
1235 =head2 to_bool
1236
1237 =head2 to_number
1238
1239 =head2 to_string
1240
1241 =head2 to_time
1242
1243 =head2 to_tristate
1244
1245 =head2 to_uuid
1246
1247 Various typecasting / coercive functions.
1248
1249 =head2 trim
1250
1251 $string = trim($string);
1252
1253 The ubiquitous C<trim> function. Removes all whitespace from both ends of a string.
1254
1255 =head2 try_load_optional
1256
1257 $package = try_load_optional($package);
1258
1259 Try to load a module that isn't required but can provide extra functionality, and return true if successful.
1260
1261 =head2 uri_escape_utf8
1262
1263 $string = uri_escape_utf8($string);
1264
1265 Percent-encode arbitrary text strings, like for a URI.
1266
1267 =head2 uri_unescape_utf8
1268
1269 $string = uri_unescape_utf8($string);
1270
1271 Inverse of L</uri_escape_utf8>.
1272
1273 =head2 uuid
1274
1275 $raw_uuid = uuid($string_uuid);
1276
1277 Pack a 128-bit UUID (given as a hexidecimal string with optional C<->'s, like
1278 C<12345678-9ABC-DEFG-1234-56789ABCDEFG>) into a string of exactly 16 octets.
1279
1280 This is the inverse of L</format_uuid>.
1281
1282 =head2 UUID_NULL
1283
1284 Get the null UUID (i.e. string of 16 null bytes).
1285
1286 =head1 BUGS
1287
1288 Please report any bugs or feature requests on the bugtracker website
1289 L<https://github.com/chazmcgarvey/File-KDBX/issues>
1290
1291 When submitting a bug or request, please include a test-file or a
1292 patch to an existing test-file that illustrates the bug or desired
1293 feature.
1294
1295 =head1 AUTHOR
1296
1297 Charles McGarvey <ccm@cpan.org>
1298
1299 =head1 COPYRIGHT AND LICENSE
1300
1301 This software is copyright (c) 2022 by Charles McGarvey.
1302
1303 This is free software; you can redistribute it and/or modify it under
1304 the same terms as the Perl 5 programming language system itself.
1305
1306 =cut
This page took 0.114703 seconds and 3 git commands to generate.