This is an automated email from the ASF dual-hosted git repository.
krickert pushed a commit to branch fix/degrees-lat-lon-pattern
in repository https://gitbox.apache.org/repos/asf/opennlp.git
The following commit(s) were added to refs/heads/fix/degrees-lat-lon-pattern by
this push:
new 1eb3fc86e Strengthen coordinate regex backtracking regression tests
1eb3fc86e is described below
commit 1eb3fc86e7a228eb1363979b96ab136d37027bf3
Author: Kristian Rickert <[email protected]>
AuthorDate: Mon Nov 2 05:29:50 2026 -0500
Strengthen coordinate regex backtracking regression tests
Add a character-read budget of eight times the input length for whitespace
runs of 1000, 2000, and 4000 characters. Bound the timing-test warmup with a
ten-second timeout.
All three read-budget cases failed against the previous greedy pattern. The
fixed pattern passes all 101 targeted RegexNameFinderFactoryTest and
RegexNameFinderTest cases on JDK 21.
Signed-off-by: Kristian Rickert <[email protected]>
---
.../tools/namefind/RegexNameFinderFactoryTest.java | 70 +++++++++++++++++++---
1 file changed, 63 insertions(+), 7 deletions(-)
diff --git
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
index c4be25348..753151780 100644
---
a/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
+++
b/opennlp-core/opennlp-runtime/src/test/java/opennlp/tools/namefind/RegexNameFinderFactoryTest.java
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
import opennlp.tools.tokenize.WhitespaceTokenizer;
import opennlp.tools.util.Span;
@@ -107,9 +108,8 @@ public class RegexNameFinderFactoryTest {
* The hardened patterns must finish quickly regardless of input length.
* <p>
* Each attack runs as its own test so a regression names the exact input
that
- * got slow. Each case does one warmup run outside the timed section, so the
- * budget measures the match itself rather than JVM cold start on throttled
- * machines.
+ * got slow. Each case uses a generous timeout for warmup before checking a
+ * tighter match budget, allowing for JVM cold start on throttled machines.
*/
private static Stream<Arguments> reDoSAttacks() {
return Stream.of(
@@ -170,13 +170,69 @@ public class RegexNameFinderFactoryTest {
assertCompletesQuickly(() -> finder.find(tokens));
}
+ @ParameterizedTest
+ @ValueSource(ints = {1000, 2000, 4000})
+ void testDegreesPatternRejectsWhitespaceWithinLinearReadBudget(int
whitespaceLength) {
+ var def =
RegexNameFinderFactory.DEFAULT_REGEX_NAME_FINDER.DEGREES_MIN_SEC_LAT_LON;
+ Pattern pattern = def.getRegexMap().get(def.getType())[0];
+ CharSequence input = new ReadLimitedCharSequence("1" + "
".repeat(whitespaceLength));
+ Assertions.assertFalse(pattern.matcher(input).find());
+ }
+
+ /**
+ * Limits character reads to a constant multiple of the input length, so
excessive
+ * backtracking fails without depending on machine speed or thread
interruption.
+ */
+ private static final class ReadLimitedCharSequence implements CharSequence {
+ private final String text;
+ private final long readLimit;
+ private long reads;
+
+ /**
+ * Wraps the input with a generous linear budget for regex engine
character reads.
+ *
+ * @param text The input to match.
+ */
+ private ReadLimitedCharSequence(String text) {
+ this.text = text;
+ this.readLimit = 8L * text.length();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public int length() {
+ return text.length();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public char charAt(int index) {
+ if (++reads > readLimit) {
+ Assertions.fail("Character reads exceeded linear budget of " +
readLimit
+ + " for input length " + text.length());
+ }
+ return text.charAt(index);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return text.subSequence(start, end);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return text;
+ }
+ }
+
/**
- * Runs {@code find} once as a warmup outside the timed section, so the
budget
- * measures the match itself rather than JVM cold start on throttled
machines,
- * then asserts that a second run finishes within the budget.
+ * Runs {@code find} once with a generous warmup timeout, then asserts that a
+ * second run finishes within the tighter match budget.
*/
private static void assertCompletesQuickly(Executable find) {
- Assertions.assertDoesNotThrow(find);
+ Assertions.assertTimeoutPreemptively(Duration.ofSeconds(10), find);
Assertions.assertTimeoutPreemptively(Duration.ofSeconds(2), find);
}