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

hsluoyz pushed a commit to branch chore/asf-release-prep
in repository https://gitbox.apache.org/repos/asf/casbin-jcasbin.git

commit ef374510b9db9a51f1a61f478d7947a069c37cfd
Author: Yang Luo <[email protected]>
AuthorDate: Thu Sep 17 21:42:12 2026 +0800

    fix: rewrite Glob to remove GPL-derived code
---
 src/main/java/org/casbin/jcasbin/util/Glob.java    | 275 +++++++++++----------
 .../java/org/casbin/jcasbin/main/GlobUnitTest.java | 126 ++++++++++
 2 files changed, 269 insertions(+), 132 deletions(-)

diff --git a/src/main/java/org/casbin/jcasbin/util/Glob.java 
b/src/main/java/org/casbin/jcasbin/util/Glob.java
index 9bfed8e..700aed4 100644
--- a/src/main/java/org/casbin/jcasbin/util/Glob.java
+++ b/src/main/java/org/casbin/jcasbin/util/Glob.java
@@ -1,183 +1,194 @@
-// Copyright 2021 The casbin Authors. All Rights Reserved.
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
 //
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
+//     https://www.apache.org/licenses/LICENSE-2.0
 //
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
 
 package org.casbin.jcasbin.util;
+
 import java.util.regex.PatternSyntaxException;
 
 /**
+ * Glob translates a path-style glob expression into a Java regular expression.
  *
- *
- * @author shy
- * @since 2021/1/13
+ * <p>Supported syntax:
+ * <ul>
+ *   <li>{@code *} matches any run of characters except {@code /}</li>
+ *   <li>{@code **} matches any run of characters including {@code /}</li>
+ *   <li>{@code ?} matches exactly one character except {@code /}</li>
+ *   <li>{@code [abc]}, {@code [a-z]} match one character from the set; {@code 
[!abc]} negates the set.
+ *       A leading {@code ^} or {@code -} is literal. The set never matches 
{@code /}.</li>
+ *   <li>{@code {a,b}} matches either alternative; groups cannot be nested</li>
+ *   <li>{@code \x} matches the character {@code x} literally</li>
+ * </ul>
+ * The separator is always {@code /}, regardless of the operating system.
  */
 public class Glob {
 
-    private static final String REGEX_META_CHARS = ".^$+{[]|()";
-    private static final String GLOB_META_CHARS = "\\*?[{";
+    private Glob() {
+    }
 
     /**
      * Creates a regex pattern from the given glob expression.
      *
      * @param globPattern the given glob expression
-     * @return the regex pattern
+     * @return the regex pattern, anchored to match the whole input
+     * @throws PatternSyntaxException if the glob expression is malformed
      */
     public static String toRegexPattern(String globPattern) {
-        boolean inGroup = false;
-        StringBuilder regex = new StringBuilder("^");
-
+        StringBuilder regex = new StringBuilder(globPattern.length() * 2 + 
2).append('^');
+        int groupStart = -1;
         int i = 0;
         while (i < globPattern.length()) {
-            char c = globPattern.charAt(i++);
+            char c = globPattern.charAt(i);
             switch (c) {
                 case '\\':
-                    // escape special characters
-                    if (i == globPattern.length()) {
-                        throw new PatternSyntaxException("No character to 
escape",
-                            globPattern, i - 1);
+                    if (i + 1 >= globPattern.length()) {
+                        throw error("Trailing escape character", globPattern, 
i);
                     }
-                    char next = globPattern.charAt(i++);
-                    if (isGlobMeta(next) || isRegexMeta(next)) {
-                        regex.append('\\');
+                    appendLiteral(regex, globPattern.charAt(i + 1));
+                    i += 2;
+                    break;
+                case '*':
+                    if (i + 1 < globPattern.length() && globPattern.charAt(i + 
1) == '*') {
+                        regex.append(".*");
+                        i += 2;
+                    } else {
+                        regex.append("[^/]*");
+                        i++;
                     }
-                    regex.append(next);
                     break;
-                case '/':
-                    regex.append(c);
+                case '?':
+                    regex.append("[^/]");
+                    i++;
                     break;
                 case '[':
-                    // don't match name separator in class
-                    regex.append("[[^/]&&[");
-                    if (next(globPattern, i) == '^') {
-                        // escape the regex negation char if it appears
-                        regex.append("\\^");
-                        i++;
-                    } else {
-                        // negation
-                        if (next(globPattern, i) == '!') {
-                            regex.append('^');
-                            i++;
-                        }
-                        // hyphen allowed at start
-                        if (next(globPattern, i) == '-') {
-                            regex.append('-');
-                            i++;
-                        }
-                    }
-                    boolean hasRangeStart = false;
-                    char last = 0;
-                    while (i < globPattern.length()) {
-                        c = globPattern.charAt(i++);
-                        if (c == ']') {
-                            break;
-                        }
-                        if (c == '/') {
-                            throw new PatternSyntaxException("Explicit 'name 
separator' in class",
-                                globPattern, i - 1);
-                        }
-                        // TBD: how to specify ']' in a class?
-                        if (c == '\\' || c == '[' || c == '&' && 
next(globPattern, i) == '&') {
-                            // escape '\', '[' or "&&" for regex class
-                            regex.append('\\');
-                        }
-                        regex.append(c);
-
-                        if (c == '-') {
-                            if (!hasRangeStart) {
-                                throw new PatternSyntaxException("Invalid 
range",
-                                    globPattern, i - 1);
-                            }
-                            if ((c = next(globPattern, i++)) == 0 || c == ']') 
{
-                                break;
-                            }
-                            if (c < last) {
-                                throw new PatternSyntaxException("Invalid 
range",
-                                    globPattern, i - 3);
-                            }
-                            regex.append(c);
-                            hasRangeStart = false;
-                        } else {
-                            hasRangeStart = true;
-                            last = c;
-                        }
-                    }
-                    if (c != ']') {
-                        throw new PatternSyntaxException("Missing ']", 
globPattern, i - 1);
-                    }
-                    regex.append("]]");
+                    i = appendCharacterSet(regex, globPattern, i);
                     break;
                 case '{':
-                    if (inGroup) {
-                        throw new PatternSyntaxException("Cannot nest groups",
-                            globPattern, i - 1);
-                    }
-                    regex.append("(?:(?:");
-                    inGroup = true;
-                    break;
-                case '}':
-                    if (inGroup) {
-                        regex.append("))");
-                        inGroup = false;
-                    } else {
-                        regex.append('}');
+                    if (groupStart >= 0) {
+                        throw error("Nested groups are not supported", 
globPattern, i);
                     }
+                    groupStart = i;
+                    regex.append("(?:");
+                    i++;
                     break;
                 case ',':
-                    if (inGroup) {
-                        regex.append(")|(?:");
+                    if (groupStart >= 0) {
+                        regex.append('|');
                     } else {
-                        regex.append(',');
+                        appendLiteral(regex, c);
                     }
+                    i++;
                     break;
-                case '*':
-                    if (next(globPattern, i) == '*') {
-                        // crosses directory boundaries
-                        regex.append(".*");
-                        i++;
+                case '}':
+                    if (groupStart >= 0) {
+                        groupStart = -1;
+                        regex.append(')');
                     } else {
-                        // within directory boundary
-                        regex.append("[^/]*");
+                        appendLiteral(regex, c);
                     }
+                    i++;
                     break;
-                case '?':
-                    regex.append("[^/]");
-                    break;
-
                 default:
-                    if (isRegexMeta(c)) {
-                        regex.append('\\');
-                    }
-                    regex.append(c);
+                    appendLiteral(regex, c);
+                    i++;
             }
         }
-
-        if (inGroup) {
-            throw new PatternSyntaxException("Missing '}", globPattern, i - 1);
+        if (groupStart >= 0) {
+            throw error("Unclosed group", globPattern, groupStart);
         }
-
         return regex.append('$').toString();
     }
 
-    private static boolean isRegexMeta(char c) {
-        return REGEX_META_CHARS.indexOf(c) != -1;
+    /**
+     * Translates the character set starting at {@code start} (which must be 
{@code [}).
+     *
+     * @return the index just past the closing {@code ]}
+     */
+    private static int appendCharacterSet(StringBuilder regex, String glob, 
int start) {
+        int i = start + 1;
+        boolean negated = i < glob.length() && glob.charAt(i) == '!';
+        if (negated) {
+            i++;
+        }
+        // Exclude the separator up front; a negated set is simply [^/...].
+        regex.append(negated ? "[^/" : "(?!/)[");
+        int members = 0;
+
+        // A leading '^' (when not negated) or '-' is an ordinary member.
+        if (i < glob.length() && (glob.charAt(i) == '-' || (!negated && 
glob.charAt(i) == '^'))) {
+            appendLiteral(regex, glob.charAt(i));
+            members++;
+            i++;
+        }
+
+        while (true) {
+            if (i >= glob.length()) {
+                throw error("Unclosed character set", glob, start);
+            }
+            char c = glob.charAt(i);
+            if (c == ']') {
+                break;
+            }
+            if (c == '/') {
+                throw error("Separator is not allowed in a character set", 
glob, i);
+            }
+            if (c == '-') {
+                throw error("Range has no start", glob, i);
+            }
+            appendLiteral(regex, c);
+            members++;
+            i++;
+            if (i < glob.length() && glob.charAt(i) == '-') {
+                i++;
+                if (i >= glob.length()) {
+                    throw error("Unclosed character set", glob, start);
+                }
+                char upper = glob.charAt(i);
+                if (upper == ']') {
+                    // "[ab-]": a dash right before the closing bracket is 
literal.
+                    appendLiteral(regex, '-');
+                    break;
+                }
+                if (upper < c) {
+                    throw error("Range end is lower than range start", glob, 
i);
+                }
+                regex.append('-');
+                appendLiteral(regex, upper);
+                i++;
+            }
+        }
+        if (members == 0) {
+            throw error("Empty character set", glob, start);
+        }
+        regex.append(']');
+        return i + 1;
     }
 
-    private static boolean isGlobMeta(char c) {
-        return GLOB_META_CHARS.indexOf(c) != -1;
+    /**
+     * Appends {@code c} so that it matches itself, both inside and outside a 
character class.
+     * Escaping every non-alphanumeric character is always safe in 
java.util.regex.
+     */
+    private static void appendLiteral(StringBuilder regex, char c) {
+        if (!Character.isLetterOrDigit(c) && c < 128) {
+            regex.append('\\');
+        }
+        regex.append(c);
     }
 
-    private static char next(String glob, int i) {
-        return i < glob.length() ? glob.charAt(i) : 0;
+    private static PatternSyntaxException error(String description, String 
glob, int index) {
+        return new PatternSyntaxException(description, glob, index);
     }
 }
diff --git a/src/test/java/org/casbin/jcasbin/main/GlobUnitTest.java 
b/src/test/java/org/casbin/jcasbin/main/GlobUnitTest.java
new file mode 100644
index 0000000..69f528b
--- /dev/null
+++ b/src/test/java/org/casbin/jcasbin/main/GlobUnitTest.java
@@ -0,0 +1,126 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.casbin.jcasbin.main;
+
+import org.casbin.jcasbin.util.Glob;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import static org.testng.Assert.assertEquals;
+
+public class GlobUnitTest {
+
+    @DataProvider
+    public Object[][] matches() {
+        return new Object[][]{
+            // literals and anchoring
+            {"", "", true},
+            {"abc", "abc", true},
+            {"ab", "abc", false},
+            {"abcd", "abc", false},
+            {"a.b", "a.b", true},
+            {"axb", "a.b", false},
+            {".$+()|^", ".$+()|^", true},
+            {"中文/路径", "中文/路径", true},
+            {"🙂", "🙂", true},
+
+            // single wildcard never crosses '/'
+            {"", "*", true},
+            {"/a/b", "/a/*", true},
+            {"/a/b/c", "/a/*", false},
+            {"/a/", "/a/*", true},
+            {"a\nb", "*", true},
+
+            // double wildcard crosses '/'
+            {"/a/b/c", "/a/**", true},
+            {"/a/", "/a/**", true},
+            {"/x/y/z", "**/z", true},
+            {"/x/y/z", "***", true},
+
+            // single character
+            {"a", "?", true},
+            {"/", "?", false},
+            {"🙂", "?", true},
+            {"a/b", "a?b", false},
+            {"a\\b", "a?b", true},
+
+            // character sets
+            {"a", "[abc]", true},
+            {"d", "[abc]", false},
+            {"z", "[a-z]", true},
+            {"Z", "[a-z]", false},
+            {"m", "[a-cx-z]", false},
+            {"c", "[!ab]", true},
+            {"a", "[!ab]", false},
+            {"/", "[!ab]", false},
+            {"^", "[^a]", true},
+            {"b", "[^a]", false},
+            {"-", "[-ab]", true},
+            {"-", "[!-ab]", false},
+            {"-", "[ab-]", true},
+            {"-", "[-]", true},
+            {"&", "[&&]", true},
+            {"[", "[[]", true},
+            {"\\", "[\\]", true},
+            {".", "[.-/]", true},
+            {"/", "[.-/]", false},
+            {"🙂", "[🙂]", true},
+
+            // alternatives
+            {"abc", "a{b,x}c", true},
+            {"axc", "a{b,x}c", true},
+            {"ac", "a{b,x}c", false},
+            {"ac", "a{,x}c", true},
+            {"", "{,}", true},
+            {"a/b", "{a/b,c}", true},
+            {"a,b", "a,b", true},
+            {"}", "}", true},
+            {"a}", "a}", true},
+
+            // escapes
+            {"*?[{", "\\*\\?\\[\\{", true},
+            {"a*", "a\\*", true},
+            {"ab", "a\\*", false},
+            {"n", "\\n", true},
+            {"🙂", "\\🙂", true},
+        };
+    }
+
+    @Test(dataProvider = "matches")
+    public void testToRegexPattern(String input, String glob, boolean 
expected) {
+        assertEquals(Pattern.matches(Glob.toRegexPattern(glob), input), 
expected,
+            "glob=" + glob + " input=" + input);
+    }
+
+    @DataProvider
+    public Object[][] malformed() {
+        return new Object[][]{
+            {"["}, {"[a"}, {"[a-"}, {"[]"}, {"[!]"}, {"[/]"}, {"[a/b]"},
+            {"[z-a]"}, {"[a--]"}, {"[a-b-c]"}, {"[^-]"}, {"[--]"},
+            {"{a"}, {"{a,{b,c}}"}, {"a\\"},
+        };
+    }
+
+    @Test(dataProvider = "malformed", expectedExceptions = 
PatternSyntaxException.class)
+    public void testMalformedGlob(String glob) {
+        Glob.toRegexPattern(glob);
+    }
+}


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

Reply via email to