File Coverage

blib/lib/Net/LDAP/Filter.pm
Criterion Covered Total %
statement 92 123 74.8
branch 61 80 76.2
condition 4 6 66.6
subroutine 9 13 69.2
pod 5 6 83.3
total 171 228 75.0


line stmt bran cond sub pod time code
1             # Copyright (c) 1997-2004 Graham Barr . All rights reserved.
2             # This program is free software; you can redistribute it and/or
3             # modify it under the same terms as Perl itself.
4              
5             package Net::LDAP::Filter;
6              
7 4     4   73788 use strict;
  4         18  
  4         4848  
8              
9             our $VERSION = '0.20';
10              
11             # filter = "(" filtercomp ")"
12             # filtercomp = and / or / not / item
13             # and = "&" filterlist
14             # or = "|" filterlist
15             # not = "!" filter
16             # filterlist = 1*filter
17             # item = simple / present / substring / extensible
18             # simple = attr filtertype value
19             # filtertype = equal / approx / greater / less
20             # equal = "="
21             # approx = "~="
22             # greater = ">="
23             # less = "<="
24             # extensible = attr [":dn"] [":" matchingrule] ":=" value
25             # / [":dn"] ":" matchingrule ":=" value
26             # present = attr "=*"
27             # substring = attr "=" [initial] any [final]
28             # initial = value
29             # any = "*" *(value "*")
30             # final = value
31             # attr = AttributeDescription from Section 2.5 of RFC 4512
32             # matchingrule = MatchingRuleId from Section 4.1.8 of RFC 4511
33             # value = AttributeValue from Section 4.1.6 of RFC 4511
34             # with some characters encoded, see below.
35             #
36             # Special Character encodings
37             # ---------------------------
38             # * \2a, \*
39             # ( \28, \(
40             # ) \29, \)
41             # \ \5c, \\
42             # NUL \00
43              
44             my $ErrStr;
45              
46             sub new {
47 142     142 1 69783 my $self = shift;
48 142   33     586 my $class = ref($self) || $self;
49              
50 142         304 my $me = bless {}, $class;
51              
52 142 50       350 if (@_) {
53 142 50       324 $me->parse(shift) or
54             return undef;
55             }
56 142         336 $me;
57             }
58              
59             my $Attr = '[-;.:\d\w]*[-;\d\w]';
60              
61             my %Op = qw(
62             & and
63             | or
64             ! not
65             = equalityMatch
66             ~= approxMatch
67             >= greaterOrEqual
68             <= lessOrEqual
69             := extensibleMatch
70             );
71              
72             my %Rop = reverse %Op;
73              
74              
75 0     0 0 0 sub errstr { $ErrStr }
76              
77             # Unescape
78             # \xx where xx is a 2-digit hex number
79             # \y where y is one of ( ) \ *
80             sub _unescape {
81 225     225   445 $_[0] =~ s/
82             \\([\da-fA-F]{2}|[()\\*])
83             /
84 20 100       103 length($1) == 1
85             ? $1
86             : chr(hex($1))
87             /soxeg;
88 225         896 $_[0];
89             }
90              
91 168     168   452 sub _escape { (my $t = $_[0]) =~ s/([\\\(\)\*\0-\37\177-\377])/sprintf('\\%02x', ord($1))/sge; $t }
  23         126  
  168         696  
92              
93             # encode a triplet ($attr,$op,$val) representing a single filter item
94             sub _encode {
95 206     206   780 my($attr, $op, $val) = @_;
96              
97             # extensible match
98 206 100       505 if ($op eq ':=') {
99              
100             # attr must be in the form type:dn:1.2.3.4
101 21 50       132 unless ($attr =~ /^([-;\d\w]*)(:dn)?(:(\w+|[.\d]+))?$/) {
102 0         0 $ErrStr = "Bad attribute $attr";
103 0         0 return undef;
104             }
105 21         72 my($type, $dn, $rule) = ($1, $2, $4);
106              
107             return ( {
108 21 100       76 extensibleMatch => {
    100          
109             matchingRule => $rule,
110             type => length($type) ? $type : undef,
111             matchValue => _unescape($val),
112             dnAttributes => $dn ? 1 : undef
113             }
114             });
115             }
116              
117             # special cases: present / substring match
118 185 100       350 if ($op eq '=') {
119              
120             # present match
121 159 100       481 if ($val eq '*') {
    100          
122 10         38 return ({ present => $attr });
123             }
124              
125             # if val contains unescaped *, then we have substring match
126             elsif ( $val =~ /^(\\.|[^\\*]+)*\*/o ) {
127              
128 33         66 my $n = [];
129 33         54 my $type = 'initial';
130              
131 33         150 while ($val =~ s/^((\\.|[^\\*]+)*)\*//) {
132 57 100 100     279 push(@$n, { $type, _unescape("$1") }) # $1 is readonly, copy it
133             if length($1) or $type eq 'any';
134              
135 57         200 $type = 'any';
136             }
137              
138 33 100       80 push(@$n, { 'final', _unescape($val) })
139             if length $val;
140              
141             return ({
142 33         143 substrings => {
143             type => $attr,
144             substrings => $n
145             }
146             });
147             }
148             }
149              
150             # in all other cases we must have an operator and no un-escaped *'s on the RHS
151             return {
152 142         313 $Op{$op} => {
153             attributeDesc => $attr, assertionValue => _unescape($val)
154             }
155             };
156             }
157              
158             # parse & encode a filter string
159             sub parse {
160 142     142 1 231 my $self = shift;
161 142         218 my $filter = shift;
162              
163 142         222 my @stack = (); # stack
164 142         225 my $cur = [];
165 142         193 my $op;
166              
167 142         215 undef $ErrStr;
168              
169             # a filter is required
170 142 50       309 if (!defined $filter) {
171 0         0 $ErrStr = 'Undefined filter';
172 0         0 return undef;
173             }
174              
175             # Algorithm depends on /^\(/;
176 142         588 $filter =~ s/^\s*//;
177              
178 142 100       491 $filter = '(' . $filter . ')'
179             unless $filter =~ /^\(/;
180              
181 142         353 while (length($filter)) {
182              
183             # Process the start of ( (...)(...)), with = [&!|]
184              
185 346 100       2637 if ($filter =~ s/^\(\s*([&!|])\s*//) {
    100          
    50          
186 70         153 push @stack, [$op, $cur];
187 70         150 $op = $1;
188 70         109 $cur = [];
189 70         142 next;
190             }
191              
192             # Process the end of ( (...)(...)), with = [&!|]
193              
194             elsif ($filter =~ s/^\)\s*//o) {
195 70 50       137 unless (@stack) {
196 0         0 $ErrStr = 'Bad filter, unmatched )';
197 0         0 return undef;
198             }
199 70         128 my($myop, $mydata) = ($op, $cur);
200 70         96 ($op, $cur) = @{ pop @stack };
  70         120  
201             # Need to do more checking here
202 70 100       239 push @$cur, { $Op{$myop} => $myop eq '!' ? $mydata->[0] : $mydata };
203 70 100       187 next if @stack;
204             }
205              
206             # process (attr op string)
207              
208             elsif ($filter =~ s/^\(\s*
209             ($Attr)\s*
210             ([:~<>]?=)
211             ((?:\\.|[^\\()]+)*)
212             \)\s*
213             //xo) {
214 206         498 push(@$cur, _encode($1, $2, $3));
215 206 100       532 next if @stack;
216             }
217              
218             # If we get here then there is an error in the filter string
219             # so exit loop with data in $filter
220 142         225 last;
221             }
222              
223 142 50       269 if (length $filter) {
224             # If we have anything left in the filter, then there is a problem
225 0         0 $ErrStr = 'Bad filter, error before ' . substr($filter, 0, 20);
226 0         0 return undef;
227             }
228 142 50       295 if (@stack) {
229 0         0 $ErrStr = 'Bad filter, unmatched (';
230 0         0 return undef;
231             }
232              
233 142         177 %$self = %{$cur->[0]};
  142         566  
234              
235 142         547 $self;
236             }
237              
238             sub print {
239 0     0 1 0 my $self = shift;
240 4     4   45 no strict 'refs'; # select may return a GLOB name
  4         10  
  4         3970  
241 0 0       0 my $fh = @_ ? shift : select;
242              
243 0         0 print $fh $self->as_string, "\n";
244             }
245              
246 92     92 1 138984 sub as_string { _string(%{$_[0]}) }
  92         367  
247              
248             sub _string { # prints things of the form ( () ... )
249 223     223   346 my $str = '';
250              
251 223         360 for ($_[0]) {
252 223 100       581 /^and/ and return '(&' . join('', map { _string(%$_) } @{$_[1]}) . ')';
  65         169  
  30         60  
253 193 100       408 /^or/ and return '(|' . join('', map { _string(%$_) } @{$_[1]}) . ')';
  49         130  
  20         46  
254 173 100       354 /^not/ and return '(!' . _string(%{$_[1]}) . ')';
  17         55  
255 156 100       375 /^present/ and return "($_[1]=*)";
256             /^(equalityMatch|greaterOrEqual|lessOrEqual|approxMatch)/
257 147 100       723 and return '(' . $_[1]->{attributeDesc} . $Rop{$1} . _escape($_[1]->{assertionValue}) .')';
258 36 100       128 /^substrings/ and do {
259 22         40 my $str = join('*', '', map { _escape($_) } map { values %$_ } @{$_[1]->{substrings}});
  43         75  
  43         119  
  22         55  
260 22 100       112 $str =~ s/^.// if exists $_[1]->{substrings}[0]{initial};
261 22 100       60 $str .= '*' unless exists $_[1]->{substrings}[-1]{final};
262 22         109 return "($_[1]->{type}=$str)";
263             };
264 14 50       54 /^extensibleMatch/ and do {
265 14         34 my $str = '(';
266 14 100       48 $str .= $_[1]->{type} if defined $_[1]->{type};
267 14 100       39 $str .= ':dn' if $_[1]->{dnAttributes};
268 14 100       45 $str .= ":$_[1]->{matchingRule}" if defined $_[1]->{matchingRule};
269 14         36 $str .= ':=' . _escape($_[1]->{matchValue}) . ')';
270 14         46 return $str;
271             };
272             }
273              
274 0           die "Internal error $_[0]";
275             }
276              
277             sub negate {
278 0     0 1   my $self = shift;
279              
280 0           %{$self} = _negate(%{$self});
  0            
  0            
281              
282 0           $self;
283             }
284              
285             sub _negate { # negate a filter tree
286 0     0     for ($_[0]) {
287 0 0         /^and/ and return ( 'or' => [ map { { _negate(%$_) }; } @{$_[1]} ] );
  0            
  0            
288 0 0         /^or/ and return ( 'and' => [ map { { _negate(%$_) }; } @{$_[1]} ] );
  0            
  0            
289 0 0         /^not/ and return %{$_[1]};
  0            
290 0 0         /^(present|equalityMatch|greaterOrEqual|lessOrEqual|approxMatch|substrings|extensibleMatch)/
291             and do return ( 'not' => { $_[0 ], $_[1] } );
292             }
293              
294 0           die "Internal error $_[0]";
295             }
296              
297             1;