Skip to content

Commit

Permalink
[bugfix] Cyclic dependency import detection for XQuery 1.0 (i.e. XQST…
Browse files Browse the repository at this point in the history
…0093)
  • Loading branch information
adamretter committed Jul 31, 2023
1 parent e047354 commit e226566
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 3 deletions.
5 changes: 5 additions & 0 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,11 @@
<artifactId>ant</artifactId>
</dependency>

<dependency>
<groupId>org.jgrapht</groupId>
<artifactId>jgrapht-opt</artifactId>
<version>1.5.2</version>
</dependency>

<dependency>
<groupId>junit</groupId>
Expand Down
53 changes: 50 additions & 3 deletions exist-core/src/main/java/org/exist/xquery/ModuleContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@


/**
* Subclass of {@link org.exist.xquery.XQueryContext} for
* imported modules.
* Subclass of {@link org.exist.xquery.XQueryContext} for imported modules.
*
* @author wolf
* @author <a href="mailto:[email protected]">Adam Retter</a>
*/
public class ModuleContext extends XQueryContext {

Expand All @@ -66,7 +66,8 @@ public ModuleContext(final XQueryContext parentContext, final String moduleNames
super(parentContext != null ? parentContext.db : null,
parentContext != null ? parentContext.getConfiguration() : null,
null,
false);
false,
null);
this.moduleNamespace = moduleNamespace;
this.modulePrefix = modulePrefix;
this.location = location;
Expand Down Expand Up @@ -95,6 +96,52 @@ public void setModuleNamespace(final String prefix, final String namespaceURI) {
this.moduleNamespace = namespaceURI;
}

@Override
protected void addModuleVertex(final ModuleVertex moduleVertex) {
getRootContext().addModuleVertex(moduleVertex);
}

protected boolean hasModuleVertex(final ModuleVertex moduleVertex) {
return getRootContext().hasModuleVertex(moduleVertex);
}

@Override
protected void addModuleEdge(final ModuleVertex source, final ModuleVertex sink) {
getRootContext().addModuleEdge(source, sink);
}

@Override
protected boolean hasModulePath(final ModuleVertex source, final ModuleVertex sink) {
return getRootContext().hasModulePath(source, sink);
}

@Override
public @Nullable Module[] importModule(@Nullable String namespaceURI, @Nullable String prefix, @Nullable AnyURIValue[] locationHints) throws XPathException {
final ModuleVertex thisModuleVertex = new ModuleVertex(moduleNamespace, location);

for (final AnyURIValue locationHint : locationHints) {
final ModuleVertex imporedModuleVertex = new ModuleVertex(namespaceURI, locationHint.toString());

if (!hasModuleVertex(imporedModuleVertex)) {
addModuleVertex(imporedModuleVertex);
} else {
// Check if there is already a path from the imported module to this module
if (getXQueryVersion() == 10 && namespaceURI != null && locationHints != null && hasModulePath(imporedModuleVertex, thisModuleVertex)) {
throw new XPathException(ErrorCodes.XQST0093, "Detected cyclic import between modules: " + getModuleNamespace() + " at: " + getLocation() + ", and: " + namespaceURI + " at: " + locationHint.toString());
}
}

if (!hasModuleVertex(thisModuleVertex)) {
// NOTE(AR) may occur when the actual module has a different namespace from that of the `import module namespace`... will later raise an XQST0047 error
addModuleVertex(thisModuleVertex);
}

addModuleEdge(thisModuleVertex, imporedModuleVertex);
}

return super.importModule(namespaceURI, prefix, locationHints);
}

@Override
protected @Nullable Module importModuleFromLocation(final String namespaceURI, @Nullable final String prefix, final AnyURIValue locationHint) throws XPathException {
// guard against self-recursive import - see: https://github.com/eXist-db/exist/issues/3448
Expand Down
131 changes: 131 additions & 0 deletions exist-core/src/main/java/org/exist/xquery/XQueryContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Consumer;
Expand Down Expand Up @@ -93,6 +94,13 @@
import org.exist.xquery.update.Modification;
import org.exist.xquery.util.SerializerUtils;
import org.exist.xquery.value.*;
import org.jgrapht.alg.interfaces.ShortestPathAlgorithm;
import org.jgrapht.alg.shortestpath.TransitNodeRoutingShortestPath;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.DefaultGraphType;
import org.jgrapht.opt.graph.fastutil.FastutilMapGraph;
import org.jgrapht.util.ConcurrencyUtil;
import org.jgrapht.util.SupplierUtil;
import org.w3c.dom.Node;

import static com.evolvedbinary.j8fu.OptionalUtil.or;
Expand Down Expand Up @@ -223,6 +231,11 @@ public class XQueryContext implements BinaryValueManager, Context {
*/
private Object2ObjectMap<String, Module[]> allModules = new Object2ObjectOpenHashMap<>();

/**
* Describes a graph of all the modules and how they import each other.
*/
private @Nullable final FastutilMapGraph<ModuleVertex, DefaultEdge> modulesDependencyGraph;

/**
* Used to save current state when modules are imported dynamically
*/
Expand Down Expand Up @@ -453,6 +466,10 @@ public XQueryContext(@Nullable final Database db, @Nullable final Configuration
}

protected XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler, final boolean loadDefaults) {
this(db, configuration, profiler, loadDefaults, new FastutilMapGraph<>(null, SupplierUtil.createDefaultEdgeSupplier(), DefaultGraphType.directedPseudograph().asUnweighted()));
}

protected XQueryContext(@Nullable final Database db, @Nullable final Configuration configuration, @Nullable final Profiler profiler, final boolean loadDefaults, final @Nullable FastutilMapGraph<ModuleVertex, DefaultEdge> modulesDependencyGraph) {
this.db = db;

// if needed, fallback to db.getConfiguration
Expand All @@ -473,6 +490,8 @@ protected XQueryContext(@Nullable final Database db, @Nullable final Configurati
this.profiler = new Profiler(null);
}

this.modulesDependencyGraph = modulesDependencyGraph;

this.watchdog = new XQueryWatchDog(this);

// load configuration defaults
Expand Down Expand Up @@ -1505,6 +1524,65 @@ public void addModule(final String namespaceURI, final Module module) {
addRootModule(namespaceURI, module);
}

/**
* Add a vertex to the Modules Dependency Graph.
*
* @param moduleVertex the module vertex
*/
protected void addModuleVertex(final ModuleVertex moduleVertex) {
modulesDependencyGraph.addVertex(moduleVertex);
}

/**
* Check if a vertex exists in the Modules Dependency Graph.
*
* @param moduleVertex the module vertex to look for
*
* @return true if the module vertex exists, false otherwise
*/
protected boolean hasModuleVertex(final ModuleVertex moduleVertex) {
return modulesDependencyGraph.containsVertex(moduleVertex);
}

/**
* Add an edge between two Modules in the Dependency Graph.
*
* @param source the importing module
* @param sink the imported module
*/
protected void addModuleEdge(final ModuleVertex source, final ModuleVertex sink) {
modulesDependencyGraph.addEdge(source, sink);
}

/**
* Look for a path between two Modules in the Dependency Graph.
*
* @param source the module to start searching from
* @param sink the destination module to attempt to reach
*
* @return true, if there is a path between the mdoules, false otherwise
*/
protected boolean hasModulePath(final ModuleVertex source, final ModuleVertex sink) {
if (modulesDependencyGraph == null) {
return false;
}

ThreadPoolExecutor executor = null;
try {
executor = ConcurrencyUtil.createThreadPoolExecutor(2);
final ShortestPathAlgorithm<ModuleVertex, DefaultEdge> spa = new TransitNodeRoutingShortestPath<>(modulesDependencyGraph, executor);
return spa.getPath(source, sink) != null;
} finally {
if (executor != null) {
try {
ConcurrencyUtil.shutdownExecutionService(executor);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}

protected void setRootModules(final String namespaceURI, @Nullable final Module[] modules) {
if (modules == null) {
allModules.remove(namespaceURI); // unbind the module
Expand Down Expand Up @@ -2617,6 +2695,11 @@ private ExternalModule compileOrBorrowModule(final String namespaceURI, final St
}

final ExternalModuleImpl modExternal = new ExternalModuleImpl(namespaceURI, prefix);

// NOTE(AR) this is needed to support cyclic imports in XQuery 3.1, see: https://github.com/eXist-db/exist/pull/4996
addModule(namespaceURI, modExternal);
addModuleVertex(new ModuleVertex(namespaceURI, location));

final XQueryContext modContext = new ModuleContext(this, namespaceURI, prefix, location);
modExternal.setContext(modContext);
final XQueryLexer lexer = new XQueryLexer(modContext, reader);
Expand Down Expand Up @@ -3522,4 +3605,52 @@ public String getStringValue() {

return sb.toString();
}

@Immutable
public static class ModuleVertex {
private final String namespaceURI;
private final String location;

public ModuleVertex(final String namespaceURI) {
this.namespaceURI = namespaceURI;
this.location = null;
}

public ModuleVertex(final String namespaceURI, final String location) {
this.namespaceURI = namespaceURI;
this.location = location;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}

if (o == null || getClass() != o.getClass()) {
return false;
}

final ModuleVertex that = (ModuleVertex) o;
if (!namespaceURI.equals(that.namespaceURI)) {
return false;
}
return location.equals(that.location);
}

@Override
public int hashCode() {
int result = namespaceURI.hashCode();
result = 31 * result + location.hashCode();
return result;
}

@Override
public String toString() {
return "Module{" +
"namespaceURI='" + namespaceURI + '\'' +
"location='" + location + '\'' +
'}';
}
}
}

0 comments on commit e226566

Please sign in to comment.