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