Skip to content

Commit 99127f7

Browse files
joewizclaude
andcommitted
[bugfix] fn:matches/analyze-string: raise XPST0017 for XPath 4.0 lookaround
The previous isXQuery40 = context.getXQueryVersion() >= 40 guard was dead on this XQuery 3.1 branch: getXQueryVersion() never returns 40 here, so a pattern using (*positive_lookahead:...) or similar XPath 4.0 syntax silently fell through to Saxon's XP30 regex compiler and produced opaque FORX0002 errors. Replace the guard with an explicit XPST0017 "XPath 4.0 lookaround syntax is not yet implemented in this XQuery 3.1 build" exception in both FunMatches.matchXmlRegex and FunAnalyzeString.analyzeString. The XQ4 translation/dispatch path stays available on v2/xq4-core-functions; when 4.0 lands on develop, swap the throw for the translateXPath4Lookaround call in one spot per file. validateXPathRegex is now called with isXQuery40=false explicitly, since this branch only runs the 3.1 dialect. Adds RegexXPath4NotImplementedTest covering both fn:matches and fn:analyze-string error paths plus a plain-pattern smoke check. Addresses Juri's review comment on PR eXist-db#6344 (preferred middle ground: keep the version check, but raise a clear "not implemented yet" error rather than silently dead-coding the branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7f93605 commit 99127f7

3 files changed

Lines changed: 127 additions & 31 deletions

File tree

exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,21 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
125125
}
126126
}
127127

128-
private void analyzeString(final MemTreeBuilder builder, final String input, String pattern, final String flags) throws XPathException {
129-
final boolean isXQuery40 = context.getXQueryVersion() >= 40;
128+
private void analyzeString(final MemTreeBuilder builder, final String input, final String pattern, final String flags) throws XPathException {
130129
final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();
131130

132-
// XQ4: translate (*positive_lookahead:...) etc. to Java regex
133-
if (isXQuery40 && org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround(pattern)) {
134-
pattern = org.exist.xquery.regex.RegexUtil.translateXPath4Lookaround(pattern);
131+
// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
132+
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
133+
// translateXPath4Lookaround() dispatch path.
134+
if (org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround(pattern)) {
135+
throw new XPathException(this, ErrorCodes.XPST0017,
136+
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
137+
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
135138
}
136139

137-
// Pre-validate: reject constructs not valid in XPath regex
140+
// Pre-validate: reject constructs not valid in XPath 3.1 regex
138141
if (!org.exist.xquery.regex.RegexUtil.hasLiteral(flags)) {
139-
org.exist.xquery.regex.RegexUtil.validateXPathRegex(this, pattern, isXQuery40);
142+
org.exist.xquery.regex.RegexUtil.validateXPathRegex(this, pattern, false);
140143
}
141144

142145
final List<String> warnings = new ArrayList<>(1);

exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -525,34 +525,20 @@ private Sequence evalGeneric(final Sequence contextSequence, final Item contextI
525525
}
526526

527527

528-
private boolean matchXmlRegex(String string, String pattern, final String flags) throws XPathException {
529-
final boolean isXQuery40 = context.getXQueryVersion() >= 40;
530-
531-
532-
// XQ4: translate (*positive_lookahead:...) etc. to Java regex (?=...) syntax
533-
if (isXQuery40 && org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround(pattern)) {
534-
pattern = org.exist.xquery.regex.RegexUtil.translateXPath4Lookaround(pattern);
528+
private boolean matchXmlRegex(String string, final String pattern, final String flags) throws XPathException {
529+
// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
530+
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
531+
// translateXPath4Lookaround / Java-regex dispatch path.
532+
if (org.exist.xquery.regex.RegexUtil.hasXPath4Lookaround(pattern)) {
533+
throw new XPathException(this, ErrorCodes.XPST0017,
534+
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
535+
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
535536
}
536537

537-
// Pre-validate: reject constructs that are not valid in XPath regex
538+
// Pre-validate: reject constructs that are not valid in XPath 3.1 regex
538539
// but that Saxon's XP30 mode accepts (Java/Perl extensions)
539540
if (!hasLiteral(flags)) {
540-
org.exist.xquery.regex.RegexUtil.validateXPathRegex(this, pattern, isXQuery40);
541-
}
542-
543-
// XQ4: patterns with \b, \B, or lookaround need Java regex since
544-
// Saxon's XP30 mode doesn't support these XPath 4.0 extensions
545-
if (isXQuery40 && org.exist.xquery.regex.RegexUtil.needsXQuery40JavaRegex(pattern)) {
546-
try {
547-
final String javaPattern = org.exist.xquery.regex.RegexUtil.translateRegexp(
548-
this, pattern, flags.contains("x"), flags.contains("i"));
549-
final int javaFlags = org.exist.xquery.regex.RegexUtil.parseFlags(this, flags);
550-
return Pattern.compile(javaPattern, javaFlags).matcher(string).find();
551-
} catch (final PatternSyntaxException e) {
552-
throw new XPathException(this, ErrorCodes.FORX0002,
553-
"Invalid regular expression: " + e.getMessage(),
554-
new StringValue(this, pattern), e);
555-
}
541+
org.exist.xquery.regex.RegexUtil.validateXPathRegex(this, pattern, false);
556542
}
557543

558544
try {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* eXist-db Open Source Native XML Database
3+
* Copyright (C) 2001 The eXist-db Authors
4+
*
5+
* info@exist-db.org
6+
* http://www.exist-db.org
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2.1 of the License, or (at your option) any later version.
12+
*
13+
* This library is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* Lesser General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public
19+
* License along with this library; if not, write to the Free Software
20+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21+
*/
22+
package org.exist.xquery.functions.fn;
23+
24+
import org.exist.source.Source;
25+
import org.exist.source.StringSource;
26+
import org.exist.storage.BrokerPool;
27+
import org.exist.storage.DBBroker;
28+
import org.exist.test.ExistEmbeddedServer;
29+
import org.exist.xquery.CompiledXQuery;
30+
import org.exist.xquery.ErrorCodes;
31+
import org.exist.xquery.XPathException;
32+
import org.exist.xquery.XQuery;
33+
import org.exist.xquery.XQueryContext;
34+
import org.junit.ClassRule;
35+
import org.junit.Test;
36+
37+
import java.util.Optional;
38+
39+
import static org.junit.Assert.assertEquals;
40+
import static org.junit.Assert.assertNotNull;
41+
import static org.junit.Assert.assertTrue;
42+
import static org.junit.Assert.fail;
43+
44+
/**
45+
* fn:matches and fn:analyze-string raise XPST0017 with a "not yet implemented"
46+
* message when an XPath 4.0 lookaround construct (e.g. (*positive_lookahead:...))
47+
* appears in the pattern. Previously the XQ4 dispatch branch was silently
48+
* inactive on the XQ 3.1 runtime, so such patterns fell through to Saxon and
49+
* produced opaque regex errors.
50+
*/
51+
public class RegexXPath4NotImplementedTest {
52+
53+
@ClassRule
54+
public static final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true);
55+
56+
@Test
57+
public void matchesLookaroundRaisesNotImplemented() {
58+
assertNotImplemented("fn:matches('foobar', '(*positive_lookahead:foo)bar')");
59+
}
60+
61+
@Test
62+
public void analyzeStringLookaroundRaisesNotImplemented() {
63+
assertNotImplemented("fn:analyze-string('foobar', '(*positive_lookahead:foo)bar')");
64+
}
65+
66+
@Test
67+
public void matchesPlainPatternStillWorks() throws Exception {
68+
assertEquals("true", executeStringValue("fn:matches('foobar', 'foo')"));
69+
}
70+
71+
private void assertNotImplemented(final String query) {
72+
try {
73+
executeStringValue(query);
74+
fail("Expected XPST0017 for XPath 4.0 lookaround");
75+
} catch (final Exception e) {
76+
final XPathException xpe = unwrap(e);
77+
assertNotNull("Expected an XPathException, got: " + e, xpe);
78+
assertEquals(ErrorCodes.XPST0017, xpe.getErrorCode());
79+
assertTrue("Expected 'not yet implemented' message, got: " + xpe.getMessage(),
80+
xpe.getMessage().contains("not yet implemented"));
81+
}
82+
}
83+
84+
private static XPathException unwrap(final Throwable t) {
85+
Throwable cur = t;
86+
while (cur != null) {
87+
if (cur instanceof XPathException xpe) {
88+
return xpe;
89+
}
90+
cur = cur.getCause();
91+
}
92+
return null;
93+
}
94+
95+
private String executeStringValue(final String query) throws Exception {
96+
final Source source = new StringSource(query);
97+
final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool();
98+
final XQuery xquery = brokerPool.getXQueryService();
99+
100+
try (final DBBroker broker = brokerPool.get(
101+
Optional.of(brokerPool.getSecurityManager().getSystemSubject()))) {
102+
final XQueryContext context = new XQueryContext(brokerPool);
103+
final CompiledXQuery compiled = xquery.compile(context, source);
104+
return xquery.execute(broker, compiled, null).getStringValue();
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)