This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/template in repository https://gitbox.apache.org/repos/asf/cxf.git
commit b9b052468a4d024f12202babb38c9e3e8c77674a Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Wed Nov 11 06:54:18 2026 +0000 Avoid polynomial backtracking for adjacent JAX-RS template variables --- .../org/apache/cxf/jaxrs/model/URITemplate.java | 20 ++++++++-- .../apache/cxf/jaxrs/model/URITemplateTest.java | 43 +++++++++++++++++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java index 0a9947a8f48..0da4f829927 100644 --- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java +++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/model/URITemplate.java @@ -43,6 +43,9 @@ public final class URITemplate { public static final String LIMITED_REGEX_SUFFIX = "(/.*)?"; public static final String FINAL_MATCH_GROUP = "FINAL_MATCH_GROUP"; private static final String DEFAULT_PATH_VARIABLE_REGEX = "([^/]+?)"; + // For an unconstrained variable directly followed by another, e.g. {a} in {a}{b}: the lazy + // default only ever binds one character there, and matching exactly one avoids backtracking. + private static final String ADJACENT_PATH_VARIABLE_REGEX = "([^/])"; private static final String CHARACTERS_TO_ESCAPE = ".*+$()"; private static final String SLASH = "/"; private static final String SLASH_QUOTE = "/;"; @@ -64,9 +67,10 @@ public final class URITemplate { CurlyBraceTokenizer tok = new CurlyBraceTokenizer(template); uriChunks = new ArrayList<>(); while (tok.hasNext()) { - String templatePart = tok.next(); - UriChunk chunk = UriChunk.createUriChunk(templatePart); - uriChunks.add(chunk); + uriChunks.add(UriChunk.createUriChunk(tok.next())); + } + for (int c = 0; c < uriChunks.size(); c++) { + UriChunk chunk = uriChunks.get(c); if (chunk instanceof Literal) { String encodedValue = HttpUtils.encodePartiallyEncoded(chunk.getValue(), false); String substr = escapeCharacters(encodedValue); @@ -89,6 +93,8 @@ public final class URITemplate { patternBuilder.append(pattern); patternBuilder.append(')'); } + } else if (isUnconstrainedVariable(c + 1)) { + patternBuilder.append(ADJACENT_PATH_VARIABLE_REGEX); } else { patternBuilder.append(DEFAULT_PATH_VARIABLE_REGEX); } @@ -106,6 +112,14 @@ public final class URITemplate { templateRegexPattern = Pattern.compile(patternBuilder.toString()); } + private boolean isUnconstrainedVariable(int index) { + if (index >= uriChunks.size()) { + return false; + } + UriChunk chunk = uriChunks.get(index); + return chunk instanceof Variable && ((Variable)chunk).getPattern() == null; + } + public String getLiteralChars() { return literals; } diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java index 7e7f968df16..086744f6e43 100644 --- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java +++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/model/URITemplateTest.java @@ -799,4 +799,45 @@ public class URITemplateTest { assertTrue("should match '/cxf' url", t2.match("/cxf", values)); Assert.assertEquals("cxf", values.get("project").get(0)); } -} \ No newline at end of file + + @Test + public void testAdjacentVariables() { + URITemplate t = new URITemplate("/{a}{b}{c}.json"); + MultivaluedMap<String, String> values = new MetadataMap<>(); + assertTrue(t.match("/xyz123.json", values)); + assertEquals("x", values.getFirst("a")); + assertEquals("y", values.getFirst("b")); + assertEquals("z123", values.getFirst("c")); + assertEquals("/", values.getFirst(URITemplate.FINAL_MATCH_GROUP)); + + values.clear(); + assertFalse(t.match("/xy.json", values)); + + values.clear(); + t = new URITemplate("/{a}{b}/rest"); + assertTrue(t.match("/abc/rest/more", values)); + assertEquals("a", values.getFirst("a")); + assertEquals("bc", values.getFirst("b")); + assertEquals("/more", values.getFirst(URITemplate.FINAL_MATCH_GROUP)); + } + + @Test + public void testAdjacentVariablesWithCustomRegex() { + // Only an unconstrained variable followed by an unconstrained variable is limited to one char + URITemplate t = new URITemplate("/{a}{b:\\d+}"); + MultivaluedMap<String, String> values = new MetadataMap<>(); + assertTrue(t.match("/abc123", values)); + assertEquals("abc", values.getFirst("a")); + assertEquals("123", values.getFirst("b")); + } + + @Test(timeout = 5000) + public void testAdjacentVariablesNoBacktracking() { + URITemplate t = new URITemplate("/{a}{b}{c}.json"); + StringBuilder sb = new StringBuilder("/"); + for (int i = 0; i < 4096; i++) { + sb.append('a'); + } + assertFalse(t.match(sb.toString(), new MetadataMap<>())); + } +}
