From 14132304f28908bd932268dfcdb8f2a445469998 Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Mon, 27 Jul 2026 14:55:51 +0200 Subject: [PATCH 1/7] Improve handling of module imports. --- .../rumbledb/compiler/ModuleImportLoader.java | 18 ++++++++++++------ .../rumbledb/compiler/TranslationVisitor.java | 16 ++++++---------- .../compiler/XQueryTranslationVisitor.java | 16 ++++++---------- .../compiler/utils/URILiteralUtils.java | 11 +++++++++-- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java index cb77c1ec35..d8c3b73ef7 100644 --- a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java +++ b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java @@ -34,12 +34,18 @@ public static LibraryModule load( ExceptionMetadata metadata ) { URI baseURI = importingModuleContext.getStaticBaseURI(); - URI namespaceURI = URILiteralUtils.resolve(baseURI, namespace, metadata); - List candidates = locationHints.isEmpty() ? List.of(namespace) : locationHints; + String normalizedNamespace = URILiteralUtils.normalizeAsAnyURI(namespace); + List candidates = locationHints.isEmpty() ? List.of(normalizedNamespace) : locationHints; Exception lastFailure = null; for (String candidate : candidates) { - URI location = URILiteralUtils.resolve(baseURI, candidate, metadata); + URI location; + try { + location = URILiteralUtils.resolve(baseURI, candidate, metadata); + } catch (RumbleException e) { + lastFailure = e; + continue; + } try { LibraryModule module = VisitorHelpers.parseLibraryModuleFromLocation( location, @@ -48,10 +54,10 @@ public static LibraryModule load( metadata ); - if (!namespaceURI.toString().equals(module.getNamespace())) { + if (!normalizedNamespace.equals(module.getNamespace())) { throw new ModuleNotFoundException( "A module with namespace " - + namespaceURI + + normalizedNamespace + " was not found. The namespace of the module at this location was: " + module.getNamespace(), metadata @@ -66,7 +72,7 @@ public static LibraryModule load( RumbleException exception = new ModuleNotFoundException( "Module not found: %s, cause: %s".formatted( - namespaceURI, + normalizedNamespace, lastFailure != null ? lastFailure.getMessage() : "unknown" ), metadata diff --git a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java index 905cdb3e36..1f94ef0ad8 100644 --- a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java @@ -371,24 +371,19 @@ public Node visitProgram(JsoniqParser.ProgramContext ctx) { @Override public Node visitLibraryModule(JsoniqParser.LibraryModuleContext ctx) { String prefix = ctx.ncName().getText(); - String namespace = processURILiteral(ctx.uriLiteral()); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.uriLiteral())); if (namespace.equals("")) { throw new EmptyModuleURIException("Module URI is empty.", createMetadataFromContext(ctx)); } - URI resolvedURI = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), - namespace, - createMetadataFromContext(ctx) - ); - this.libraryModuleNamespace = resolvedURI.toString(); + this.libraryModuleNamespace = namespace; bindNamespace( prefix, - resolvedURI.toString(), + namespace, createMetadataFromContext(ctx) ); Prolog prolog = (Prolog) this.visitProlog(ctx.prolog()); - LibraryModule module = new LibraryModule(prolog, resolvedURI.toString(), createMetadataFromContext(ctx)); + LibraryModule module = new LibraryModule(prolog, namespace, createMetadataFromContext(ctx)); module.setStaticContext(this.moduleContext); return module; } @@ -4062,9 +4057,10 @@ private String resolveCollationUri(UriLiteralContext ctx) { } public LibraryModule processModuleImport(JsoniqParser.ModuleImportContext ctx) { - String namespace = processURILiteral(ctx.targetNamespace); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.targetNamespace)); List locationHints = ctx.locations.stream() .map(this::processURILiteral) + .map(URILiteralUtils::normalizeAsAnyURI) .collect(Collectors.toList()); LibraryModule libraryModule = ModuleImportLoader.load( namespace, diff --git a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java index 5b889c1943..48a41c384e 100644 --- a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java @@ -355,24 +355,19 @@ public Node visitProgram(XQueryParser.ProgramContext ctx) { @Override public Node visitLibraryModule(XQueryParser.LibraryModuleContext ctx) { String prefix = ctx.ncName().getText(); - String namespace = processURILiteral(ctx.uriLiteral()); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.uriLiteral())); if (namespace.equals("")) { throw new EmptyModuleURIException("Module URI is empty.", createMetadataFromContext(ctx)); } - URI resolvedURI = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), - namespace, - createMetadataFromContext(ctx) - ); - this.libraryModuleNamespace = resolvedURI.toString(); + this.libraryModuleNamespace = namespace; bindNamespace( prefix, - resolvedURI.toString(), + namespace, createMetadataFromContext(ctx) ); Prolog prolog = (Prolog) this.visitProlog(ctx.prolog()); - LibraryModule module = new LibraryModule(prolog, resolvedURI.toString(), createMetadataFromContext(ctx)); + LibraryModule module = new LibraryModule(prolog, namespace, createMetadataFromContext(ctx)); module.setStaticContext(this.moduleContext); return module; } @@ -3754,9 +3749,10 @@ private String resolveCollationUri(UriLiteralContext ctx) { } public LibraryModule processModuleImport(XQueryParser.ModuleImportContext ctx) { - String namespace = processURILiteral(ctx.targetNamespace); + String namespace = URILiteralUtils.normalizeAsAnyURI(processURILiteral(ctx.targetNamespace)); List locationHints = ctx.locations.stream() .map(this::processURILiteral) + .map(URILiteralUtils::normalizeAsAnyURI) .collect(Collectors.toList()); LibraryModule libraryModule = ModuleImportLoader.load( namespace, diff --git a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java index 7bf2467cc0..3e5396fa68 100644 --- a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java +++ b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java @@ -16,15 +16,22 @@ public final class URILiteralUtils { + private static final String XML_WHITESPACE_SEQUENCE = "[\\t\\n\\r ]+"; + private URILiteralUtils() { } + public static String normalizeAsAnyURI(String literal) { + return literal.replaceAll(XML_WHITESPACE_SEQUENCE, " ").trim(); + } + public static URI resolve(URI baseURI, String literal, ExceptionMetadata metadata) { + String normalizedLiteral = normalizeAsAnyURI(literal); try { - return FileSystemUtil.resolveURI(baseURI, literal, metadata); + return FileSystemUtil.resolveURI(baseURI, normalizedLiteral, metadata); } catch (CannotRetrieveResourceException exception) { InvalidURILiteralException result = new InvalidURILiteralException( - "Invalid URI literal: " + literal, + "Invalid URI literal: " + normalizedLiteral, metadata ); result.initCause(exception); From 984358f894f6314f1c44f863442514561a07a962 Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Mon, 27 Jul 2026 15:09:40 +0200 Subject: [PATCH 2/7] Test new harness. --- .github/workflows/run-qt3.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-qt3.yml b/.github/workflows/run-qt3.yml index bd86df6b34..37d5bcd515 100644 --- a/.github/workflows/run-qt3.yml +++ b/.github/workflows/run-qt3.yml @@ -104,7 +104,7 @@ jobs: qt3-jsoniq: needs: [resolve-baseline, build] - uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@master + uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@AtHint with: tested_parser: jsoniq rumble_artifact_name: rumble-build @@ -112,7 +112,7 @@ jobs: qt3-xquery: needs: [resolve-baseline, build] - uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@master + uses: RumbleDB/rumble-test-suite/.github/workflows/qt3-suite.yml@AtHint with: tested_parser: xquery rumble_artifact_name: rumble-build From ef745a4138ebe890c0e43662a4335ad39fd7eab4 Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Mon, 27 Jul 2026 16:03:58 +0200 Subject: [PATCH 3/7] Fix tests. --- .../test_files/runtime/LibraryModules/ModuleImport6.jq | 4 ++-- .../test_files/runtime/LibraryModules/ModuleImport7.jq | 4 ++-- .../test_files/runtime/LibraryModules/modulerepeatedimport.jq | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq index d445884062..e2c2014f0e 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport6.jq @@ -1,4 +1,4 @@ (:JIQS: ShouldRun; Output="20" :) -import module namespace my-module = "modulerepeatedimport.jq"; +import module namespace my-module = "http://example.com/modulerepeatedimport.jq" +at "modulerepeatedimport.jq"; my-module:func() - diff --git a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq index 1a4ab73dd2..e2c2014f0e 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/ModuleImport7.jq @@ -1,4 +1,4 @@ (:JIQS: ShouldRun; Output="20" :) -import module namespace my-module = "https://raw.githubusercontent.com/RumbleDB/rumble/master/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq"; +import module namespace my-module = "http://example.com/modulerepeatedimport.jq" +at "modulerepeatedimport.jq"; my-module:func() - diff --git a/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq b/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq index 3e4b842a89..aa203b4615 100644 --- a/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq +++ b/src/test/resources/test_files/runtime/LibraryModules/modulerepeatedimport.jq @@ -1,5 +1,5 @@ (:JIQS: ShouldNotParse; ErrorCode="XPST0003"; ErrorMetadata="LINE:2:COLUMN:0:" :) -module namespace my-module = "modulerepeatedimport.jq"; +module namespace my-module = "http://example.com/modulerepeatedimport.jq"; import module namespace mod1 = "moduleimportingmodule.jq"; import module namespace mod2 = "module.jq"; From 157b326f016d274558686e291e581f847903afaf Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Tue, 28 Jul 2026 10:31:55 +0200 Subject: [PATCH 4/7] Fix static URI behavior. --- .../rumbledb/compiler/TranslationVisitor.java | 2 +- .../compiler/XQueryTranslationVisitor.java | 2 +- .../context/RuntimeStaticContext.java | 2 ++ .../org/rumbledb/context/StaticContext.java | 19 +++++++++++++++++++ .../java/org/rumbledb/items/AnyURIItem.java | 15 ++++++++++----- .../StaticBaseURIFunctionIterator.java | 2 +- 6 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java index 8394f1882e..6f1166b18d 100644 --- a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java @@ -475,7 +475,7 @@ public Node visitProlog(JsoniqParser.PrologContext ctx) { uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); baseURISet = true; continue; } diff --git a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java index da1e9691b3..3c529b6833 100644 --- a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java @@ -569,7 +569,7 @@ private void processPrologPhase1Setter(SetterContext setterContext, PrologPhase1 uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); flags.baseURISet = true; return; } diff --git a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java index 37d77ff943..9a29be493a 100644 --- a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java +++ b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java @@ -24,6 +24,7 @@ public class RuntimeStaticContext implements Serializable { private static final long serialVersionUID = 1L; private final URI staticURI; + private final String staticURIString; /** * Query language associated with this context, which is used for error reporting and to determine the @@ -134,6 +135,7 @@ public static class RuntimeStaticContextBuilder { public static RuntimeStaticContextBuilder fromStaticContext(@NonNull StaticContext staticContext) { return builder() .staticURI(staticContext.getStaticBaseURI()) + .staticURIString(staticContext.getStaticBaseUriString()) .queryLanguage(staticContext.getQueryLanguage()) .staticallyKnownNamespaces(staticContext.getInScopeNamespaceBindings()) .staticallyKnownCollations(staticContext.getStaticallyKnownCollations()) diff --git a/src/main/java/org/rumbledb/context/StaticContext.java b/src/main/java/org/rumbledb/context/StaticContext.java index 86666c30ca..4f17d346ff 100644 --- a/src/main/java/org/rumbledb/context/StaticContext.java +++ b/src/main/java/org/rumbledb/context/StaticContext.java @@ -50,6 +50,7 @@ public class StaticContext { private String queryLanguage; private StaticContext parent; private URI staticBaseURI; + private String staticBaseUriString; private boolean emptySequenceOrderLeast; private boolean boundarySpacePreserve; private boolean copyNamespacesPreserve; @@ -96,6 +97,7 @@ public class StaticContext { public StaticContext() { this.parent = null; this.staticBaseURI = null; + this.staticBaseUriString = null; this.queryLanguage = null; this.inScopeVariables = null; this.userDefinedFunctionExecutionModes = null; @@ -118,6 +120,7 @@ public StaticContext() { public StaticContext(URI staticBaseURI, RumbleRuntimeConfiguration configuration) { this.parent = null; this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseURI == null ? null : staticBaseURI.toString(); this.queryLanguage = configuration.getQueryLanguage() != null ? configuration.getQueryLanguage() : this.queryLanguage; @@ -212,8 +215,24 @@ public URI getStaticBaseURI() { throw new OurBadException("Static base URI not set."); } + public String getStaticBaseUriString() { + if (this.staticBaseUriString != null) { + return this.staticBaseUriString; + } + if (this.parent != null) { + return this.parent.getStaticBaseUriString(); + } + throw new OurBadException("Static base URI not set."); + } + public void setStaticBaseUri(URI staticBaseURI) { this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseURI == null ? null : staticBaseURI.toString(); + } + + public void setStaticBaseUri(URI staticBaseURI, String staticBaseUriString) { + this.staticBaseURI = staticBaseURI; + this.staticBaseUriString = staticBaseUriString; } public boolean isInScope(Name varName) { diff --git a/src/main/java/org/rumbledb/items/AnyURIItem.java b/src/main/java/org/rumbledb/items/AnyURIItem.java index 6eaf6a2c50..0ede049020 100644 --- a/src/main/java/org/rumbledb/items/AnyURIItem.java +++ b/src/main/java/org/rumbledb/items/AnyURIItem.java @@ -37,6 +37,7 @@ public class AnyURIItem implements Item { @Serial private static final long serialVersionUID = 1L; + private String lexicalValue; private URI value; public AnyURIItem() { @@ -45,12 +46,16 @@ public AnyURIItem() { public AnyURIItem(String value) { super(); + if (value == null) { + throw new IllegalArgumentException(); + } + this.lexicalValue = value; this.value = parseAnyURIString(value); } @Override public Item copy(boolean mutable) { - return new AnyURIItem(this.value.toString()); + return new AnyURIItem(this.lexicalValue); } @Override @@ -73,23 +78,23 @@ static URI parseAnyURIString(String anyURIString) throws IllegalArgumentExceptio try { return new URI(anyURIString); } catch (URISyntaxException e) { - throw new IllegalArgumentException(e.getMessage()); + return null; } } @Override public String getStringValue() { - return this.value.toString(); + return this.lexicalValue; } @Override public boolean getEffectiveBooleanValue() { - return !this.value.toString().isEmpty(); + return !this.lexicalValue.isEmpty(); } @Override public int hashCode() { - return this.value.hashCode(); + return this.lexicalValue.hashCode(); } public URI getValue() { diff --git a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java index a92389e2d8..00e33c13a8 100644 --- a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java +++ b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java @@ -25,6 +25,6 @@ public StaticBaseURIFunctionIterator( @Override public Item materializeFirstItemOrNull(DynamicContext context) { - return ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURI().toString()); + return ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURIString()); } } From e5645e1ea9487f4f445b67cfb0b8f3d1cf459447 Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Tue, 28 Jul 2026 10:47:15 +0200 Subject: [PATCH 5/7] Resolve base URI if relative. --- .../org/rumbledb/compiler/TranslationVisitor.java | 2 +- .../rumbledb/compiler/XQueryTranslationVisitor.java | 2 +- .../org/rumbledb/compiler/utils/URILiteralUtils.java | 11 +++++++++++ .../resources/test_files/runtime/CastableAnyURI.jq | 2 +- .../runtime/FunctionAnyURI/FunctionAnyURIError4.jq | 4 ++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java index 6f1166b18d..d1ced45760 100644 --- a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java @@ -475,7 +475,7 @@ public Node visitProlog(JsoniqParser.PrologContext ctx) { uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.toStaticBaseUriString(uri, uriString)); baseURISet = true; continue; } diff --git a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java index 3c529b6833..d40cab9d25 100644 --- a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java @@ -569,7 +569,7 @@ private void processPrologPhase1Setter(SetterContext setterContext, PrologPhase1 uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); - this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.normalizeAsAnyURI(uriString)); + this.moduleContext.setStaticBaseUri(uri, URILiteralUtils.toStaticBaseUriString(uri, uriString)); flags.baseURISet = true; return; } diff --git a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java index 3e5396fa68..02656da28a 100644 --- a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java +++ b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java @@ -13,6 +13,7 @@ import org.rumbledb.runtime.functions.input.FileSystemUtil; import java.net.URI; +import java.net.URISyntaxException; public final class URILiteralUtils { @@ -38,4 +39,14 @@ public static URI resolve(URI baseURI, String literal, ExceptionMetadata metadat throw result; } } + + public static String toStaticBaseUriString(URI resolvedURI, String literal) { + String normalizedLiteral = normalizeAsAnyURI(literal); + try { + URI parsedLiteral = new URI(normalizedLiteral); + return parsedLiteral.isAbsolute() ? normalizedLiteral : resolvedURI.toString(); + } catch (URISyntaxException e) { + return normalizedLiteral; + } + } } diff --git a/src/test/resources/test_files/runtime/CastableAnyURI.jq b/src/test/resources/test_files/runtime/CastableAnyURI.jq index 7ebb6e759e..7966f93cca 100644 --- a/src/test/resources/test_files/runtime/CastableAnyURI.jq +++ b/src/test/resources/test_files/runtime/CastableAnyURI.jq @@ -1,4 +1,4 @@ -(:JIQS: ShouldRun; Output="(true, true, true, false, false, false, false, false, false, false, false, false, false, false, false, false)" :) +(:JIQS: ShouldRun; Output="(true, true, true, true, false, false, false, false, false, false, false, false, false, false, false, false)" :) anyURI("mailto:rumble") castable as string, anyURI("mailto:rumble") castable as anyURI, "mailto:rumble" castable as anyURI, diff --git a/src/test/resources/test_files/runtime/FunctionAnyURI/FunctionAnyURIError4.jq b/src/test/resources/test_files/runtime/FunctionAnyURI/FunctionAnyURIError4.jq index 36340bfa0b..e2c678238b 100644 --- a/src/test/resources/test_files/runtime/FunctionAnyURI/FunctionAnyURIError4.jq +++ b/src/test/resources/test_files/runtime/FunctionAnyURI/FunctionAnyURIError4.jq @@ -1,4 +1,4 @@ -(:JIQS: ShouldCrash; ErrorCode="FORG0001"; ErrorMetadata="LINE:2:COLUMN:0:" :) +(:JIQS: ShouldRun; Output="mailto:" :) anyURI("mailto:") -(: non-conformant URI string :) +(: xs:anyURI allows this lexical form; processors need not reject scheme-specific URI oddities :) From 275068f749c27f485af7b1b2d7c55f2f7d28d4eb Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Tue, 28 Jul 2026 10:53:20 +0200 Subject: [PATCH 6/7] Make sure the static base URI is absent if it cannot be resolved to an absolute URI, and that an appropriate error is thrown upon attempting a resolution against an absent static base URI. --- .../rumbledb/compiler/InferTypeVisitor.java | 4 +-- .../rumbledb/compiler/ModuleImportLoader.java | 2 +- .../rumbledb/compiler/TranslationVisitor.java | 9 +++--- .../compiler/XQueryTranslationVisitor.java | 9 +++--- .../compiler/utils/URILiteralUtils.java | 18 ++++++++++++ .../context/RuntimeStaticContext.java | 4 +-- .../org/rumbledb/context/StaticContext.java | 20 +++++++++++++ .../org/rumbledb/errorcodes/ErrorCode.java | 1 + .../functions/input/FileSystemUtil.java | 28 +++++++++++++++---- .../functions/io/UnparsedTextReader.java | 7 +++++ .../strings/ResolveURIFunctionIterator.java | 16 ++++++++--- .../StaticBaseURIFunctionIterator.java | 3 ++ .../SerializationParameterUtils.java | 2 +- 13 files changed, 99 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java b/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java index e926bd0f08..d9fbfd754b 100644 --- a/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java +++ b/src/main/java/org/rumbledb/compiler/InferTypeVisitor.java @@ -782,7 +782,7 @@ private boolean tryAnnotateSpecificFunctions(FunctionCallExpression expression, ) { String path = stringLiteralExpr.getValue(); URI uri = FileSystemUtil.resolveFileSystemURI( - staticContext.getStaticBaseURI(), + staticContext.getStaticBaseURIOrNull(), path, expression.getMetadata() ); @@ -812,7 +812,7 @@ private boolean tryAnnotateSpecificFunctions(FunctionCallExpression expression, ) { String path = stringLiteralExpr.getValue(); URI uri = FileSystemUtil.resolveFileSystemURI( - staticContext.getStaticBaseURI(), + staticContext.getStaticBaseURIOrNull(), path, expression.getMetadata() ); diff --git a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java index d8c3b73ef7..bf16b1697e 100644 --- a/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java +++ b/src/main/java/org/rumbledb/compiler/ModuleImportLoader.java @@ -33,7 +33,7 @@ public static LibraryModule load( CompilationConfiguration compilationConfiguration, ExceptionMetadata metadata ) { - URI baseURI = importingModuleContext.getStaticBaseURI(); + URI baseURI = importingModuleContext.getStaticBaseURIOrNull(); String normalizedNamespace = URILiteralUtils.normalizeAsAnyURI(namespace); List candidates = locationHints.isEmpty() ? List.of(normalizedNamespace) : locationHints; Exception lastFailure = null; diff --git a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java index d1ced45760..3407f60ec6 100644 --- a/src/main/java/org/rumbledb/compiler/TranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/TranslationVisitor.java @@ -470,8 +470,8 @@ public Node visitProlog(JsoniqParser.PrologContext ctx) { ); } String uriString = processURILiteral(setterContext.baseURIDecl().uriLiteral()); - URI uri = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), + URI uri = URILiteralUtils.resolveStaticBaseUri( + this.moduleContext.getStaticBaseURIOrNull(), uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); @@ -4049,7 +4049,7 @@ private void processDefaultCollation(DefaultCollationDeclContext ctx) { private String resolveCollationUri(UriLiteralContext ctx) { String uriString = processURILiteral(ctx); URI uri = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), + this.moduleContext.getStaticBaseURIOrNull(), uriString, createMetadataFromContext(ctx) ); @@ -4094,7 +4094,8 @@ public LibraryModule processModuleImport(JsoniqParser.ModuleImportContext ctx) { } public ExceptionMetadata generateMetadata(Token start, Token end) { - return ExceptionMetadata.fromTokens(this.moduleContext.getStaticBaseURI().toString(), start, end, this.code); + String location = this.moduleContext.getStaticBaseUriStringOrNull(); + return ExceptionMetadata.fromTokens(location == null ? "unknown" : location, start, end, this.code); } private List processAnnotations(JsoniqParser.AnnotationsContext annotations) { diff --git a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java index d40cab9d25..5d2ab0a269 100644 --- a/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java +++ b/src/main/java/org/rumbledb/compiler/XQueryTranslationVisitor.java @@ -564,8 +564,8 @@ private void processPrologPhase1Setter(SetterContext setterContext, PrologPhase1 ); } String uriString = processURILiteral(setterContext.baseURIDecl().uriLiteral()); - URI uri = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), + URI uri = URILiteralUtils.resolveStaticBaseUri( + this.moduleContext.getStaticBaseURIOrNull(), uriString, createMetadataFromContext(setterContext.baseURIDecl()) ); @@ -3741,7 +3741,7 @@ private void processDefaultCollation(DefaultCollationDeclContext ctx) { private String resolveCollationUri(UriLiteralContext ctx) { String uriString = processURILiteral(ctx); URI uri = URILiteralUtils.resolve( - this.moduleContext.getStaticBaseURI(), + this.moduleContext.getStaticBaseURIOrNull(), uriString, createMetadataFromContext(ctx) ); @@ -3786,7 +3786,8 @@ public LibraryModule processModuleImport(XQueryParser.ModuleImportContext ctx) { } public ExceptionMetadata generateMetadata(Token start, Token end) { - return ExceptionMetadata.fromTokens(this.moduleContext.getStaticBaseURI().toString(), start, end, this.code); + String location = this.moduleContext.getStaticBaseUriStringOrNull(); + return ExceptionMetadata.fromTokens(location == null ? "unknown" : location, start, end, this.code); } private List processAnnotations(XQueryParser.AnnotationsContext annotations) { diff --git a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java index 02656da28a..7d60b519cf 100644 --- a/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java +++ b/src/main/java/org/rumbledb/compiler/utils/URILiteralUtils.java @@ -40,6 +40,24 @@ public static URI resolve(URI baseURI, String literal, ExceptionMetadata metadat } } + public static URI resolveStaticBaseUri(URI baseURI, String literal, ExceptionMetadata metadata) { + String normalizedLiteral = normalizeAsAnyURI(literal); + try { + URI parsedLiteral = FileSystemUtil.parseURIReference(normalizedLiteral); + if (!parsedLiteral.isAbsolute() && baseURI == null) { + return null; + } + return FileSystemUtil.resolveURI(baseURI, normalizedLiteral, metadata); + } catch (CannotRetrieveResourceException | URISyntaxException exception) { + InvalidURILiteralException result = new InvalidURILiteralException( + "Invalid URI literal: " + normalizedLiteral, + metadata + ); + result.initCause(exception); + throw result; + } + } + public static String toStaticBaseUriString(URI resolvedURI, String literal) { String normalizedLiteral = normalizeAsAnyURI(literal); try { diff --git a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java index 9a29be493a..ef11a5562d 100644 --- a/src/main/java/org/rumbledb/context/RuntimeStaticContext.java +++ b/src/main/java/org/rumbledb/context/RuntimeStaticContext.java @@ -134,8 +134,8 @@ public static class RuntimeStaticContextBuilder { */ public static RuntimeStaticContextBuilder fromStaticContext(@NonNull StaticContext staticContext) { return builder() - .staticURI(staticContext.getStaticBaseURI()) - .staticURIString(staticContext.getStaticBaseUriString()) + .staticURI(staticContext.getStaticBaseURIOrNull()) + .staticURIString(staticContext.getStaticBaseUriStringOrNull()) .queryLanguage(staticContext.getQueryLanguage()) .staticallyKnownNamespaces(staticContext.getInScopeNamespaceBindings()) .staticallyKnownCollations(staticContext.getStaticallyKnownCollations()) diff --git a/src/main/java/org/rumbledb/context/StaticContext.java b/src/main/java/org/rumbledb/context/StaticContext.java index 4f17d346ff..b3ccdd4c55 100644 --- a/src/main/java/org/rumbledb/context/StaticContext.java +++ b/src/main/java/org/rumbledb/context/StaticContext.java @@ -215,6 +215,16 @@ public URI getStaticBaseURI() { throw new OurBadException("Static base URI not set."); } + public URI getStaticBaseURIOrNull() { + if (this.staticBaseURI != null) { + return this.staticBaseURI; + } + if (this.parent != null) { + return this.parent.getStaticBaseURIOrNull(); + } + return null; + } + public String getStaticBaseUriString() { if (this.staticBaseUriString != null) { return this.staticBaseUriString; @@ -225,6 +235,16 @@ public String getStaticBaseUriString() { throw new OurBadException("Static base URI not set."); } + public String getStaticBaseUriStringOrNull() { + if (this.staticBaseUriString != null) { + return this.staticBaseUriString; + } + if (this.parent != null) { + return this.parent.getStaticBaseUriStringOrNull(); + } + return null; + } + public void setStaticBaseUri(URI staticBaseURI) { this.staticBaseURI = staticBaseURI; this.staticBaseUriString = staticBaseURI == null ? null : staticBaseURI.toString(); diff --git a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java index 3afaee7d83..8fe04b23bc 100644 --- a/src/main/java/org/rumbledb/errorcodes/ErrorCode.java +++ b/src/main/java/org/rumbledb/errorcodes/ErrorCode.java @@ -149,6 +149,7 @@ public String toString() { public static final ErrorCode DynamicTypeTreatErrorCode = registerBuiltIn("XPDY0050"); public static final ErrorCode RuntimeExceptionErrorCode = registerBuiltIn("XPDY0130"); + public static final ErrorCode StaticBaseUriAbsentErrorCode = registerBuiltIn("XPST0001"); public static final ErrorCode ParsingErrorCode = registerBuiltIn("XPST0003"); public static final ErrorCode StaticallyInferredEmptySequenceNotFromCommaExpression = registerBuiltIn("XPST0005"); public static final ErrorCode UndeclaredVariableErrorCode = registerBuiltIn("XPST0008"); diff --git a/src/main/java/org/rumbledb/runtime/functions/input/FileSystemUtil.java b/src/main/java/org/rumbledb/runtime/functions/input/FileSystemUtil.java index d3575ac579..3c201b6d5d 100644 --- a/src/main/java/org/rumbledb/runtime/functions/input/FileSystemUtil.java +++ b/src/main/java/org/rumbledb/runtime/functions/input/FileSystemUtil.java @@ -13,6 +13,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.rumbledb.config.RumbleRuntimeConfiguration; +import org.rumbledb.exceptions.AbsentStaticBaseUriException; import org.rumbledb.exceptions.CannotRetrieveResourceException; import org.rumbledb.exceptions.ExceptionMetadata; import org.rumbledb.exceptions.OurBadException; @@ -55,14 +56,23 @@ public static URI resolveFileSystemURI(URI base, String url, ExceptionMetadata m } private static URI resolve(URI base, String url, ExceptionMetadata metadata, boolean fileSystemPath) { - if (!base.isAbsolute()) { - throw new OurBadException( - "The base URI is not absolute!", - metadata - ); - } try { URI relativeURI = fileSystemPath ? parseFileSystemURI(url) : parseURI(url); + if (relativeURI.isAbsolute()) { + return relativeURI; + } + if (base == null) { + throw new AbsentStaticBaseUriException( + "Static base URI is absent, so the relative URI reference cannot be resolved: " + url, + metadata + ); + } + if (!base.isAbsolute()) { + throw new OurBadException( + "The base URI is not absolute!", + metadata + ); + } URI resolvedURI = base.resolve(relativeURI); if (url.endsWith("/")) { // preserve trailing slash if any for correct resolution against it as a directory in the future. @@ -73,6 +83,8 @@ private static URI resolve(URI base, String url, ExceptionMetadata metadata, boo } } return resolvedURI; + } catch (AbsentStaticBaseUriException e) { + throw e; } catch (Exception e) { RumbleException rumbleException = new CannotRetrieveResourceException( "Malformed URI: " + url + " Cause: " + e.getMessage(), @@ -83,6 +95,10 @@ private static URI resolve(URI base, String url, ExceptionMetadata metadata, boo } } + public static URI parseURIReference(String value) throws URISyntaxException { + return parseURI(value); + } + private static URI parseURI(String value) throws URISyntaxException { String escapedValue = value.replace(" ", "%20"); URI uri = new URI(escapedValue); diff --git a/src/main/java/org/rumbledb/runtime/functions/io/UnparsedTextReader.java b/src/main/java/org/rumbledb/runtime/functions/io/UnparsedTextReader.java index 5ac71977b0..785fd2fca7 100644 --- a/src/main/java/org/rumbledb/runtime/functions/io/UnparsedTextReader.java +++ b/src/main/java/org/rumbledb/runtime/functions/io/UnparsedTextReader.java @@ -1,6 +1,7 @@ package org.rumbledb.runtime.functions.io; import org.rumbledb.config.RumbleRuntimeConfiguration; +import org.rumbledb.exceptions.AbsentStaticBaseUriException; import org.rumbledb.exceptions.ExceptionMetadata; import org.rumbledb.exceptions.InvalidEncodingException; import org.rumbledb.exceptions.RumbleException; @@ -22,6 +23,12 @@ private UnparsedTextReader() { private static URI resolveHref(URI staticBaseUri, String href, ExceptionMetadata metadata) { if (href.isEmpty()) { + if (staticBaseUri == null) { + throw new AbsentStaticBaseUriException( + "Static base URI is absent, so the relative URI reference cannot be resolved.", + metadata + ); + } return staticBaseUri; } return FileSystemUtil.resolveURI(staticBaseUri, href, metadata); diff --git a/src/main/java/org/rumbledb/runtime/functions/strings/ResolveURIFunctionIterator.java b/src/main/java/org/rumbledb/runtime/functions/strings/ResolveURIFunctionIterator.java index 6ae01df494..c4be02734f 100644 --- a/src/main/java/org/rumbledb/runtime/functions/strings/ResolveURIFunctionIterator.java +++ b/src/main/java/org/rumbledb/runtime/functions/strings/ResolveURIFunctionIterator.java @@ -3,6 +3,7 @@ import org.rumbledb.api.Item; import org.rumbledb.context.DynamicContext; import org.rumbledb.context.RuntimeStaticContext; +import org.rumbledb.exceptions.AbsentStaticBaseUriException; import org.rumbledb.exceptions.InvalidArgumentValueException; import org.rumbledb.items.ItemFactory; import org.rumbledb.runtime.AtMostOneItemLocalRuntimeIterator; @@ -35,16 +36,23 @@ public Item materializeFirstItemOrNull(DynamicContext context) { if (this.getChildren().size() == 2) { base = this.getChild(1).materializeFirstItemOrNull(context); } else { - base = ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURI().toString()); - } - if (base == null) { - return null; + if (this.staticContext.getStaticURI() == null) { + base = null; + } else { + base = ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURI().toString()); + } } String stringRelative = relative.getStringValue(); URI relativeURI = parseURI(stringRelative); if (relativeURI.isAbsolute()) { return ItemFactory.getInstance().createAnyURIItem(stringRelative); } + if (base == null) { + throw new AbsentStaticBaseUriException( + "Static base URI is absent, so the relative URI reference cannot be resolved: " + stringRelative, + getMetadata() + ); + } URI uri = parseURI(base.getStringValue()); String stringURI = uri.resolve(relativeURI).toString(); diff --git a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java index 00e33c13a8..b35cbeb99b 100644 --- a/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java +++ b/src/main/java/org/rumbledb/runtime/functions/strings/StaticBaseURIFunctionIterator.java @@ -25,6 +25,9 @@ public StaticBaseURIFunctionIterator( @Override public Item materializeFirstItemOrNull(DynamicContext context) { + if (this.staticContext.getStaticURIString() == null) { + return null; + } return ItemFactory.getInstance().createAnyURIItem(this.staticContext.getStaticURIString()); } } diff --git a/src/main/java/org/rumbledb/serialization/SerializationParameterUtils.java b/src/main/java/org/rumbledb/serialization/SerializationParameterUtils.java index 6c76b88561..9f020e53b6 100644 --- a/src/main/java/org/rumbledb/serialization/SerializationParameterUtils.java +++ b/src/main/java/org/rumbledb/serialization/SerializationParameterUtils.java @@ -80,7 +80,7 @@ public static void applyParameterDocument( ExceptionMetadata metadata ) { try { - URI uri = FileSystemUtil.resolveURI(staticContext.getStaticBaseURI(), location, metadata); + URI uri = FileSystemUtil.resolveURI(staticContext.getStaticBaseURIOrNull(), location, metadata); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); From dce97e4f95789ac4be51684d987f659872dccab8 Mon Sep 17 00:00:00 2001 From: Ghislain Fourny Date: Tue, 28 Jul 2026 10:54:35 +0200 Subject: [PATCH 7/7] Add file. --- .../exceptions/AbsentStaticBaseUriException.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/org/rumbledb/exceptions/AbsentStaticBaseUriException.java diff --git a/src/main/java/org/rumbledb/exceptions/AbsentStaticBaseUriException.java b/src/main/java/org/rumbledb/exceptions/AbsentStaticBaseUriException.java new file mode 100644 index 0000000000..54d0bbdd7e --- /dev/null +++ b/src/main/java/org/rumbledb/exceptions/AbsentStaticBaseUriException.java @@ -0,0 +1,12 @@ +package org.rumbledb.exceptions; + +import org.rumbledb.errorcodes.ErrorCode; + +public class AbsentStaticBaseUriException extends RumbleException { + + private static final long serialVersionUID = 1L; + + public AbsentStaticBaseUriException(String message, ExceptionMetadata metadata) { + super(message, ErrorCode.StaticBaseUriAbsentErrorCode, metadata); + } +}