From bf8f7b9c20a7628293b2492440c0ade8775c3256 Mon Sep 17 00:00:00 2001 From: jdymitarai Date: Sat, 12 Sep 2026 13:56:57 +0800 Subject: [PATCH] Skip literal prefix index hits inside surrogate pairs (fixes #207) When a pattern's literal prefix begins with a lone low surrogate, MachineInput.UTF16Input.index() previously jumped to raw indexOf hits. If the hit is preceded by a high surrogate, it is inside a valid surrogate pair rather than a codepoint boundary. In step(), decoding starting from inside the pair caused it to treat the low surrogate as an isolated rune and erroneously match. Patterns without literal prefixes step codepoint by codepoint and correctly skip such pairs. This commit updates UTF16Input.index() to detect when an indexOf hit is a low surrogate preceded by a high surrogate, skipping it and resuming search at the next character. --- java/com/google/re2j/MachineInput.java | 15 ++++++++++++--- javatests/com/google/re2j/MatcherTest.java | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/java/com/google/re2j/MachineInput.java b/java/com/google/re2j/MachineInput.java index f69ba043..9808eb26 100644 --- a/java/com/google/re2j/MachineInput.java +++ b/java/com/google/re2j/MachineInput.java @@ -200,9 +200,18 @@ boolean canCheckPrefix() { @Override int index(RE2 re2, int pos) { - pos += start; - int i = indexOf(str, re2.prefix, pos); - return i < 0 ? i : i - pos; + int p = pos + start; + while (true) { + int i = indexOf(str, re2.prefix, p); + if (i < 0) { + return -1; + } + if (i > 0 && Character.isLowSurrogate(str.charAt(i)) && Character.isHighSurrogate(str.charAt(i - 1))) { + p = i + 1; + continue; + } + return i - (pos + start); + } } @Override diff --git a/javatests/com/google/re2j/MatcherTest.java b/javatests/com/google/re2j/MatcherTest.java index 4f24ae2e..5747150e 100644 --- a/javatests/com/google/re2j/MatcherTest.java +++ b/javatests/com/google/re2j/MatcherTest.java @@ -524,4 +524,23 @@ public void testPatternLongestMatch() { assertEquals("aaa bbb", text.substring(matcher.start(), matcher.end())); } } + + @Test + public void testSurrogatePairInteriorNoMatch() { + // A pattern whose literal prefix is a lone low surrogate should not match + // inside a valid surrogate pair (issue #207). + assertFalse(Pattern.compile("\uDC21").matcher("a\uD801\uDC21b").find()); + assertFalse(Pattern.compile("\uDC21|\uDC22").matcher("a\uD801\uDC21b").find()); + assertFalse(Pattern.compile("[\uD800-\uDFFF]").matcher("a\uD801\uDC21b").find()); + + // Search must skip the interior hit and resume to find an isolated low surrogate: + Matcher m = Pattern.compile("\uDC21").matcher("\uD801\uDC21\uDC21"); + assertTrue(m.find()); + assertEquals(2, m.start()); + assertEquals(3, m.end()); + + // StringBuilder CharSequence input should also behave identically: + Matcher mSb = Pattern.compile("\uDC21").matcher(new StringBuilder("a\uD801\uDC21b")); + assertFalse(mSb.find()); + } }