File Coverage

blib/lib/Config/IOD/Base.pm
Criterion Covered Total %
statement 225 259 86.8
branch 111 168 66.0
condition 41 57 71.9
subroutine 26 26 100.0
pod 3 3 100.0
total 406 513 79.1


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