|
14 | 14 |
|
15 | 15 | import ipaddress |
16 | 16 | import re |
| 17 | +from functools import lru_cache |
17 | 18 | from datetime import datetime |
18 | 19 | import wcmatch.glob as glob |
19 | 20 |
|
|
23 | 24 | KEY_MATCH5_PATTERN = re.compile(r"{[^/]+}") |
24 | 25 |
|
25 | 26 |
|
| 27 | +WILDCARD_PATTERN = re.compile(r"\.\*|\[\^\\?/\]\+|\.|[^\\^$.|?*+()\[\]{}]") |
| 28 | + |
| 29 | + |
| 30 | +def _any_char(c): |
| 31 | + return c != "\n" |
| 32 | + |
| 33 | + |
| 34 | +def _segment_char(c): |
| 35 | + return c != "/" |
| 36 | + |
| 37 | + |
| 38 | +@lru_cache(maxsize=1024) |
| 39 | +def _parse_wildcards(pattern): |
| 40 | + """splits a path pattern made of literals, ".", "[^/]+" and two or more ".*" into (char test, repeatable) steps, |
| 41 | + returns None for any other pattern.""" |
| 42 | + if pattern.count(".*") < 2: |
| 43 | + return None |
| 44 | + |
| 45 | + steps = [] |
| 46 | + pos = 0 |
| 47 | + while pos < len(pattern): |
| 48 | + m = WILDCARD_PATTERN.match(pattern, pos) |
| 49 | + if not m: |
| 50 | + return None |
| 51 | + token = m.group() |
| 52 | + if token == ".*": |
| 53 | + steps.append((_any_char, True)) |
| 54 | + elif token == ".": |
| 55 | + steps.append((_any_char, False)) |
| 56 | + elif len(token) > 1: |
| 57 | + steps.append((_segment_char, False)) |
| 58 | + steps.append((_segment_char, True)) |
| 59 | + else: |
| 60 | + steps.append((token.__eq__, False)) |
| 61 | + pos = m.end() |
| 62 | + return tuple(steps) |
| 63 | + |
| 64 | + |
| 65 | +def _path_match(key1, pattern): |
| 66 | + """determines whether the whole key1 matches the path pattern. A pattern with several ".*" is matched in |
| 67 | + O(len(key1) * len(pattern)), because re.match() backtracks polynomially on it.""" |
| 68 | + steps = _parse_wildcards(pattern) |
| 69 | + if steps is None: |
| 70 | + return regex_match(key1, "^" + pattern + "$") |
| 71 | + |
| 72 | + end = len(steps) |
| 73 | + |
| 74 | + def add(states, i): |
| 75 | + states.add(i) |
| 76 | + while i < end and steps[i][1]: |
| 77 | + i += 1 |
| 78 | + states.add(i) |
| 79 | + |
| 80 | + states = set() |
| 81 | + add(states, 0) |
| 82 | + for c in key1: |
| 83 | + next_states = set() |
| 84 | + for i in states: |
| 85 | + if i < end and steps[i][0](c): |
| 86 | + add(next_states, i if steps[i][1] else i + 1) |
| 87 | + if not next_states: |
| 88 | + return False |
| 89 | + states = next_states |
| 90 | + return end in states |
| 91 | + |
| 92 | + |
26 | 93 | def key_match(key1, key2): |
27 | 94 | """determines whether key1 matches the pattern of key2 (similar to RESTful path), key2 can contain a *. |
28 | 95 | For example, "/foo/bar" matches "/foo/*" |
@@ -72,7 +139,7 @@ def key_match2(key1, key2): |
72 | 139 | if key2 == "*": |
73 | 140 | key2 = "(.*)" |
74 | 141 |
|
75 | | - return regex_match(key1, "^" + key2 + "$") |
| 142 | + return _path_match(key1, key2) |
76 | 143 |
|
77 | 144 |
|
78 | 145 | def key_match2_func(*args): |
@@ -114,7 +181,7 @@ def key_match3(key1, key2): |
114 | 181 | key2 = key2.replace("/*", "/.*") |
115 | 182 | key2 = KEY_MATCH3_PATTERN.sub(r"\g<1>[^\/]+\g<2>", key2, 0) |
116 | 183 |
|
117 | | - return regex_match(key1, "^" + key2 + "$") |
| 184 | + return _path_match(key1, key2) |
118 | 185 |
|
119 | 186 |
|
120 | 187 | def key_match3_func(*args): |
@@ -213,7 +280,7 @@ def key_match5(key1: str, key2: str) -> bool: |
213 | 280 |
|
214 | 281 | key2 = KEY_MATCH5_PATTERN.sub(r"[^/]+", key2, 0) |
215 | 282 |
|
216 | | - return regex_match(key1, "^" + key2 + "$") |
| 283 | + return _path_match(key1, key2) |
217 | 284 |
|
218 | 285 |
|
219 | 286 | def key_match5_func(*args) -> bool: |
|
0 commit comments