This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-pycasbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 598d64a  fix: speed up key_match with multiple wildcards
598d64a is described below

commit 598d64abcd0b99cf1349ed287bf885a279d3792e
Author: Yang Luo <[email protected]>
AuthorDate: Sat Oct 10 01:55:30 2026 +0800

    fix: speed up key_match with multiple wildcards
---
 casbin/synced_enforcer.py            |  4 +-
 casbin/util/builtin_operators.py     | 73 ++++++++++++++++++++++++++++++++++--
 tests/util/test_builtin_operators.py |  9 +++++
 3 files changed, 81 insertions(+), 5 deletions(-)

diff --git a/casbin/synced_enforcer.py b/casbin/synced_enforcer.py
index 8e5e028..bfd714a 100644
--- a/casbin/synced_enforcer.py
+++ b/casbin/synced_enforcer.py
@@ -215,7 +215,7 @@ class SyncedEnforcer:
     def get_adapter(self):
         """gets the current adapter."""
         with self._rl:
-            self._e.get_adapter()
+            return self._e.get_adapter()
 
     def set_adapter(self, adapter):
         """sets the current adapter."""
@@ -712,7 +712,7 @@ class SyncedEnforcer:
     def is_filtered(self):
         """returns true if the loaded policy has been filtered."""
         with self._rl:
-            self._e.is_filtered()
+            return self._e.is_filtered()
 
     def add_policies(self, rules):
         """adds authorization rules to the current policy.
diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py
index 2aff2ad..99075a9 100644
--- a/casbin/util/builtin_operators.py
+++ b/casbin/util/builtin_operators.py
@@ -14,6 +14,7 @@
 
 import ipaddress
 import re
+from functools import lru_cache
 from datetime import datetime
 import wcmatch.glob as glob
 
@@ -23,6 +24,72 @@ KEY_MATCH4_PATTERN = re.compile(r"{([^/]+)}")
 KEY_MATCH5_PATTERN = re.compile(r"{[^/]+}")
 
 
+WILDCARD_PATTERN = re.compile(r"\.\*|\[\^\\?/\]\+|\.|[^\\^$.|?*+()\[\]{}]")
+
+
+def _any_char(c):
+    return c != "\n"
+
+
+def _segment_char(c):
+    return c != "/"
+
+
+@lru_cache(maxsize=1024)
+def _parse_wildcards(pattern):
+    """splits a path pattern made of literals, ".", "[^/]+" and two or more 
".*" into (char test, repeatable) steps,
+    returns None for any other pattern."""
+    if pattern.count(".*") < 2:
+        return None
+
+    steps = []
+    pos = 0
+    while pos < len(pattern):
+        m = WILDCARD_PATTERN.match(pattern, pos)
+        if not m:
+            return None
+        token = m.group()
+        if token == ".*":
+            steps.append((_any_char, True))
+        elif token == ".":
+            steps.append((_any_char, False))
+        elif len(token) > 1:
+            steps.append((_segment_char, False))
+            steps.append((_segment_char, True))
+        else:
+            steps.append((token.__eq__, False))
+        pos = m.end()
+    return tuple(steps)
+
+
+def _path_match(key1, pattern):
+    """determines whether the whole key1 matches the path pattern. A pattern 
with several ".*" is matched in
+    O(len(key1) * len(pattern)), because re.match() backtracks polynomially on 
it."""
+    steps = _parse_wildcards(pattern)
+    if steps is None:
+        return regex_match(key1, "^" + pattern + "$")
+
+    end = len(steps)
+
+    def add(states, i):
+        states.add(i)
+        while i < end and steps[i][1]:
+            i += 1
+            states.add(i)
+
+    states = set()
+    add(states, 0)
+    for c in key1:
+        next_states = set()
+        for i in states:
+            if i < end and steps[i][0](c):
+                add(next_states, i if steps[i][1] else i + 1)
+        if not next_states:
+            return False
+        states = next_states
+    return end in states
+
+
 def key_match(key1, key2):
     """determines whether key1 matches the pattern of key2 (similar to RESTful 
path), key2 can contain a *.
     For example, "/foo/bar" matches "/foo/*"
@@ -72,7 +139,7 @@ def key_match2(key1, key2):
     if key2 == "*":
         key2 = "(.*)"
 
-    return regex_match(key1, "^" + key2 + "$")
+    return _path_match(key1, key2)
 
 
 def key_match2_func(*args):
@@ -114,7 +181,7 @@ def key_match3(key1, key2):
     key2 = key2.replace("/*", "/.*")
     key2 = KEY_MATCH3_PATTERN.sub(r"\g<1>[^\/]+\g<2>", key2, 0)
 
-    return regex_match(key1, "^" + key2 + "$")
+    return _path_match(key1, key2)
 
 
 def key_match3_func(*args):
@@ -213,7 +280,7 @@ def key_match5(key1: str, key2: str) -> bool:
 
     key2 = KEY_MATCH5_PATTERN.sub(r"[^/]+", key2, 0)
 
-    return regex_match(key1, "^" + key2 + "$")
+    return _path_match(key1, key2)
 
 
 def key_match5_func(*args) -> bool:
diff --git a/tests/util/test_builtin_operators.py 
b/tests/util/test_builtin_operators.py
index 984e0e4..ccdb7d5 100644
--- a/tests/util/test_builtin_operators.py
+++ b/tests/util/test_builtin_operators.py
@@ -74,6 +74,15 @@ class TestBuiltinOperators(TestCase):
 
         self.assertFalse(util.key_match2_func("/alice/all", "/:/all"))
 
+    def test_key_match2_multiple_wildcards(self):
+        self.assertTrue(util.key_match2_func("/api/a/b/c/detail", 
"/api/*/*/*/detail"))
+        self.assertTrue(util.key_match2_func("/api////detail", 
"/api/*/*/*/detail"))
+        self.assertTrue(util.key_match2_func("/api/a/b/c/d/1/detail", 
"/api/*/*/:id/detail"))
+        self.assertFalse(util.key_match2_func("/api/a/b/detail", 
"/api/*/*/*/detail"))
+        self.assertFalse(util.key_match2_func("/api/a/b/c/detail/x", 
"/api/*/*/*/detail"))
+        # used to take minutes because of regex backtracking
+        self.assertFalse(util.key_match2_func("/api/" + "/" * 5000, 
"/api/*/*/*/detail"))
+
     def test_key_get2(self):
         self.assertEqual(util.key_get2("/foo", "/foo", "id"), "")
         self.assertEqual(util.key_get2("/foo", "/foo*", "id"), "")


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to