File Coverage

blib/lib/Config/IOD/Base.pm
Criterion Covered Total %
statement 225 262 85.8
branch 111 170 65.2
condition 41 57 71.9
subroutine 26 27 96.3
pod 3 3 100.0
total 406 519 78.2


line stmt bran cond sub pod time code
1             package Config::IOD::Base;
2              
3 3     3   1340 use 5.010001;
  3         10  
4 3     3   12 use strict;
  3         5  
  3         62  
5 3     3   12 use warnings;
  3         4  
  3         318  
6             #use Carp; # avoided to shave a bit of startup time
7              
8             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
9             our $DATE = '2022-05-02'; # DATE
10             our $DIST = 'Config-IOD-Reader'; # DIST
11             our $VERSION = '0.344'; # VERSION
12              
13             use constant +{
14 3         7062 COL_V_ENCODING => 0, # either "!j"... or '"', '[', '{', '~'
15             COL_V_WS1 => 1,
16             COL_V_VALUE => 2,
17             COL_V_WS2 => 3,
18             COL_V_COMMENT_CHAR => 4,
19             COL_V_COMMENT => 5,
20 3     3   24 };
  3         6  
21              
22             sub new {
23 26     26 1 88618 my ($class, %attrs) = @_;
24 26   100     129 $attrs{default_section} //= 'GLOBAL';
25 26   100     87 $attrs{allow_bang_only} //= 1;
26 26   100     75 $attrs{allow_duplicate_key} //= 1;
27 26   100     78 $attrs{enable_directive} //= 1;
28 26   100     78 $attrs{enable_encoding} //= 1;
29 26   100     81 $attrs{enable_quoting} //= 1;
30 26   100     86 $attrs{enable_bracket} //= 1;
31 26   100     110 $attrs{enable_brace} //= 1;
32 26   50     75 $attrs{enable_tilde} //= 1;
33 26   100     68 $attrs{enable_expr} //= 0;
34 26   100     79 $attrs{expr_vars} //= {};
35 26   100     76 $attrs{ignore_unknown_directive} //= 0;
36             # allow_encodings
37             # disallow_encodings
38             # allow_directives
39             # disallow_directives
40             # warn_perl
41 26         80 bless \%attrs, $class;
42             }
43              
44             # borrowed from Parse::CommandLine. differences: returns arrayref. return undef
45             # on error (instead of dying).
46             sub _parse_command_line {
47 22     22   86 my ($self, $str) = @_;
48              
49 22         43 $str =~ s/\A\s+//ms;
50 22         92 $str =~ s/\s+\z//ms;
51              
52 22         83 my @argv;
53             my $buf;
54 22         0 my $escaped;
55 22         0 my $double_quoted;
56 22         0 my $single_quoted;
57              
58 22         77 for my $char (split //, $str) {
59 140 50       201 if ($escaped) {
60 0         0 $buf .= $char;
61 0         0 $escaped = undef;
62 0         0 next;
63             }
64              
65 140 50       206 if ($char eq '\\') {
66 0 0       0 if ($single_quoted) {
67 0         0 $buf .= $char;
68             }
69             else {
70 0         0 $escaped = 1;
71             }
72 0         0 next;
73             }
74              
75 140 100       226 if ($char =~ /\s/) {
76 2 50 33     11 if ($single_quoted || $double_quoted) {
77 0         0 $buf .= $char;
78             }
79             else {
80 2 50       7 push @argv, $buf if defined $buf;
81 2         3 undef $buf;
82             }
83 2         4 next;
84             }
85              
86 138 100       197 if ($char eq '"') {
87 5 50       11 if ($single_quoted) {
88 0         0 $buf .= $char;
89 0         0 next;
90             }
91 5         7 $double_quoted = !$double_quoted;
92 5         8 next;
93             }
94              
95 133 50       177 if ($char eq "'") {
96 0 0       0 if ($double_quoted) {
97 0         0 $buf .= $char;
98 0         0 next;
99             }
100 0         0 $single_quoted = !$single_quoted;
101 0         0 next;
102             }
103              
104 133         161 $buf .= $char;
105             }
106 22 100       63 push @argv, $buf if defined $buf;
107              
108 22 100 33     110 if ($escaped || $single_quoted || $double_quoted) {
      66        
109 1         4 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
110             }
111              
112 21         59 \@argv;
113             }
114              
115             # return ($err, $res, $decoded_val)
116             sub _parse_raw_value {
117 66     66   135 my ($self, $val, $needs_res) = @_;
118              
119 66 100 100     403 if ($val =~ /\A!/ && $self->{enable_encoding}) {
    100 66        
    100 66        
    100 66        
    100 66        
120              
121 38 50       223 $val =~ s/!(\w+)(\s+)// or return ("Invalid syntax in encoded value");
122 38         110 my ($enc, $ws1) = ($1, $2);
123              
124 38 50       49 my $res; $res = [
  38         63  
125             "!$enc", # COL_V_ENCODING
126             $ws1, # COL_V_WS1
127             $1, # COL_V_VALUE
128             $2, # COL_V_WS2
129             $3, # COL_V_COMMENT_CHAR
130             $4, # COL_V_COMMENT
131             ] if $needs_res;
132              
133             # canonicalize shorthands
134 38 100       80 $enc = "json" if $enc eq 'j';
135 38 100       71 $enc = "hex" if $enc eq 'h';
136 38 100       65 $enc = "expr" if $enc eq 'e';
137              
138 38 100       76 if ($self->{allow_encodings}) {
139             return ("Encoding '$enc' is not in ".
140             "allow_encodings list")
141 4 100       5 unless grep {$_ eq $enc} @{$self->{allow_encodings}};
  4         17  
  4         9  
142             }
143 37 100       74 if ($self->{disallow_encodings}) {
144             return ("Encoding '$enc' is in ".
145             "disallow_encodings list")
146 4 100       4 if grep {$_ eq $enc} @{$self->{disallow_encodings}};
  4         21  
  4         7  
147             }
148              
149 34 100 100     149 if ($enc eq 'json') {
    100          
    100          
    100          
    100          
    100          
150              
151             # XXX imperfect regex for simplicity, comment should not contain
152             # "]", '"', or '}' or it will be gobbled up as value by greedy regex
153             # quantifier
154 13 50       72 $val =~ /\A
155             (".*"|\[.*\]|\{.*\}|\S+)
156             (\s*)
157             (?: ([;#])(.*) )?
158             \z/x or return ("Invalid syntax in JSON-encoded value");
159 13         39 my $decode_res = $self->_decode_json($val);
160 13 100       34 return ($decode_res->[1]) unless $decode_res->[0] == 200;
161 12         42 return (undef, $res, $decode_res->[2]);
162              
163             } elsif ($enc eq 'path' || $enc eq 'paths') {
164              
165 4         8 my $decode_res = $self->_decode_path_or_paths($val, $enc);
166 4 50       19 return ($decode_res->[1]) unless $decode_res->[0] == 200;
167 4         15 return (undef, $res, $decode_res->[2]);
168              
169             } elsif ($enc eq 'hex') {
170              
171 2 50       20 $val =~ /\A
172             ([0-9A-Fa-f]*)
173             (\s*)
174             (?: ([;#])(.*) )?
175             \z/x or return ("Invalid syntax in hex-encoded value");
176 2         15 my $decode_res = $self->_decode_hex($1);
177 2 50       7 return ($decode_res->[1]) unless $decode_res->[0] == 200;
178 2         9 return (undef, $res, $decode_res->[2]);
179              
180             } elsif ($enc eq 'base64') {
181              
182 2 50       13 $val =~ m!\A
183             ([A-Za-z0-9+/]*=*)
184             (\s*)
185             (?: ([;#])(.*) )?
186             \z!x or return ("Invalid syntax in base64-encoded value");
187 2         13 my $decode_res = $self->_decode_base64($1);
188 2 50       7 return ($decode_res->[1]) unless $decode_res->[0] == 200;
189 2         9 return (undef, $res, $decode_res->[2]);
190              
191             } elsif ($enc eq 'none') {
192              
193 5         16 return (undef, $res, $val);
194              
195             } elsif ($enc eq 'expr') {
196              
197             return ("expr is not allowed (enable_expr=0)")
198 7 100       20 unless $self->{enable_expr};
199             # XXX imperfect regex, expression can't contain # and ; because it
200             # will be assumed as comment
201 6 50       39 $val =~ m!\A
202             ((?:[^#;])+?)
203             (\s*)
204             (?: ([;#])(.*) )?
205             \z!x or return ("Invalid syntax in expr-encoded value");
206 6         23 my $decode_res = $self->_decode_expr($1);
207 6 100       23 return ($decode_res->[1]) unless $decode_res->[0] == 200;
208 5         22 return (undef, $res, $decode_res->[2]);
209              
210             } else {
211              
212 1         6 return ("unknown encoding '$enc'");
213              
214             }
215              
216             } elsif ($val =~ /\A"/ && $self->{enable_quoting}) {
217              
218 11 50       93 $val =~ /\A
219             "( (?:
220             \\\\ | # backslash
221             \\. | # escaped something
222             [^"\\]+ # non-doublequote or non-backslash
223             )* )"
224             (\s*)
225             (?: ([;#])(.*) )?
226             \z/x or return ("Invalid syntax in quoted string value");
227 11 50       18 my $res; $res = [
  11         27  
228             '"', # COL_V_ENCODING
229             '', # COL_V_WS1
230             $1, # VOL_V_VALUE
231             $2, # COL_V_WS2
232             $3, # COL_V_COMMENT_CHAR
233             $4, # COL_V_COMMENT
234             ] if $needs_res;
235 11         56 my $decode_res = $self->_decode_json(qq("$1"));
236 11 50       33 return ($decode_res->[1]) unless $decode_res->[0] == 200;
237 11         38 return (undef, $res, $decode_res->[2]);
238              
239             } elsif ($val =~ /\A\[/ && $self->{enable_bracket}) {
240              
241             # XXX imperfect regex for simplicity, comment should not contain "]" or
242             # it will be gobbled up as value by greedy regex quantifier
243 6 50       31 $val =~ /\A
244             \[(.*)\]
245             (?:
246             (\s*)
247             ([#;])(.*)
248             )?
249             \z/x or return ("Invalid syntax in bracketed array value");
250 6 50       10 my $res; $res = [
  6         17  
251             '[', # COL_V_ENCODING
252             '', # COL_V_WS1
253             $1, # VOL_V_VALUE
254             $2, # COL_V_WS2
255             $3, # COL_V_COMMENT_CHAR
256             $4, # COL_V_COMMENT
257             ] if $needs_res;
258 6         24 my $decode_res = $self->_decode_json("[$1]");
259 6 50       16 return ($decode_res->[1]) unless $decode_res->[0] == 200;
260 6         19 return (undef, $res, $decode_res->[2]);
261              
262             } elsif ($val =~ /\A\{/ && $self->{enable_brace}) {
263              
264             # XXX imperfect regex for simplicity, comment should not contain "}" or
265             # it will be gobbled up as value by greedy regex quantifier
266 2 50       20 $val =~ /\A
267             \{(.*)\}
268             (?:
269             (\s*)
270             ([#;])(.*)
271             )?
272             \z/x or return ("Invalid syntax in braced hash value");
273 2 50       5 my $res; $res = [
  2         5  
274             '{', # COL_V_ENCODING
275             '', # COL_V_WS1
276             $1, # VOL_V_VALUE
277             $2, # COL_V_WS2
278             $3, # COL_V_COMMENT_CHAR
279             $4, # COL_V_COMMENT
280             ] if $needs_res;
281 2         46 my $decode_res = $self->_decode_json("{$1}");
282 2 50       18 return ($decode_res->[1]) unless $decode_res->[0] == 200;
283 2         7 return (undef, $res, $decode_res->[2]);
284              
285             } elsif ($val =~ /\A~/ && $self->{enable_tilde}) {
286              
287 1 50       4 $val =~ /\A
288             ~(.*)
289             (\s*)
290             (?: ([;#])(.*) )?
291             \z/x or return ("Invalid syntax in path value");
292 1 50       3 my $res; $res = [
  1         2  
293             '~', # COL_V_ENCODING
294             '', # COL_V_WS1
295             $1, # VOL_V_VALUE
296             $2, # COL_V_WS2
297             $3, # COL_V_COMMENT_CHAR
298             $4, # COL_V_COMMENT
299             ] if $needs_res;
300              
301 1         6 my $decode_res = $self->_decode_path_or_paths($val, 'path');
302 1 50       3 return ($decode_res->[1]) unless $decode_res->[0] == 200;
303 1         4 return (undef, $res, $decode_res->[2]);
304              
305             } else {
306              
307 8 50       82 $val =~ /\A
308             (.*?)
309             (\s*)
310             (?: ([#;])(.*) )?
311             \z/x or return ("Invalid syntax in value"); # shouldn't happen, regex should match any string
312 8 50       11 my $res; $res = [
  8         16  
313             '', # COL_V_ENCODING
314             '', # COL_V_WS1
315             $1, # VOL_V_VALUE
316             $2, # COL_V_WS2
317             $3, # COL_V_COMMENT_CHAR
318             $4, # COL_V_COMMENT
319             ] if $needs_res;
320 8         31 return (undef, $res, $1);
321              
322             }
323             # should not be reached
324             }
325              
326             sub _get_my_user_name {
327 1 50   1   907 if ($^O eq 'MSWin32') {
328 0         0 return $ENV{USERNAME};
329             } else {
330 1 50       5 return $ENV{USER} if $ENV{USER};
331 1         2 my @pw;
332 1         2 eval { @pw = getpwuid($>) };
  1         1495  
333 1 50       13 return $pw[0] if @pw;
334             }
335             }
336              
337             # borrowed from PERLANCAR::File::HomeDir 0.04
338             sub _get_my_home_dir {
339 3 50   3   15 if ($^O eq 'MSWin32') {
340             # File::HomeDir always uses exists($ENV{x}) first, does it want to avoid
341             # accidentally creating env vars?
342 0 0       0 return $ENV{HOME} if $ENV{HOME};
343 0 0       0 return $ENV{USERPROFILE} if $ENV{USERPROFILE};
344             return join($ENV{HOMEDRIVE}, "\\", $ENV{HOMEPATH})
345 0 0 0     0 if $ENV{HOMEDRIVE} && $ENV{HOMEPATH};
346             } else {
347 3 50       15 return $ENV{HOME} if $ENV{HOME};
348 0         0 my @pw;
349 0         0 eval { @pw = getpwuid($>) };
  0         0  
350 0 0       0 return $pw[7] if @pw;
351             }
352              
353 0         0 die "Can't get home directory";
354             }
355              
356             # borrowed from PERLANCAR::File::HomeDir 0.05, with some modifications
357             sub _get_user_home_dir {
358 1     1   3 my ($name) = @_;
359              
360 1 50       4 if ($^O eq 'MSWin32') {
361             # not yet implemented
362 0         0 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
363             } else {
364             # IF and only if we have getpwuid support, and the name of the user is
365             # our own, shortcut to my_home. This is needed to handle HOME
366             # environment settings.
367 1 50       61 if ($name eq getpwuid($<)) {
368 1         5 return _get_my_home_dir();
369             }
370              
371             SCOPE: {
372 0         0 my $home = (getpwnam($name))[7];
  0         0  
373 0 0 0     0 return $home if $home and -d $home;
374             }
375              
376 0         0 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
377             }
378              
379             }
380              
381             sub _decode_json {
382 60     60   135 my ($self, $val) = @_;
383 60         82 state $json = do {
384 3 50       6 if (eval { require Cpanel::JSON::XS; 1 }) {
  3         21  
  3         18  
385 3         38 Cpanel::JSON::XS->new->allow_nonref;
386             } else {
387 0         0 require JSON::PP;
388 0         0 JSON::PP->new->allow_nonref;
389             }
390             };
391 60         78 my $res;
392 60         102 eval { $res = $json->decode($val) };
  60         701  
393 60 100       152 if ($@) {
394 1         7 return [500, "Invalid JSON: $@"];
395             } else {
396 59         180 return [200, "OK", $res];
397             }
398             }
399              
400             sub _decode_path_or_paths {
401 5     5   9 my ($self, $val, $which) = @_;
402              
403 5 100       14 if ($val =~ m!\A~([^/]+)?(?:/|\z)!) {
404 2 100       8 my $home_dir = length($1) ?
405             _get_user_home_dir($1) : _get_my_home_dir();
406 2 50       5 unless ($home_dir) {
407 0 0       0 if (length $1) {
408 0         0 return [500, "Can't get home directory for user '$1' in path"];
409             } else {
410 0         0 return [500, "Can't get home directory for current user in path"];
411             }
412             }
413 2         9 $val =~ s!\A~([^/]+)?!$home_dir!;
414             }
415 5         8 $val =~ s!(?<=.)/\z!!;
416              
417 5 100       11 if ($which eq 'path') {
418 2         5 return [200, "OK", $val];
419             } else {
420 3         200 return [200, "OK", [glob $val]];
421             }
422             }
423              
424             sub _decode_hex {
425 2     2   6 my ($self, $val) = @_;
426 2         12 [200, "OK", pack("H*", $val)];
427             }
428              
429             sub _decode_base64 {
430 2     2   7 my ($self, $val) = @_;
431 2         544 require MIME::Base64;
432 2         1429 [200, "OK", MIME::Base64::decode_base64($val)];
433             }
434              
435             sub _decode_expr {
436 6     6   1114 require Config::IOD::Expr;
437              
438 6         22 my ($self, $val) = @_;
439 3     3   37 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
  3         4  
  3         1450  
440 6         28 local *{"Config::IOD::Expr::_Compiled::val"} = sub {
441 2     2   4 my $arg = shift;
442 2 100       9 if ($arg =~ /(.+)\.(.+)/) {
443 1         23 return $self->{_res}{$1}{$2};
444             } else {
445 1         8 return $self->{_res}{ $self->{_cur_section} }{$arg};
446             }
447 6         24 };
448 6         17 Config::IOD::Expr::_parse_expr($val);
449             }
450              
451             sub _warn {
452 0     0   0 my ($self, $msg) = @_;
453             warn join(
454             "",
455 0 0       0 @{ $self->{_include_stack} } ? "$self->{_include_stack}[0] " : "",
  0         0  
456             "line $self->{_linum}: ",
457             $msg
458             );
459             }
460              
461             sub _err {
462 22     22   47 my ($self, $msg) = @_;
463             die join(
464             "",
465 22 100       29 @{ $self->{_include_stack} } ? "$self->{_include_stack}[0] " : "",
  22         295  
466             "line $self->{_linum}: ",
467             $msg
468             );
469             }
470              
471             sub _push_include_stack {
472 45     45   365 require Cwd;
473              
474 45         119 my ($self, $path) = @_;
475              
476             # included file's path is based on the main (topmost) file
477 45 100       74 if (@{ $self->{_include_stack} }) {
  45         156  
478 6         18 require File::Spec;
479             my ($vol, $dir, $file) =
480 6         93 File::Spec->splitpath($self->{_include_stack}[-1]);
481 6         157 $path = File::Spec->rel2abs($path, File::Spec->catpath($vol, $dir));
482             }
483              
484 45 50       2084 my $abs_path = Cwd::abs_path($path) or return [400, "Invalid path name"];
485             return [409, "Recursive", $abs_path]
486 45 100       127 if grep { $_ eq $abs_path } @{ $self->{_include_stack} };
  7         26  
  45         158  
487 44         61 push @{ $self->{_include_stack} }, $abs_path;
  44         109  
488 44         172 return [200, "OK", $abs_path];
489             }
490              
491             sub _pop_include_stack {
492 32     32   47 my $self = shift;
493              
494             die "BUG: Overpopped _pop_include_stack"
495 32 50       39 unless @{$self->{_include_stack}};
  32         85  
496 32         42 pop @{ $self->{_include_stack} };
  32         66  
497             }
498              
499             sub _init_read {
500 64     64   94 my $self = shift;
501              
502 64         165 $self->{_include_stack} = [];
503              
504             # set expr variables
505             {
506 64 100       99 last unless $self->{enable_expr};
  64         223  
507 3     3   21 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
  3         5  
  3         983  
508 41         71 my $pkg = \%{"Config::IOD::Expr::_Compiled::"};
  41         211  
509 41         150 undef ${"Config::IOD::Expr::_Compiled::$_"} for keys %$pkg;
  115         316  
510 41         102 my $vars = $self->{expr_vars};
511 41         128 ${"Config::IOD::Expr::_Compiled::$_"} = $vars->{$_} for keys %$vars;
  117         285  
512             }
513             }
514              
515             sub _read_file {
516 72     72   242 my ($self, $filename) = @_;
517 72 100       3083 open my $fh, "<", $filename
518             or die "Can't open file '$filename': $!";
519 71     1   1082 binmode($fh, ":encoding(utf8)");
  1         7  
  1         1  
  1         12  
520 71         14206 local $/;
521 71         2646 my $res = scalar <$fh>;
522 71         2090 close $fh;
523 71         655 $res;
524             }
525              
526             sub read_file {
527 39     39 1 109895 my $self = shift;
528 39         91 my $filename = shift;
529 39         204 $self->_init_read;
530 39         144 my $res = $self->_push_include_stack($filename);
531 39 50       114 die "Can't read '$filename': $res->[1]" unless $res->[0] == 200;
532 39         120 $res =
533             $self->_read_string($self->_read_file($filename), @_);
534 28         139 $self->_pop_include_stack;
535 28         69 $res;
536             }
537              
538             sub read_string {
539 25     25 1 32 my $self = shift;
540 25         66 $self->_init_read;
541 25         116 $self->_read_string(@_);
542             }
543              
544             1;
545             # ABSTRACT: Base class for Config::IOD and Config::IOD::Reader
546              
547             __END__