This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch fix/degrees-lat-lon-pattern in repository https://gitbox.apache.org/repos/asf/opennlp.git
commit 9e556ea542e7aec502a2fc0cafed067cb8de2d4d 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); }
