File Coverage

blib/lib/Config/Scoped.pm
Criterion Covered Total %
statement 274 281 97.5
branch 130 186 69.8
condition 45 71 63.3
subroutine 32 32 100.0
pod 10 10 100.0
total 491 580 84.6


line stmt bran cond sub pod time code
1             # Copyright (c) 2004-2012 by Karl Gaissmaier, Ulm University, Germany
2             #
3             # This library is free software; you can redistribute it and/or modify
4             # it under the same terms as Perl itself.
5             #
6              
7              
8             # for documentation see Config::Scoped.pod
9              
10             package Config::Scoped;
11              
12 17     17   454584 use strict;
  17         43  
  17         666  
13 17     17   83 use warnings;
  17         36  
  17         595  
14              
15 17     17   23356 use Storable qw(dclone lock_nstore lock_retrieve);
  17         97592  
  17         1889  
16 17     17   354 use Carp;
  17         38  
  17         1274  
17 17     17   19651 use Safe;
  17         889803  
  17         1473  
18 17     17   199 use Digest::MD5 qw(md5_base64);
  17         34  
  17         1658  
19 17     17   107 use File::Basename qw(fileparse);
  17         32  
  17         1688  
20 17     17   104 use File::Spec;
  17         33  
  17         470  
21 17     17   17899 use Config::Scoped::Error;
  17         69  
  17         219  
22 17     17   1185 use base 'Parse::RecDescent';
  17         37  
  17         39490  
23              
24             our $VERSION = '0.22';
25              
26             my $grammar;
27             {
28             local $/;
29             $grammar = ;
30             close DATA;
31             }
32              
33             my @state_hashes = qw(config params macros warnings includes);
34              
35             sub new {
36 51     51 1 662 my $class = shift;
37              
38 51 100       535 Config::Scoped::Error->throw(
39             -text => Carp::shortmess("odd number of arguments,") )
40             if @_ % 2;
41              
42 50         214 my %args = @_;
43              
44             ##############################################
45             # Delayed compilation of grammar in method parse()
46             #
47 50         130 my $empty_grammar = '';
48 50         147 $args{compiled} = undef;
49              
50             ##############################################
51             # create the parser object, delayed grammar
52             #
53 50 50       540 my $thisparser = $class->SUPER::new($empty_grammar)
54             or Config::Scoped::Error->throw(
55             -text => "Can't create a '$class' parser," );
56              
57             ##############################################
58             # store the args in the P::RD object below 'local'
59             # don't use deep copy since we use always one and
60             # only one global config hash
61             #
62 50         41955 $thisparser->{local} = {%args};
63              
64             # frequent typos, be polite
65 50   66     438 $thisparser->{local}{warnings} ||= $thisparser->{local}{warning};
66 50   66     370 $thisparser->{local}{lc} ||= $thisparser->{local}{lowercase};
67 50   66     345 $thisparser->{local}{safe} ||= $thisparser->{local}{Safe};
68 50   66     284 $thisparser->{local}{file} ||= $thisparser->{local}{File};
69              
70             ##############################################
71             # validate and munge the 'file' param
72             #
73             # a cfg_file isn't necessary, the parse method can be feeded
74             # with a plain text string
75 50 100       202 if ( my $cfg_file = $thisparser->{local}{file} ) {
76              
77 14 100       237 Config::Scoped::Error->throw(
78             -text => Carp::shortmess("can't use filehandle as cfg file") )
79             if ref $cfg_file;
80              
81             # retrieve the dir part, later on needed for relative include files
82 13 50       489 my ( undef, $cfg_dir ) = fileparse($cfg_file)
83             or Config::Scoped::Error->throw(
84             -text => "error in fileparse",
85             -file => $thisparser->_get_file(%args),
86             -line => $thisparser->_get_line(%args),
87             );
88              
89 13 50       479 $cfg_file = File::Spec->rel2abs($cfg_file)
90             or Config::Scoped::Error->throw(
91             -text => "error in rel2abs",
92             -file => $thisparser->_get_file(%args),
93             -line => $thisparser->_get_line(%args),
94             );
95              
96 13         98 $thisparser->{local}{cfg_file} = $cfg_file;
97 13         50 $thisparser->{local}{cfg_dir} = $cfg_dir;
98             }
99              
100             else {
101              
102             # no cfg_file defined, use _STRING and cwd
103 36         138 $thisparser->{local}{cfg_file} = '_STRING';
104 36         2125 $thisparser->{local}{cfg_dir} =
105             File::Spec->rel2abs( File::Spec->curdir );
106             }
107              
108             ##############################################
109             # check for warnings
110             #
111             # set the default to all on
112 49 100       334 $thisparser->{local}{warnings} = { all => 'on' }
113             unless $thisparser->{local}{warnings};
114              
115             # allow the simple form: 'warnings' => 'on/off'
116 49 100       327 if ( ref $thisparser->{local}{warnings} ne 'HASH' ) {
117 2 50       13 $thisparser->{local}{warnings} = { all => 'on' }
118             if $thisparser->{local}{warnings} =~ m/on/i;
119 2 50       20 $thisparser->{local}{warnings} = { all => 'off' }
120             if $thisparser->{local}{warnings} =~ m/off/i;
121             }
122              
123             # store the warnings in a normalized form
124 49         97 foreach my $name ( keys %{ $thisparser->{local}{warnings} } ) {
  49         319  
125 52         190 my $switch = delete $thisparser->{local}{warnings}{$name};
126 52         288 $thisparser->_set_warnings(
127             name => $name,
128             switch => $switch,
129             );
130             }
131              
132             ##############################################
133             # preset the state hashes
134             #
135             # use empty state_hashes if not defined
136 49         172 foreach my $hash_name (@state_hashes) {
137 241   100     1156 $thisparser->{local}{$hash_name} ||= {};
138              
139             # be defensive
140 241 100       1073 Config::Scoped::Error->throw(
141             -text => Carp::shortmess("$hash_name is no hash ref") )
142             unless ref $thisparser->{local}{$hash_name} eq 'HASH';
143             }
144              
145             # install/create Safe compartment for perl_code
146 48         146 my $compartment = $thisparser->{local}{safe};
147 48 100       192 if ( $thisparser->{local}{safe} ) {
148 7 100       216 Config::Scoped::Error->throw(
149             -text => Carp::shortmess("can't find method 'reval' on compartment")
150             )
151             unless UNIVERSAL::can( $thisparser->{local}{safe}, 'reval' );
152             }
153             else {
154 41 50       412 $thisparser->{local}{safe} = Safe->new
155             or Config::Scoped::Error->throw(
156             -text => "can't create a Safe compartment!" );
157             }
158              
159 47         57175 return $thisparser;
160             }
161              
162             sub parse {
163 46     46 1 104656 my $thisparser = shift;
164              
165 46 100       487 Config::Scoped::Error->throw(
166             -text => Carp::shortmess("odd number of arguments,") )
167             if @_ % 2;
168              
169 45 100       283 unless (defined $thisparser->{local}{compiled} ){
170 44         336 $thisparser->Replace($grammar);
171 44         10455423 $thisparser->{local}{compiled} = 1;
172             }
173              
174 45         416 my %args = @_;
175              
176 45         152 my $cfg_text = $args{text};
177              
178 45 100       302 unless ( defined $cfg_text ) {
179 14 50       94 my $cfg_file = $thisparser->{local}{cfg_file}
180             or Config::Scoped::Error->throw(
181             -text => Carp::shortmess("no cfg_file defined"),
182             -file => $thisparser->_get_file(%args),
183             -line => $thisparser->_get_line(%args),
184             );
185              
186 14 100       91 Config::Scoped::Error->throw( -text => "no text to parse defined" )
187             if $cfg_file eq '_STRING';
188              
189             # slurp the cfg file
190 13         111 $cfg_text = $thisparser->_get_cfg_text( %args, file => $cfg_file );
191              
192 12 100       55 Config::Scoped::Error->throw(
193             -file => $thisparser->_get_file(%args),
194             -line => $thisparser->_get_line(%args),
195             -text => "'$cfg_file' is empty"
196             )
197             unless $cfg_text;
198              
199             # calculate the message digest and remember this cfg text in includes
200 11         115 my $digest = md5_base64($cfg_text);
201              
202 11 100       84 Config::Scoped::Error->throw(
203             -file => $thisparser->_get_file(%args),
204             -line => $thisparser->_get_line(%args),
205             -text => "include loop for '$cfg_file' encountered",
206             )
207             if $thisparser->{local}{includes}{$digest};
208              
209 10         89 $thisparser->{local}{includes}{$digest} = $cfg_file;
210             }
211              
212             # call the P::RD with the startrule of the grammar
213 41         1112 $thisparser->config($cfg_text);
214              
215             ##############################################
216             # no declarations but parameters in scope?
217             #
218             # copy them to an automatically generated _GLOBAL hash
219             # first use some shortcuts
220 37         243344 my $params = $thisparser->{local}{params};
221 37         132 my $config = $thisparser->{local}{config};
222              
223             # all $config keys other than _GLOBAL are real declarations
224 37         246 my @declarations = grep !/^_GLOBAL$/, keys %$config;
225              
226             # no declarations but parameters in global scope
227 37 100 100     394 if ( !@declarations && %$params ) {
228              
229             # the overall parent scope overrides scopes from include files
230 13         386 $config->{_GLOBAL} = dclone $params;
231             }
232             else {
233              
234             # perhaps a prior parse for an include file filled this slot
235 24         105 delete $config->{_GLOBAL};
236             }
237              
238 37         471 return $thisparser->{local}{config};
239             }
240              
241             sub warnings_on {
242 224     224 1 1261 my $thisparser = shift;
243              
244 224 50       961 Config::Scoped::Error->throw(
245             -text => Carp::shortmess("odd number of arguments,") )
246             if @_ % 2;
247              
248 224         680 my %args = @_;
249              
250 224 50       772 Config::Scoped::Error->throw(
251             -text => Carp::shortmess("missing parameters") )
252             unless defined $args{name};
253              
254 224         526 my $name = $args{name};
255 224         542 my $warnings = $thisparser->{local}{warnings};
256              
257 224         1010 $name = $thisparser->_trim_warnings($name);
258              
259 224 100 100     1585 return undef if exists $warnings->{$name} && $warnings->{$name} eq 'off';
260 143 100 66     748 return 1 if exists $warnings->{$name} && $warnings->{$name} eq 'on';
261              
262             # use 'all'
263 140 100 100     1171 return undef if exists $warnings->{all} && $warnings->{all} eq 'off';
264 134 100 66     1164 return 1 if exists $warnings->{all} && $warnings->{all} eq 'on';
265              
266             # hmm, name and all not defined, defaults to on
267 29         121 return 1;
268             }
269              
270             sub set_warnings {
271 64     64 1 549 my $thisparser = shift;
272              
273 64 50       322 Config::Scoped::Error->throw(
274             -text => Carp::shortmess("odd number of arguments,") )
275             if @_ % 2;
276              
277 64         341 my %args = @_;
278              
279 64 50       255 Config::Scoped::Error->throw(
280             -text => Carp::shortmess("no warnings switch (on/off) defined") )
281             unless defined $args{switch};
282              
283 64         169 my $warnings = $thisparser->{local}{warnings};
284 64   100     224 my $name = $args{name} || 'all';
285 64         128 my $switch = $args{switch};
286              
287 64         846 $name = $thisparser->_trim_warnings($name);
288              
289             # trim the switch, convert to lowercase
290 64         200 $switch = lc($switch);
291              
292 64 100       212 if ( $name eq 'all' ) {
293              
294             # reset the hash
295 38         78 %{$warnings} = ();
  38         101  
296 38         162 $warnings->{all} = $args{switch};
297             }
298             else {
299              
300             # override the key, key is 'macro', 'declaration', 'parameter', ...
301 26         86 $warnings->{$name} = $args{switch};
302             }
303              
304 64         504 return 1;
305             }
306              
307             # just a wrapper for the same method without leading _
308             # this method is called in the grammar file whereas the set_warnings
309             # may be overriden by the application
310             sub _set_warnings {
311 61     61   90067 my $thisparser = shift;
312 61         290 $thisparser->set_warnings(@_);
313             }
314              
315             # shortcuts allowed, less spelling errors
316             sub _trim_warnings {
317 288     288   577 my ( $thisparser, $name ) = @_;
318              
319             # trim the names
320 288 100       1229 return 'declaration' if $name =~ /^decl/i;
321 253 100       8449 return 'parameter' if $name =~ /^param/i;
322 93 100       378 return 'macro' if $name =~ /^mac/i;
323 82 100       715 return 'permissions' if $name =~ /^perm/i;
324 48 100       235 return 'digests' if $name =~ /^dig/i;
325 46         114 return $name;
326             }
327              
328             sub store_cache {
329 1     1 1 2 my $thisparser = shift;
330              
331 1 50       6 Config::Scoped::Error->throw(
332             -text => Carp::shortmess("odd number of arguments,") )
333             if @_ % 2;
334              
335 1         3 my %args = @_;
336              
337 1         2 my $cache_file = $args{cache};
338              
339 1 50       5 unless ($cache_file) {
340 1 50       6 my $cfg_file = $thisparser->{local}{cfg_file}
341             or Config::Scoped::Error->throw(
342             -text => Carp::shortmess("no cache_file and no cfg_file defined") );
343              
344 1 50       5 Config::Scoped::Error->throw( -text =>
345             Carp::shortmess("parameter 'cache' needed for parsed strings") )
346             if $cfg_file eq '_STRING';
347              
348 1         4 $cache_file = $cfg_file . '.dump';
349             }
350              
351 1         5 my $cfg_hash = {
352             includes => $thisparser->{local}{includes},
353             config => $thisparser->{local}{config},
354             };
355              
356 1         2 my $result = eval { lock_nstore( $cfg_hash, $cache_file ); };
  1         7  
357              
358 1 50       4289 Config::Scoped::Error->throw( -text => Carp::shortmess($@) ) if $@;
359              
360 1 50       11 Config::Scoped::Error->throw(
361             -text => Carp::shortmess("can't store the cfg hash to '$cache_file'") )
362             unless $result;
363             }
364              
365             sub retrieve_cache {
366 1     1 1 3 my $thisparser = shift;
367              
368 1 50       6 Config::Scoped::Error->throw(
369             -text => Carp::shortmess("odd number of arguments,") )
370             if @_ % 2;
371              
372 1         3 my %args = @_;
373              
374 1         4 my $cache_file = $args{cache};
375 1         5 $args{parent_file} = $cache_file; # for better error messages
376              
377 1 50       6 unless ($cache_file) {
378 1 50       7 my $cfg_file = $thisparser->{local}{cfg_file}
379             or Config::Scoped::Error->throw(
380             -text => Carp::shortmess("no cache_file and no cfg_file defined") );
381              
382 1 50       5 Config::Scoped::Error->throw(
383             -text => Carp::shortmess("cache not supported for strings") )
384             if $cfg_file eq '_STRING';
385              
386 1         4 $cache_file = $cfg_file . '.dump';
387             }
388              
389             Config::Scoped::Error::IO->throw(
390 1 50       35 -text => Carp::shortmess("Can't read the cfg_cache '$cache_file'") )
391             unless -r $cache_file;
392              
393             # check the permission and ownership, I know, it's no handle and of
394             # restricted usage
395 1 50       8 Config::Scoped::Error::Validate::Permissions->throw(
396             -text => Carp::shortmess(
397             "permissions_validate returned false for cache_file '$cache_file'")
398             )
399             unless $thisparser->permissions_validate( %args, file => $cache_file );
400              
401 1         3 my $cfg_cache = eval { lock_retrieve($cache_file); };
  1         5  
402              
403 1 50       135 Config::Scoped::Error->throw( -text => Carp::shortmess($@) ) if $@;
404              
405 1 50       4 Config::Scoped::Error->throw(
406             -text => Carp::shortmess( "cfg cache is empty", ) )
407             unless $cfg_cache;
408              
409             # warnings for digests enabled?
410 1 50       5 return $cfg_cache->{config}
411             unless $thisparser->warnings_on( %args, name => 'digests', );
412              
413             # check the include digests for modification
414 1         3 while ( my ( $digest, $file ) = each %{ $cfg_cache->{includes} } ) {
  3         13  
415              
416 2         9 my $text = $thisparser->_get_cfg_text( %args, file => $file, );
417              
418 2 50       18 if ( $digest ne md5_base64($text) ) {
419 0         0 Config::Scoped::Error->throw(
420             -text => Carp::shortmess(
421             "'$file' modified, can't use the cache '$cache_file',")
422             );
423             }
424             }
425              
426 1         9 return $cfg_cache->{config};
427             }
428              
429             # _include
430             #
431             # this method is called as an action in the INCLUDE grammar rule
432             # the current localized $thisparser->{local}... parameters are used and adjusted
433             # and a new P::RD parser with the same grammar is created and started
434             # for the include file.
435             # After that the parse in the parent cfg file is continued.
436              
437             # We don't change the $text and don't resync the linecounter in P::RD, since
438             # this would result in awfully wrong line numbers in error messages and
439             # we would still have no hint in which include file the error happened.
440             #
441             # The current scope, macro and warnings hash is used during include file parsing
442             # so the include file can use (or overwrite) the current parse state.
443             #
444             # The changed state during the include file parse is propagated to the
445             # parent parser state (except warnings). If this import isn't intended
446             # put the include # in a own block: { %include filename; }
447             #
448              
449             sub _include {
450 6     6   49713 my $thisparser = shift;
451 6         31 my %args = @_;
452              
453 6 50       41 Config::Scoped::Error->throw(
454             -file => $thisparser->_get_file(%args),
455             -line => $thisparser->_get_line(%args),
456             -text => Carp::shortmess("missing parameters"),
457             )
458             unless defined $args{file};
459              
460 6         18 my $include_file = $args{file};
461 6         27 my $parent_cfg_file = $thisparser->{local}{cfg_file};
462 6         22 my $parent_cfg_dir = $thisparser->{local}{cfg_dir};
463              
464             # absolute path? else concat with parent cfg dir
465 6 50       106 unless ( File::Spec->file_name_is_absolute($include_file) ) {
466 6 50       193 $include_file = File::Spec->catfile( $parent_cfg_dir, $include_file )
467             or Config::Scoped::Error->throw(
468             -file => $parent_cfg_file,
469             -line => $thisparser->_get_line(%args),
470             -text => "error in catfile for '$include_file'"
471             );
472             }
473              
474             # Create a new parser for this include file parsing.
475             # Use the current parser states (perhaps already localized
476             # in a grammar { action }), and change some args for the new
477             # include parser creation.
478             #
479 6         97 my $clone_parser =
480             ( ref $thisparser )
481 6 50       21 ->new( %{ $thisparser->{local} }, file => $include_file )
482              
483             or Config::Scoped::Error->throw(
484             -file => $parent_cfg_file,
485             -line => $thisparser->_get_line(%args),
486             -text => "Internal error: Can't create a clone parser"
487             );
488              
489             # parse the include file (recursively) and return to the parent
490             # cfg parse. Loop includes are detected (via md5) and throws an exception.
491 6         64 return $clone_parser->parse(
492             parent_file => $parent_cfg_file, # for better error reporting
493             );
494             }
495              
496             # this method is called as an action in the MACRO rule in order
497             # to store the macro in the macros hash
498             sub _store_macro {
499 10     10   37687 my $thisparser = shift;
500 10         84 my %args = @_;
501              
502 10 50 33     111 Config::Scoped::Error->throw(
503             -text => Carp::shortmess("missing parameters") )
504             unless ( defined $args{name} && defined $args{value} );
505              
506             # macro validation, may be overwritten by the application
507 10         102 my $valid_macro = $thisparser->macro_validate(%args);
508              
509 10         275 return $thisparser->{local}{macros}{ $args{name} } = $valid_macro;
510             }
511              
512             sub macro_validate {
513 10     10 1 20 my $thisparser = shift;
514 10         34 my %args = @_;
515              
516 10 50 33     126 Config::Scoped::Error->throw(
517             -text => Carp::shortmess("missing parameters") )
518             unless ( defined $args{name} && defined $args{value} );
519              
520 10         26 my $name = $args{name};
521 10         23 my $value = $args{value};
522              
523             # warnings for macros enabled?
524 10 100       55 if ( $thisparser->warnings_on( name => 'macro', ) ) {
525 9 50       48 Config::Scoped::Error::Validate::Macro->throw(
526             -file => $thisparser->_get_file(%args),
527             -line => $thisparser->_get_line(%args),
528             -text => "macro redefinition for '$name"
529             )
530             if exists $thisparser->{local}{macros}{$name};
531             }
532              
533             # return unchanged, subclass methods may do it different
534 10         40 return $value;
535             }
536              
537             # macro expansion
538             sub _expand_macro {
539 93     93   154 my $thisparser = shift;
540 93         356 my %args = @_;
541              
542 93 50       282 Config::Scoped::Error->throw(
543             -text => Carp::shortmess("missing parameters") )
544             unless defined $args{value};
545              
546 93         174 my $value = $args{value};
547              
548 93         139 while ( my ( $macro, $defn ) = each %{ $thisparser->{local}{macros} } ) {
  122         699  
549 29         262 $value =~ s/\Q$macro\E/$defn/g;
550             }
551              
552             # a P::RD rule can't return undef, then the rule will fail
553 93 50       454 return defined $value ? $value : '';
554             }
555              
556             # parameter storage, called as action from within the grammar
557             sub _store_parameter {
558 152     152   595353 my $thisparser = shift;
559 152         739 my %args = @_;
560              
561 152 50 33     1538 Config::Scoped::Error->throw(
562             -text => Carp::shortmess("missing parameters") )
563             unless ( defined $args{value} && defined $args{name} );
564              
565 152 100       655 $args{name} = lc( $args{name} ) if $thisparser->{local}{lc};
566              
567             # parameter validation, may be overwritten by the application
568 152         1367 my $valid_value = $thisparser->parameter_validate(%args);
569              
570             # store the return value in the params hash
571 151         4364 return $thisparser->{local}{params}{ $args{name} } = $valid_value;
572             }
573              
574             sub parameter_validate {
575 152     152 1 273 my $thisparser = shift;
576 152         641 my %args = @_;
577              
578 152 50 33     1101 Config::Scoped::Error->throw(
579             -text => Carp::shortmess("missing parameters") )
580             unless ( defined $args{value} && defined $args{name} );
581              
582             # warnings for parameters enabled?
583 152 100       775 if ( $thisparser->warnings_on( name => 'parameter', ) ) {
584 89 100       440 Config::Scoped::Error::Validate::Parameter->throw(
585             -file => $thisparser->_get_file(%args),
586             -line => $thisparser->_get_line(%args),
587             -text => "parameter redefinition for '$args{name}'"
588             )
589             if exists $thisparser->{local}{params}{ $args{name} };
590             }
591              
592             # return unchanged, subclass methods may do it different
593 151         584 return $args{value};
594             }
595              
596             # declaration storage, called as action from within the grammar
597             sub _store_declaration {
598 34     34   82354 my $thisparser = shift;
599 34         141 my %args = @_;
600              
601 34 50 33     326 Config::Scoped::Error->throw(
602             -text => Carp::shortmess("missing parameters") )
603             unless ( defined $args{name} && defined $args{value} );
604              
605             {
606 34         61 local $_;
  34         89  
607 34 100       167 map { $_ = lc($_) } @{ $args{name} }
  3         8  
  1         4  
608             if $thisparser->{local}{lc};
609             }
610              
611             # convert declaration: foo bar ... baz { parameters }
612             # to the data structure
613             # $config->{foo}{bar}...{baz} = { parameters };
614 34         96 my $tail = $thisparser->{local}{config};
615              
616             # walking down the street ...
617 34         73 foreach my $name ( @{ $args{name} } ) {
  34         109  
618 44 100       203 $tail->{$name} = {} unless exists $tail->{$name};
619 44         141 $tail = $tail->{$name};
620             }
621              
622             # now we have baz = {}
623              
624             # application validation
625 34         191 my $valid_value = $thisparser->declaration_validate( %args, tail => $tail );
626              
627             # store the current scope in the last $config->{foo}...{baz} = $params
628             # use deep copy to break dependencies when config parameters
629             # get's changed in the application in different declarations
630 34         118 return %$tail = %{ dclone( $args{value} ) };
  34         2204  
631             }
632              
633             sub declaration_validate {
634 34     34 1 64 my $thisparser = shift;
635 34         124 my %args = @_;
636              
637 34 50 33     261 Config::Scoped::Error->throw(
638             -text => Carp::shortmess("missing parameters") )
639             unless ( defined $args{name} && defined $args{value} );
640              
641             # warnings for declarations enabled and 'tail' already set?
642 34 100       126 if ( $thisparser->warnings_on( name => 'declaration', ) ) {
643 33         172 Config::Scoped::Error::Validate::Declaration->throw(
644             -file => $thisparser->_get_file(%args),
645             -line => $thisparser->_get_line(%args),
646 0         0 -text => "declaration redefinition for '@{$args{name}}'"
647             )
648 33 50       52 if %{ $args{tail} };
649             }
650              
651             # return unchanged, subclass methods may do it different
652 34         185 return $args{value};
653             }
654              
655             sub permissions_validate {
656 16     16 1 42 my $thisparser = shift;
657 16         78 my %args = @_;
658              
659 16 50 66     97 Config::Scoped::Error->throw(
660             -text => Carp::shortmess("missing parameters") )
661             unless ( defined $args{handle} || defined $args{file} );
662              
663 16         101 my $warnings = $thisparser->{local}{warnings};
664              
665             # warnings for files enabled?
666 16 100       97 return 1
667             unless $thisparser->warnings_on(
668             name => 'permissions',
669             warnings => $warnings,
670             );
671              
672 1   33     5 my $fh = $args{handle} || $args{file};
673              
674             # mysteriously vaporized
675 1 50       18 Config::Scoped::Error::IO->throw(
676             -file => $thisparser->_get_file(%args),
677             -line => $thisparser->_get_line(%args),
678             -text => "'$args{file}' can't stat cfg file/handle: $!"
679             )
680             unless stat $fh;
681              
682 1         6 my ( $dev, $ino, $mode, $nlink, $uid, $gid ) = stat(_);
683              
684             # owner is not root and not real uid
685 1 50 33     20 Config::Scoped::Error::Validate::Permissions->throw(
686             -file => $thisparser->_get_file(%args),
687             -line => $thisparser->_get_line(%args),
688             -text => "'$args{file}' is unsafe: owner is not root and not real uid",
689             )
690             if $uid != 0 && $uid != $<;
691              
692 0 0       0 Config::Scoped::Error::Validate::Permissions->throw(
693             -file => $thisparser->_get_file(%args),
694             -line => $thisparser->_get_line(%args),
695             -text => "'$args{file}' is unsafe: writeable by group or others",
696             )
697             if $mode & 022;
698              
699 0         0 return 1;
700             }
701              
702             # handle quoted strings, expand macro's and interpolate backslash
703             # patterns like \t, \n, etc. Called as action from within the grammar.
704             sub _quotelike {
705 84     84   429928 my $thisparser = shift;
706 84         297 my %args = @_;
707              
708 84 50       415 Config::Scoped::Error->throw(
709             -text => Carp::shortmess("missing parameter") )
710             unless defined $args{value};
711              
712 84         163 my $value = $args{value};
713              
714             # accepts only '', "", <
715             # not q, qq, qx, qw, ..., s///, tr/// etc.
716 84         397 my %accept = ( single => 1, double => 1, '<<' => 1 );
717              
718             # see Text::Balanced::extract_quotelike() to understand this
719             # and of course Parse::RecDescent directive
720 84         188 my $quote_name = $value->[0];
721 84         195 my $quote_delim = substr( $value->[1], 0, 1 );
722 84         407 my $quote_text = $value->[2];
723              
724             # the quote_name isn't set with plain quotes, set it now
725 84 100       685 unless ($quote_name) {
726 69 100       232 $quote_name = 'double' if $quote_delim eq '"';
727 69 100       241 $quote_name = 'single' if $quote_delim eq "'";
728             }
729              
730             # let the rule fail if not an accepted quote name
731 84 50       298 return undef unless $accept{$quote_name};
732              
733             # backslash substitution in double quoted strings is
734             # done by reval() in the Safe compartment since
735             # it's possible to smuggle a subroutine call
736             # in a double quoted string.
737             #
738 84 100 100     795 $quote_text = $thisparser->_perl_code( expr => "\"$quote_text\"" )
739             unless $quote_name eq 'single' || $quote_delim eq "'";
740              
741             # macro expansion for double quoted constructs
742 84 100 100     587 $quote_text = $thisparser->_expand_macro( %args, value => $quote_text )
743             unless $quote_name eq 'single' || $quote_delim eq "'";
744              
745             # a P::RD rule can't return undef, then the rule would fail
746 84 50       2729 return defined $quote_text ? $quote_text : '';
747             }
748              
749             # slurp in the cfg files
750             sub _get_cfg_text {
751 15     15   45 my $thisparser = shift;
752 15         83 my %args = @_;
753              
754 15 50       75 Config::Scoped::Error->throw(
755             -text => Carp::shortmess("no cfg_file defined") )
756             unless defined $args{file};
757 15         41 my $cfg_file = $args{file};
758              
759 15         57 local *CFG;
760              
761             # open the cfg file
762 15 50       1287 Config::Scoped::Error::IO->throw(
763             -file => $thisparser->_get_file(%args),
764             -line => $thisparser->_get_line(%args),
765             -text => "Can't open cfg_file '$cfg_file': $!"
766             )
767             unless open( CFG, $cfg_file );
768              
769             # check the permission and ownership
770 15 50       118 Config::Scoped::Error::Validate::Permissions->throw(
771             -file => $thisparser->_get_file(%args),
772             -line => $thisparser->_get_line(%args),
773             -text => "permissions_validate returned false for cfg_file '$cfg_file'"
774             )
775             unless $thisparser->permissions_validate( %args, handle => \*CFG );
776              
777             # slurp the cfg_file, close the handle and return the text
778 14         774 my $cfg_text = join '', ;
779              
780 14 50       519 Config::Scoped::Error::IO->throw(
781             -file => $thisparser->_get_file(%args),
782             -line => $thisparser->_get_line(%args),
783             -text => "Can't close cfg_file '$cfg_file' : $!"
784             )
785             unless close CFG;
786              
787 14         108 return $cfg_text;
788             }
789              
790             # eval perlcode in Safe compartment, called as action from within the grammar.
791             sub _perl_code {
792 55     55   71419 my $thisparser = shift;
793 55         292 my %args = @_;
794              
795 55 50       311 Config::Scoped::Error->throw(
796             -text => Carp::shortmess("no expression to eval defined") )
797             unless defined $args{expr};
798              
799 55         126 my $expr = $args{expr};
800              
801             # macro expansion before code evaluation
802 55         299 $expr = $thisparser->_expand_macro( %args, value => $expr );
803              
804 55         174 my $compartment = $thisparser->{local}{safe};
805              
806             # eval in Safe compartment
807 55         343 my $result = $compartment->reval($expr);
808              
809             # adjust error message and rethrow
810 55 50 33     34024 if ( !defined $result && $@ ) {
811 0         0 chomp $@;
812 0         0 $@ .= "\n... (re)blessed and propagated via perl_code{}";
813              
814 0         0 Config::Scoped::Error::Parse->throw(
815             -file => $thisparser->_get_file(%args),
816             -line => $thisparser->_get_line(%args),
817             -text => $@,
818             );
819             }
820              
821             # a P::RD rule can't return undef, then the rule would fail
822 55 50       733 return defined $result ? $result : '';
823             }
824              
825             # used for well spotted error messages
826             sub _get_file {
827 4     4   10 my $thisparser = shift;
828 4         15 my %args = @_;
829 4   50     72 return $args{parent_file}
830             || $args{file}
831             || $thisparser->{local}{cfg_file}
832             || '?';
833             }
834              
835             # used for well spotted error messages
836             sub _get_line {
837 4     4   10 my $thisparser = shift;
838 4         11 my %args = @_;
839 4   100     136 return $args{line} || $thisparser->{local}{line} || 0;
840             }
841              
842             1;
843              
844             __DATA__