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.021';
5 1     1   465 use strict;
  1         2  
  1         28  
6 1     1   4 use warnings;
  1         1  
  1         31  
7 1     1   3 use Carp;
  1         1  
  1         47  
8 1     1   4 use Crypt::SecretBuffer qw/ secret MATCH_NEGATE MATCH_MULTI ISO8859_1 /;
  1         1  
  1         4  
9              
10              
11             sub new {
12 6     6 1 258835 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         24 while (@_) {
22 6         20 my ($attr, $val)= splice(@_, 0, 2);
23 6         24 $self->$attr($val);
24             }
25 6         15 $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 291 }
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 153 }
38             sub section_delim {
39             @_ > 1? ($_[0]{section_delim}= $_[1]) : $_[0]{section_delim}
40 11 100   11 1 41 }
41             sub comment_delim {
42             @_ > 1? ($_[0]{comment_delim}= $_[1]) : $_[0]{comment_delim}
43 44 50   44 1 147 }
44             sub inline_comments {
45             @_ > 1? ($_[0]{inline_comments}= $_[1]) : $_[0]{inline_comments}
46 52 100   52 1 204 }
47              
48              
49             sub field_config {
50             @_ > 1? ($_[0]{field_config}= _coerce_field_rules($_[1])) : $_[0]{field_config}
51 22 100   22 1 81 }
52              
53             sub _coerce_field_rules {
54 4     4   10 my $rule_spec= shift;
55 4 50       14 ref $rule_spec eq 'ARRAY'
56             or croak "field rules must be an arrayref";
57 4         8 my @rules;
58 4         13 for (my $i= 0; $i < @$rule_spec; $i++) {
59 8         17 my $rule= $rule_spec->[$i];
60             # scalar or regexpref are treated as keys
61 8 50 66     31 if (!ref $rule or ref $rule eq 'Regexp') {
    0          
62 8         20 my $v= $rule_spec->[++$i];
63 8 100       25 if (ref $v eq 'ARRAY') {
    50          
    0          
64 1         6 $rule= { section => $rule, rules => $v };
65             } elsif (ref $v eq 'HASH') {
66 7         56 $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       32 if ref $rule->{rules} eq 'ARRAY';
79 8         24 push @rules, $rule;
80             }
81 4         19 return \@rules;
82             }
83             sub _find_field_rule {
84 22     22   60 my ($self, $rules, $section, $key)= @_;
85 22         54 for (@$rules) {
86 29 100 66     86 if (defined $_->{key}) {
    100          
87             # It's a rule for keys. Return it if the key matches.
88 23 100       163 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         31 my $sep= $self->section_delim;
96 5         10 my $key_rule;
97 5 50 66     38 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         11 $key_rule= $self->_find_field_rule($_->{rules}, undef, $key);
118             } elsif (defined $sep
119             && $_->{section} eq substr($section, 0, length $_->{section})
120             ) {
121 1         22 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       19 return $key_rule if defined $key_rule;
128             }
129             }
130 13         39 return undef;
131             }
132              
133              
134             sub parse_next {
135 44     44 1 13062 my ($self, $span)= @_;
136 44         107 my ($trim_chars, $comment_delim)= ($self->trim_chars, $self->comment_delim);
137 44         75 my %result;
138 44   100     282 while ($span->len && !keys %result) {
139 40         344 my $line= $span->parse(qr/[^\n]+/);
140 40 50       305 if (!$span->parse("\n")) {
141 0         0 $result{error}= 'No newline on end of file';
142             }
143 40         205 $line->rtrim("\r");
144 40 100       231 if ($line->parse('[')) {
    100          
145 12         118 my $header= $line->parse(qr/[^]]+/)->trim($trim_chars);
146 12 50       99 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         26 $result{section}= $header;
152 12         41 $line->ltrim($trim_chars);
153             }
154             }
155             elsif (!$line->starts_with($comment_delim)) {
156 26         75 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     79 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       55 if ($line->parse($self->key_value_delim)) {
    50          
163 25         121 $result{key}= $key->trim($trim_chars);
164             # TODO: handle optional quoting here
165 25 100       50 if ($self->inline_comments) {
166 2         17 $result{value}= $line->parse($comment_delim, MATCH_NEGATE|MATCH_MULTI)->trim($trim_chars);
167             } else {
168 23         131 $result{value}= $line->new->trim($trim_chars);
169 23         84 $line->pos($line->lim);
170             }
171             } elsif ($self->bare_keys) {
172 0         0 $result{key}= $key->trim($trim_chars);
173             } else {
174 1         3 $result{error}= 'Line lacks delimiter "'.$self->key_value_delim.'"';
175 1         2 $result{context}= $key;
176             }
177             }
178 40 100       377 if ($line->parse($comment_delim)) {
    50          
179 4         28 $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       199 return keys %result? \%result : undef;
185             }
186              
187              
188             sub parse {
189 4     4 1 32 my ($self, $buf_or_span)= @_;
190 4 100       34 my $span= $buf_or_span->can('subspan')? $buf_or_span : $buf_or_span->span;
191 4         26 my ($node, $section, $key, $value);
192 4         13 my $sep= $self->section_delim;
193 4 100 66     59 $sep= qr/\Q$sep\E/ if defined $sep && ref $sep ne 'Regexp';
194 4 100       12 my $root= defined $sep? {} : [];
195 4         13 while (my $tokens= $self->parse_next($span)) {
196             croak $tokens->{error}
197 29 50       82 if defined $tokens->{error};
198 29 100       65 if (defined $tokens->{section}) {
199 10         99 $tokens->{section}->copy_to($section= '');
200 10 100       26 if (defined $sep) {
201 8         16 $node= $root;
202 8         81 for (split $sep, $section) {
203             croak("conflict between section name and pre-existing key of parent section")
204 13 50 66     57 if defined $node->{$_} && ref $node->{$_} ne 'HASH';
205 13   100     56 $node= ($node->{$_} ||= {});
206             }
207             } else {
208 2         8 push @$root, $section, ($node= {});
209             }
210             }
211 29 100       98 if (defined $tokens->{key}) {
212 19 100       45 if (!defined $node) {
213 3 100       10 if (defined $sep) {
214 1         3 $node= $root;
215             } else {
216 2         6 push @$root, '', ($node= {});
217             }
218             }
219 19         114 $tokens->{key}->copy_to($key= '');
220 19 50       56 if (!$tokens->{value}) {
221 0         0 $value= undef;
222             } else {
223 19         87 my $rule= $self->_find_field_rule($self->field_config, $section, $key);
224             $tokens->{value}->encoding($rule->{encoding})
225 19 100       66 if defined $rule->{encoding};
226 19 100       42 if ($rule->{secret}) {
227 7         72 $tokens->{value}->copy_to(($value= secret), encoding => ISO8859_1);
228             } else {
229 12         71 $tokens->{value}->copy_to($value= '');
230             }
231             }
232 19         145 $node->{$key}= $value;
233             }
234             }
235 4         35 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__