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