| 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__ |