From 0ff7cd86b6d5756a39e1a6ed37e4e505c81210d0 Mon Sep 17 00:00:00 2001 From: Theo <87375548+Chimou0@users.noreply.github.com> Date: Sat, 4 Oct 2025 23:21:33 +0800 Subject: [PATCH 1/2] feat: use wcmatch.glob for glob_match with "**" support (#402) --- casbin/util/builtin_operators.py | 88 ++++++++-------------------- requirements.txt | 3 +- tests/util/test_builtin_operators.py | 69 ++++++++++++++++++++++ 3 files changed, 96 insertions(+), 64 deletions(-) diff --git a/casbin/util/builtin_operators.py b/casbin/util/builtin_operators.py index c39c3366..62e0f026 100644 --- a/casbin/util/builtin_operators.py +++ b/casbin/util/builtin_operators.py @@ -15,6 +15,7 @@ import ipaddress import re from datetime import datetime +import wcmatch.glob as glob KEY_MATCH2_PATTERN = re.compile(r"(.*?):[^\/]+(.*?)") KEY_MATCH3_PATTERN = re.compile(r"(.*?){[^\/]+?}(.*?)") @@ -290,70 +291,31 @@ def range_match(pattern, pattern_index, test): def glob_match(string, pattern): """determines whether string matches the pattern in glob expression.""" - pattern_len = len(pattern) - string_len = len(string) - if pattern_len == 0: - return string_len == 0 - pattern_index = 0 - string_index = 0 - while True: - if pattern_index == pattern_len: - return string_len == string_index - c = pattern[pattern_index] - pattern_index += 1 - if c == "?": - if string_index == string_len: - return False - if string[string_index] == "/": - return False - string_index += 1 - continue - if c == "*": - while (pattern_index != pattern_len) and (c == "*"): - c = pattern[pattern_index] - pattern_index += 1 - if pattern_index == pattern_len: - return string.find("/", string_index) == -1 + def doublestar_to_wcmatch(pattern): + """ + Converts glob patterns with double stars (**) to wcmatch-compatible format. + """ + parts = pattern.split("/") + new_parts = [] + for part in parts: + if part == "**": + new_parts.append("**") else: - if c == "/": - string_index = string.find("/", string_index) - if string_index == -1: - return False - else: - string_index += 1 - # General case, use recursion. - while string_index != string_len: - if glob_match(string[string_index:], pattern[pattern_index:]): - return True - if string[string_index] == "/": - break - string_index += 1 - continue - if c == "[": - if string_index == string_len: - return False - if string[string_index] == "/": - return False - pattern_index = range_match(pattern, pattern_index, string[string_index]) - if pattern_index == -1: - return False - string_index += 1 - continue - if c == "\\": - if pattern_index == pattern_len: - c = "\\" - else: - c = pattern[pattern_index] - pattern_index += 1 - # fall through - # other cases and c == "\\" - if string_index == string_len: - return False - else: - if c == string[string_index]: - string_index += 1 - else: - return False + part = re.sub(r"\*{2,}", "*", part) + new_parts.append(part) + result_pattern = "/".join(new_parts) + + if result_pattern.endswith("/**"): + base_pattern = result_pattern[:-3] + result_pattern = "{" + base_pattern + "," + result_pattern + "}" + + return result_pattern + + if pattern.startswith("*/"): + return glob_match(string, pattern[1:]) + + pattern = doublestar_to_wcmatch(pattern) + return glob.globmatch(string, pattern, flags=glob.GLOBSTAR | glob.BRACE) def glob_match_func(*args): diff --git a/requirements.txt b/requirements.txt index 8b00a8fe..1c6b0bde 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -simpleeval >= 1.0.3 \ No newline at end of file +simpleeval >= 1.0.3 +wcmatch >= 10.1 \ No newline at end of file diff --git a/tests/util/test_builtin_operators.py b/tests/util/test_builtin_operators.py index c6fb57c4..984e0e45 100644 --- a/tests/util/test_builtin_operators.py +++ b/tests/util/test_builtin_operators.py @@ -249,6 +249,7 @@ def test_glob_match(self): self.assertFalse(util.glob_match_func("/foobar", "/foo")) self.assertTrue(util.glob_match_func("/foobar", "/foo*")) self.assertFalse(util.glob_match_func("/foobar", "/foo/*")) + self.assertTrue(util.glob_match_func("/foo", "*/foo")) self.assertTrue(util.glob_match_func("/foo", "*/foo*")) self.assertFalse(util.glob_match_func("/foo", "*/foo/*")) @@ -277,6 +278,74 @@ def test_glob_match(self): self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "*/foo*")) self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "*/foo/*")) + self.assertTrue(util.glob_match_func("/foo", "**/foo")) + self.assertTrue(util.glob_match_func("/foo", "**/foo**")) + self.assertTrue(util.glob_match_func("/foo", "**/foo/**")) + self.assertFalse(util.glob_match_func("/foo/bar", "**/foo")) + self.assertFalse(util.glob_match_func("/foo/bar", "**/foo**")) + self.assertTrue(util.glob_match_func("/foo/bar", "**/foo/**")) + self.assertFalse(util.glob_match_func("/foobar", "**/foo")) + self.assertTrue(util.glob_match_func("/foobar", "**/foo**")) + self.assertFalse(util.glob_match_func("/foobar", "**/foo/**")) + + self.assertTrue(util.glob_match_func("/prefix/foo", "**/foo")) + self.assertTrue(util.glob_match_func("/prefix/foo", "**/foo**")) + self.assertTrue(util.glob_match_func("/prefix/foo", "**/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/foo/bar", "**/foo")) + self.assertFalse(util.glob_match_func("/prefix/foo/bar", "**/foo**")) + self.assertTrue(util.glob_match_func("/prefix/foo/bar", "**/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/foobar", "**/foo")) + self.assertTrue(util.glob_match_func("/prefix/foobar", "**/foo**")) + self.assertFalse(util.glob_match_func("/prefix/foobar", "**/foo/**")) + + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo", "**/foo")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo", "**/foo**")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo", "**/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo/bar", "**/foo")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo/bar", "**/foo**")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo/bar", "**/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "**/foo")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foobar", "**/foo**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "**/foo/**")) + + self.assertTrue(util.glob_match_func("/foo", "*/foo**")) + self.assertTrue(util.glob_match_func("/foo", "**/foo*")) + self.assertTrue(util.glob_match_func("/foo", "*/foo/**")) + self.assertFalse(util.glob_match_func("/foo", "**/foo/*")) + self.assertFalse(util.glob_match_func("/foo/bar", "*/foo**")) + self.assertFalse(util.glob_match_func("/foo/bar", "**/foo*")) + self.assertTrue(util.glob_match_func("/foo/bar", "*/foo/**")) + self.assertTrue(util.glob_match_func("/foo/bar", "**/foo/*")) + self.assertTrue(util.glob_match_func("/foobar", "*/foo**")) + self.assertTrue(util.glob_match_func("/foobar", "**/foo*")) + self.assertFalse(util.glob_match_func("/foobar", "*/foo/**")) + self.assertFalse(util.glob_match_func("/foobar", "**/foo/*")) + self.assertFalse(util.glob_match_func("/prefix/foo", "*/foo**")) + self.assertTrue(util.glob_match_func("/prefix/foo", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/foo", "*/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/foo", "**/foo/*")) + self.assertFalse(util.glob_match_func("/prefix/foo/bar", "*/foo**")) + self.assertFalse(util.glob_match_func("/prefix/foo/bar", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/foo/bar", "*/foo/**")) + self.assertTrue(util.glob_match_func("/prefix/foo/bar", "**/foo/*")) + self.assertFalse(util.glob_match_func("/prefix/foobar", "*/foo**")) + self.assertTrue(util.glob_match_func("/prefix/foobar", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/foobar", "*/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/foobar", "**/foo/*")) + + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo", "*/foo**")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo", "*/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo", "**/foo/*")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo/bar", "*/foo**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo/bar", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foo/bar", "*/foo/**")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foo/bar", "**/foo/*")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "*/foo**")) + self.assertTrue(util.glob_match_func("/prefix/subprefix/foobar", "**/foo*")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "*/foo/**")) + self.assertFalse(util.glob_match_func("/prefix/subprefix/foobar", "**/foo/*")) + self.assertTrue(util.glob_match_func("/f", "/?")) self.assertTrue(util.glob_match_func("/foobar", "/foo?ar")) self.assertFalse(util.glob_match_func("/fooar", "/foo?ar")) From 243d51f89939fd1fb462e0807a064fdef9cba7db Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 4 Oct 2025 15:40:54 +0000 Subject: [PATCH 2/2] chore(release): 2.3.0 [skip ci] # [2.3.0](https://github.com/casbin/pycasbin/compare/v2.2.0...v2.3.0) (2025-10-04) ### Features * use wcmatch.glob for glob_match with "**" support ([#402](https://github.com/casbin/pycasbin/issues/402)) ([0ff7cd8](https://github.com/casbin/pycasbin/commit/0ff7cd86b6d5756a39e1a6ed37e4e505c81210d0)) --- CHANGELOG.md | 7 +++++++ pyproject.toml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e30e142..ab5fc43f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Semantic Versioning Changelog +# [2.3.0](https://github.com/casbin/pycasbin/compare/v2.2.0...v2.3.0) (2025-10-04) + + +### Features + +* use wcmatch.glob for glob_match with "**" support ([#402](https://github.com/casbin/pycasbin/issues/402)) ([0ff7cd8](https://github.com/casbin/pycasbin/commit/0ff7cd86b6d5756a39e1a6ed37e4e505c81210d0)) + # [2.2.0](https://github.com/casbin/pycasbin/compare/v2.1.0...v2.2.0) (2025-08-24) diff --git a/pyproject.toml b/pyproject.toml index 72b84bbc..627e6a31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pycasbin" -version = "2.2.0" +version = "2.3.0" authors = [ {name = "Casbin", email = "admin@casbin.org"}, ]