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   1147 use 5.010001;
  3         8  
4 3     3   10 use strict;
  3         5  
  3         96  
5 3     3   11 use warnings;
  3         4  
  3         240  
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.345'; # VERSION
12              
13             use constant +{
14 3         6441 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   16 };
  3         4  
21              
22             sub new {
23 26     26 1 92104 my ($class, %attrs) = @_;
24 26   100     159 $attrs{default_section} //= 'GLOBAL';
25 26   100     122 $attrs{allow_bang_only} //= 1;
26 26   100     95 $attrs{allow_duplicate_key} //= 1;
27 26   100     109 $attrs{enable_directive} //= 1;
28 26   100     110 $attrs{enable_encoding} //= 1;
29 26   100     95 $attrs{enable_quoting} //= 1;
30 26   100     123 $attrs{enable_bracket} //= 1;
31 26   100     111 $attrs{enable_brace} //= 1;
32 26   50     85 $attrs{enable_tilde} //= 1;
33 26   100     106 $attrs{enable_expr} //= 0;
34 26   100     96 $attrs{expr_vars} //= {};
35 26   100     111 $attrs{ignore_unknown_directive} //= 0;
36             # allow_encodings
37             # disallow_encodings
38             # allow_directives
39             # disallow_directives
40             # warn_perl
41 26         94 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   59 my ($self, $str) = @_;
48              
49 22         55 $str =~ s/\A\s+//ms;
50 22         107 $str =~ s/\s+\z//ms;
51              
52 22         101 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         105 for my $char (split //, $str) {
59 140 50       196 if ($escaped) {
60 0         0 $buf .= $char;
61 0         0 $escaped = undef;
62 0         0 next;
63             }
64              
65 140 50       220 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       225 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         6 undef $buf;
82             }
83 2         4 next;
84             }
85              
86 138 100       192 if ($char eq '"') {
87 5 50       10 if ($single_quoted) {
88 0         0 $buf .= $char;
89 0         0 next;
90             }
91 5         9 $double_quoted = !$double_quoted;
92 5         12 next;
93             }
94              
95 133 50       184 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         182 $buf .= $char;
105             }
106 22 100       78 push @argv, $buf if defined $buf;
107              
108 22 100 33     131 if ($escaped || $single_quoted || $double_quoted) {
      66        
109 1         3 return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef
110             }
111              
112 21         70 \@argv;
113             }
114              
115             # return ($err, $res, $decoded_val)
116             sub _parse_raw_value {
117 66     66   149 my ($self, $val, $needs_res) = @_;
118              
119 66 100 100     476 if ($val =~ /\A!/ && $self->{enable_encoding}) {
    100 66        
    100 66        
    100 66        
    100 66        
120              
121 38 50       228 $val =~ s/!(\w+)(\s+)// or return ("Invalid syntax in encoded value");
122 38         119 my ($enc, $ws1) = ($1, $2);
123              
124 38 50       57 my $res; $res = [
  38         81  
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       83 $enc = "json" if $enc eq 'j';
135 38 100       89 $enc = "hex" if $enc eq 'h';
136 38 100       76 $enc = "expr" if $enc eq 'e';
137              
138 38 100       90 if ($self->{allow_encodings}) {
139             return ("Encoding '$enc' is not in ".
140             "allow_encodings list")
141 4 100       4 unless grep {$_ eq $enc} @{$self->{allow_encodings}};
  4         18  
  4         8  
142             }
143 37 100       90 if ($self->{disallow_encodings}) {
144             return ("Encoding '$enc' is in ".
145             "disallow_encodings list")
146 4 100       8 if grep {$_ eq $enc} @{$self->{disallow_encodings}};
  4         22  
  4         10  
147             }
148              
149 34 100 100     254 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       81 $val =~ /\A
155             (".*"|\[.*\]|\{.*\}|\S+)
156             (\s*)
157             (?: ([;#])(.*) )?
158             \z/x or return ("Invalid syntax in JSON-encoded value");
159 13         49 my $decode_res = $self->_decode_json($val);
160 13 100       39 return ($decode_res->[1]) unless $decode_res->[0] == 200;
161 12         46 return (undef, $res, $decode_res->[2]);
162              
163             } elsif ($enc eq 'path' || $enc eq 'paths') {
164              
165 4         6 my $decode_res = $self->_decode_path_or_paths($val, $enc);
166 4 50       18 return ($decode_res->[1]) unless $decode_res->[0] == 200;
167 4         14 return (undef, $res, $decode_res->[2]);
168              
169             } elsif ($enc eq 'hex') {
170              
171 2 50       13 $val =~ /\A
172             ([0-9A-Fa-f]*)
173             (\s*)
174             (?: ([;#])(.*) )?
175             \z/x or return ("Invalid syntax in hex-encoded value");
176 2         16 my $decode_res = $self->_decode_hex($1);
177 2 50       8 return ($decode_res->[1]) unless $decode_res->[0] == 200;
178 2         10 return (undef, $res, $decode_res->[2]);
179              
180             } elsif ($enc eq 'base64') {
181              
182 2 50       15 $val =~ m!\A
183             ([A-Za-z0-9+/]*=*)
184             (\s*)
185             (?: ([;#])(.*) )?
186             \z!x or return ("Invalid syntax in base64-encoded value");
187 2         16 my $decode_res = $self->_decode_base64($1);
188 2 50       9 return ($decode_res->[1]) unless $decode_res->[0] == 200;
189 2         10 return (undef, $res, $decode_res->[2]);
190              
191             } elsif ($enc eq 'none') {
192              
193 5         15 return (undef, $res, $val);
194              
195             } elsif ($enc eq 'expr') {
196              
197             return ("expr is not allowed (enable_expr=0)")
198 7 100       23 unless $self->{enable_expr};
199             # XXX imperfect regex, expression can't contain # and ; because it
200             # will be assumed as comment
201 6 50       48 $val =~ m!\A
202             ((?:[^#;])+?)
203             (\s*)
204             (?: ([;#])(.*) )?
205             \z!x or return ("Invalid syntax in expr-encoded value");
206 6         33 my $decode_res = $self->_decode_expr($1);
207 6 100       26 return ($decode_res->[1]) unless $decode_res->[0] == 200;
208 5         26 return (undef, $res, $decode_res->[2]);
209              
210             } else {
211              
212 1         7 return ("unknown encoding '$enc'");
213              
214             }
215              
216             } elsif ($val =~ /\A"/ && $self->{enable_quoting}) {
217              
218 11 50       125 $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       24 my $res; $res = [
  11         22  
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         64 my $decode_res = $self->_decode_json(qq("$1"));
236 11 50       32 return ($decode_res->[1]) unless $decode_res->[0] == 200;
237 11         44 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       34 $val =~ /\A
244             \[(.*)\]
245             (?:
246             (\s*)
247             ([#;])(.*)
248             )?
249             \z/x or return ("Invalid syntax in bracketed array value");
250 6 50       14 my $res; $res = [
  6         11  
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         28 my $decode_res = $self->_decode_json("[$1]");
259 6 50       17 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       22 $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         7  
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       19 return ($decode_res->[1]) unless $decode_res->[0] == 200;
283 2         9 return (undef, $res, $decode_res->[2]);
284              
285             } elsif ($val =~ /\A~/ && $self->{enable_tilde}) {
286              
287 1 50       5 $val =~ /\A
288             ~(.*)
289             (\s*)
290             (?: ([;#])(.*) )?
291             \z/x or return ("Invalid syntax in path value");
292 1 50       2 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         7 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       89 $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         17  
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         32 return (undef, $res, $1);
321              
322             }
323             # should not be reached
324             }
325              
326             sub _get_my_user_name {
327 1 50   1   854 if ($^O eq 'MSWin32') {
328 0         0 return $ENV{USERNAME};
329             } else {
330 1 50       4 return $ENV{USER} if $ENV{USER};
331 1         2 my @pw;
332 1         2 eval { @pw = getpwuid($>) };
  1         667  
333 1 50       9 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       11 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       3 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       51 if ($name eq getpwuid($<)) {
368 1         4 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   174 my ($self, $val) = @_;
383 60         106 state $json = do {
384 3 50       11 if (eval { require Cpanel::JSON::XS; 1 }) {
  3         24  
  3         11  
385 3         48 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         90 my $res;
392 60         91 eval { $res = $json->decode($val) };
  60         784  
393 60 100       153 if ($@) {
394 1         6 return [500, "Invalid JSON: $@"];
395             } else {
396 59         177 return [200, "OK", $res];
397             }
398             }
399              
400             sub _decode_path_or_paths {
401 5     5   10 my ($self, $val, $which) = @_;
402              
403 5 100       10 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         10 $val =~ s!(?<=.)/\z!!;
416              
417 5 100       8 if ($which eq 'path') {
418 2         6 return [200, "OK", $val];
419             } else {
420 3         179 return [200, "OK", [glob $val]];
421             }
422             }
423              
424             sub _decode_hex {
425 2     2   8 my ($self, $val) = @_;
426 2         14 [200, "OK", pack("H*", $val)];
427             }
428              
429             sub _decode_base64 {
430 2     2   8 my ($self, $val) = @_;
431 2         647 require MIME::Base64;
432 2         884 [200, "OK", MIME::Base64::decode_base64($val)];
433             }
434              
435             sub _decode_expr {
436 6     6   1257 require Config::IOD::Expr;
437              
438 6         26 my ($self, $val) = @_;
439 3     3   21 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
  3         5  
  3         1352  
440 6         37 local *{"Config::IOD::Expr::_Compiled::val"} = sub {
441 2     2   3 my $arg = shift;
442 2 100       9 if ($arg =~ /(.+)\.(.+)/) {
443 1         10 return $self->{_res}{$1}{$2};
444             } else {
445 1         9 return $self->{_res}{ $self->{_cur_section} }{$arg};
446             }
447 6         26 };
448 6         19 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   59 my ($self, $msg) = @_;
463             die join(
464             "",
465 22 100       49 @{ $self->{_include_stack} } ? "$self->{_include_stack}[0] " : "",
  22         445  
466             "line $self->{_linum}: ",
467             $msg
468             );
469             }
470              
471             sub _push_include_stack {
472 45     45   488 require Cwd;
473              
474 45         127 my ($self, $path) = @_;
475              
476             # included file's path is based on the main (topmost) file
477 45 100       66 if (@{ $self->{_include_stack} }) {
  45         134  
478 6         21 require File::Spec;
479             my ($vol, $dir, $file) =
480 6         128 File::Spec->splitpath($self->{_include_stack}[-1]);
481 6         199 $path = File::Spec->rel2abs($path, File::Spec->catpath($vol, $dir));
482             }
483              
484 45 50       2867 my $abs_path = Cwd::abs_path($path) or return [400, "Invalid path name"];
485             return [409, "Recursive", $abs_path]
486 45 100       135 if grep { $_ eq $abs_path } @{ $self->{_include_stack} };
  7         26  
  45         198  
487 44         113 push @{ $self->{_include_stack} }, $abs_path;
  44         139  
488 44         161 return [200, "OK", $abs_path];
489             }
490              
491             sub _pop_include_stack {
492 32     32   63 my $self = shift;
493              
494             die "BUG: Overpopped _pop_include_stack"
495 32 50       107 unless @{$self->{_include_stack}};
  32         99  
496 32         45 pop @{ $self->{_include_stack} };
  32         84  
497             }
498              
499             sub _init_read {
500 64     64   130 my $self = shift;
501              
502 64         240 $self->{_include_stack} = [];
503              
504             # set expr variables
505             {
506 64 100       134 last unless $self->{enable_expr};
  64         265  
507 3     3   19 no strict 'refs'; ## no critic: TestingAndDebugging::ProhibitNoStrict
  3         4  
  3         829  
508 41         84 my $pkg = \%{"Config::IOD::Expr::_Compiled::"};
  41         611  
509 41         181 undef ${"Config::IOD::Expr::_Compiled::$_"} for keys %$pkg;
  115         350  
510 41         118 my $vars = $self->{expr_vars};
511 41         194 ${"Config::IOD::Expr::_Compiled::$_"} = $vars->{$_} for keys %$vars;
  117         355  
512             }
513             }
514              
515             sub _read_file {
516 72     72   312 my ($self, $filename) = @_;
517 72 100       4365 open my $fh, "<", $filename
518             or die "Can't open file '$filename': $!";
519 71     1   1315 binmode($fh, ":encoding(utf8)");
  1         7  
  1         2  
  1         15  
520 71         15433 local $/;
521 71         4305 my $res = scalar <$fh>;
522 71         2433 close $fh;
523 71         759 $res;
524             }
525              
526             sub read_file {
527 39     39 1 124998 my $self = shift;
528 39         107 my $filename = shift;
529 39         261 $self->_init_read;
530 39         174 my $res = $self->_push_include_stack($filename);
531 39 50       172 die "Can't read '$filename': $res->[1]" unless $res->[0] == 200;
532 39         166 $res =
533             $self->_read_string($self->_read_file($filename), @_);
534 28         135 $self->_pop_include_stack;
535 28         85 $res;
536             }
537              
538             sub read_string {
539 25     25 1 45 my $self = shift;
540 25         110 $self->_init_read;
541 25         81 $self->_read_string(@_);
542             }
543              
544             1;
545             # ABSTRACT: Base class for Config::IOD and Config::IOD::Reader
546              
547             __END__