File Coverage

blib/lib/Crypt/SecretBuffer/INI.pm
Criterion Covered Total %
statement 104 126 82.5
branch 73 102 71.5
condition 17 23 73.9
subroutine 16 16 100.0
pod 10 10 100.0
total 220 277 79.4


line stmt bran cond sub pod time code
1             package Crypt::SecretBuffer::INI;
2             # VERSION
3             # ABSTRACT: Parse INI format from a SecretBuffer
4             $Crypt::SecretBuffer::INI::VERSION = '0.020';
5 1     1   573 use strict;
  1         2  
  1         50  
6 1     1   4 use warnings;
  1         3  
  1         39  
7 1     1   3 use Carp;
  1         2  
  1         93  
8 1     1   5 use Crypt::SecretBuffer qw/ secret MATCH_NEGATE MATCH_MULTI ISO8859_1 /;
  1         1  
  1         6  
9              
10              
11             sub new {
12 6     6 1 218699 my $self= bless {
13             comment_delim => qr/[;#]/,
14             key_value_delim => '=',
15             section_delim => undef,
16             trim_chars => qr/[ \t]+/,
17             inline_comments => !!0,
18             bare_keys => !!0,
19             field_config => [],
20             }, shift;
21 6         35 while (@_) {
22 6         28 my ($attr, $val)= splice(@_, 0, 2);
23 6         26 $self->$attr($val);
24             }
25 6         20 $self;
26             }
27              
28              
29             sub key_value_delim {
30             @_ > 1? ($_[0]{key_value_delim}= $_[1]) : $_[0]{key_value_delim}
31 53 50   53 1 247 }
32             sub bare_keys {
33             @_ > 1? ($_[0]{bare_keys}= !!$_[1]) : $_[0]{bare_keys}
34 1 50   1 1 5 }
35             sub trim_chars {
36             @_ > 1? ($_[0]{trim_chars}= $_[1]) : $_[0]{trim_chars}
37 44 50   44 1 119 }
38             sub section_delim {
39             @_ > 1? ($_[0]{section_delim}= $_[1]) : $_[0]{section_delim}
40 11 100   11 1 34 }
41             sub comment_delim {
42             @_ > 1? ($_[0]{comment_delim}= $_[1]) : $_[0]{comment_delim}
43 44 50   44 1 95 }
44             sub inline_comments {
45             @_ > 1? ($_[0]{inline_comments}= $_[1]) : $_[0]{inline_comments}
46 52 100   52 1 154 }
47              
48              
49             sub field_config {
50             @_ > 1? ($_[0]{field_config}= _coerce_field_rules($_[1])) : $_[0]{field_config}
51 22 100   22 1 71 }
52              
53             sub _coerce_field_rules {
54 4     4   6 my $rule_spec= shift;
55 4 50       18 ref $rule_spec eq 'ARRAY'
56             or croak "field rules must be an arrayref";
57 4         5 my @rules;
58 4         16 for (my $i= 0; $i < @$rule_spec; $i++) {
59 8         11 my $rule= $rule_spec->[$i];
60             # scalar or regexpref are treated as keys
61 8 50 66     23 if (!ref $rule or ref $rule eq 'Regexp') {
    0          
62 8         13 my $v= $rule_spec->[++$i];
63 8 100       19 if (ref $v eq 'ARRAY') {
    50          
    0          
64 1         5 $rule= { section => $rule, rules => $v };
65             } elsif (ref $v eq 'HASH') {
66 7         22 $rule= { key => $rule, %$v };
67             } elsif (defined $v) {
68 0         0 $rule= { key => $rule, flags => $v };
69             } else {
70 0         0 croak "Value paired with '$rule' should be arrayref, hashref, or scalar";
71             }
72             }
73             # hashrefs remain as-is
74             elsif ($rule ne 'HASH') {
75 0         0 croak "Expected scalar, Regexp, or hashref at '$rule'";
76             }
77             $rule->{rules}= _coerce_field_rules($rule->{rules})
78 8 100       25 if ref $rule->{rules} eq 'ARRAY';
79 8         19 push @rules, $rule;
80             }
81 4         14 return \@rules;
82             }
83             sub _find_field_rule {
84 22     22   49 my ($self, $rules, $section, $key)= @_;
85 22         38 for (@$rules) {
86 29 100 66     65 if (defined $_->{key}) {
    100          
87             # It's a rule for keys. Return it if the key matches.
88 23 100       115 return $_ if ref $_->{key} eq 'Regexp'? $key =~ $_->{key} : $key eq $_->{key};
    100          
89             }
90             elsif (defined $_->{section} && defined $section) {
91             # It's a rule for sections. Matching the section name is a bit complex because
92             # it can match in ful, or in part, and if the user defines a section hierarchy
93             # separator then we need to determine how much of the section name to pass to the
94             # recursive call.
95 5         11 my $sep= $self->section_delim;
96 5         7 my $key_rule;
97 5 50 66     62 if (ref $_->{section} eq 'Regexp') {
    100          
    100          
98 0 0       0 if ($section =~ $_->{section}) {
99 0         0 my $subsection;
100 0 0       0 if (defined $sep) {
101             # This gets complicated. The regex matched the whole section name, but the
102             # user maybe intended it to only match some upper portion of the section
103             # hierarchy.
104 0 0       0 $sep= qr/\Q$sep\E/ unless ref $sep eq 'Regexp';
105 0         0 while ($section =~ /$sep/g) {
106 0 0       0 if (substr($section, 0, $-[0]) =~ $_->{section}) {
107 0         0 $subsection= substr($section, $+[0]);
108             }
109             }
110 0         0 my $rule= $self->_find_field_rule($_->{rules}, $section, $key);
111             } else {
112 0         0 $subsection= $section;
113             }
114 0         0 $key_rule= $self->_find_field_rule($_->{rules}, $subsection, $key);
115             }
116             } elsif ($section eq $_->{section}) {
117 2         9 $key_rule= $self->_find_field_rule($_->{rules}, undef, $key);
118             } elsif (defined $sep
119             && $_->{section} eq substr($section, 0, length $_->{section})
120             ) {
121 1         815 my ($remainder, $subsection)= split $sep, substr($section, length $_->{section}), 2;
122             # It was only a match of a hierarchy if the separator matched immediately after
123             # the length of $_->{section}.
124 1 50       9 $key_rule= $self->_find_field_rule($_->{rules}, $subsection, $key)
125             if !length $remainder;
126             }
127 5 100       14 return $key_rule if defined $key_rule;
128             }
129             }
130 13         19 return undef;
131             }
132              
133              
134             sub parse_next {
135 44     44 1 14001 my ($self, $span)= @_;
136 44         92 my ($trim_chars, $comment_delim)= ($self->trim_chars, $self->comment_delim);
137 44         59 my %result;
138 44   100     181 while ($span->len && !keys %result) {
139 40         278 my $line= $span->parse(qr/[^\n]+/);
140 40 50       289 if (!$span->parse("\n")) {
141 0         0 $result{error}= 'No newline on end of file';
142             }
143 40         148 $line->rtrim("\r");
144 40 100       180 if ($line->parse('[')) {
    100          
145 12         83 my $header= $line->parse(qr/[^]]+/)->trim($trim_chars);
146 12 50       55 if (!$line->parse(']')) {
147 0         0 $result{error}= "Missing ']' in section header";
148 0         0 $header->pos($header->pos-1);
149 0         0 $result{context}= $header;
150             } else {
151 12         21 $result{section}= $header;
152 12         33 $line->ltrim($trim_chars);
153             }
154             }
155             elsif (!$line->starts_with($comment_delim)) {
156 26         56 my $key= $line->parse($self->key_value_delim, MATCH_NEGATE|MATCH_MULTI);
157             # Make sure key delimiter was found before comment, if inline_comments allowed
158 26 50 66     59 if ($self->inline_comments && (my $comment_start= $key->scan($comment_delim))) {
159 0         0 $key->lim($comment_start->pos);
160 0         0 $line->pos($comment_start->pos);
161             }
162 26 100       45 if ($line->parse($self->key_value_delim)) {
    50          
163 25         109 $result{key}= $key->trim($trim_chars);
164             # TODO: handle optional quoting here
165 25 100       39 if ($self->inline_comments) {
166 2         13 $result{value}= $line->parse($comment_delim, MATCH_NEGATE|MATCH_MULTI)->trim($trim_chars);
167             } else {
168 23         105 $result{value}= $line->new->trim($trim_chars);
169 23         70 $line->pos($line->lim);
170             }
171             } elsif ($self->bare_keys) {
172 0         0 $result{key}= $key->trim($trim_chars);
173             } else {
174 1         5 $result{error}= 'Line lacks delimiter "'.$self->key_value_delim.'"';
175 1         4 $result{context}= $key;
176             }
177             }
178 40 100       275 if ($line->parse($comment_delim)) {
    50          
179 4         31 $result{comment}= $line->trim($trim_chars);
180             } elsif ($line->len) {
181 0         0 $result{error}= 'extra text encountered before end of line';
182             }
183             }
184 44 100       190 return keys %result? \%result : undef;
185             }
186              
187              
188             sub parse {
189 4     4 1 30 my ($self, $buf_or_span)= @_;
190 4 100       29 my $span= $buf_or_span->can('subspan')? $buf_or_span : $buf_or_span->span;
191 4         8 my ($node, $section, $key, $value);
192 4         11 my $sep= $self->section_delim;
193 4 100 66     87 $sep= qr/\Q$sep\E/ if defined $sep && ref $sep ne 'Regexp';
194 4 100       12 my $root= defined $sep? {} : [];
195 4         12 while (my $tokens= $self->parse_next($span)) {
196             croak $tokens->{error}
197 29 50       60 if defined $tokens->{error};
198 29 100       55 if (defined $tokens->{section}) {
199 10         44 $tokens->{section}->copy_to($section= '');
200 10 100       40 if (defined $sep) {
201 8         10 $node= $root;
202 8         59 for (split $sep, $section) {
203             croak("conflict between section name and pre-existing key of parent section")
204 13 50 66     43 if defined $node->{$_} && ref $node->{$_} ne 'HASH';
205 13   100     44 $node= ($node->{$_} ||= {});
206             }
207             } else {
208 2         6 push @$root, $section, ($node= {});
209             }
210             }
211 29 100       90 if (defined $tokens->{key}) {
212 19 100       36 if (!defined $node) {
213 3 100       8 if (defined $sep) {
214 1         2 $node= $root;
215             } else {
216 2         6 push @$root, '', ($node= {});
217             }
218             }
219 19         85 $tokens->{key}->copy_to($key= '');
220 19 50       43 if (!$tokens->{value}) {
221 0         0 $value= undef;
222             } else {
223 19         98 my $rule= $self->_find_field_rule($self->field_config, $section, $key);
224             $tokens->{value}->encoding($rule->{encoding})
225 19 100       52 if defined $rule->{encoding};
226 19 100       37 if ($rule->{secret}) {
227 7         54 $tokens->{value}->copy_to(($value= secret), encoding => ISO8859_1);
228             } else {
229 12         46 $tokens->{value}->copy_to($value= '');
230             }
231             }
232 19         94 $node->{$key}= $value;
233             }
234             }
235 4         25 return $root;
236             }
237              
238             # avoid depending on namespace::clean
239             delete @{Crypt::SecretBuffer::INI::}{qw(
240             carp confess croak secret MATCH_NEGATE MATCH_MULTI ISO8859_1
241             )};
242              
243             1;
244              
245             __END__