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