├── .DS_Store
├── .coveragerc
├── .gitignore
├── LICENSE.txt
├── MANIFEST.in
├── README.md
├── docs
└── parser.peg
├── ldap_filter
├── __init__.py
├── filter.py
├── parser.py
└── soundex.py
├── pyproject.toml
├── setup.cfg
├── setup.py
├── tests
├── test_filter_builder.py
├── test_filter_match.py
├── test_filter_output.py
└── test_filter_parser.py
└── tox.ini
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SteveEwell/python-ldap-filter/c608ea4fc2b069a92703fde17e288794f19adb78/.DS_Store
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = ldap_filter/
4 | omit = ldap_filter/parser.py
5 |
6 | [report]
7 | ignore_errors = False
8 | precision = 1
9 | exclude_lines =
10 | pragma: no cover
11 | raise NotImplementedError
12 | if 0:
13 | if __name__ == .__main__.:
14 | if PY2
15 | if not PY2
16 |
17 | [paths]
18 | source =
19 | ldap_filter/
20 | .tox/*/lib/python*/site-packages/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | *.iws
3 | /out/
4 | .idea_modules/
5 | atlassian-ide-plugin.xml
6 | com_crashlytics_export_strings.xml
7 | crashlytics.properties
8 | crashlytics-build.properties
9 | fabric.properties
10 | __pycache__/
11 | *.py[cod]
12 | *$py.class
13 | *.so
14 | .Python
15 | env/
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | *.manifest
31 | *.spec
32 | pip-log.txt
33 | pip-delete-this-directory.txt
34 | htmlcov/
35 | .tox/
36 | .coverage
37 | .coverage.*
38 | .cache
39 | nosetests.xml
40 | coverage.xml
41 | *,cover
42 | .hypothesis/
43 | *.mo
44 | *.pot
45 | *.log
46 | local_settings.py
47 | instance/
48 | .webassets-cache
49 | .scrapy
50 | docs/_build/
51 | target/
52 | .ipynb_checkpoints
53 | .python-version
54 | celerybeat-schedule
55 | .env
56 | venv/
57 | ENV/
58 | .spyderproject
59 | .ropeproject
60 | .pytest_cache/
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Stephen Ewell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE.txt
2 | include README.md
3 |
4 | recursive-include test *
5 |
6 | global-exclude __pycache__
7 | global-exclude *.py[co]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python LDAP Filter · [](https://pypi.python.org/pypi/ldap-filter) [](https://pypi.python.org/pypi/ldap-filter)
2 |
3 | > Build, generate, and validate LDAP filters
4 |
5 |
6 | A Python 3 utility library for working with Lightweight Directory Access Protocol (LDAP) filters.
7 |
8 | This project is a Python port of the [node-ldap-filters](https://github.com/tapmodo/node-ldap-filters) project. The filters produced by the library are based on [RFC 4515](https://tools.ietf.org/html/rfc4515).
9 |
10 | **Note:** This project is currently only compatible with Python 3.4 or higher.
11 |
12 | # Usage #
13 |
14 | ## Installation ##
15 |
16 | Install via pip:
17 |
18 | ``` bash
19 | pip install ldap-filter
20 | ```
21 |
22 | ## Building a Filter ##
23 |
24 | This library exposes a number of APIs that allow you to build filters programmatically. The logical and attribute methods of the `Filter` object can be combined in a number of ways to generate filters ranging from very simple to very complex.
25 |
26 | The following is a quick example of how you might build a filter programmatically:
27 |
28 | ``` python
29 | from ldap_filter import Filter
30 |
31 | output = Filter.AND([
32 | Filter.attribute('name').equal_to('bob'),
33 | Filter.attribute('mail').ends_with('@example.com'),
34 | Filter.OR([
35 | Filter.attribute('dept').equal_to('accounting'),
36 | Filter.attribute('dept').equal_to('operations')
37 | ])
38 | ])
39 |
40 | print(output.to_string()) # (&(name=bob)(mail=*@example.com)(|(dept=accounting)(dept=operations)))
41 | ```
42 |
43 |
44 | ### Attribute Methods
45 |
46 | Attribute methods are used to create LDAP attribute filter strings. The `Filter.attribute(name)` method returns an `Attribute` object that the following filter methods can be applied to.
47 |
48 | ``` python
49 | output = Filter.attribute('name').equal_to('bob') # (name=bob)
50 | ```
51 |
52 | #### Methods:
53 |
54 | - **Attribute.present()** - Tests if an attribute is present.
55 | - *Output:* `(attribute=*)`
56 |
57 | - **Attribute.equal_to(value)** - Tests if an attribute is equal to the provided `value`.
58 | - *Output:* `(attribute=value)`
59 |
60 | - **Attribute.contains(value)** - Tests if an attribute contains the provided `value`.
61 | - *Output:* `(attribute=*value*)`
62 |
63 | - **Attribute.starts_with(value)** - Tests if an attribute starts with the provided `value`.
64 | - *Output:* `(attribute=value*)`
65 |
66 | - **Attribute.ends_with(value)** - Tests if an attribute ends with the provided `value`.
67 | - *Output:* `(attribute=*value)`
68 |
69 | - **Attribute.approx(value)** - Tests if an attribute is an approximate match to the provided `value`.
70 | - *Output:* `(attribute~=value)`
71 |
72 | - **Attribute.gte(value)** - Tests if an attribute is greater than or equal to the provided `value`.
73 | - *Output:* `(attribute>=value)`
74 |
75 | - **Attribute.lte(value)** - Tests if an attribute is less than or equal to the provided `value`.
76 | - *Output:* `(attribute<=value)`
77 |
78 | - **Attribute.raw(value)** - Allows for a custom filter with escaped `value` output.
79 | - *Output:* `(attribute=value)`
80 |
81 |
82 |
83 | ### Logical Methods
84 |
85 | Logical methods are used to aggregate simple attribute filters. You can nest as many logical methods as needed to produce complex filters.
86 |
87 | ``` python
88 | output = Filter.OR([
89 | Filter.attribute('name').equal_to('bob'),
90 | Filter.attribute('name').equal_to('bill')
91 | ])
92 |
93 | print(output) # (|(name=bob)(name=bill))
94 | ```
95 |
96 | #### Methods:
97 |
98 | - **Filter.AND(filt)** - Accepts a list of `Filter`, `Attribute`, or `Group` objects.
99 | - *Output:* `(&(filt=1)(filt=2)..)`
100 |
101 | - **Filter.OR(filt)** - Accepts a list of `Filter`, `Attribute`, or `Group` objects.
102 | - *Output:* `(|(filt=1)(filt=2)..)`
103 |
104 | - **Filter.NOT(filt)** - Accepts a single `Attribute` object.
105 | - *Output:* `(!(filt=1))`
106 |
107 | ## Filter Parsing ##
108 |
109 | The `Filter.parse(input)` method can be used to create a `Filter` object from an existing LDAP filter. This method can also be used to determine if a string is a valid LDAP filter or not.
110 |
111 | ``` python
112 | input = '(|(name=bob)(name=bill))'
113 |
114 | Filter.parse(input)
115 | ```
116 |
117 | If an invalid LDAP filter string is passed a `ParseError` exception will be thrown.
118 |
119 | ``` python
120 | from ldap_filter import Filter, ParseError
121 |
122 |
123 | input = '(|(name=bob)name=bill))'
124 |
125 | try:
126 | Filter.parse(input)
127 | except ParseError as e:
128 | print(e)
129 | ```
130 |
131 | *Error Output:*
132 |
133 | ```
134 | Line 1: expected [\x20], [\x09], "\r\n", "\n", '(', ')'
135 | (|(name=bob)name=bill)
136 | ^
137 | ```
138 |
139 | ## Simplifying Filters ##
140 |
141 | The `Filter.simplify()` method can be used to eliminate unnecessary AND/OR filters that only have one child node.
142 |
143 | ``` python
144 | input = '(&(name=bob))'
145 | complex = Filter.parse(input)
146 |
147 | print(complex.simplify()) # (name=bob)
148 | ```
149 |
150 | ## Filter Output ##
151 |
152 | There are a few options for getting a string output from your `Filter` object with optional custom formatting.
153 |
154 | ### Simple String ###
155 |
156 | You can get simple filter string by calling the `Filter.to_string()` method. The `Filter` class also implements Python's `__str__` method, allowing you to type cast the `Filter` object directly to a string or concatenate with other strings.
157 |
158 | ``` python
159 | output = Filter.AND([
160 | Filter.attribute('name').equal_to('bob'),
161 | Filter.attribute('mail').ends_with('@example.com'),
162 | ])
163 |
164 | # Filter.to_string() output.
165 | print(output.to_string()) # (&(name=bob)(mail=*@example.com))
166 |
167 | # Typecast output.
168 | print(str(output)) # (&(name=bob)(mail=*@example.com))
169 |
170 | # String concatenate output
171 | print('LDAP Filter: ' + output) # LDAP Filter: (&(name=bob)(mail=*@example.com))
172 | ```
173 |
174 | ### Beautified String ###
175 |
176 | The `Filter.to_string()` method provides additional formatting options to produce beautified filter strings.
177 |
178 | You can get the default beautified format by passing `True` to the `Filter.to_string(indent)` method
179 |
180 | ``` python
181 | output = Filter.AND([
182 | Filter.attribute('name').equal_to('bob'),
183 | Filter.attribute('mail').ends_with('@example.com'),
184 | Filter.OR([
185 | Filter.attribute('dept').equal_to('accounting'),
186 | Filter.attribute('dept').equal_to('operations')
187 | ])
188 | ])
189 |
190 | print(output.to_string(True))
191 | ```
192 |
193 | *Default Beautified Output:*
194 |
195 | ```
196 | (&
197 | (name=bob)
198 | (mail=*@example.com)
199 | (|
200 | (dept=accounting)
201 | (dept=operations)
202 | )
203 | )
204 | ```
205 |
206 | or you can customize the output by passing the `indent` and/or `indt_char` parameters to `Filter.to_string(indent, indt_char)`. The `indent` parameter accepts an integer value while the `indt_char` parameter accepts any string or character value.
207 |
208 | ``` python
209 | output = Filter.AND([
210 | Filter.attribute('name').equal_to('bob'),
211 | Filter.attribute('mail').ends_with('@example.com'),
212 | Filter.OR([
213 | Filter.attribute('dept').equal_to('accounting'),
214 | Filter.attribute('dept').equal_to('operations')
215 | ])
216 | ])
217 |
218 | print(output.to_string(2, '.'))
219 | ```
220 |
221 | *Custom Beautified Output:*
222 |
223 | ```
224 | (&
225 | ..(name=bob)
226 | ..(mail=*@example.com)
227 | ..(|
228 | ....(dept=accounting)
229 | ....(dept=operations)
230 | ..)
231 | )
232 | ```
233 |
234 | ## Filter Matching ##
235 |
236 | The `Filter.match(data)` method allows you to evaluate a Python dictionary with attributes against an LDAP filter. The method will return `True` if a match is found or `False` if there is no match (or if an attribute matches a **NOT** exclusion).
237 |
238 | ``` python
239 | filt = Filter.AND([
240 | Filter.attribute('department').equal_to('accounting'),
241 | Filter.NOT(
242 | Filter.attribute('status').equal_to('terminated')
243 | )
244 | ])
245 |
246 | employee1 = {
247 | 'name': 'Bob Smith',
248 | 'department': 'Accounting',
249 | 'status': 'Active'
250 | }
251 |
252 | print(filt.match(employee1)) # True
253 |
254 | employee2 = {
255 | 'name': 'Jane Brown',
256 | 'department': 'Accounting',
257 | 'status': 'Terminated'
258 | }
259 |
260 | print(filt.match(employee2)) # False
261 |
262 | employee3 = {
263 | 'name': 'Bob Smith',
264 | 'department': 'Marketing',
265 | 'status': 'Active'
266 | }
267 |
268 | print(filt.match(employee3)) # False
269 |
270 | ```
271 |
272 | # Unit Tests
273 |
274 | In order to run the test suite the pytest library is required. You can install pytest by running:
275 |
276 | ``` bash
277 | pip install pytest
278 | ```
279 |
280 | To run the unit tests simply type `pytest` in the projects root directory
281 |
282 | # Home Page
283 |
284 | Project home page is https://github.com/SteveEwell/python-ldap-filter
285 |
286 | # License
287 |
288 | The **Python LDAP Filter** project is open source software released under the [MIT licence](https://en.wikipedia.org/wiki/MIT_License). Copyright 2024 Stephen Ewell
--------------------------------------------------------------------------------
/docs/parser.peg:
--------------------------------------------------------------------------------
1 | # PEG file for Canopy (http://canopy.jcoglan.com/)
2 |
3 | grammar LDAP
4 | root <- filter / filter_item
5 | filter <- FILL* '(' filt:filtercomp ')' FILL* %return_filter
6 | filter_item <- FILL* filt:item %return_filter
7 | filtercomp <- and / or / not / item
8 | and <- '&' FILL* filters:filterlist FILL* %return_and_filter
9 | or <- '|' FILL* filters:filterlist FILL* %return_or_filter
10 | not <- '!' FILL* filt:filter FILL* %return_not_filter
11 | filterlist <- filter+
12 | item <- wildcard / simple
13 | simple <- attr filtertype value %return_simple_filter
14 | filtertype <- equal / approx / greater / less
15 | equal <- '='
16 | approx <- '~='
17 | greater <- '>='
18 | less <- '<='
19 | wildcard <- attr equal wildcard_value %return_wildcard
20 | wildcard_value <- value? any value?
21 | any <- '*' (value '*')* %return_string
22 | attr <- AttributeDescription
23 | value <- AttributeValue+ %return_string
24 | AttributeDescription <- attr:AttributeType opts:(";" options)? %return_options
25 | AttributeOptions <- ";" options
26 | AttributeType <- LDAP_OID / AttrTypeName
27 | AttrTypeName <- (ALPHA AttrTypeChars*) %return_attr_type
28 | AttrTypeChars <- ALPHA / DIGIT / "-"
29 | LDAP_OID <- (DIGIT+ ("." DIGIT+)*) %return_oid_type
30 | options <- (option ";" options) %return_string / option
31 | option <- (AttrTypeChars+) %return_string
32 | AttributeValue <- EscapedCharacter / [^\x29]
33 | EscapedCharacter <- '\\' ASCII_VALUE %return_escaped_char
34 | ASCII_VALUE <- HEX_CHAR HEX_CHAR %return_hex
35 | HEX_CHAR <- [a-fA-F0-9]
36 | FILL <- SPACE / TAB / SEP
37 | SPACE <- [\x20]
38 | TAB <- [\x09]
39 | DIGIT <- [0-9]
40 | ALPHA <- [a-zA-Z]
41 | SEP <- "\r\n" / "\n"
--------------------------------------------------------------------------------
/ldap_filter/__init__.py:
--------------------------------------------------------------------------------
1 | from .filter import Filter
2 | from .parser import ParseError
3 |
--------------------------------------------------------------------------------
/ldap_filter/filter.py:
--------------------------------------------------------------------------------
1 | import re
2 | import platform
3 | import ldap_filter.parser as parser
4 |
5 | from ldap_filter.soundex import soundex_compare
6 |
7 |
8 | class LDAPBase:
9 | indent = 4
10 | collapsed = False
11 | filters = None
12 |
13 | def simplify(self):
14 | if self.filters:
15 | if len(self.filters) == 1:
16 | return self.filters[0].simplify()
17 | else:
18 | self.filters = list(map(lambda x: x.simplify(), self.filters))
19 |
20 | return self
21 |
22 | def to_string(self, indent, indt_char, level):
23 | raise NotImplementedError
24 |
25 | def match(self, data):
26 | raise NotImplementedError
27 |
28 | @staticmethod
29 | def _indent(indent, indt_char=' ', level=0):
30 | if type(indent) == bool and indent:
31 | indent = LDAPBase.indent
32 | else:
33 | try:
34 | indent = int(indent)
35 | except ValueError:
36 | return ''
37 |
38 | try:
39 | indt_char = str(indt_char)
40 | except ValueError:
41 | raise InvalidIndentChar('Indent value must convertible to a string')
42 |
43 | return indt_char * (level * indent)
44 |
45 | @staticmethod
46 | def parse(filt):
47 | filt = _strip_whitespace(filt)
48 | return parser.parse(filt, actions=ParserActions())
49 |
50 | @staticmethod
51 | def escape(data):
52 | escaped = data.replace('\\', '\\5c')
53 | escaped = escaped.replace('*', '\\2a')
54 | escaped = escaped.replace('(', '\\28')
55 | escaped = escaped.replace(')', '\\29')
56 | escaped = escaped.replace('\x00', '\\00')
57 |
58 | return escaped
59 |
60 | @staticmethod
61 | def unescape(data):
62 | unescaped = data.replace('\\5c', '\\')
63 | unescaped = unescaped.replace('\\2a', '*')
64 | unescaped = unescaped.replace('\\28', '(')
65 | unescaped = unescaped.replace('\\29', ')')
66 | unescaped = unescaped.replace('\\00', '\x00')
67 |
68 | return unescaped
69 |
70 | @staticmethod
71 | def match_string(data, filt):
72 | match = _as_list(data)
73 | if '*' not in filt:
74 | return any(_ms_helper(m, filt) for m in match)
75 |
76 | return Filter.match_substring(data, filt)
77 |
78 | @staticmethod
79 | def match_substring(data, filt):
80 | match = _as_list(data)
81 |
82 | return any(_ss_helper(m, filt) for m in match)
83 |
84 | @staticmethod
85 | def match_approx(data, filt):
86 | match = _as_list(data)
87 |
88 | return any(_approx_helper(m, filt) for m in match)
89 |
90 | @staticmethod
91 | def match_lte(data, filt):
92 | match = _as_list(data)
93 |
94 | return any(_lte_helper(m, filt) for m in match)
95 |
96 | @staticmethod
97 | def match_gte(data, filt):
98 | match = _as_list(data)
99 |
100 | return any(_gte_helper(m, filt) for m in match)
101 |
102 | @staticmethod
103 | def AND(filt):
104 | return GroupAnd(filt)
105 |
106 | @staticmethod
107 | def OR(filt):
108 | return GroupOr(filt)
109 |
110 | @staticmethod
111 | def NOT(filt):
112 | filt = _as_list(filt)
113 | if not len(filt) == 1: # TODO: Error code here.
114 | raise Exception
115 |
116 | return GroupNot(filt)
117 |
118 |
119 | class Filter(LDAPBase):
120 | def __init__(self, attr, comp, val):
121 | self.type = 'filter'
122 | self.attr = attr
123 | self.comp = comp
124 | self.val = val
125 |
126 | def __repr__(self):
127 | return self.to_string()
128 |
129 | def __str__(self):
130 | return self.to_string()
131 |
132 | def __add__(self, other):
133 | return str(self) + other
134 |
135 | def __radd__(self, other):
136 | return other + str(self)
137 |
138 | def match(self, data):
139 | value = self.val
140 |
141 | try:
142 | attrval = data[self.attr]
143 | except KeyError:
144 | return False
145 |
146 | if self.comp == '=':
147 | if value == '*' and attrval:
148 | return True
149 | else:
150 | return Filter.match_string(attrval, value)
151 | elif self.comp == '<=':
152 | return Filter.match_lte(attrval, value)
153 | elif self.comp == '>=':
154 | return Filter.match_gte(attrval, value)
155 | elif self.comp == '~=':
156 | return Filter.match_approx(attrval, value)
157 | else:
158 | pass
159 |
160 | def to_string(self, indent=False, indt_char=' ', level=0):
161 | return ''.join([
162 | self._indent(indent, indt_char, level),
163 | '(',
164 | self.attr,
165 | self.comp,
166 | self.val,
167 | ')'
168 | ])
169 |
170 | @staticmethod
171 | def attribute(name):
172 | return Attribute(name)
173 |
174 |
175 | class Group(LDAPBase):
176 | def __init__(self, comp, filters):
177 | self.type = 'group'
178 | self.comp = comp
179 | self.filters = filters
180 |
181 | def __repr__(self):
182 | return self.to_string()
183 |
184 | def __str__(self):
185 | return self.to_string()
186 |
187 | def __add__(self, other):
188 | return str(self) + other
189 |
190 | def __radd__(self, other):
191 | return other + str(self)
192 |
193 | def match(self, data):
194 | raise NotImplementedError
195 |
196 | def to_string(self, indent=False, indt_char=' ', level=0):
197 | id_str = self._indent(indent, indt_char, level)
198 | id_str2 = id_str
199 | nl = ''
200 |
201 | # If running on Windows use Windows style newlines,
202 | # if anything else default to POSIX style.
203 | if platform.system() == 'Windows' and indent:
204 | nl = '\r\n'
205 | elif indent:
206 | nl = '\n'
207 |
208 | if not Filter.collapsed and self.comp == '!':
209 | nl = ''
210 | id_str2 = ''
211 | indent = 0
212 |
213 | return ''.join([
214 | id_str,
215 | '(',
216 | self.comp,
217 | nl,
218 | nl.join(list(map(lambda x: x.to_string(indent, indt_char, level + 1), self.filters))),
219 | nl,
220 | id_str2,
221 | ')'
222 | ])
223 |
224 |
225 | class GroupOr(Group):
226 | def __init__(self, filters):
227 | super().__init__(comp='|', filters=filters)
228 |
229 | def match(self, data):
230 | return any(f.match(data) for f in self.filters)
231 |
232 |
233 | class GroupAnd(Group):
234 | def __init__(self, filters):
235 | super().__init__(comp='&', filters=filters)
236 |
237 | def match(self, data):
238 | return all(f.match(data) for f in self.filters)
239 |
240 |
241 | class GroupNot(Group):
242 | def __init__(self, filters):
243 | super().__init__(comp='!', filters=filters)
244 |
245 | def match(self, data):
246 | return not any(_not_helper(f, data) for f in self.filters)
247 |
248 | def simplify(self):
249 | return self
250 |
251 |
252 | class Attribute:
253 | def __init__(self, name):
254 | self.name = name
255 |
256 | def present(self):
257 | return Filter(self.name, '=', '*')
258 |
259 | def raw(self, value):
260 | return Filter(self.name, '=', _to_string(value))
261 |
262 | def equal_to(self, value):
263 | return Filter(self.name, '=', self.escape(_to_string(value)))
264 |
265 | def starts_with(self, value):
266 | return Filter(self.name, '=', self.escape(_to_string(value)) + '*')
267 |
268 | def ends_with(self, value):
269 | return Filter(self.name, '=', '*' + self.escape(_to_string(value)))
270 |
271 | def contains(self, value):
272 | return Filter(self.name, '=', '*' + self.escape(_to_string(value)) + '*')
273 |
274 | def approx(self, value):
275 | return Filter(self.name, '~=', self.escape(_to_string(value)))
276 |
277 | def lte(self, value):
278 | return Filter(self.name, '<=', self.escape(_to_string(value)))
279 |
280 | def gte(self, value):
281 | return Filter(self.name, '>=', self.escape(_to_string(value)))
282 |
283 | @staticmethod
284 | def escape(data):
285 | escaped = data.replace('\\', '\\5c')
286 | escaped = escaped.replace('*', '\\2a')
287 | escaped = escaped.replace('(', '\\28')
288 | escaped = escaped.replace(')', '\\29')
289 | escaped = escaped.replace('\x00', '\\00')
290 |
291 | return escaped
292 |
293 |
294 | def _as_list(val):
295 | if not isinstance(val, (list, tuple)):
296 | return [val]
297 |
298 | return val
299 |
300 |
301 | def _ss_regex(filt):
302 | pattern = re.sub(r'\*', '.*', filt)
303 | pattern = re.sub(r'(?<=\\)([0-9a-fA-F]{,2})', _ss_regex_escaped, pattern)
304 | return re.compile('^' + pattern + '$', re.I)
305 |
306 |
307 | def _ss_regex_escaped(match):
308 | s = match.group(0) if match else None
309 |
310 | if s in ['28', '29', '5c', '2a']:
311 | s = 'x{}'.format(match.group(0).upper())
312 |
313 | return s
314 |
315 |
316 | def _strip_whitespace(filt):
317 | if ' ' or '\n' or '\r\n' in filt:
318 | att_val = re.findall(r'(?<=[=])(?<=[~=]|[>=]|[<=])(.*?)(?=\))', filt)
319 | filt = filt.replace('\r\n', '')
320 | filt = filt.replace('\n', '')
321 | filt = filt.replace(' ', '')
322 |
323 | for s in att_val:
324 | key = s.replace('\r\n', '')
325 | key = key.replace('\n', '')
326 | key = key.replace(' ', '')
327 | filt = filt.replace(key, s)
328 |
329 | att = re.findall(r'(?<=[(])[a-zA-Z0-9 -.]*?(?=[~=]|[>=]|[<=]|[=])', filt)
330 |
331 | for s in att:
332 | if ' ' in s:
333 | regex = re.compile('(?<=[(])' + s + '?(?=[~=]|[>=]|[<=]|[=])', re.I)
334 | filt = re.sub(regex, s.replace(' ', ''), filt)
335 |
336 | return filt
337 |
338 |
339 | def _ss_helper(cv, filt):
340 | regex = _ss_regex(filt)
341 |
342 | return regex.match(cv)
343 |
344 |
345 | def _ms_helper(cv, filt):
346 | if cv:
347 | return cv.lower() == Filter.unescape(filt).lower()
348 |
349 |
350 | def _approx_helper(cv, filt):
351 | return soundex_compare(cv, filt)
352 |
353 |
354 | def _lte_helper(cv, filt):
355 | try:
356 | val = int(cv) <= int(filt)
357 | except ValueError:
358 | val = str(cv) <= str(filt)
359 | return val
360 |
361 |
362 | def _gte_helper(cv, filt):
363 | try:
364 | val = int(cv) >= int(filt)
365 | except ValueError:
366 | val = str(cv) >= str(filt)
367 | return val
368 |
369 |
370 | def _not_helper(filt, data):
371 | try:
372 | return filt.match(data)
373 | except AttributeError:
374 | pass
375 |
376 |
377 | def _to_string(val):
378 | try:
379 | val = str(val)
380 | except ValueError:
381 | print('Could not convert data to a string.')
382 | raise
383 | return val
384 |
385 |
386 | class ParserActions:
387 |
388 | @staticmethod
389 | def elements_to_string(elements=None):
390 | if elements:
391 | string = ''
392 |
393 | for e in elements:
394 | try:
395 | string += e.text if e else ''
396 | except AttributeError:
397 | string += str(e) if e else ''
398 | return string
399 |
400 | def return_string(self, input, start, end, elements=None):
401 | return self.elements_to_string(elements)
402 |
403 | def return_hex(self, input, start, end, elements=None):
404 | string = self.elements_to_string(elements)
405 |
406 | if string:
407 | return int(string, 16)
408 |
409 | def return_escaped_char(self, input, start, end, elements=None):
410 | string = self.elements_to_string(elements)
411 |
412 | if string:
413 | chr_code = int(string.replace('\\', ''))
414 |
415 | return chr(chr_code)
416 |
417 | @staticmethod
418 | def return_options(input, start, end, attr, opts=None, elements=None):
419 | if opts:
420 | opts.pop(0)
421 | opts = opts.pop(0)
422 | opts = opts.split(';')
423 |
424 | attr[0]['options'] = opts if opts else []
425 | return attr[0]
426 |
427 | def return_oid_type(self, input, start, end, elements=None):
428 | oid = self.elements_to_string(elements)
429 |
430 | if oid:
431 | return {
432 | 'type': 'oid',
433 | 'attribute': oid
434 | }
435 |
436 | def return_attr_type(self, input, start, end, elements=None):
437 | name = self.elements_to_string(elements)
438 |
439 | if name:
440 | return {
441 | 'type': 'attribute',
442 | 'attribute': name
443 | }
444 |
445 | @staticmethod
446 | def return_simple_filter(input, start, end, elements=None):
447 | attr = elements[0]['attribute']
448 | comp = getattr(elements[1], 'text')
449 | value = elements[2]
450 |
451 | return Filter(attr, comp, value)
452 |
453 | @staticmethod
454 | def return_present_filter(input, start, end, elements=None):
455 | attr = elements[0]['attribute']
456 |
457 | return Filter.attribute(attr).present()
458 |
459 | @staticmethod
460 | def return_wildcard(input, start, end, elements=None):
461 | attr = elements[0]['attribute']
462 | value = getattr(elements[2], 'text')
463 |
464 | return Filter(attr, '=', value)
465 |
466 | @staticmethod
467 | def return_filter(input, start, end, filt=None, elements=None):
468 | for f in filt:
469 | if isinstance(f, (Filter, GroupAnd, GroupOr, GroupNot)):
470 | return f
471 |
472 | @staticmethod
473 | def return_and_filter(input, start, end, filters=None, elements=None):
474 | for f in filters:
475 | if f.elements:
476 | return Filter.AND(f.elements)
477 |
478 | @staticmethod
479 | def return_or_filter(input, start, end, filters=None, elements=None):
480 | for f in filters:
481 | if f.elements:
482 | return Filter.OR(f.elements)
483 |
484 | @staticmethod
485 | def return_not_filter(input, start, end, filt=None, elements=None):
486 | for f in filt:
487 | if isinstance(f, (Filter, GroupAnd, GroupOr, GroupNot)):
488 | return Filter.NOT(f)
489 |
490 |
491 | class InvalidIndentChar(Exception):
492 | pass
493 |
--------------------------------------------------------------------------------
/ldap_filter/parser.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """LDAP Parser
3 |
4 | This parser is generated by the canopy
5 | library (`canopy.jcoglan.com `__).
6 |
7 | """
8 |
9 | from collections import defaultdict
10 | import re
11 |
12 |
13 | class TreeNode(object):
14 | def __init__(self, text, offset, elements=None):
15 | self.text = text
16 | self.offset = offset
17 | self.elements = elements or []
18 |
19 | def __iter__(self):
20 | for el in self.elements:
21 | yield el
22 |
23 |
24 | class TreeNode1(TreeNode):
25 | def __init__(self, text, offset, elements):
26 | super(TreeNode1, self).__init__(text, offset, elements)
27 | self.filt = elements[2]
28 | self.filtercomp = elements[2]
29 |
30 |
31 | class TreeNode2(TreeNode):
32 | def __init__(self, text, offset, elements):
33 | super(TreeNode2, self).__init__(text, offset, elements)
34 | self.filt = elements[1]
35 | self.item = elements[1]
36 |
37 |
38 | class TreeNode3(TreeNode):
39 | def __init__(self, text, offset, elements):
40 | super(TreeNode3, self).__init__(text, offset, elements)
41 | self.filters = elements[2]
42 | self.filterlist = elements[2]
43 |
44 |
45 | class TreeNode4(TreeNode):
46 | def __init__(self, text, offset, elements):
47 | super(TreeNode4, self).__init__(text, offset, elements)
48 | self.filters = elements[2]
49 | self.filterlist = elements[2]
50 |
51 |
52 | class TreeNode5(TreeNode):
53 | def __init__(self, text, offset, elements):
54 | super(TreeNode5, self).__init__(text, offset, elements)
55 | self.filt = elements[2]
56 | self.filter = elements[2]
57 |
58 |
59 | class TreeNode6(TreeNode):
60 | def __init__(self, text, offset, elements):
61 | super(TreeNode6, self).__init__(text, offset, elements)
62 | self.attr = elements[0]
63 | self.filtertype = elements[1]
64 | self.value = elements[2]
65 |
66 |
67 | class TreeNode7(TreeNode):
68 | def __init__(self, text, offset, elements):
69 | super(TreeNode7, self).__init__(text, offset, elements)
70 | self.attr = elements[0]
71 | self.equal = elements[1]
72 | self.wildcard_value = elements[2]
73 |
74 |
75 | class TreeNode8(TreeNode):
76 | def __init__(self, text, offset, elements):
77 | super(TreeNode8, self).__init__(text, offset, elements)
78 | self.any = elements[1]
79 |
80 |
81 | class TreeNode9(TreeNode):
82 | def __init__(self, text, offset, elements):
83 | super(TreeNode9, self).__init__(text, offset, elements)
84 | self.value = elements[0]
85 |
86 |
87 | class TreeNode10(TreeNode):
88 | def __init__(self, text, offset, elements):
89 | super(TreeNode10, self).__init__(text, offset, elements)
90 | self.attr = elements[0]
91 | self.AttributeType = elements[0]
92 | self.opts = elements[1]
93 |
94 |
95 | class TreeNode11(TreeNode):
96 | def __init__(self, text, offset, elements):
97 | super(TreeNode11, self).__init__(text, offset, elements)
98 | self.options = elements[1]
99 |
100 |
101 | class TreeNode12(TreeNode):
102 | def __init__(self, text, offset, elements):
103 | super(TreeNode12, self).__init__(text, offset, elements)
104 | self.options = elements[1]
105 |
106 |
107 | class TreeNode13(TreeNode):
108 | def __init__(self, text, offset, elements):
109 | super(TreeNode13, self).__init__(text, offset, elements)
110 | self.ALPHA = elements[0]
111 |
112 |
113 | class TreeNode14(TreeNode):
114 | def __init__(self, text, offset, elements):
115 | super(TreeNode14, self).__init__(text, offset, elements)
116 | self.option = elements[0]
117 | self.options = elements[2]
118 |
119 |
120 | class TreeNode15(TreeNode):
121 | def __init__(self, text, offset, elements):
122 | super(TreeNode15, self).__init__(text, offset, elements)
123 | self.ASCII_VALUE = elements[1]
124 |
125 |
126 | class TreeNode16(TreeNode):
127 | def __init__(self, text, offset, elements):
128 | super(TreeNode16, self).__init__(text, offset, elements)
129 | self.HEX_CHAR = elements[1]
130 |
131 |
132 | class ParseError(SyntaxError):
133 | pass
134 |
135 |
136 | FAILURE = object()
137 |
138 |
139 | class Grammar(object):
140 | _cache = None
141 | _input = None
142 | _input_size = None
143 | _actions = None
144 |
145 | REGEX_1 = re.compile('^[^\\x29]')
146 | REGEX_2 = re.compile('^[a-fA-F0-9]')
147 | REGEX_3 = re.compile('^[\\x20]')
148 | REGEX_4 = re.compile('^[\\x09]')
149 | REGEX_5 = re.compile('^[0-9]')
150 | REGEX_6 = re.compile('^[a-zA-Z:.]')
151 |
152 | def _read_root(self):
153 | address0, index0 = FAILURE, self._offset
154 | cached = self._cache['root'].get(index0)
155 | if cached:
156 | self._offset = cached[1]
157 | return cached[0]
158 | index1 = self._offset
159 | address0 = self._read_filter()
160 | if address0 is FAILURE:
161 | self._offset = index1
162 | address0 = self._read_filter_item()
163 | if address0 is FAILURE:
164 | self._offset = index1
165 | self._cache['root'][index0] = (address0, self._offset)
166 | return address0
167 |
168 | def _read_filter(self):
169 | address0, index0 = FAILURE, self._offset
170 | cached = self._cache['filter'].get(index0)
171 | if cached:
172 | self._offset = cached[1]
173 | return cached[0]
174 | index1, elements0 = self._offset, []
175 | remaining0, index2, elements1, address2 = 0, self._offset, [], True
176 | while address2 is not FAILURE:
177 | address2 = self._read_FILL()
178 | if address2 is not FAILURE:
179 | elements1.append(address2)
180 | remaining0 -= 1
181 | if remaining0 <= 0:
182 | address1 = TreeNode(self._input[index2:self._offset], index2, elements1)
183 | self._offset = self._offset
184 | else:
185 | address1 = FAILURE
186 | if address1 is not FAILURE:
187 | elements0.append(address1)
188 | chunk0 = None
189 | if self._offset < self._input_size:
190 | chunk0 = self._input[self._offset:self._offset + 1]
191 | if chunk0 == '(':
192 | address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
193 | self._offset = self._offset + 1
194 | else:
195 | address3 = FAILURE
196 | if self._offset > self._failure:
197 | self._failure = self._offset
198 | self._expected = []
199 | if self._offset == self._failure:
200 | self._expected.append('\'(\'')
201 | if address3 is not FAILURE:
202 | elements0.append(address3)
203 | address4 = self._read_filtercomp()
204 | if address4 is not FAILURE:
205 | elements0.append(address4)
206 | chunk1 = None
207 | if self._offset < self._input_size:
208 | chunk1 = self._input[self._offset:self._offset + 1]
209 | if chunk1 == ')':
210 | address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
211 | self._offset = self._offset + 1
212 | else:
213 | address5 = FAILURE
214 | if self._offset > self._failure:
215 | self._failure = self._offset
216 | self._expected = []
217 | if self._offset == self._failure:
218 | self._expected.append('\')\'')
219 | if address5 is not FAILURE:
220 | elements0.append(address5)
221 | remaining1, index3, elements2, address7 = 0, self._offset, [], True
222 | while address7 is not FAILURE:
223 | address7 = self._read_FILL()
224 | if address7 is not FAILURE:
225 | elements2.append(address7)
226 | remaining1 -= 1
227 | if remaining1 <= 0:
228 | address6 = TreeNode(self._input[index3:self._offset], index3, elements2)
229 | self._offset = self._offset
230 | else:
231 | address6 = FAILURE
232 | if address6 is not FAILURE:
233 | elements0.append(address6)
234 | else:
235 | elements0 = None
236 | self._offset = index1
237 | else:
238 | elements0 = None
239 | self._offset = index1
240 | else:
241 | elements0 = None
242 | self._offset = index1
243 | else:
244 | elements0 = None
245 | self._offset = index1
246 | else:
247 | elements0 = None
248 | self._offset = index1
249 | if elements0 is None:
250 | address0 = FAILURE
251 | else:
252 | address0 = self._actions.return_filter(self._input, index1, self._offset, elements0)
253 | self._offset = self._offset
254 | self._cache['filter'][index0] = (address0, self._offset)
255 | return address0
256 |
257 | def _read_filter_item(self):
258 | address0, index0 = FAILURE, self._offset
259 | cached = self._cache['filter_item'].get(index0)
260 | if cached:
261 | self._offset = cached[1]
262 | return cached[0]
263 | index1, elements0 = self._offset, []
264 | remaining0, index2, elements1, address2 = 0, self._offset, [], True
265 | while address2 is not FAILURE:
266 | address2 = self._read_FILL()
267 | if address2 is not FAILURE:
268 | elements1.append(address2)
269 | remaining0 -= 1
270 | if remaining0 <= 0:
271 | address1 = TreeNode(self._input[index2:self._offset], index2, elements1)
272 | self._offset = self._offset
273 | else:
274 | address1 = FAILURE
275 | if address1 is not FAILURE:
276 | elements0.append(address1)
277 | address3 = self._read_item()
278 | if address3 is not FAILURE:
279 | elements0.append(address3)
280 | else:
281 | elements0 = None
282 | self._offset = index1
283 | else:
284 | elements0 = None
285 | self._offset = index1
286 | if elements0 is None:
287 | address0 = FAILURE
288 | else:
289 | address0 = self._actions.return_filter(self._input, index1, self._offset, elements0)
290 | self._offset = self._offset
291 | self._cache['filter_item'][index0] = (address0, self._offset)
292 | return address0
293 |
294 | def _read_filtercomp(self):
295 | address0, index0 = FAILURE, self._offset
296 | cached = self._cache['filtercomp'].get(index0)
297 | if cached:
298 | self._offset = cached[1]
299 | return cached[0]
300 | index1 = self._offset
301 | address0 = self._read_and()
302 | if address0 is FAILURE:
303 | self._offset = index1
304 | address0 = self._read_or()
305 | if address0 is FAILURE:
306 | self._offset = index1
307 | address0 = self._read_not()
308 | if address0 is FAILURE:
309 | self._offset = index1
310 | address0 = self._read_item()
311 | if address0 is FAILURE:
312 | self._offset = index1
313 | self._cache['filtercomp'][index0] = (address0, self._offset)
314 | return address0
315 |
316 | def _read_and(self):
317 | address0, index0 = FAILURE, self._offset
318 | cached = self._cache['and'].get(index0)
319 | if cached:
320 | self._offset = cached[1]
321 | return cached[0]
322 | index1, elements0 = self._offset, []
323 | chunk0 = None
324 | if self._offset < self._input_size:
325 | chunk0 = self._input[self._offset:self._offset + 1]
326 | if chunk0 == '&':
327 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
328 | self._offset = self._offset + 1
329 | else:
330 | address1 = FAILURE
331 | if self._offset > self._failure:
332 | self._failure = self._offset
333 | self._expected = []
334 | if self._offset == self._failure:
335 | self._expected.append('\'&\'')
336 | if address1 is not FAILURE:
337 | elements0.append(address1)
338 | remaining0, index2, elements1, address3 = 0, self._offset, [], True
339 | while address3 is not FAILURE:
340 | address3 = self._read_FILL()
341 | if address3 is not FAILURE:
342 | elements1.append(address3)
343 | remaining0 -= 1
344 | if remaining0 <= 0:
345 | address2 = TreeNode(self._input[index2:self._offset], index2, elements1)
346 | self._offset = self._offset
347 | else:
348 | address2 = FAILURE
349 | if address2 is not FAILURE:
350 | elements0.append(address2)
351 | address4 = self._read_filterlist()
352 | if address4 is not FAILURE:
353 | elements0.append(address4)
354 | remaining1, index3, elements2, address6 = 0, self._offset, [], True
355 | while address6 is not FAILURE:
356 | address6 = self._read_FILL()
357 | if address6 is not FAILURE:
358 | elements2.append(address6)
359 | remaining1 -= 1
360 | if remaining1 <= 0:
361 | address5 = TreeNode(self._input[index3:self._offset], index3, elements2)
362 | self._offset = self._offset
363 | else:
364 | address5 = FAILURE
365 | if address5 is not FAILURE:
366 | elements0.append(address5)
367 | else:
368 | elements0 = None
369 | self._offset = index1
370 | else:
371 | elements0 = None
372 | self._offset = index1
373 | else:
374 | elements0 = None
375 | self._offset = index1
376 | else:
377 | elements0 = None
378 | self._offset = index1
379 | if elements0 is None:
380 | address0 = FAILURE
381 | else:
382 | address0 = self._actions.return_and_filter(self._input, index1, self._offset, elements0)
383 | self._offset = self._offset
384 | self._cache['and'][index0] = (address0, self._offset)
385 | return address0
386 |
387 | def _read_or(self):
388 | address0, index0 = FAILURE, self._offset
389 | cached = self._cache['or'].get(index0)
390 | if cached:
391 | self._offset = cached[1]
392 | return cached[0]
393 | index1, elements0 = self._offset, []
394 | chunk0 = None
395 | if self._offset < self._input_size:
396 | chunk0 = self._input[self._offset:self._offset + 1]
397 | if chunk0 == '|':
398 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
399 | self._offset = self._offset + 1
400 | else:
401 | address1 = FAILURE
402 | if self._offset > self._failure:
403 | self._failure = self._offset
404 | self._expected = []
405 | if self._offset == self._failure:
406 | self._expected.append('\'|\'')
407 | if address1 is not FAILURE:
408 | elements0.append(address1)
409 | remaining0, index2, elements1, address3 = 0, self._offset, [], True
410 | while address3 is not FAILURE:
411 | address3 = self._read_FILL()
412 | if address3 is not FAILURE:
413 | elements1.append(address3)
414 | remaining0 -= 1
415 | if remaining0 <= 0:
416 | address2 = TreeNode(self._input[index2:self._offset], index2, elements1)
417 | self._offset = self._offset
418 | else:
419 | address2 = FAILURE
420 | if address2 is not FAILURE:
421 | elements0.append(address2)
422 | address4 = self._read_filterlist()
423 | if address4 is not FAILURE:
424 | elements0.append(address4)
425 | remaining1, index3, elements2, address6 = 0, self._offset, [], True
426 | while address6 is not FAILURE:
427 | address6 = self._read_FILL()
428 | if address6 is not FAILURE:
429 | elements2.append(address6)
430 | remaining1 -= 1
431 | if remaining1 <= 0:
432 | address5 = TreeNode(self._input[index3:self._offset], index3, elements2)
433 | self._offset = self._offset
434 | else:
435 | address5 = FAILURE
436 | if address5 is not FAILURE:
437 | elements0.append(address5)
438 | else:
439 | elements0 = None
440 | self._offset = index1
441 | else:
442 | elements0 = None
443 | self._offset = index1
444 | else:
445 | elements0 = None
446 | self._offset = index1
447 | else:
448 | elements0 = None
449 | self._offset = index1
450 | if elements0 is None:
451 | address0 = FAILURE
452 | else:
453 | address0 = self._actions.return_or_filter(self._input, index1, self._offset, elements0)
454 | self._offset = self._offset
455 | self._cache['or'][index0] = (address0, self._offset)
456 | return address0
457 |
458 | def _read_not(self):
459 | address0, index0 = FAILURE, self._offset
460 | cached = self._cache['not'].get(index0)
461 | if cached:
462 | self._offset = cached[1]
463 | return cached[0]
464 | index1, elements0 = self._offset, []
465 | chunk0 = None
466 | if self._offset < self._input_size:
467 | chunk0 = self._input[self._offset:self._offset + 1]
468 | if chunk0 == '!':
469 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
470 | self._offset = self._offset + 1
471 | else:
472 | address1 = FAILURE
473 | if self._offset > self._failure:
474 | self._failure = self._offset
475 | self._expected = []
476 | if self._offset == self._failure:
477 | self._expected.append('\'!\'')
478 | if address1 is not FAILURE:
479 | elements0.append(address1)
480 | remaining0, index2, elements1, address3 = 0, self._offset, [], True
481 | while address3 is not FAILURE:
482 | address3 = self._read_FILL()
483 | if address3 is not FAILURE:
484 | elements1.append(address3)
485 | remaining0 -= 1
486 | if remaining0 <= 0:
487 | address2 = TreeNode(self._input[index2:self._offset], index2, elements1)
488 | self._offset = self._offset
489 | else:
490 | address2 = FAILURE
491 | if address2 is not FAILURE:
492 | elements0.append(address2)
493 | address4 = self._read_filter()
494 | if address4 is not FAILURE:
495 | elements0.append(address4)
496 | remaining1, index3, elements2, address6 = 0, self._offset, [], True
497 | while address6 is not FAILURE:
498 | address6 = self._read_FILL()
499 | if address6 is not FAILURE:
500 | elements2.append(address6)
501 | remaining1 -= 1
502 | if remaining1 <= 0:
503 | address5 = TreeNode(self._input[index3:self._offset], index3, elements2)
504 | self._offset = self._offset
505 | else:
506 | address5 = FAILURE
507 | if address5 is not FAILURE:
508 | elements0.append(address5)
509 | else:
510 | elements0 = None
511 | self._offset = index1
512 | else:
513 | elements0 = None
514 | self._offset = index1
515 | else:
516 | elements0 = None
517 | self._offset = index1
518 | else:
519 | elements0 = None
520 | self._offset = index1
521 | if elements0 is None:
522 | address0 = FAILURE
523 | else:
524 | address0 = self._actions.return_not_filter(self._input, index1, self._offset, elements0)
525 | self._offset = self._offset
526 | self._cache['not'][index0] = (address0, self._offset)
527 | return address0
528 |
529 | def _read_filterlist(self):
530 | address0, index0 = FAILURE, self._offset
531 | cached = self._cache['filterlist'].get(index0)
532 | if cached:
533 | self._offset = cached[1]
534 | return cached[0]
535 | remaining0, index1, elements0, address1 = 1, self._offset, [], True
536 | while address1 is not FAILURE:
537 | address1 = self._read_filter()
538 | if address1 is not FAILURE:
539 | elements0.append(address1)
540 | remaining0 -= 1
541 | if remaining0 <= 0:
542 | address0 = TreeNode(self._input[index1:self._offset], index1, elements0)
543 | self._offset = self._offset
544 | else:
545 | address0 = FAILURE
546 | self._cache['filterlist'][index0] = (address0, self._offset)
547 | return address0
548 |
549 | def _read_item(self):
550 | address0, index0 = FAILURE, self._offset
551 | cached = self._cache['item'].get(index0)
552 | if cached:
553 | self._offset = cached[1]
554 | return cached[0]
555 | index1 = self._offset
556 | address0 = self._read_wildcard()
557 | if address0 is FAILURE:
558 | self._offset = index1
559 | address0 = self._read_simple()
560 | if address0 is FAILURE:
561 | self._offset = index1
562 | self._cache['item'][index0] = (address0, self._offset)
563 | return address0
564 |
565 | def _read_simple(self):
566 | address0, index0 = FAILURE, self._offset
567 | cached = self._cache['simple'].get(index0)
568 | if cached:
569 | self._offset = cached[1]
570 | return cached[0]
571 | index1, elements0 = self._offset, []
572 | address1 = self._read_attr()
573 | if address1 is not FAILURE:
574 | elements0.append(address1)
575 | address2 = self._read_filtertype()
576 | if address2 is not FAILURE:
577 | elements0.append(address2)
578 | address3 = self._read_value()
579 | if address3 is not FAILURE:
580 | elements0.append(address3)
581 | else:
582 | elements0 = None
583 | self._offset = index1
584 | else:
585 | elements0 = None
586 | self._offset = index1
587 | else:
588 | elements0 = None
589 | self._offset = index1
590 | if elements0 is None:
591 | address0 = FAILURE
592 | else:
593 | address0 = self._actions.return_simple_filter(self._input, index1, self._offset, elements0)
594 | self._offset = self._offset
595 | self._cache['simple'][index0] = (address0, self._offset)
596 | return address0
597 |
598 | def _read_filtertype(self):
599 | address0, index0 = FAILURE, self._offset
600 | cached = self._cache['filtertype'].get(index0)
601 | if cached:
602 | self._offset = cached[1]
603 | return cached[0]
604 | index1 = self._offset
605 | address0 = self._read_equal()
606 | if address0 is FAILURE:
607 | self._offset = index1
608 | address0 = self._read_approx()
609 | if address0 is FAILURE:
610 | self._offset = index1
611 | address0 = self._read_greater()
612 | if address0 is FAILURE:
613 | self._offset = index1
614 | address0 = self._read_less()
615 | if address0 is FAILURE:
616 | self._offset = index1
617 | self._cache['filtertype'][index0] = (address0, self._offset)
618 | return address0
619 |
620 | def _read_equal(self):
621 | address0, index0 = FAILURE, self._offset
622 | cached = self._cache['equal'].get(index0)
623 | if cached:
624 | self._offset = cached[1]
625 | return cached[0]
626 | chunk0 = None
627 | if self._offset < self._input_size:
628 | chunk0 = self._input[self._offset:self._offset + 1]
629 | if chunk0 == '=':
630 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
631 | self._offset = self._offset + 1
632 | else:
633 | address0 = FAILURE
634 | if self._offset > self._failure:
635 | self._failure = self._offset
636 | self._expected = []
637 | if self._offset == self._failure:
638 | self._expected.append('\'=\'')
639 | self._cache['equal'][index0] = (address0, self._offset)
640 | return address0
641 |
642 | def _read_approx(self):
643 | address0, index0 = FAILURE, self._offset
644 | cached = self._cache['approx'].get(index0)
645 | if cached:
646 | self._offset = cached[1]
647 | return cached[0]
648 | chunk0 = None
649 | if self._offset < self._input_size:
650 | chunk0 = self._input[self._offset:self._offset + 2]
651 | if chunk0 == '~=':
652 | address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset)
653 | self._offset = self._offset + 2
654 | else:
655 | address0 = FAILURE
656 | if self._offset > self._failure:
657 | self._failure = self._offset
658 | self._expected = []
659 | if self._offset == self._failure:
660 | self._expected.append('\'~=\'')
661 | self._cache['approx'][index0] = (address0, self._offset)
662 | return address0
663 |
664 | def _read_greater(self):
665 | address0, index0 = FAILURE, self._offset
666 | cached = self._cache['greater'].get(index0)
667 | if cached:
668 | self._offset = cached[1]
669 | return cached[0]
670 | chunk0 = None
671 | if self._offset < self._input_size:
672 | chunk0 = self._input[self._offset:self._offset + 2]
673 | if chunk0 == '>=':
674 | address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset)
675 | self._offset = self._offset + 2
676 | else:
677 | address0 = FAILURE
678 | if self._offset > self._failure:
679 | self._failure = self._offset
680 | self._expected = []
681 | if self._offset == self._failure:
682 | self._expected.append('\'>=\'')
683 | self._cache['greater'][index0] = (address0, self._offset)
684 | return address0
685 |
686 | def _read_less(self):
687 | address0, index0 = FAILURE, self._offset
688 | cached = self._cache['less'].get(index0)
689 | if cached:
690 | self._offset = cached[1]
691 | return cached[0]
692 | chunk0 = None
693 | if self._offset < self._input_size:
694 | chunk0 = self._input[self._offset:self._offset + 2]
695 | if chunk0 == '<=':
696 | address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset)
697 | self._offset = self._offset + 2
698 | else:
699 | address0 = FAILURE
700 | if self._offset > self._failure:
701 | self._failure = self._offset
702 | self._expected = []
703 | if self._offset == self._failure:
704 | self._expected.append('\'<=\'')
705 | self._cache['less'][index0] = (address0, self._offset)
706 | return address0
707 |
708 | def _read_wildcard(self):
709 | address0, index0 = FAILURE, self._offset
710 | cached = self._cache['wildcard'].get(index0)
711 | if cached:
712 | self._offset = cached[1]
713 | return cached[0]
714 | index1, elements0 = self._offset, []
715 | address1 = self._read_attr()
716 | if address1 is not FAILURE:
717 | elements0.append(address1)
718 | address2 = self._read_equal()
719 | if address2 is not FAILURE:
720 | elements0.append(address2)
721 | address3 = self._read_wildcard_value()
722 | if address3 is not FAILURE:
723 | elements0.append(address3)
724 | else:
725 | elements0 = None
726 | self._offset = index1
727 | else:
728 | elements0 = None
729 | self._offset = index1
730 | else:
731 | elements0 = None
732 | self._offset = index1
733 | if elements0 is None:
734 | address0 = FAILURE
735 | else:
736 | address0 = self._actions.return_wildcard(self._input, index1, self._offset, elements0)
737 | self._offset = self._offset
738 | self._cache['wildcard'][index0] = (address0, self._offset)
739 | return address0
740 |
741 | def _read_wildcard_value(self):
742 | address0, index0 = FAILURE, self._offset
743 | cached = self._cache['wildcard_value'].get(index0)
744 | if cached:
745 | self._offset = cached[1]
746 | return cached[0]
747 | index1, elements0 = self._offset, []
748 | index2 = self._offset
749 | address1 = self._read_value()
750 | if address1 is FAILURE:
751 | address1 = TreeNode(self._input[index2:index2], index2)
752 | self._offset = index2
753 | if address1 is not FAILURE:
754 | elements0.append(address1)
755 | address2 = self._read_any()
756 | if address2 is not FAILURE:
757 | elements0.append(address2)
758 | index3 = self._offset
759 | address3 = self._read_value()
760 | if address3 is FAILURE:
761 | address3 = TreeNode(self._input[index3:index3], index3)
762 | self._offset = index3
763 | if address3 is not FAILURE:
764 | elements0.append(address3)
765 | else:
766 | elements0 = None
767 | self._offset = index1
768 | else:
769 | elements0 = None
770 | self._offset = index1
771 | else:
772 | elements0 = None
773 | self._offset = index1
774 | if elements0 is None:
775 | address0 = FAILURE
776 | else:
777 | address0 = TreeNode8(self._input[index1:self._offset], index1, elements0)
778 | self._offset = self._offset
779 | self._cache['wildcard_value'][index0] = (address0, self._offset)
780 | return address0
781 |
782 | def _read_any(self):
783 | address0, index0 = FAILURE, self._offset
784 | cached = self._cache['any'].get(index0)
785 | if cached:
786 | self._offset = cached[1]
787 | return cached[0]
788 | index1, elements0 = self._offset, []
789 | chunk0 = None
790 | if self._offset < self._input_size:
791 | chunk0 = self._input[self._offset:self._offset + 1]
792 | if chunk0 == '*':
793 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
794 | self._offset = self._offset + 1
795 | else:
796 | address1 = FAILURE
797 | if self._offset > self._failure:
798 | self._failure = self._offset
799 | self._expected = []
800 | if self._offset == self._failure:
801 | self._expected.append('\'*\'')
802 | if address1 is not FAILURE:
803 | elements0.append(address1)
804 | remaining0, index2, elements1, address3 = 0, self._offset, [], True
805 | while address3 is not FAILURE:
806 | index3, elements2 = self._offset, []
807 | address4 = self._read_value()
808 | if address4 is not FAILURE:
809 | elements2.append(address4)
810 | chunk1 = None
811 | if self._offset < self._input_size:
812 | chunk1 = self._input[self._offset:self._offset + 1]
813 | if chunk1 == '*':
814 | address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
815 | self._offset = self._offset + 1
816 | else:
817 | address5 = FAILURE
818 | if self._offset > self._failure:
819 | self._failure = self._offset
820 | self._expected = []
821 | if self._offset == self._failure:
822 | self._expected.append('\'*\'')
823 | if address5 is not FAILURE:
824 | elements2.append(address5)
825 | else:
826 | elements2 = None
827 | self._offset = index3
828 | else:
829 | elements2 = None
830 | self._offset = index3
831 | if elements2 is None:
832 | address3 = FAILURE
833 | else:
834 | address3 = TreeNode9(self._input[index3:self._offset], index3, elements2)
835 | self._offset = self._offset
836 | if address3 is not FAILURE:
837 | elements1.append(address3)
838 | remaining0 -= 1
839 | if remaining0 <= 0:
840 | address2 = TreeNode(self._input[index2:self._offset], index2, elements1)
841 | self._offset = self._offset
842 | else:
843 | address2 = FAILURE
844 | if address2 is not FAILURE:
845 | elements0.append(address2)
846 | else:
847 | elements0 = None
848 | self._offset = index1
849 | else:
850 | elements0 = None
851 | self._offset = index1
852 | if elements0 is None:
853 | address0 = FAILURE
854 | else:
855 | address0 = self._actions.return_string(self._input, index1, self._offset, elements0)
856 | self._offset = self._offset
857 | self._cache['any'][index0] = (address0, self._offset)
858 | return address0
859 |
860 | def _read_attr(self):
861 | address0, index0 = FAILURE, self._offset
862 | cached = self._cache['attr'].get(index0)
863 | if cached:
864 | self._offset = cached[1]
865 | return cached[0]
866 | address0 = self._read_AttributeDescription()
867 | self._cache['attr'][index0] = (address0, self._offset)
868 | return address0
869 |
870 | def _read_value(self):
871 | address0, index0 = FAILURE, self._offset
872 | cached = self._cache['value'].get(index0)
873 | if cached:
874 | self._offset = cached[1]
875 | return cached[0]
876 | remaining0, index1, elements0, address1 = 1, self._offset, [], True
877 | while address1 is not FAILURE:
878 | address1 = self._read_AttributeValue()
879 | if address1 is not FAILURE:
880 | elements0.append(address1)
881 | remaining0 -= 1
882 | if remaining0 <= 0:
883 | address0 = self._actions.return_string(self._input, index1, self._offset, elements0)
884 | self._offset = self._offset
885 | else:
886 | address0 = FAILURE
887 | self._cache['value'][index0] = (address0, self._offset)
888 | return address0
889 |
890 | def _read_AttributeDescription(self):
891 | address0, index0 = FAILURE, self._offset
892 | cached = self._cache['AttributeDescription'].get(index0)
893 | if cached:
894 | self._offset = cached[1]
895 | return cached[0]
896 | index1, elements0 = self._offset, []
897 | address1 = self._read_AttributeType()
898 | if address1 is not FAILURE:
899 | elements0.append(address1)
900 | index2 = self._offset
901 | index3, elements1 = self._offset, []
902 | chunk0 = None
903 | if self._offset < self._input_size:
904 | chunk0 = self._input[self._offset:self._offset + 1]
905 | if chunk0 == ';':
906 | address3 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
907 | self._offset = self._offset + 1
908 | else:
909 | address3 = FAILURE
910 | if self._offset > self._failure:
911 | self._failure = self._offset
912 | self._expected = []
913 | if self._offset == self._failure:
914 | self._expected.append('";"')
915 | if address3 is not FAILURE:
916 | elements1.append(address3)
917 | address4 = self._read_options()
918 | if address4 is not FAILURE:
919 | elements1.append(address4)
920 | else:
921 | elements1 = None
922 | self._offset = index3
923 | else:
924 | elements1 = None
925 | self._offset = index3
926 | if elements1 is None:
927 | address2 = FAILURE
928 | else:
929 | address2 = TreeNode11(self._input[index3:self._offset], index3, elements1)
930 | self._offset = self._offset
931 | if address2 is FAILURE:
932 | address2 = TreeNode(self._input[index2:index2], index2)
933 | self._offset = index2
934 | if address2 is not FAILURE:
935 | elements0.append(address2)
936 | else:
937 | elements0 = None
938 | self._offset = index1
939 | else:
940 | elements0 = None
941 | self._offset = index1
942 | if elements0 is None:
943 | address0 = FAILURE
944 | else:
945 | address0 = self._actions.return_options(self._input, index1, self._offset, elements0)
946 | self._offset = self._offset
947 | self._cache['AttributeDescription'][index0] = (address0, self._offset)
948 | return address0
949 |
950 | def _read_AttributeOptions(self):
951 | address0, index0 = FAILURE, self._offset
952 | cached = self._cache['AttributeOptions'].get(index0)
953 | if cached:
954 | self._offset = cached[1]
955 | return cached[0]
956 | index1, elements0 = self._offset, []
957 | chunk0 = None
958 | if self._offset < self._input_size:
959 | chunk0 = self._input[self._offset:self._offset + 1]
960 | if chunk0 == ';':
961 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
962 | self._offset = self._offset + 1
963 | else:
964 | address1 = FAILURE
965 | if self._offset > self._failure:
966 | self._failure = self._offset
967 | self._expected = []
968 | if self._offset == self._failure:
969 | self._expected.append('";"')
970 | if address1 is not FAILURE:
971 | elements0.append(address1)
972 | address2 = self._read_options()
973 | if address2 is not FAILURE:
974 | elements0.append(address2)
975 | else:
976 | elements0 = None
977 | self._offset = index1
978 | else:
979 | elements0 = None
980 | self._offset = index1
981 | if elements0 is None:
982 | address0 = FAILURE
983 | else:
984 | address0 = TreeNode12(self._input[index1:self._offset], index1, elements0)
985 | self._offset = self._offset
986 | self._cache['AttributeOptions'][index0] = (address0, self._offset)
987 | return address0
988 |
989 | def _read_AttributeType(self):
990 | address0, index0 = FAILURE, self._offset
991 | cached = self._cache['AttributeType'].get(index0)
992 | if cached:
993 | self._offset = cached[1]
994 | return cached[0]
995 | index1 = self._offset
996 | address0 = self._read_LDAP_OID()
997 | if address0 is FAILURE:
998 | self._offset = index1
999 | address0 = self._read_AttrTypeName()
1000 | if address0 is FAILURE:
1001 | self._offset = index1
1002 | self._cache['AttributeType'][index0] = (address0, self._offset)
1003 | return address0
1004 |
1005 | def _read_AttrTypeName(self):
1006 | address0, index0 = FAILURE, self._offset
1007 | cached = self._cache['AttrTypeName'].get(index0)
1008 | if cached:
1009 | self._offset = cached[1]
1010 | return cached[0]
1011 | index1, elements0 = self._offset, []
1012 | address1 = self._read_ALPHA()
1013 | if address1 is not FAILURE:
1014 | elements0.append(address1)
1015 | remaining0, index2, elements1, address3 = 0, self._offset, [], True
1016 | while address3 is not FAILURE:
1017 | address3 = self._read_AttrTypeChars()
1018 | if address3 is not FAILURE:
1019 | elements1.append(address3)
1020 | remaining0 -= 1
1021 | if remaining0 <= 0:
1022 | address2 = TreeNode(self._input[index2:self._offset], index2, elements1)
1023 | self._offset = self._offset
1024 | else:
1025 | address2 = FAILURE
1026 | if address2 is not FAILURE:
1027 | elements0.append(address2)
1028 | else:
1029 | elements0 = None
1030 | self._offset = index1
1031 | else:
1032 | elements0 = None
1033 | self._offset = index1
1034 | if elements0 is None:
1035 | address0 = FAILURE
1036 | else:
1037 | address0 = self._actions.return_attr_type(self._input, index1, self._offset, elements0)
1038 | self._offset = self._offset
1039 | self._cache['AttrTypeName'][index0] = (address0, self._offset)
1040 | return address0
1041 |
1042 | def _read_AttrTypeChars(self):
1043 | address0, index0 = FAILURE, self._offset
1044 | cached = self._cache['AttrTypeChars'].get(index0)
1045 | if cached:
1046 | self._offset = cached[1]
1047 | return cached[0]
1048 | index1 = self._offset
1049 | address0 = self._read_ALPHA()
1050 | if address0 is FAILURE:
1051 | self._offset = index1
1052 | address0 = self._read_DIGIT()
1053 | if address0 is FAILURE:
1054 | self._offset = index1
1055 | chunk0 = None
1056 | if self._offset < self._input_size:
1057 | chunk0 = self._input[self._offset:self._offset + 1]
1058 | if chunk0 == '-':
1059 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1060 | self._offset = self._offset + 1
1061 | else:
1062 | address0 = FAILURE
1063 | if self._offset > self._failure:
1064 | self._failure = self._offset
1065 | self._expected = []
1066 | if self._offset == self._failure:
1067 | self._expected.append('"-"')
1068 | if address0 is FAILURE:
1069 | self._offset = index1
1070 | self._cache['AttrTypeChars'][index0] = (address0, self._offset)
1071 | return address0
1072 |
1073 | def _read_LDAP_OID(self):
1074 | address0, index0 = FAILURE, self._offset
1075 | cached = self._cache['LDAP_OID'].get(index0)
1076 | if cached:
1077 | self._offset = cached[1]
1078 | return cached[0]
1079 | index1, elements0 = self._offset, []
1080 | remaining0, index2, elements1, address2 = 1, self._offset, [], True
1081 | while address2 is not FAILURE:
1082 | address2 = self._read_DIGIT()
1083 | if address2 is not FAILURE:
1084 | elements1.append(address2)
1085 | remaining0 -= 1
1086 | if remaining0 <= 0:
1087 | address1 = TreeNode(self._input[index2:self._offset], index2, elements1)
1088 | self._offset = self._offset
1089 | else:
1090 | address1 = FAILURE
1091 | if address1 is not FAILURE:
1092 | elements0.append(address1)
1093 | remaining1, index3, elements2, address4 = 0, self._offset, [], True
1094 | while address4 is not FAILURE:
1095 | index4, elements3 = self._offset, []
1096 | chunk0 = None
1097 | if self._offset < self._input_size:
1098 | chunk0 = self._input[self._offset:self._offset + 1]
1099 | if chunk0 == '.':
1100 | address5 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1101 | self._offset = self._offset + 1
1102 | else:
1103 | address5 = FAILURE
1104 | if self._offset > self._failure:
1105 | self._failure = self._offset
1106 | self._expected = []
1107 | if self._offset == self._failure:
1108 | self._expected.append('"."')
1109 | if address5 is not FAILURE:
1110 | elements3.append(address5)
1111 | address6 = FAILURE
1112 | remaining2, index5, elements4, address7 = 1, self._offset, [], True
1113 | while address7 is not FAILURE:
1114 | address7 = self._read_DIGIT()
1115 | if address7 is not FAILURE:
1116 | elements4.append(address7)
1117 | remaining2 -= 1
1118 | if remaining2 <= 0:
1119 | address6 = TreeNode(self._input[index5:self._offset], index5, elements4)
1120 | self._offset = self._offset
1121 | else:
1122 | address6 = FAILURE
1123 | if address6 is not FAILURE:
1124 | elements3.append(address6)
1125 | else:
1126 | elements3 = None
1127 | self._offset = index4
1128 | else:
1129 | elements3 = None
1130 | self._offset = index4
1131 | if elements3 is None:
1132 | address4 = FAILURE
1133 | else:
1134 | address4 = TreeNode(self._input[index4:self._offset], index4, elements3)
1135 | self._offset = self._offset
1136 | if address4 is not FAILURE:
1137 | elements2.append(address4)
1138 | remaining1 -= 1
1139 | if remaining1 <= 0:
1140 | address3 = TreeNode(self._input[index3:self._offset], index3, elements2)
1141 | self._offset = self._offset
1142 | else:
1143 | address3 = FAILURE
1144 | if address3 is not FAILURE:
1145 | elements0.append(address3)
1146 | else:
1147 | elements0 = None
1148 | self._offset = index1
1149 | else:
1150 | elements0 = None
1151 | self._offset = index1
1152 | if elements0 is None:
1153 | address0 = FAILURE
1154 | else:
1155 | address0 = self._actions.return_oid_type(self._input, index1, self._offset, elements0)
1156 | self._offset = self._offset
1157 | self._cache['LDAP_OID'][index0] = (address0, self._offset)
1158 | return address0
1159 |
1160 | def _read_options(self):
1161 | address0, index0 = FAILURE, self._offset
1162 | cached = self._cache['options'].get(index0)
1163 | if cached:
1164 | self._offset = cached[1]
1165 | return cached[0]
1166 | index1 = self._offset
1167 | index2, elements0 = self._offset, []
1168 | address1 = self._read_option()
1169 | if address1 is not FAILURE:
1170 | elements0.append(address1)
1171 | chunk0 = None
1172 | if self._offset < self._input_size:
1173 | chunk0 = self._input[self._offset:self._offset + 1]
1174 | if chunk0 == ';':
1175 | address2 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1176 | self._offset = self._offset + 1
1177 | else:
1178 | address2 = FAILURE
1179 | if self._offset > self._failure:
1180 | self._failure = self._offset
1181 | self._expected = []
1182 | if self._offset == self._failure:
1183 | self._expected.append('";"')
1184 | if address2 is not FAILURE:
1185 | elements0.append(address2)
1186 | address3 = self._read_options()
1187 | if address3 is not FAILURE:
1188 | elements0.append(address3)
1189 | else:
1190 | elements0 = None
1191 | self._offset = index2
1192 | else:
1193 | elements0 = None
1194 | self._offset = index2
1195 | else:
1196 | elements0 = None
1197 | self._offset = index2
1198 | if elements0 is None:
1199 | address0 = FAILURE
1200 | else:
1201 | address0 = self._actions.return_string(self._input, index2, self._offset, elements0)
1202 | self._offset = self._offset
1203 | if address0 is FAILURE:
1204 | self._offset = index1
1205 | address0 = self._read_option()
1206 | if address0 is FAILURE:
1207 | self._offset = index1
1208 | self._cache['options'][index0] = (address0, self._offset)
1209 | return address0
1210 |
1211 | def _read_option(self):
1212 | address0, index0 = FAILURE, self._offset
1213 | cached = self._cache['option'].get(index0)
1214 | if cached:
1215 | self._offset = cached[1]
1216 | return cached[0]
1217 | remaining0, index1, elements0, address1 = 1, self._offset, [], True
1218 | while address1 is not FAILURE:
1219 | address1 = self._read_AttrTypeChars()
1220 | if address1 is not FAILURE:
1221 | elements0.append(address1)
1222 | remaining0 -= 1
1223 | if remaining0 <= 0:
1224 | address0 = self._actions.return_string(self._input, index1, self._offset, elements0)
1225 | self._offset = self._offset
1226 | else:
1227 | address0 = FAILURE
1228 | self._cache['option'][index0] = (address0, self._offset)
1229 | return address0
1230 |
1231 | def _read_AttributeValue(self):
1232 | address0, index0 = FAILURE, self._offset
1233 | cached = self._cache['AttributeValue'].get(index0)
1234 | if cached:
1235 | self._offset = cached[1]
1236 | return cached[0]
1237 | index1 = self._offset
1238 | address0 = self._read_EscapedCharacter()
1239 | if address0 is FAILURE:
1240 | self._offset = index1
1241 | chunk0 = None
1242 | if self._offset < self._input_size:
1243 | chunk0 = self._input[self._offset:self._offset + 1]
1244 | if chunk0 is not None and Grammar.REGEX_1.search(chunk0):
1245 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1246 | self._offset = self._offset + 1
1247 | else:
1248 | address0 = FAILURE
1249 | if self._offset > self._failure:
1250 | self._failure = self._offset
1251 | self._expected = []
1252 | if self._offset == self._failure:
1253 | self._expected.append('[^!*\\x29]')
1254 | if address0 is FAILURE:
1255 | self._offset = index1
1256 | self._cache['AttributeValue'][index0] = (address0, self._offset)
1257 | return address0
1258 |
1259 | def _read_EscapedCharacter(self):
1260 | address0, index0 = FAILURE, self._offset
1261 | cached = self._cache['EscapedCharacter'].get(index0)
1262 | if cached:
1263 | self._offset = cached[1]
1264 | return cached[0]
1265 | index1, elements0 = self._offset, []
1266 | chunk0 = None
1267 | if self._offset < self._input_size:
1268 | chunk0 = self._input[self._offset:self._offset + 1]
1269 | if chunk0 == '\\':
1270 | address1 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1271 | self._offset = self._offset + 1
1272 | else:
1273 | address1 = FAILURE
1274 | if self._offset > self._failure:
1275 | self._failure = self._offset
1276 | self._expected = []
1277 | if self._offset == self._failure:
1278 | self._expected.append('\'\\\\\'')
1279 | if address1 is not FAILURE:
1280 | elements0.append(address1)
1281 | address2 = self._read_ASCII_VALUE()
1282 | if address2 is not FAILURE:
1283 | elements0.append(address2)
1284 | else:
1285 | elements0 = None
1286 | self._offset = index1
1287 | else:
1288 | elements0 = None
1289 | self._offset = index1
1290 | if elements0 is None:
1291 | address0 = FAILURE
1292 | else:
1293 | address0 = self._actions.return_escaped_char(self._input, index1, self._offset, elements0)
1294 | self._offset = self._offset
1295 | self._cache['EscapedCharacter'][index0] = (address0, self._offset)
1296 | return address0
1297 |
1298 | def _read_ASCII_VALUE(self):
1299 | address0, index0 = FAILURE, self._offset
1300 | cached = self._cache['ASCII_VALUE'].get(index0)
1301 | if cached:
1302 | self._offset = cached[1]
1303 | return cached[0]
1304 | index1, elements0 = self._offset, []
1305 | address1 = self._read_HEX_CHAR()
1306 | if address1 is not FAILURE:
1307 | elements0.append(address1)
1308 | address2 = self._read_HEX_CHAR()
1309 | if address2 is not FAILURE:
1310 | elements0.append(address2)
1311 | else:
1312 | elements0 = None
1313 | self._offset = index1
1314 | else:
1315 | elements0 = None
1316 | self._offset = index1
1317 | if elements0 is None:
1318 | address0 = FAILURE
1319 | else:
1320 | address0 = self._actions.return_hex(self._input, index1, self._offset, elements0)
1321 | self._offset = self._offset
1322 | self._cache['ASCII_VALUE'][index0] = (address0, self._offset)
1323 | return address0
1324 |
1325 | def _read_HEX_CHAR(self):
1326 | address0, index0 = FAILURE, self._offset
1327 | cached = self._cache['HEX_CHAR'].get(index0)
1328 | if cached:
1329 | self._offset = cached[1]
1330 | return cached[0]
1331 | chunk0 = None
1332 | if self._offset < self._input_size:
1333 | chunk0 = self._input[self._offset:self._offset + 1]
1334 | if chunk0 is not None and Grammar.REGEX_2.search(chunk0):
1335 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1336 | self._offset = self._offset + 1
1337 | else:
1338 | address0 = FAILURE
1339 | if self._offset > self._failure:
1340 | self._failure = self._offset
1341 | self._expected = []
1342 | if self._offset == self._failure:
1343 | self._expected.append('[a-fA-F0-9]')
1344 | self._cache['HEX_CHAR'][index0] = (address0, self._offset)
1345 | return address0
1346 |
1347 | def _read_FILL(self):
1348 | address0, index0 = FAILURE, self._offset
1349 | cached = self._cache['FILL'].get(index0)
1350 | if cached:
1351 | self._offset = cached[1]
1352 | return cached[0]
1353 | index1 = self._offset
1354 | address0 = self._read_SPACE()
1355 | if address0 is FAILURE:
1356 | self._offset = index1
1357 | address0 = self._read_TAB()
1358 | if address0 is FAILURE:
1359 | self._offset = index1
1360 | address0 = self._read_SEP()
1361 | if address0 is FAILURE:
1362 | self._offset = index1
1363 | self._cache['FILL'][index0] = (address0, self._offset)
1364 | return address0
1365 |
1366 | def _read_SPACE(self):
1367 | address0, index0 = FAILURE, self._offset
1368 | cached = self._cache['SPACE'].get(index0)
1369 | if cached:
1370 | self._offset = cached[1]
1371 | return cached[0]
1372 | chunk0 = None
1373 | if self._offset < self._input_size:
1374 | chunk0 = self._input[self._offset:self._offset + 1]
1375 | if chunk0 is not None and Grammar.REGEX_3.search(chunk0):
1376 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1377 | self._offset = self._offset + 1
1378 | else:
1379 | address0 = FAILURE
1380 | if self._offset > self._failure:
1381 | self._failure = self._offset
1382 | self._expected = []
1383 | if self._offset == self._failure:
1384 | self._expected.append('[\\x20]')
1385 | self._cache['SPACE'][index0] = (address0, self._offset)
1386 | return address0
1387 |
1388 | def _read_TAB(self):
1389 | address0, index0 = FAILURE, self._offset
1390 | cached = self._cache['TAB'].get(index0)
1391 | if cached:
1392 | self._offset = cached[1]
1393 | return cached[0]
1394 | chunk0 = None
1395 | if self._offset < self._input_size:
1396 | chunk0 = self._input[self._offset:self._offset + 1]
1397 | if chunk0 is not None and Grammar.REGEX_4.search(chunk0):
1398 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1399 | self._offset = self._offset + 1
1400 | else:
1401 | address0 = FAILURE
1402 | if self._offset > self._failure:
1403 | self._failure = self._offset
1404 | self._expected = []
1405 | if self._offset == self._failure:
1406 | self._expected.append('[\\x09]')
1407 | self._cache['TAB'][index0] = (address0, self._offset)
1408 | return address0
1409 |
1410 | def _read_DIGIT(self):
1411 | address0, index0 = FAILURE, self._offset
1412 | cached = self._cache['DIGIT'].get(index0)
1413 | if cached:
1414 | self._offset = cached[1]
1415 | return cached[0]
1416 | chunk0 = None
1417 | if self._offset < self._input_size:
1418 | chunk0 = self._input[self._offset:self._offset + 1]
1419 | if chunk0 is not None and Grammar.REGEX_5.search(chunk0):
1420 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1421 | self._offset = self._offset + 1
1422 | else:
1423 | address0 = FAILURE
1424 | if self._offset > self._failure:
1425 | self._failure = self._offset
1426 | self._expected = []
1427 | if self._offset == self._failure:
1428 | self._expected.append('[0-9]')
1429 | self._cache['DIGIT'][index0] = (address0, self._offset)
1430 | return address0
1431 |
1432 | def _read_ALPHA(self):
1433 | address0, index0 = FAILURE, self._offset
1434 | cached = self._cache['ALPHA'].get(index0)
1435 | if cached:
1436 | self._offset = cached[1]
1437 | return cached[0]
1438 | chunk0 = None
1439 | if self._offset < self._input_size:
1440 | chunk0 = self._input[self._offset:self._offset + 1]
1441 | if chunk0 is not None and Grammar.REGEX_6.search(chunk0):
1442 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1443 | self._offset = self._offset + 1
1444 | else:
1445 | address0 = FAILURE
1446 | if self._offset > self._failure:
1447 | self._failure = self._offset
1448 | self._expected = []
1449 | if self._offset == self._failure:
1450 | self._expected.append('[a-zA-Z]')
1451 | self._cache['ALPHA'][index0] = (address0, self._offset)
1452 | return address0
1453 |
1454 | def _read_SEP(self):
1455 | address0, index0 = FAILURE, self._offset
1456 | cached = self._cache['SEP'].get(index0)
1457 | if cached:
1458 | self._offset = cached[1]
1459 | return cached[0]
1460 | index1 = self._offset
1461 | chunk0 = None
1462 | if self._offset < self._input_size:
1463 | chunk0 = self._input[self._offset:self._offset + 2]
1464 | if chunk0 == '\r\n':
1465 | address0 = TreeNode(self._input[self._offset:self._offset + 2], self._offset)
1466 | self._offset = self._offset + 2
1467 | else:
1468 | address0 = FAILURE
1469 | if self._offset > self._failure:
1470 | self._failure = self._offset
1471 | self._expected = []
1472 | if self._offset == self._failure:
1473 | self._expected.append('"\\r\\n"')
1474 | if address0 is FAILURE:
1475 | self._offset = index1
1476 | chunk1 = None
1477 | if self._offset < self._input_size:
1478 | chunk1 = self._input[self._offset:self._offset + 1]
1479 | if chunk1 == '\n':
1480 | address0 = TreeNode(self._input[self._offset:self._offset + 1], self._offset)
1481 | self._offset = self._offset + 1
1482 | else:
1483 | address0 = FAILURE
1484 | if self._offset > self._failure:
1485 | self._failure = self._offset
1486 | self._expected = []
1487 | if self._offset == self._failure:
1488 | self._expected.append('"\\n"')
1489 | if address0 is FAILURE:
1490 | self._offset = index1
1491 | self._cache['SEP'][index0] = (address0, self._offset)
1492 | return address0
1493 |
1494 |
1495 | class Parser(Grammar):
1496 | def __init__(self, inpt, actions, types):
1497 | self._input = inpt
1498 | self._input_size = len(inpt)
1499 | self._actions = actions
1500 | self._types = types
1501 | self._offset = 0
1502 | self._cache = defaultdict(dict)
1503 | self._failure = 0
1504 | self._expected = []
1505 |
1506 | def parse(self):
1507 | tree = self._read_root()
1508 | if tree is not FAILURE and self._offset == self._input_size:
1509 | return tree
1510 | if not self._expected:
1511 | self._failure = self._offset
1512 | self._expected.append('')
1513 | raise ParseError(format_error(self._input, self._failure, self._expected))
1514 |
1515 |
1516 | def format_error(inpt, offset, expected):
1517 | lines, line_no, position = inpt.split('\n'), 0, 0
1518 | while position <= offset:
1519 | position += len(lines[line_no]) + 1
1520 | line_no += 1
1521 | message, line = 'Line ' + str(line_no) + ': expected ' + ', '.join(expected) + '\n', lines[line_no - 1]
1522 | message += line + '\n'
1523 | position -= len(line) + 1
1524 | message += ' ' * (offset - position)
1525 | return message + '^'
1526 |
1527 |
1528 | def parse(inpt, actions=None, types=None):
1529 | parser = Parser(inpt, actions, types)
1530 | return parser.parse()
1531 |
--------------------------------------------------------------------------------
/ldap_filter/soundex.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | def soundex(string, scale=4):
5 | string = string.upper()
6 | code = string[0]
7 | string = re.sub(r'[AEIOUYHW]', '', string)
8 | chr_key = {'BFPV': '1', 'CGJKQSXZ': '2', 'DT': '3', 'L': '4', 'MN': '5', 'R': '6'}
9 |
10 | for c in string[1:]:
11 | for k, v in chr_key.items():
12 | if (c in k) and (v != code[-1]):
13 | code += v
14 | break
15 |
16 | return code.ljust(scale, '0')
17 |
18 |
19 | def soundex_compare(val1, val2):
20 | return soundex(val1) == soundex(val2)
21 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "ldap-filter"
7 | version = "1.0.1"
8 | description = "A Python utility library for working with Lightweight Directory Access Protocol (LDAP) filters."
9 | readme = "README.md"
10 | authors = [{ name = "Stephen Ewell", email = "steve@ewell.io" }]
11 | license = { file = "LICENSE.txt" }
12 | classifiers = [
13 | "Intended Audience :: Developers",
14 | "Topic :: Software Development :: Libraries :: Python Modules",
15 | "License :: OSI Approved :: MIT License",
16 | "Programming Language :: Python",
17 | "Programming Language :: Python :: 3",
18 | "Programming Language :: Python :: 3.4",
19 | "Programming Language :: Python :: 3.5",
20 | "Programming Language :: Python :: 3.6",
21 | "Programming Language :: Python :: 3.7",
22 | "Programming Language :: Python :: 3.8",
23 | "Programming Language :: Python :: 3.9",
24 | "Programming Language :: Python :: 3.10",
25 | "Programming Language :: Python :: 3.11",
26 | "Programming Language :: Python :: 3.12",
27 | ]
28 | keywords = ["ldap", "filter", "rfc4515", "utility", "development"]
29 | requires-python = ">=3.4"
30 |
31 | [project.optional-dependencies]
32 | test = ["pytest", "coverage"]
33 |
34 | [project.urls]
35 | Homepage = "https://github.com/SteveEwell/python-ldap-filter"
36 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """
2 | python-ldap-filter
3 | ==================
4 |
5 | **Build and generate LDAP filters**
6 |
7 | A Python utility library for working with Lightweight Directory Access
8 | Protocol (LDAP) filters.
9 |
10 | This project is a port of the
11 | `node-ldap-filters `__ and
12 | implements many of the same APIs. The filters produced by the library
13 | are based on `RFC 4515 `__.
14 |
15 | Links
16 | -----
17 |
18 | `GitHub `_
19 |
20 | """
21 |
22 | import os
23 | import sys
24 | from setuptools import setup
25 |
26 | if sys.version_info[0] <= 2 or (sys.version_info[0] == 3 and sys.version_info < (3, 4)):
27 | raise RuntimeError('This software requires Python version 3.4 or higher.')
28 |
29 | if os.path.isfile("MANIFEST"):
30 | os.unlink("MANIFEST")
31 |
32 | rootdir = os.path.dirname(__file__) or "."
33 |
34 |
35 | setup()
36 |
--------------------------------------------------------------------------------
/tests/test_filter_builder.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from ldap_filter import Filter
3 |
4 |
5 | class TestFilterAttributes:
6 | def test_present(self):
7 | filt = Filter.attribute('attr').present()
8 | string = filt.to_string()
9 | assert string == '(attr=*)'
10 |
11 | def test_equal_to(self):
12 | filt = Filter.attribute('attr').equal_to('value')
13 | string = filt.to_string()
14 | assert string == '(attr=value)'
15 |
16 | def test_contains(self):
17 | filt = Filter.attribute('attr').contains('value')
18 | string = filt.to_string()
19 | assert string == '(attr=*value*)'
20 |
21 | def test_starts_with(self):
22 | filt = Filter.attribute('attr').starts_with('value')
23 | string = filt.to_string()
24 | assert string == '(attr=value*)'
25 |
26 | def test_ends_with(self):
27 | filt = Filter.attribute('attr').ends_with('value')
28 | string = filt.to_string()
29 | assert string == '(attr=*value)'
30 |
31 | def test_approx(self):
32 | filt = Filter.attribute('attr').approx('value')
33 | string = filt.to_string()
34 | assert string == '(attr~=value)'
35 |
36 | def test_greater_than(self):
37 | filt = Filter.attribute('attr').gte('value')
38 | string = filt.to_string()
39 | assert string == '(attr>=value)'
40 |
41 | def test_lesser_than(self):
42 | filt = Filter.attribute('attr').lte('value')
43 | string = filt.to_string()
44 | assert string == '(attr<=value)'
45 |
46 | def test_raw(self):
47 | filt = Filter.attribute('attr').raw('value*value')
48 | string = filt.to_string()
49 | assert string == '(attr=value*value)'
50 |
51 |
52 | class TestFilterEscapes:
53 | def test_escape(self):
54 | string = Filter.escape('a * (complex) \\value')
55 | assert string == 'a \\2a \\28complex\\29 \\5cvalue'
56 |
57 | def test_unescape(self):
58 | string = Filter.unescape('a \\2a \\28complex\\29 \\5cvalue')
59 | assert string == 'a * (complex) \\value'
60 |
61 | def test_filter_escape(self):
62 | filt = Filter.attribute('escaped').equal_to('a * (complex) \\value')
63 | string = filt.to_string()
64 | assert string == '(escaped=a \\2a \\28complex\\29 \\5cvalue)'
65 |
66 | def test_filter_convert_int(self):
67 | filt = Filter.attribute('number').equal_to(1000)
68 | string = filt.to_string()
69 | assert string == '(number=1000)'
70 |
71 | def test_filter_convert_float(self):
72 | filt = Filter.attribute('number').equal_to(10.26)
73 | string = filt.to_string()
74 | assert string == '(number=10.26)'
75 |
76 | def test_filter_convert_negative(self):
77 | filt = Filter.attribute('number').equal_to(-10)
78 | string = filt.to_string()
79 | assert string == '(number=-10)'
80 |
81 |
82 | class TestFilterAggregates:
83 | def test_and_aggregate(self):
84 | filt = Filter.AND([
85 | Filter.attribute('givenName').equal_to('bilbo'),
86 | Filter.attribute('sn').equal_to('baggens')
87 | ])
88 | string = filt.to_string()
89 | assert string == '(&(givenName=bilbo)(sn=baggens))'
90 |
91 | def test_or_aggregate(self):
92 | filt = Filter.OR([
93 | Filter.attribute('givenName').equal_to('bilbo'),
94 | Filter.attribute('sn').equal_to('baggens')
95 | ])
96 | string = filt.to_string()
97 | assert string == '(|(givenName=bilbo)(sn=baggens))'
98 |
99 | def test_not_aggregate(self):
100 | filt = Filter.NOT([
101 | Filter.attribute('givenName').equal_to('bilbo')
102 | ])
103 | string = filt.to_string()
104 | assert string == '(!(givenName=bilbo))'
105 |
--------------------------------------------------------------------------------
/tests/test_filter_match.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from ldap_filter import Filter
3 |
4 |
5 | class TestFilterMatch:
6 | def test_equality(self):
7 | filt = Filter.attribute('sn').equal_to('smith')
8 | assert filt.match({'sn': 'smith'})
9 | assert filt.match({'sn': 'SMITH'})
10 | assert not filt.match({'sn': 'bob'})
11 |
12 | def test_multi_value_equality(self):
13 | filt = Filter.attribute('sn').equal_to('smith')
14 | data = {'sn': ['Sam', 'Smith', 'Swanson', 'Samson']}
15 | assert filt.match(data)
16 | data = {'sn': ['Sam', 'Swanson', 'Samson']}
17 | assert not filt.match(data)
18 |
19 | def test_present(self):
20 | filt = Filter.attribute('sn').present()
21 | assert filt.match({'sn': 'smith'})
22 | assert filt.match({'sn': 'alex'})
23 | assert not filt.match({'mail': 'smith'})
24 |
25 | def test_present_parsed(self):
26 | filt = Filter.parse('(sn=*)')
27 | assert filt.match({'sn': 'smith'})
28 | assert filt.match({'sn': 'alex'})
29 | assert not filt.match({'mail': 'smith'})
30 |
31 | def test_contains(self):
32 | filt = Filter.attribute('sn').contains('smith')
33 | assert filt.match({'sn': 'smith'})
34 | assert filt.match({'sn': 'smith-jonson'})
35 | assert filt.match({'sn': 'jonson-smith'})
36 | assert filt.match({'sn': 'Von Ubersmith'})
37 | assert not filt.match({'sn': 'Jonson'})
38 |
39 | def test_starts_with(self):
40 | filt = Filter.attribute('sn').starts_with('smith')
41 | assert filt.match({'sn': 'smith'})
42 | assert filt.match({'sn': 'smith-jonson'})
43 | assert not filt.match({'sn': 'Von Ubersmith'})
44 |
45 | def test_ends_with(self):
46 | filt = Filter.attribute('sn').ends_with('smith')
47 | assert filt.match({'sn': 'smith'})
48 | assert filt.match({'sn': 'Von Ubersmith'})
49 | assert not filt.match({'sn': 'smith-jonson'})
50 |
51 | def test_greater_than_numeric(self):
52 | filt = Filter.attribute('age').gte('10')
53 | assert filt.match({'age': 10})
54 | assert filt.match({'age': '10'})
55 | assert filt.match({'age': 11})
56 | assert filt.match({'age': '11'})
57 | assert not filt.match({'age': 9})
58 | assert not filt.match({'age': '9'})
59 |
60 | def test_greater_than_lexical(self):
61 | filt = Filter.attribute('name').gte('bob')
62 | assert filt.match({'name': 'bob'})
63 | assert filt.match({'name': 'cell'})
64 | assert not filt.match({'name': 'acme'})
65 |
66 | def test_less_than_numeric(self):
67 | filt = Filter.attribute('age').lte('10')
68 | assert filt.match({'age': 9})
69 | assert filt.match({'age': '9'})
70 | assert filt.match({'age': 10})
71 | assert filt.match({'age': '10'})
72 | assert not filt.match({'age': 11})
73 | assert not filt.match({'age': '11'})
74 |
75 | def test_less_than_lexical(self):
76 | filt = Filter.attribute('name').lte('bob')
77 | assert filt.match({'name': 'acme'})
78 | assert filt.match({'name': 'bob'})
79 | assert not filt.match({'name': 'cell'})
80 |
81 | def test_approx(self):
82 | filt = Filter.attribute('name').approx('ashcroft')
83 | assert filt.match({'name': 'Ashcroft'})
84 | assert filt.match({'name': 'Ashcraft'})
85 | assert not filt.match({'name': 'Ashsoft'})
86 |
87 | def test_and_aggregate(self):
88 | filt = Filter.AND([
89 | Filter.attribute('firstName').equal_to('Alice'),
90 | Filter.attribute('lastName').ends_with('Chains')
91 | ])
92 | assert filt.match({'firstName': 'Alice', 'lastName': 'Chains'})
93 | assert filt.match({'firstName': 'Alice', 'lastName': 'In-Chains'})
94 | assert not filt.match({'firstName': 'Bob', 'lastName': 'Chains'})
95 | assert not filt.match({'firstName': 'Alice'})
96 |
97 | def test_or_aggregate(self):
98 | filt = Filter.OR([
99 | Filter.attribute('firstName').equal_to('Alice'),
100 | Filter.attribute('lastName').ends_with('Chains')
101 | ])
102 | assert filt.match({'firstName': 'Alice', 'lastName': 'Chains'})
103 | assert filt.match({'firstName': 'Alice', 'lastName': 'In-Chains'})
104 | assert filt.match({'firstName': 'Bob', 'lastName': 'Chains'})
105 | assert filt.match({'firstName': 'Alice'})
106 | assert not filt.match({'firstName': 'Bob', 'lastName': 'Smith'})
107 | assert not filt.match({'firstName': 'Bob'})
108 | assert not filt.match({})
109 |
110 | def test_not_aggregate(self):
111 | filt = Filter.NOT([
112 | Filter.attribute('firstName').equal_to('Alice')
113 | ])
114 | assert filt.match({'firstName': 'Bob'})
115 | assert filt.match({})
116 | assert not filt.match({'firstName': 'Alice'})
117 | assert not filt.match({'firstName': 'Alice', 'lastName': 'Chains'})
118 |
119 | def test_escaped(self):
120 | filt = Filter.attribute('escaped').equal_to('*(test)*')
121 | assert filt.match({'escaped': '*(test)*'})
122 | assert not filt.match({'escaped': '(test)'})
123 | assert not filt.match({})
124 |
125 | def test_match_substrings(self):
126 | filt = Filter.attribute('sub').raw('*jer* jo*e*')
127 | assert filt.match({'sub': 'Jerry Jones'})
128 |
129 | def test_match_escaped(self):
130 | filt = Filter.attribute('sub').raw('jerry\\2a \\28jones\\29 \\5c')
131 | assert filt.match({'sub': 'Jerry* (Jones) \\'})
132 |
133 | def test_match_escaped_substrings(self):
134 | filt = Filter.attribute('sub').raw('*jerry\\5c \\2a j*s*')
135 | assert filt.match({'sub': 'Jerry\\ * Jones'})
136 |
--------------------------------------------------------------------------------
/tests/test_filter_output.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from ldap_filter import Filter
3 |
4 |
5 | class TestFilterOutput:
6 | def test_to_string(self):
7 | filt = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
8 | parsed = Filter.parse(filt)
9 | string = parsed.to_string()
10 | assert string == filt
11 |
12 | def test_string_typecast(self):
13 | filt = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
14 | string = str(Filter.parse(filt))
15 | assert string == filt
16 |
17 | def test_to_simple_concat(self):
18 | filt = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
19 | string = Filter.parse(filt) + ''
20 | assert string == filt
21 |
22 | def test_to_complex_concat(self):
23 | filt = '(&(sn=ron)(sn=bob))'
24 | string = Filter.parse(filt) + 'test'
25 | assert string == '(&(sn=ron)(sn=bob))test'
26 |
27 |
28 | class TestFilterFormatting:
29 | def test_default_beautify(self):
30 | filt = '(&(|(sn=ron)(sn=bob))(mail=*))'
31 | parsed = Filter.parse(filt)
32 | string = parsed.to_string(True)
33 | assert string == '(&\n (|\n (sn=ron)\n (sn=bob)\n )\n (mail=*)\n)'
34 |
35 | def test_custom_indent_beautify(self):
36 | filt = '(&(|(sn=ron)(sn=bob))(mail=*))'
37 | parsed = Filter.parse(filt)
38 | string = parsed.to_string(2)
39 | assert string == '(&\n (|\n (sn=ron)\n (sn=bob)\n )\n (mail=*)\n)'
40 |
41 | def test_custom_indent_char_beautify(self):
42 | filt = '(&(|(sn=ron)(sn=bob))(mail=*))'
43 | parsed = Filter.parse(filt)
44 | string = parsed.to_string(indent=2, indt_char='!')
45 | assert string == '(&\n!!(|\n!!!!(sn=ron)\n!!!!(sn=bob)\n!!)\n!!(mail=*)\n)'
46 |
47 |
48 | class TestFilterSimplify:
49 | def test_optimized_filter(self):
50 | filt = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
51 | parsed = Filter.parse(filt)
52 | string = parsed.simplify().to_string()
53 | assert string == filt
54 |
55 | def test_unoptimized_filter(self):
56 | filt = '(&(|(sn=ron)(&(sn=bob)))(|(mail=*))(!(account=disabled)))'
57 | optimized = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
58 | parsed = Filter.parse(filt)
59 | string = parsed.simplify().to_string()
60 | assert string == optimized
61 |
--------------------------------------------------------------------------------
/tests/test_filter_parser.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from ldap_filter import Filter, ParseError
3 |
4 |
5 | class TestFilterParser:
6 | def test_simple_filter(self):
7 | filt = '(sn=ron)'
8 | parsed = Filter.parse(filt)
9 | string = parsed.to_string()
10 | assert string == filt
11 |
12 | def test_complex_filter(self):
13 | filt = '(&(|(sn=ron)(sn=bob))(mail=*)(!(account=disabled)))'
14 | parsed = Filter.parse(filt)
15 | string = parsed.to_string()
16 | assert string == filt
17 |
18 | def test_negative_group_filter(self):
19 | filt = "(!(|(cn=admins)))"
20 | parsed = Filter.parse(filt)
21 | assert parsed is not None
22 | filt = '(&(!(|(sn=ron)(sn=bob)))(mail=*)(|(cn=john)(cn=alex)(cn=rob)))'
23 | parsed = Filter.parse(filt)
24 | string = parsed.to_string()
25 | assert string == filt
26 |
27 | def test_allows_whitespace(self):
28 | filt = ' (& (sn=smith with spaces)(one-two<=morespaces) (objectType=object Type) \n ) '
29 | parsed = Filter.parse(filt)
30 | string = parsed.to_string()
31 | assert string == '(&(sn=smith with spaces)(one-two<=morespaces)(objectType=object Type))'
32 |
33 | def test_allows_value_with_exclamation(self):
34 | filt = '(&(name=Test!)(mail=*@example.com)(|(dept=accounting)(dept=operations)))'
35 | parsed = Filter.parse(filt)
36 | test = {'name': 'Test!', 'mail': 'ron@example.com', 'dept': 'operations'}
37 | assert parsed.match(test)
38 | test_fail = {'name': 'Test', 'mail': 'ron@example.com', 'dept': 'operations'}
39 | assert not parsed.match(test_fail)
40 |
41 | def test_allowed_characters(self):
42 | filt = '(orgUnit=%)'
43 | parsed = Filter.parse(filt)
44 | string = parsed.to_string()
45 | assert string == filt
46 |
47 | def test_oid_attributes(self):
48 | filt = '(1.3.6.1.4.1.1466.115.121.1.38=picture)'
49 | parsed = Filter.parse(filt)
50 | string = parsed.to_string()
51 | assert string == filt
52 |
53 | def test_escaped_values(self):
54 | filt = '(o=Parens R Us \\28for all your parenthetical needs\\29)'
55 | parsed = Filter.parse(filt)
56 | string = parsed.to_string()
57 | assert string == '(o=Parens R Us (for all your parenthetical needs))'
58 |
59 | def test_substring_match(self):
60 | filt = '(sn=*sammy*)'
61 | parsed = Filter.parse(filt)
62 | assert getattr(parsed, 'type') == 'filter'
63 | assert getattr(parsed, 'comp') == '='
64 | assert getattr(parsed, 'attr') == 'sn'
65 | assert getattr(parsed, 'val') == '*sammy*'
66 |
67 | def test_no_parenthesis(self):
68 | filt = 'sn=ron'
69 | parsed = Filter.parse(filt)
70 | string = parsed.to_string()
71 | assert string == '(sn=ron)'
72 |
73 | def test_allows_whitespace_no_parenthesis(self):
74 | filt = ' \n sn=ron '
75 | parsed = Filter.parse(filt)
76 | string = parsed.to_string()
77 | assert string == '(sn=ron)'
78 |
79 | def test_parser_matching(self):
80 | filt = '(&(|(sn=ron)(sn=bob))(mail=*))'
81 | parsed = Filter.parse(filt)
82 | test = {'sn': 'ron', 'mail': 'ron@example.com'}
83 | assert parsed.match(test)
84 | test_fail = {'sn': 'ron'}
85 | assert not parsed.match(test_fail)
86 |
87 | def test_simple_malformed_error(self):
88 | with pytest.raises(ParseError):
89 | filt = '(sn=sammy'
90 | Filter.parse(filt)
91 |
92 | def test_complex_malformed_error(self):
93 | with pytest.raises(ParseError):
94 | filt = '(&(orgUnit=accounting))\n(mail=ron@example.com) f'
95 | Filter.parse(filt)
96 |
97 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{38,39,310,311,312},coverage-report
3 |
4 | [testenv]
5 | commands = py.test -v
6 | deps =
7 | pytest
8 | coverage
9 | coveralls
10 | tox
11 |
12 | [pytest]
13 | addopts = --ignore=setup.py
14 | python_files = *.py
15 | python_functions = test_
--------------------------------------------------------------------------------