This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 4ee32aa56 Fix low surrogate false match in
containsAny/containsNone/indexOfAny (#1771)
4ee32aa56 is described below
commit 4ee32aa56d596e31f50fbf325c2641591e9e1ea4
Author: alhuda <[email protected]>
AuthorDate: Wed Aug 12 21:04:18 2026 +0530
Fix low surrogate false match in containsAny/containsNone/indexOfAny (#1771)
A low surrogate in the char[] search set matched the low half of a
different supplementary code point in the input; the shared surrogate guard now
verifies the preceding high surrogate for low-surrogate matches.
---
.../java/org/apache/commons/lang3/StringUtils.java | 12 ++++++---
.../commons/lang3/StringUtilsContainsTest.java | 31 ++++++++++++++++++++++
.../lang3/StringUtilsEqualsIndexOfTest.java | 14 ++++++++++
.../org/apache/commons/lang3/Supplementary.java | 9 +++++++
4 files changed, 63 insertions(+), 3 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java
b/src/main/java/org/apache/commons/lang3/StringUtils.java
index 7d40648c0..3feecd47a 100644
--- a/src/main/java/org/apache/commons/lang3/StringUtils.java
+++ b/src/main/java/org/apache/commons/lang3/StringUtils.java
@@ -1091,7 +1091,9 @@ public static boolean containsAny(final CharSequence cs,
final char... searchCha
final char ch = cs.charAt(i);
for (int j = 0; j < searchLength; j++) {
if (searchChars[j] == ch) {
- if (!Character.isHighSurrogate(ch) || j == searchLast || i
< csLast && searchChars[j + 1] == cs.charAt(i + 1)) {
+ if (Character.isHighSurrogate(ch)
+ ? j == searchLast || i < csLast && searchChars[j +
1] == cs.charAt(i + 1)
+ : j == 0 || !Character.isLowSurrogate(ch) ||
!Character.isHighSurrogate(searchChars[j - 1]) || i > 0 && searchChars[j - 1]
== cs.charAt(i - 1)) {
return true;
}
}
@@ -1261,7 +1263,9 @@ public static boolean containsNone(final CharSequence cs,
final char... searchCh
final char ch = cs.charAt(i);
for (int j = 0; j < searchLen; j++) {
if (searchChars[j] == ch) {
- if (!Character.isHighSurrogate(ch) || j == searchLast || i
< csLast && searchChars[j + 1] == cs.charAt(i + 1)) {
+ if (Character.isHighSurrogate(ch)
+ ? j == searchLast || i < csLast && searchChars[j +
1] == cs.charAt(i + 1)
+ : j == 0 || !Character.isLowSurrogate(ch) ||
!Character.isHighSurrogate(searchChars[j - 1]) || i > 0 && searchChars[j - 1]
== cs.charAt(i - 1)) {
return false;
}
}
@@ -2835,7 +2839,9 @@ public static int indexOfAny(final CharSequence cs, final
int csStart, final cha
final char ch = cs.charAt(i);
for (int j = 0; j < searchLen; j++) {
if (searchChars[j] == ch) {
- if (!Character.isHighSurrogate(ch) || j == searchLast || i
< csLast && searchChars[j + 1] == cs.charAt(i + 1)) {
+ if (Character.isHighSurrogate(ch)
+ ? j == searchLast || i < csLast && searchChars[j +
1] == cs.charAt(i + 1)
+ : j == 0 || !Character.isLowSurrogate(ch) ||
!Character.isHighSurrogate(searchChars[j - 1]) || i > 0 && searchChars[j - 1]
== cs.charAt(i - 1)) {
return i;
}
}
diff --git
a/src/test/java/org/apache/commons/lang3/StringUtilsContainsTest.java
b/src/test/java/org/apache/commons/lang3/StringUtilsContainsTest.java
index 54eab7a8f..7f9de3ecd 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsContainsTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsContainsTest.java
@@ -18,6 +18,7 @@
import static org.apache.commons.lang3.Supplementary.CharU20000;
import static org.apache.commons.lang3.Supplementary.CharU20001;
+import static org.apache.commons.lang3.Supplementary.CharU24000;
import static org.apache.commons.lang3.Supplementary.CharUSuppCharHigh;
import static org.apache.commons.lang3.Supplementary.CharUSuppCharLow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -141,6 +142,23 @@ void
testContainsAny_StringCharArrayWithSupplementaryChars() {
assertFalse(StringUtils.containsAny(CharU20001,
CharU20000.toCharArray()));
}
+ /**
+ * Two supplementary code points that share their low surrogate but not
their high surrogate must not match, otherwise the low half of one pair is
treated as
+ * the low half of the other. See
https://www.oracle.com/technical-resources/articles/javase/supplementary.html
+ */
+ @Test
+ void testContainsAny_StringCharArrayWithSharedLowSurrogate() {
+ // Sanity check: same low surrogate, different code point.
+ assertEquals(CharU20000.charAt(1), CharU24000.charAt(1));
+ assertEquals(-1, CharU20000.indexOf(CharU24000));
+ // Test:
+ assertFalse(StringUtils.containsAny(CharU20000,
CharU24000.toCharArray()));
+ assertFalse(StringUtils.containsAny(CharU24000,
CharU20000.toCharArray()));
+ assertFalse(StringUtils.containsAny("abc" + CharU20000 + "xyz",
CharU24000.toCharArray()));
+ // A genuine occurrence of the same pair still matches.
+ assertTrue(StringUtils.containsAny("abc" + CharU24000 + "xyz",
CharU24000.toCharArray()));
+ }
+
@Test
void testContainsAny_StringString() {
assertFalse(StringUtils.containsAny(null, (String) null));
@@ -344,6 +362,19 @@ void testContainsNone_CharArrayWithSupplementaryChars() {
assertTrue(StringUtils.containsNone(CharU20001,
CharU20000.toCharArray()));
}
+ /**
+ * Two supplementary code points that share their low surrogate but not
their high surrogate must not match. See
+ *
https://www.oracle.com/technical-resources/articles/javase/supplementary.html
+ */
+ @Test
+ void testContainsNone_CharArrayWithSharedLowSurrogate() {
+ assertEquals(CharU20000.charAt(1), CharU24000.charAt(1));
+ assertTrue(StringUtils.containsNone(CharU20000,
CharU24000.toCharArray()));
+ assertTrue(StringUtils.containsNone(CharU24000,
CharU20000.toCharArray()));
+ // A genuine occurrence of the same pair is still found.
+ assertFalse(StringUtils.containsNone("abc" + CharU24000,
CharU24000.toCharArray()));
+ }
+
@Test
void testContainsNone_String() {
final String str1 = "a";
diff --git
a/src/test/java/org/apache/commons/lang3/StringUtilsEqualsIndexOfTest.java
b/src/test/java/org/apache/commons/lang3/StringUtilsEqualsIndexOfTest.java
index 2be5fb722..1b78d9e36 100644
--- a/src/test/java/org/apache/commons/lang3/StringUtilsEqualsIndexOfTest.java
+++ b/src/test/java/org/apache/commons/lang3/StringUtilsEqualsIndexOfTest.java
@@ -18,6 +18,7 @@
import static org.apache.commons.lang3.Supplementary.CharU20000;
import static org.apache.commons.lang3.Supplementary.CharU20001;
+import static org.apache.commons.lang3.Supplementary.CharU24000;
import static org.apache.commons.lang3.Supplementary.CharUSuppCharHigh;
import static org.apache.commons.lang3.Supplementary.CharUSuppCharLow;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -417,6 +418,19 @@ void
testIndexOfAny_StringCharArrayWithSupplementaryChars() {
assertEquals(-1, StringUtils.indexOfAny("abc" + CharUSuppCharHigh,
CharU20000.toCharArray()));
}
+ /**
+ * A low surrogate that is the low half of a supplementary code point in
the search set must not match the low half of a different code point in the
input,
+ * otherwise the returned index points inside a surrogate pair. See
https://www.oracle.com/technical-resources/articles/javase/supplementary.html
+ */
+ @Test
+ void testIndexOfAny_StringCharArrayWithSharedLowSurrogate() {
+ assertEquals(CharU20000.charAt(1), CharU24000.charAt(1));
+ assertEquals(-1, StringUtils.indexOfAny(CharU20000,
CharU24000.toCharArray()));
+ assertEquals(-1, StringUtils.indexOfAny("abc" + CharU20000,
CharU24000.toCharArray()));
+ // A genuine occurrence of the same pair is found at the start of the
pair.
+ assertEquals(3, StringUtils.indexOfAny("abc" + CharU24000,
CharU24000.toCharArray()));
+ }
+
@Test
void testIndexOfAny_StringIntCharArray() {
// default cases
diff --git a/src/test/java/org/apache/commons/lang3/Supplementary.java
b/src/test/java/org/apache/commons/lang3/Supplementary.java
index a6cec7896..81ee3e74f 100644
--- a/src/test/java/org/apache/commons/lang3/Supplementary.java
+++ b/src/test/java/org/apache/commons/lang3/Supplementary.java
@@ -48,6 +48,15 @@ public class Supplementary {
*/
static final String CharU20001 = "\uD840\uDC01";
+ /**
+ * Supplementary character U+24000 See
https://www.oracle.com/technical-resources/articles/javase/supplementary.html
+ * <p>
+ * Shares its low surrogate (U+DC00) with {@link #CharU20000} but has a
different high surrogate, so the two are distinct code points that agree on
their
+ * second UTF-16 code unit.
+ * </p>
+ */
+ static final String CharU24000 = "\uD850\uDC00";
+
/**
* Incomplete supplementary character U+20000, <em>high surrogate
only</em>.
* <p>