├── .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 · [![Latest Version](https://img.shields.io/pypi/v/ldap-filter.svg)](https://pypi.python.org/pypi/ldap-filter) [![License](https://img.shields.io/pypi/l/ldap-filter.svg)](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_ --------------------------------------------------------------------------------