|
9 | 9 | from lark import Lark |
10 | 10 | from lark import Token |
11 | 11 | from lark import Transformer |
| 12 | +from lark import Tree |
12 | 13 | from lark import v_args |
13 | 14 | from lark.exceptions import LarkError |
14 | 15 | from lark.exceptions import VisitError |
@@ -208,51 +209,115 @@ def null_value(self, _token: Token) -> None: |
208 | 209 | return None |
209 | 210 |
|
210 | 211 |
|
| 212 | +_MAX_DEPTH = 32 |
| 213 | +"""How many expressions a filter or a path may nest. |
| 214 | +
|
| 215 | +A deeper tree would exhaust the stack of the recursive visitors, and no client |
| 216 | +sends one. |
| 217 | +""" |
| 218 | + |
| 219 | +_MAX_CACHED_LENGTH = 1024 |
| 220 | +"""How long a filter or a path may be to stay in the parser cache. |
| 221 | +
|
| 222 | +Real filters and paths are much shorter. Parsing the longer ones each time |
| 223 | +bounds the memory the cache holds. |
| 224 | +""" |
| 225 | + |
| 226 | +_NESTING_RULES = frozenset( |
| 227 | + {"or_expr", "and_expr", "not_expr", "value_path", "value_path_sub"} |
| 228 | +) |
| 229 | + |
| 230 | + |
| 231 | +def _depth(tree: Tree[Token]) -> int: |
| 232 | + """Count the expressions nested in a parse tree, without recursion. |
| 233 | +
|
| 234 | + Parentheses leave no node in the tree, and a chain of ``and`` or ``or`` is a |
| 235 | + single node, so neither adds to the depth. |
| 236 | + """ |
| 237 | + depths: dict[int, int] = {} |
| 238 | + for subtree in tree.iter_subtrees(): |
| 239 | + children = ( |
| 240 | + depths[id(child)] for child in subtree.children if isinstance(child, Tree) |
| 241 | + ) |
| 242 | + nested = max(children, default=0) |
| 243 | + depths[id(subtree)] = nested + (subtree.data in _NESTING_RULES) |
| 244 | + return depths[id(tree)] |
| 245 | + |
| 246 | + |
211 | 247 | # One parser for both entry points: the two start rules share a grammar, and |
212 | 248 | # building their tables together halves the cost of importing this module. |
213 | 249 | _PARSER = Lark(_GRAMMAR, start=["filter", "path"], parser="lalr") |
214 | 250 | _BUILDER = _AstBuilder() |
215 | 251 |
|
216 | 252 |
|
217 | | -@lru_cache(maxsize=1024) |
218 | 253 | def _parse_filter(expression: str) -> FilterNode: |
219 | 254 | """Parse a SCIM filter expression into its root node. |
220 | 255 |
|
221 | | - The expression is the one a ``filter`` query parameter or a |
222 | | - ``SearchRequest.filter`` carries. A syntactically invalid one raises |
| 256 | + The expression is the one a filter query parameter or a |
| 257 | + SearchRequest.filter carries. A syntactically invalid one raises |
223 | 258 | InvalidFilterException. |
224 | | -
|
225 | | - >>> from scim2_models.path.grammar import _parse_filter |
226 | | - >>> _parse_filter('userName eq "bjensen"') |
227 | | - Comparison(attr_path=AttrPath(attr='userName', sub_attr=None, uri=None), op=<CompareOperator.eq: 'eq'>, value='bjensen') |
228 | 259 | """ |
| 260 | + if len(expression) > _MAX_CACHED_LENGTH: |
| 261 | + return _build_filter(expression) |
| 262 | + return _cached_filter(expression) |
| 263 | + |
| 264 | + |
| 265 | +def _build_filter(expression: str) -> FilterNode: |
| 266 | + """Parse a SCIM filter expression, without the cache.""" |
229 | 267 | try: |
230 | 268 | tree = _PARSER.parse(expression, start="filter") |
| 269 | + except LarkError as exc: |
| 270 | + raise InvalidFilterException( |
| 271 | + filter=expression, detail=_error_detail(exc) |
| 272 | + ) from exc |
| 273 | + |
| 274 | + if _depth(tree) > _MAX_DEPTH: |
| 275 | + raise InvalidFilterException( |
| 276 | + filter=expression, |
| 277 | + detail=f"the filter nests more than {_MAX_DEPTH} expressions", |
| 278 | + ) |
| 279 | + |
| 280 | + try: |
231 | 281 | return cast(FilterNode, _BUILDER.transform(tree)) |
232 | 282 | except LarkError as exc: |
233 | 283 | raise InvalidFilterException( |
234 | 284 | filter=expression, detail=_error_detail(exc) |
235 | 285 | ) from exc |
236 | 286 |
|
237 | 287 |
|
238 | | -@lru_cache(maxsize=1024) |
239 | 288 | def _parse_path(path: str) -> PathNode: |
240 | 289 | """Parse a SCIM PATCH path into an abstract syntax tree. |
241 | 290 |
|
242 | | - The path is the one a ``PatchOperation.path`` carries. A syntactically |
| 291 | + The path is the one a PatchOperation.path carries. A syntactically |
243 | 292 | invalid one raises InvalidPathException. |
244 | | -
|
245 | | - >>> from scim2_models.path.grammar import _parse_path |
246 | | - >>> _parse_path("name.familyName") |
247 | | - AttrPath(attr='name', sub_attr='familyName', uri=None) |
248 | 293 | """ |
| 294 | + if len(path) > _MAX_CACHED_LENGTH: |
| 295 | + return _build_path(path) |
| 296 | + return _cached_path(path) |
| 297 | + |
| 298 | + |
| 299 | +def _build_path(path: str) -> PathNode: |
| 300 | + """Parse a SCIM PATCH path, without the cache.""" |
249 | 301 | try: |
250 | 302 | tree = _PARSER.parse(path, start="path") |
| 303 | + except LarkError as exc: |
| 304 | + raise InvalidPathException(path=path, detail=_error_detail(exc)) from exc |
| 305 | + |
| 306 | + if _depth(tree) > _MAX_DEPTH: |
| 307 | + raise InvalidPathException( |
| 308 | + path=path, detail=f"the path nests more than {_MAX_DEPTH} expressions" |
| 309 | + ) |
| 310 | + |
| 311 | + try: |
251 | 312 | return cast(PathNode, _BUILDER.transform(tree)) |
252 | 313 | except LarkError as exc: |
253 | 314 | raise InvalidPathException(path=path, detail=_error_detail(exc)) from exc |
254 | 315 |
|
255 | 316 |
|
| 317 | +_cached_filter = lru_cache(maxsize=1024)(_build_filter) |
| 318 | +_cached_path = lru_cache(maxsize=1024)(_build_path) |
| 319 | + |
| 320 | + |
256 | 321 | def _error_detail(exc: LarkError) -> str: |
257 | 322 | """Build a human readable message out of a lark failure.""" |
258 | 323 | if isinstance(exc, VisitError): |
|
0 commit comments