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]