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

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-jsieve.git


The following commit(s) were added to refs/heads/master by this push:
     new 47af32b  [ENHANCEMENT] Improve header comparator
47af32b is described below

commit 47af32b525f3749d80bd5ecc438bfc98a96945ed
Author: Benoit TELLIER <[email protected]>
AuthorDate: Wed Oct 28 22:19:46 2026 +0100

    [ENHANCEMENT] Improve header comparator
---
 .../apache/jsieve/comparators/AsciiCasemap.java    |  28 ++-
 .../apache/jsieve/comparators/ComparatorUtils.java |  96 +++++++-
 .../test/java/org/apache/jsieve/HeaderTest.java    |  59 ++++-
 .../jsieve/comparator/ComparatorUtilsTest.java     | 251 +++++++++++++++++++++
 .../jsieve/comparators/AsciiCasemapTest.java       |  85 +++++++
 5 files changed, 499 insertions(+), 20 deletions(-)

diff --git a/core/src/main/java/org/apache/jsieve/comparators/AsciiCasemap.java 
b/core/src/main/java/org/apache/jsieve/comparators/AsciiCasemap.java
index 16b322e..d3e5ff7 100644
--- a/core/src/main/java/org/apache/jsieve/comparators/AsciiCasemap.java
+++ b/core/src/main/java/org/apache/jsieve/comparators/AsciiCasemap.java
@@ -40,16 +40,16 @@ public class AsciiCasemap implements Comparator {
      * @see org.apache.jsieve.comparators.Equals#equals(String, String)
      */
     public boolean equals(String string1, String string2) {
-        return ComparatorUtils.equals(string1.toUpperCase(), string2
-                .toUpperCase());
+        return ComparatorUtils.equals(toAsciiUpperCase(string1),
+                toAsciiUpperCase(string2));
     }
 
     /**
      * @see org.apache.jsieve.comparators.Contains#contains(String, String)
      */
     public boolean contains(String container, String content) {
-        return ComparatorUtils.contains(container.toUpperCase(), content
-                .toUpperCase());
+        return ComparatorUtils.contains(toAsciiUpperCase(container),
+                toAsciiUpperCase(content));
     }
 
     /**
@@ -57,8 +57,24 @@ public class AsciiCasemap implements Comparator {
      */
     public boolean matches(String string, String glob)
             throws SievePatternException {
-        return ComparatorUtils
-                .matches(string.toUpperCase(), glob.toUpperCase());
+        return ComparatorUtils.matches(toAsciiUpperCase(string),
+                toAsciiUpperCase(glob));
+    }
+
+    /**
+     * Maps a-z onto A-Z and leaves every other character untouched, as
+     * defined by RFC 4790 section 9.2. Unlike {@link String#toUpperCase()},
+     * this neither depends on the default locale nor changes the length of
+     * the string.
+     */
+    static String toAsciiUpperCase(String string) {
+        final char[] chars = string.toCharArray();
+        for (int i = 0; i < chars.length; i++) {
+            if (chars[i] >= 'a' && chars[i] <= 'z') {
+                chars[i] = (char) (chars[i] - ('a' - 'A'));
+            }
+        }
+        return new String(chars);
     }
 
 }
diff --git 
a/core/src/main/java/org/apache/jsieve/comparators/ComparatorUtils.java 
b/core/src/main/java/org/apache/jsieve/comparators/ComparatorUtils.java
index b21036d..63af3eb 100644
--- a/core/src/main/java/org/apache/jsieve/comparators/ComparatorUtils.java
+++ b/core/src/main/java/org/apache/jsieve/comparators/ComparatorUtils.java
@@ -19,9 +19,7 @@
 
 package org.apache.jsieve.comparators;
 
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
+import java.util.Arrays;
 
 import org.apache.jsieve.SieveContext;
 import org.apache.jsieve.exception.LookupException;
@@ -75,6 +73,14 @@ public class ComparatorUtils {
      * Method <code>matches</code> answers a boolean indicating if the
      * parameter <code>string</code> matches the glob pattern described by
      * parameter <code>glob</code>.
+     * </p>
+     * <p>
+     * The glob is matched by a dedicated iterative matcher rather than being
+     * translated to a backtracking regular expression: worst case is
+     * O(string length * glob length) whatever the number of <code>*</code>.
+     * As mandated by RFC 5228 section 2.7.1, <code>*</code> and
+     * <code>?</code> match any character, line terminators included.
+     * </p>
      * 
      * @param string
      * @param glob
@@ -83,13 +89,68 @@ public class ComparatorUtils {
      */
     static public boolean matches(String string, String glob)
             throws SievePatternException {
-        try {
-            String regex = sieveToJavaRegex(glob);
-            final Matcher matcher = Pattern.compile(regex).matcher(string);
-            return matcher.matches();
-        } catch (PatternSyntaxException e) {
-            throw new SievePatternException(e.getMessage());
+        final int[] pattern = compileGlob(glob);
+        final int[] target = string.codePoints().toArray();
+
+        int t = 0;
+        int p = 0;
+        // Position in the pattern right after the last star seen, -1 if none
+        int afterStar = -1;
+        // Position in the target the last star is currently matched up to
+        int starMatchEnd = 0;
+        while (t < target.length) {
+            if (p < pattern.length && pattern[p] == GLOB_STAR) {
+                afterStar = ++p;
+                starMatchEnd = t;
+            } else if (p < pattern.length
+                    && (pattern[p] == GLOB_ANY || pattern[p] == target[t])) {
+                p++;
+                t++;
+            } else if (afterStar >= 0) {
+                // Mismatch: let the last star swallow one more character.
+                // Earlier stars never need revisiting.
+                p = afterStar;
+                t = ++starMatchEnd;
+            } else {
+                return false;
+            }
         }
+        while (p < pattern.length && pattern[p] == GLOB_STAR) {
+            p++;
+        }
+        return p == pattern.length;
+    }
+
+    private static final int GLOB_STAR = -1;
+
+    private static final int GLOB_ANY = -2;
+
+    /**
+     * Compiles a Sieve glob into code points, wildcards being replaced by
+     * {@link #GLOB_STAR} and {@link #GLOB_ANY}. As per RFC 5228 sections
+     * 2.7.1 and 2.4.2, a backslash makes the following character literal,
+     * an undefined escape such as <code>\a</code> standing for <code>a</code>.
+     * A trailing backslash stands for itself. Consecutive stars are merged.
+     */
+    private static int[] compileGlob(String glob) {
+        final int[] codePoints = glob.codePoints().toArray();
+        final int[] result = new int[codePoints.length];
+        int length = 0;
+        for (int i = 0; i < codePoints.length; i++) {
+            final int c = codePoints[i];
+            if (c == '*') {
+                if (length == 0 || result[length - 1] != GLOB_STAR) {
+                    result[length++] = GLOB_STAR;
+                }
+            } else if (c == '?') {
+                result[length++] = GLOB_ANY;
+            } else if (c == '\\' && i + 1 < codePoints.length) {
+                result[length++] = codePoints[++i];
+            } else {
+                result[length++] = c;
+            }
+        }
+        return Arrays.copyOf(result, length);
     }
 
     /**
@@ -139,8 +200,13 @@ public class ComparatorUtils {
     }
 
     /**
-     * Converts a Sieve pattern in a java regex pattern
+     * Converts a Sieve pattern in a java regex pattern.
+     *
+     * @deprecated no longer used for matching: the resulting regex
+     *             backtracks catastrophically on globs with several stars.
+     *             Use {@link #matches(String, String)}.
      */
+    @Deprecated
     public static String sieveToJavaRegex(String pattern) {
         int ch;
         StringBuilder buffer = new StringBuilder(2 * pattern.length());
@@ -165,8 +231,14 @@ public class ComparatorUtils {
                     buffer.append('\\');
                 else if (isSieveMatcherSpecialChar(pattern.charAt(ch + 1)))
                     buffer.append(pattern.charAt(++ch));
-                else
-                    buffer.append('\\');
+                else {
+                    // Undefined escape: the next character stands for itself
+                    buffer.setLength(buffer.length() - 1);
+                    final char escaped = pattern.charAt(++ch);
+                    if (isRegexSpecialChar(escaped))
+                        buffer.append('\\');
+                    buffer.append(escaped);
+                }
                 break;
             default:
                 if (isRegexSpecialChar(nextChar))
diff --git a/core/src/test/java/org/apache/jsieve/HeaderTest.java 
b/core/src/test/java/org/apache/jsieve/HeaderTest.java
index 67b1f59..36b24df 100644
--- a/core/src/test/java/org/apache/jsieve/HeaderTest.java
+++ b/core/src/test/java/org/apache/jsieve/HeaderTest.java
@@ -355,12 +355,14 @@ public class HeaderTest {
     }
 
     /**
-     * Test for special char escaping: \\? is a ? and \\* is an *
+     * Test for special char escaping: \\? is a ? and \\* is an *. A literal
+     * backslash is matched by \\\\ in the script, the glob itself then holding
+     * \\ (RFC 5228 section 2.7.1)
      */
     @Test
     public void testSpecialCharsEscapingInMatch() {
         boolean isTestPassed = false;
-        String script = "if header :matches \"X-Caffeine\" 
\"my,\\\\,?,\\\\?,*,\\\\*,pattern\" {throwTestException;}";
+        String script = "if header :matches \"X-Caffeine\" 
\"my,\\\\\\\\,?,\\\\?,*,\\\\*,pattern\" {throwTestException;}";
         try {
             SieveMailAdapter mail = (SieveMailAdapter) JUnitUtils.createMail();
             mail.getMessage().addHeader("X-Caffeine",
@@ -375,6 +377,24 @@ public class HeaderTest {
         Assert.assertTrue(isTestPassed);
     }
 
+    /**
+     * An undefined escape stands for the escaped character (RFC 5228 section
+     * 2.4.2): the script string "my\\,pattern" is the glob my\,pattern, which
+     * matches my,pattern
+     */
+    @Test
+    public void testUndefinedEscapeInMatch() throws Exception {
+        String script = "if header :matches \"X-Caffeine\" \"my\\\\,pattern\" 
{throwTestException;}";
+        SieveMailAdapter mail = (SieveMailAdapter) JUnitUtils.createMail();
+        mail.getMessage().addHeader("X-Caffeine", "my,pattern");
+        try {
+            JUnitUtils.interpret(mail, script);
+            Assert.fail("my\\,pattern should match my,pattern");
+        } catch (ThrowTestException.TestException e) {
+            // expected
+        }
+    }
+
     /**
      * Test for special char escaping: \\? is a ? and \\* is an *
      */
@@ -396,4 +416,39 @@ public class HeaderTest {
         Assert.assertTrue(isTestPassed);
     }
 
+
+    @Test(timeout = 2000)
+    public void multiStarGlobOnLongHeaderShouldBeEvaluatedInBoundedTime() 
throws Exception {
+        String script = "if header :matches \"Subject\" 
\"*2*^*a*a*e*a*a*a*a*a*\\\\\\\\a?*\" {throwTestException;}";
+        StringBuilder subject = new StringBuilder("2^");
+        for (int i = 0; i < 5000; i++) {
+            subject.append(i % 7 == 0 ? 'e' : 'a');
+        }
+        SieveMailAdapter mail = (SieveMailAdapter) JUnitUtils.createMail();
+        mail.getMessage().setSubject(subject.toString());
+        // No backslash in the subject: no match, and no endless backtracking
+        JUnitUtils.interpret(mail, script);
+    }
+
+    @Test
+    public void matchesShouldSpanFoldedHeaderLines() throws Exception {
+        String script = "if header :matches \"X-Caffeine\" \"first*second*\" 
{throwTestException;}";
+        SieveMailAdapter mail = (SieveMailAdapter) JUnitUtils.createMail();
+        mail.getMessage().addHeader("X-Caffeine", "first line\r\n second 
line");
+        try {
+            JUnitUtils.interpret(mail, script);
+            Assert.fail("first*second* should match a folded header");
+        } catch (ThrowTestException.TestException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void defaultComparatorShouldOnlyFoldAsciiCase() throws Exception {
+        String script = "if header :matches \"X-Caffeine\" \"*CAF\u00c9*\" 
{throwTestException;}";
+        SieveMailAdapter mail = (SieveMailAdapter) JUnitUtils.createMail();
+        mail.getMessage().addHeader("X-Caffeine", "un caf\u00e9");
+        // i;ascii-casemap folds c, a and f, but not \u00e9 (RFC 4790 section 
9.2)
+        JUnitUtils.interpret(mail, script);
+    }
 }
diff --git 
a/core/src/test/java/org/apache/jsieve/comparator/ComparatorUtilsTest.java 
b/core/src/test/java/org/apache/jsieve/comparator/ComparatorUtilsTest.java
index fd12591..45b5e8e 100644
--- a/core/src/test/java/org/apache/jsieve/comparator/ComparatorUtilsTest.java
+++ b/core/src/test/java/org/apache/jsieve/comparator/ComparatorUtilsTest.java
@@ -19,6 +19,8 @@
 
 package org.apache.jsieve.comparator;
 
+import java.util.Random;
+
 import org.apache.jsieve.comparators.ComparatorUtils;
 import org.apache.jsieve.exception.SievePatternException;
 import org.junit.Assert;
@@ -72,4 +74,253 @@ public class ComparatorUtilsTest {
         String expected = "\\[test\\] .\\\\.\\?.*\\\\.*\\*\\\\";
         Assert.assertEquals(expected, res);
     }
+
+    @Test(timeout = 2000)
+    public void multiStarGlobShouldMatchInLinearishTime() throws Exception {
+        StringBuilder target = new StringBuilder();
+        for (int i = 0; i < 2000; i++) {
+            target.append('a');
+        }
+        Assert.assertFalse(ComparatorUtils.matches(target.toString(), 
"*a*a*a*a*a*a*a*a*b"));
+    }
+
+    @Test(timeout = 2000)
+    public void fuzzedGlobShouldNotBacktrack() throws Exception {
+        StringBuilder target = new StringBuilder("2\u00e9^\u00e9");
+        for (int i = 0; i < 180; i++) {
+            target.append(i % 3 == 0 ? 'e' : 'a');
+        }
+        Assert.assertFalse(ComparatorUtils.matches(target.toString(),
+                "*2\u00e9*^\u00e9*a*a*e*a*a*a*a\u00df*a****\\\\a?*"));
+    }
+
+    @Test
+    public void wildcardsShouldMatchLineTerminators() throws Exception {
+        Assert.assertTrue(ComparatorUtils.matches("foo\r\n bar", "foo*bar"));
+        Assert.assertTrue(ComparatorUtils.matches("foo\nbar", "foo?bar"));
+    }
+
+    @Test
+    public void questionMarkShouldMatchASupplementaryCharacter() throws 
Exception {
+        Assert.assertTrue(ComparatorUtils.matches("a\uD83D\uDE00b", "a?b"));
+    }
+
+    @Test
+    public void supplementaryCharacterShouldNotBeAWildcard() throws Exception {
+        // U+1002A must not be truncated into '*'
+        Assert.assertFalse(ComparatorUtils.matches("x", "\uD800\uDC2A"));
+        Assert.assertTrue(ComparatorUtils.matches("\uD800\uDC2A", 
"\\\uD800\uDC2A"));
+    }
+
+    @Test
+    public void undefinedEscapeShouldStandForTheEscapedCharacter() throws 
Exception {
+        Assert.assertTrue(ComparatorUtils.matches("a", "\\a"));
+        Assert.assertFalse(ComparatorUtils.matches("\\a", "\\a"));
+        Assert.assertTrue(ComparatorUtils.matches("x.y", "x\\.y"));
+    }
+
+    @Test
+    public void trailingBackslashShouldStandForItself() throws Exception {
+        Assert.assertTrue(ComparatorUtils.matches("foo\\", "foo\\"));
+        Assert.assertFalse(ComparatorUtils.matches("foo", "foo\\"));
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void regexTranslationShouldHandleUndefinedEscapes() {
+        Assert.assertEquals("a\\.b", 
ComparatorUtils.sieveToJavaRegex("\\a\\.b"));
+    }
+
+    @Test
+    public void edgeCases() throws Exception {
+        assertMatch("", "");
+        assertMatch("", "*");
+        assertMatch("", "***");
+        assertNoMatch("", "?");
+        assertNoMatch("", "a");
+        assertNoMatch("a", "");
+        assertMatch("abc", "***");
+        assertMatch("abc", "a*");
+        assertMatch("abc", "*c");
+        assertMatch("abc", "*b*");
+        assertNoMatch("abc", "*b");
+        assertNoMatch("abc", "b*");
+        assertMatch("abc", "???");
+        assertNoMatch("abc", "??");
+        assertNoMatch("abc", "????");
+        assertMatch("abc", "?*?");
+        assertMatch("ab", "?*?");
+        assertNoMatch("a", "?*?");
+        assertMatch("*", "\\*");
+        assertNoMatch("a", "\\*");
+        assertMatch("?", "\\?");
+        assertNoMatch("a", "\\?");
+        assertMatch("a*b", "*\\**");
+        assertNoMatch("ab", "*\\**");
+        assertMatch("\\", "\\\\");
+        assertNoMatch("\\\\", "\\\\");
+    }
+
+    @Test
+    public void starShouldBacktrackPastAPartialMatch() throws Exception {
+        assertMatch("abcabd", "*abd");
+        assertMatch("ababab", "*ab*ab");
+        assertMatch("aaa", "a*a*a");
+        assertNoMatch("aa", "a*a*a");
+        assertMatch("mississippi", "*sip*");
+        assertMatch("mississippi", "m*iss*ppi");
+        assertNoMatch("mississippi", "m*iss*ppix");
+        assertMatch("xaaab", "x*a?b");
+        assertNoMatch("xaab", "x*a??b");
+    }
+
+    @Test
+    public void caseShouldBeSignificant() throws Exception {
+        assertNoMatch("ABC", "abc");
+        assertNoMatch("ABC", "a*");
+    }
+
+    @Test(timeout = 5000)
+    public void adversarialInputsShouldMatchInBoundedTime() throws Exception {
+        String as = repeat("a", 10000);
+        // Many stars, the last literal never occurs
+        assertNoMatch(as, repeat("*a", 200) + "b");
+        // Many stars, and a match only at the very end
+        assertMatch(as + "b", repeat("*a", 200) + "*b");
+        // Repeated multi-character literal between stars
+        assertNoMatch(repeat("ab", 5000), repeat("*ab", 100) + "c");
+        // Question marks after a star
+        assertNoMatch(as, "*" + repeat("?", 5000) + "b");
+        assertMatch(as, "*" + repeat("?", 5000));
+        // Long header with a single star
+        assertNoMatch(repeat("x", 500000), "*y*");
+        // Stars and question marks interleaved
+        assertNoMatch(as, repeat("*?", 200) + "b");
+    }
+
+    @Test
+    public void matchesShouldAgreeWithReferenceImplementationOnRandomGlobs() 
throws Exception {
+        Random random = new Random(42);
+        for (int run = 0; run < 50000; run++) {
+            String glob = randomString(random, 12);
+            String target = randomString(random, 20);
+            assertAgreesWithReference(target, glob);
+        }
+    }
+
+    @Test
+    public void 
matchesShouldAgreeWithReferenceImplementationOnGlobsDerivedFromTarget() throws 
Exception {
+        Random random = new Random(43);
+        for (int run = 0; run < 50000; run++) {
+            String target = randomString(random, 30);
+            // Without mutation, the derived glob must match its target
+            String exact = deriveGlob(random, target, false);
+            assertMatch(target, exact);
+            assertAgreesWithReference(target, exact);
+            assertAgreesWithReference(target, deriveGlob(random, target, 
true));
+        }
+    }
+
+    private static final String[] ALPHABET = {"a", "b", "A", "*", "?", "\\", 
".", "\n", "\r",
+            "ß", "😀"};
+
+    private static String randomString(Random random, int maxLength) {
+        int length = random.nextInt(maxLength + 1);
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < length; i++) {
+            builder.append(ALPHABET[random.nextInt(ALPHABET.length)]);
+        }
+        return builder.toString();
+    }
+
+    private static String repeat(String s, int count) {
+        StringBuilder builder = new StringBuilder(s.length() * count);
+        for (int i = 0; i < count; i++) {
+            builder.append(s);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Builds a glob matching the given target: each character is either kept
+     * (escaped if needed), replaced by a ?, or a run of characters is replaced
+     * by a *. When mutating, undefined escapes and a trailing random character
+     * may be added, the result being left to the reference implementation to
+     * judge.
+     */
+    private static String deriveGlob(Random random, String target, boolean 
mutate) {
+        int[] codePoints = target.codePoints().toArray();
+        StringBuilder glob = new StringBuilder();
+        int i = 0;
+        while (i < codePoints.length) {
+            int choice = random.nextInt(10);
+            if (choice < 2) {
+                glob.append('*');
+                i += random.nextInt(Math.min(4, codePoints.length - i) + 1);
+            } else if (choice < 4) {
+                glob.append('?');
+                i++;
+            } else {
+                int c = codePoints[i++];
+                if (c == '*' || c == '?' || c == '\\' || (mutate && 
random.nextInt(20) == 0)) {
+                    glob.append('\\');
+                }
+                glob.appendCodePoint(c);
+            }
+        }
+        if (mutate && random.nextInt(4) == 0) {
+            glob.append(ALPHABET[random.nextInt(ALPHABET.length)]);
+        }
+        return glob.toString();
+    }
+
+    private static void assertAgreesWithReference(String target, String glob) 
throws Exception {
+        Assert.assertEquals("glob=" + glob + " target=" + target,
+                referenceMatches(target, glob),
+                ComparatorUtils.matches(target, glob));
+    }
+
+    /**
+     * Textbook dynamic programming glob matcher, written straight from RFC
+     * 5228: escapes are resolved first, then reachable[i][j] tells whether
+     * the first i characters of the target match the first j glob tokens.
+     */
+    private static boolean referenceMatches(String target, String glob) {
+        int[] t = target.codePoints().toArray();
+        int[] g = glob.codePoints().toArray();
+        int[] tokens = new int[g.length];
+        boolean[] literal = new boolean[g.length];
+        int n = 0;
+        for (int i = 0; i < g.length; i++, n++) {
+            if (g[i] == '\\' && i + 1 < g.length) {
+                tokens[n] = g[++i];
+                literal[n] = true;
+            } else {
+                tokens[n] = g[i];
+                literal[n] = g[i] != '*' && g[i] != '?';
+            }
+        }
+        boolean[][] reachable = new boolean[t.length + 1][n + 1];
+        reachable[0][0] = true;
+        for (int i = 0; i <= t.length; i++) {
+            for (int j = 1; j <= n; j++) {
+                boolean star = !literal[j - 1] && tokens[j - 1] == '*';
+                if (star) {
+                    reachable[i][j] = reachable[i][j - 1] || (i > 0 && 
reachable[i - 1][j]);
+                } else if (i > 0) {
+                    boolean any = !literal[j - 1] && tokens[j - 1] == '?';
+                    reachable[i][j] = reachable[i - 1][j - 1] && (any || 
tokens[j - 1] == t[i - 1]);
+                }
+            }
+        }
+        return reachable[t.length][n];
+    }
+
+    private static void assertMatch(String target, String glob) throws 
Exception {
+        Assert.assertTrue("glob=" + glob + " target=" + target, 
ComparatorUtils.matches(target, glob));
+    }
+
+    private static void assertNoMatch(String target, String glob) throws 
Exception {
+        Assert.assertFalse("glob=" + glob + " target=" + target, 
ComparatorUtils.matches(target, glob));
+    }
 }
diff --git 
a/core/src/test/java/org/apache/jsieve/comparators/AsciiCasemapTest.java 
b/core/src/test/java/org/apache/jsieve/comparators/AsciiCasemapTest.java
new file mode 100644
index 0000000..e11470b
--- /dev/null
+++ b/core/src/test/java/org/apache/jsieve/comparators/AsciiCasemapTest.java
@@ -0,0 +1,85 @@
+/****************************************************************
+ * 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   *
+ *                                                              *
+ *   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.                                           *
+ ****************************************************************/
+
+package org.apache.jsieve.comparators;
+
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * i;ascii-casemap only folds a-z onto A-Z, other characters being compared
+ * as is (RFC 4790 section 9.2).
+ */
+public class AsciiCasemapTest {
+
+    private final AsciiCasemap subject = new AsciiCasemap();
+
+    @Test
+    public void matchesShouldIgnoreAsciiCase() throws Exception {
+        Assert.assertTrue(subject.matches("Hello World", "*hello w*"));
+        Assert.assertTrue(subject.matches("hello world", "HELLO?WORLD"));
+        Assert.assertFalse(subject.matches("hello world", "HELLO?WORLDS"));
+    }
+
+    @Test(timeout = 2000)
+    public void multiStarGlobShouldMatchInBoundedTime() throws Exception {
+        StringBuilder target = new StringBuilder();
+        for (int i = 0; i < 2000; i++) {
+            target.append(i % 2 == 0 ? 'a' : 'A');
+        }
+        Assert.assertFalse(subject.matches(target.toString(), 
"*a*a*a*a*a*a*a*a*b"));
+    }
+
+    @Test
+    public void questionMarkShouldMatchSharpS() throws Exception {
+        // "ß".toUpperCase() is "SS": two characters
+        Assert.assertTrue(subject.matches("ß", "?"));
+        Assert.assertFalse(subject.matches("ß", "??"));
+    }
+
+    @Test
+    public void sharpSShouldNotEqualSS() throws Exception {
+        Assert.assertFalse(subject.matches("SS", "ß"));
+        Assert.assertFalse(subject.equals("SS", "ß"));
+        Assert.assertFalse(subject.contains("SS", "ß"));
+    }
+
+    @Test
+    public void nonAsciiLettersShouldNotBeFolded() throws Exception {
+        Assert.assertFalse(subject.matches("é", "É"));
+        Assert.assertFalse(subject.equals("é", "É"));
+        Assert.assertFalse(subject.contains("été", "É"));
+    }
+
+    @Test
+    public void foldingShouldNotDependOnTheDefaultLocale() throws Exception {
+        Locale defaultLocale = Locale.getDefault();
+        Locale.setDefault(new Locale("tr", "TR"));
+        try {
+            // In Turkish, "i".toUpperCase() is a dotted capital I
+            Assert.assertTrue(subject.matches("title", "TITLE"));
+            Assert.assertTrue(subject.equals("title", "TITLE"));
+            Assert.assertTrue(subject.contains("title", "IT"));
+        } finally {
+            Locale.setDefault(defaultLocale);
+        }
+    }
+}


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

Reply via email to