diff --git a/modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePath.groovy b/modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePathException.groovy similarity index 93% rename from modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePath.groovy rename to modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePathException.groovy index 2129a21898..599b217067 100644 --- a/modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePath.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/exception/IllegalModulePathException.groovy @@ -24,5 +24,5 @@ import groovy.transform.InheritConstructors * @author Paolo Di Tommaso */ @InheritConstructors -class IllegalModulePath extends Exception { +class IllegalModulePathException extends Exception { } diff --git a/modules/nextflow/src/main/groovy/nextflow/module/DefaultRemoteModuleResolver.groovy b/modules/nextflow/src/main/groovy/nextflow/module/DefaultRemoteModuleResolver.groovy index 82e17d735e..f0b28dc3fd 100644 --- a/modules/nextflow/src/main/groovy/nextflow/module/DefaultRemoteModuleResolver.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/module/DefaultRemoteModuleResolver.groovy @@ -22,7 +22,7 @@ import nextflow.Global import nextflow.config.ConfigBuilder import nextflow.config.RegistryConfig -import nextflow.exception.IllegalModulePath +import nextflow.exception.IllegalModulePathException import nextflow.module.spi.RemoteModuleResolver import java.nio.file.Path @@ -45,8 +45,8 @@ import java.nio.file.Path class DefaultRemoteModuleResolver implements RemoteModuleResolver { @Override - Path resolve(String moduleName, Path baseDir) { - + Path resolve(String moduleName, Path projectDir) { + final baseDir = projectDir ?: Path.of('.').toAbsolutePath() final config = Global.config ?: new ConfigBuilder().setBaseDir(baseDir).build() final registryConfig = config.navigate('registry') as RegistryConfig @@ -65,7 +65,7 @@ class DefaultRemoteModuleResolver implements RemoteModuleResolver { log.debug "Module ${reference} resolved to ${mainFile}" return mainFile } catch (Exception e) { - throw new IllegalModulePath("Failed to resolve remote module ${moduleName}: ${e.message}", e) + throw new IllegalModulePathException("Failed to resolve remote module ${moduleName}: ${e.message}", e) } } diff --git a/modules/nextflow/src/main/groovy/nextflow/module/ModuleResolver.groovy b/modules/nextflow/src/main/groovy/nextflow/module/ModuleResolver.groovy index 0adb0d1689..b87fc8f4aa 100644 --- a/modules/nextflow/src/main/groovy/nextflow/module/ModuleResolver.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/module/ModuleResolver.groovy @@ -161,7 +161,7 @@ class ModuleResolver { // Install to modules directory (will compute directory checksum for future integrity checks) InstalledModule installed = storage.installModule(reference, version, tempFile, downloadUrl) - log.info "Module ${reference}@${version} installed successfully at ${installed.mainFile.parent}" + log.info "Module ${reference}@${version} installed successfully at ${installed.mainFile.parent.toAbsolutePath()}" return installed.mainFile } finally { diff --git a/modules/nextflow/src/main/groovy/nextflow/script/IncludeDef.groovy b/modules/nextflow/src/main/groovy/nextflow/script/IncludeDef.groovy index 531e6c4cc4..6f5736514a 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/IncludeDef.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/IncludeDef.groovy @@ -27,7 +27,7 @@ import groovy.transform.PackageScope import groovy.util.logging.Slf4j import nextflow.NF import nextflow.Session -import nextflow.exception.IllegalModulePath +import nextflow.exception.IllegalModulePathException import nextflow.exception.ScriptCompilationException import nextflow.module.ModuleReference import nextflow.module.spi.RemoteModuleResolverProvider @@ -167,7 +167,7 @@ class IncludeDef { final result = include as Path if( result.isAbsolute() ) { if( result.scheme == 'file' ) return result - throw new IllegalModulePath("Cannot resolve module path: ${result.toUriString()}") + throw new IllegalModulePathException("Cannot resolve module path: ${result.toUriString()}") } final str = include.toString() if( str.startsWith('./') || str.startsWith('../') ) { @@ -212,10 +212,10 @@ class IncludeDef { @PackageScope void checkValidPath(path) { if( !path ) - throw new IllegalModulePath("Missing module path attribute") + throw new IllegalModulePathException("Missing module path attribute") if( path instanceof Path && path.scheme != 'file' ) - throw new IllegalModulePath("Remote modules are not allowed -- Offending module: ${path.toUriString()}") + throw new IllegalModulePathException("Remote modules are not allowed -- Offending module: ${path.toUriString()}") final str = path.toString() if( str.startsWith('/') || str.startsWith('./') || str.startsWith('../') || str.startsWith('plugin/') ) @@ -225,7 +225,7 @@ class IncludeDef { try { ModuleReference.parse(str) } catch( Exception e ) { - throw new IllegalModulePath("Module path must start with '/', './', '../' or 'plugin/' prefix, or be a valid remote module reference (scope/name) -- Offending module: $str") + throw new IllegalModulePathException("Module path must start with '/', './', '../' or 'plugin/' prefix, or be a valid remote module reference (scope/name) -- Offending module: $str") } } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptCompiler.java b/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptCompiler.java index f9ac288fc7..a193e629cd 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptCompiler.java +++ b/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptCompiler.java @@ -88,16 +88,18 @@ public class ScriptCompiler { private final CompilerConfiguration config; private final GroovyClassLoader loader; + private final Path projectDir; private Compiler compiler; - public ScriptCompiler(boolean debug, Path targetDirectory, ClassLoader parent) { - this(getConfig(debug, targetDirectory), parent); + public ScriptCompiler(boolean debug, Path targetDirectory, ClassLoader parent, Path projectDir) { + this(getConfig(debug, targetDirectory), parent, projectDir); } - public ScriptCompiler(CompilerConfiguration config, ClassLoader parent) { + public ScriptCompiler(CompilerConfiguration config, ClassLoader parent, Path projectDir) { this.config = config; this.loader = new GroovyClassLoader(parent, config); + this.projectDir = projectDir; } private static CompilerConfiguration getConfig(boolean debug, Path targetDirectory) { @@ -269,7 +271,7 @@ public void addPhaseOperation(IPrimaryClassNodeOperation op, int phase) { private void analyze(SourceUnit source) { // on first pass, recursively add included modules to queue if( entry == null ) { - modules = new ModuleResolver(compiler).resolve(source, uri -> createSourceUnit(uri)); + modules = new ModuleResolver(projectDir, compiler).resolve(source, uri -> createSourceUnit(uri)); for( var su : modules ) addSource(su); entry = source; @@ -283,7 +285,7 @@ private void analyze(SourceUnit source) { var cn = source.getAST().getClasses().get(0); // perform strict syntax checking - var includeResolver = new ResolveIncludeVisitor(source, compiler); + var includeResolver = new ResolveIncludeVisitor(source, projectDir, compiler); includeResolver.visit(); for( var error : includeResolver.getErrors() ) source.getErrorCollector().addErrorAndContinue(error); diff --git a/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptLoaderV2.groovy b/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptLoaderV2.groovy index 4359f49427..92d92831aa 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptLoaderV2.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/parser/v2/ScriptLoaderV2.groovy @@ -175,7 +175,7 @@ class ScriptLoaderV2 implements ScriptLoader { private ScriptCompiler getCompiler() { if( !compiler ) - compiler = new ScriptCompiler(session.debug, session.classesDir, session.getClassLoader()) + compiler = new ScriptCompiler(session.debug, session.classesDir, session.getClassLoader(), session.baseDir) return compiler } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/IncludeDefTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/IncludeDefTest.groovy index 527cbbfbbd..d877124251 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/IncludeDefTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/IncludeDefTest.groovy @@ -24,7 +24,7 @@ import java.nio.file.NoSuchFileException import java.nio.file.Path import nextflow.ast.NextflowDSL -import nextflow.exception.IllegalModulePath +import nextflow.exception.IllegalModulePathException import nextflow.file.FileHelper import org.codehaus.groovy.control.CompilerConfiguration import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer @@ -51,7 +51,7 @@ class IncludeDefTest extends Specification { when: include.resolveModulePath('http://foo.com/bar') then: - thrown(IllegalModulePath) + thrown(IllegalModulePathException) } @@ -147,17 +147,17 @@ class IncludeDefTest extends Specification { when: include.checkValidPath('invalid!') then: - thrown(IllegalModulePath) + thrown(IllegalModulePathException) when: include.checkValidPath( 'http://foo.com/x.y') then: - thrown(IllegalModulePath) + thrown(IllegalModulePathException) when: include.checkValidPath(FileHelper.asPath('http://foo.com/x/y/z')) then: - thrown(IllegalModulePath) + thrown(IllegalModulePathException) } diff --git a/modules/nf-lang/src/main/java/nextflow/module/spi/FallbackRemoteModuleResolver.java b/modules/nf-lang/src/main/java/nextflow/module/spi/FallbackRemoteModuleResolver.java index 57b7a1020a..4f993323f8 100644 --- a/modules/nf-lang/src/main/java/nextflow/module/spi/FallbackRemoteModuleResolver.java +++ b/modules/nf-lang/src/main/java/nextflow/module/spi/FallbackRemoteModuleResolver.java @@ -31,14 +31,15 @@ public class FallbackRemoteModuleResolver implements RemoteModuleResolver { @Override - public Path resolve(String moduleName, Path baseDir) { - final var resolved = baseDir.resolve(moduleName).normalize(); - // Prevent path traversal outside the base directory - if (!resolved.startsWith(baseDir.normalize())) { - throw new IllegalStateException("Invalid module name '" + moduleName + "' - path escapes the modules directory"); + public Path resolve(String moduleName, Path projectDir) { + var baseDir = projectDir != null ? projectDir : Path.of(".").toAbsolutePath(); + var modulesDir = baseDir.resolve("modules").normalize(); + var resolved = modulesDir.resolve(moduleName).normalize(); + if( !resolved.startsWith(modulesDir) ) { + throw new IllegalStateException("Invalid module name '" + moduleName + "' -- path escapes the modules directory"); } - if (!Files.exists(resolved)) { - throw new IllegalStateException("Module '" + moduleName + "' not locally found at 'modules' folder - use 'nextflow module install' to download module files"); + if( !Files.exists(resolved) ) { + throw new IllegalStateException("Module '" + moduleName + "' not found in 'modules' directory -- use 'nextflow module install' to install module first"); } return resolved.resolve("main.nf"); } @@ -47,4 +48,4 @@ public Path resolve(String moduleName, Path baseDir) { public int getPriority() { return Integer.MIN_VALUE; // Fallback has lowest possible priority } -} \ No newline at end of file +} diff --git a/modules/nf-lang/src/main/java/nextflow/module/spi/RemoteModuleResolver.java b/modules/nf-lang/src/main/java/nextflow/module/spi/RemoteModuleResolver.java index 5c6cf1fc9b..6eb8b3bbdc 100644 --- a/modules/nf-lang/src/main/java/nextflow/module/spi/RemoteModuleResolver.java +++ b/modules/nf-lang/src/main/java/nextflow/module/spi/RemoteModuleResolver.java @@ -47,11 +47,11 @@ public interface RemoteModuleResolver { * * * @param moduleName The module reference string (e.g., '@scope/name' or '@scope/name@version') - * @param baseDir The base directory for the project (used to locate the modules directory) + * @param projectDir The base directory for the project (used to locate the modules directory) * @return Path to the resolved module's main.nf file * @throws IllegalArgumentException if the module reference is invalid or resolution fails */ - Path resolve(String moduleName, Path baseDir); + Path resolve(String moduleName, Path projectDir); /** * Get the priority of this resolver. Higher priority resolvers are tried first. @@ -65,4 +65,4 @@ public interface RemoteModuleResolver { default int getPriority() { return 0; } -} \ No newline at end of file +} diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ModuleResolver.java b/modules/nf-lang/src/main/java/nextflow/script/control/ModuleResolver.java index 6c104e016e..9070106e14 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ModuleResolver.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ModuleResolver.java @@ -37,9 +37,11 @@ public class ModuleResolver { private Compiler compiler; + private Path projectDir; - public ModuleResolver(Compiler compiler) { + public ModuleResolver(Path projectDir, Compiler compiler) { this.compiler = compiler; + this.projectDir = projectDir; } /** @@ -74,17 +76,8 @@ private SourceUnit resolveInclude(IncludeNode node, SourceUnit sourceUnit, Funct if( source.startsWith("plugin/") ) return null; - var parent = Path.of(sourceUnit.getSource().getURI()).getParent(); - - // Resolve remote module paths (scope/name format, not starting with local prefixes) - if( isRemoteModule(source) ) { - var modules = Path.of("./modules"); - var resolver = RemoteModuleResolverProvider.getInstance(); - resolver.resolve(source, modules.getParent()); - parent = modules; - } - - var includeUri = getIncludeUri(parent, source); + var uri = sourceUnit.getSource().getURI(); + var includeUri = getIncludeUri(uri, source); if( compiler.getSource(includeUri) != null ) return null; if( !Files.exists(Path.of(includeUri)) ) @@ -97,12 +90,25 @@ private SourceUnit resolveInclude(IncludeNode node, SourceUnit sourceUnit, Funct return includeSource; } + private URI getIncludeUri(URI uri, String source) { + if( isRemoteModule(source) ) { + return RemoteModuleResolverProvider.getInstance() + .resolve(source, projectDir) + .normalize() + .toUri(); + } + else { + var parent = Path.of(uri).getParent(); + return getLocalIncludeUri(parent, source); + } + } + /** * Module name pattern matching the canonical format used by ModuleReference. * Scope: lowercase alphanumeric with dots/underscores/hyphens. * Name: one or more slash-separated segments, each lowercase alphanumeric with dots/underscores/hyphens. */ - static final String REMOTE_MODULE_PATTERN = "^[a-z0-9][a-z0-9._\\-]*/[a-z][a-z0-9._\\-]*(/[a-z][a-z0-9._\\-]*)*$"; + private static final String REMOTE_MODULE_PATTERN = "^[a-z0-9][a-z0-9._\\-]*/[a-z][a-z0-9._\\-]*(/[a-z][a-z0-9._\\-]*)*$"; static boolean isRemoteModule(String source) { if( source.startsWith("/") || source.startsWith("./") || source.startsWith("../") ) @@ -110,7 +116,7 @@ static boolean isRemoteModule(String source) { return source.matches(REMOTE_MODULE_PATTERN); } - private static URI getIncludeUri(Path parent, String source) { + private static URI getLocalIncludeUri(Path parent, String source) { Path includePath = parent.resolve(source); if( Files.isDirectory(includePath) ) includePath = includePath.resolve("main.nf"); diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ResolveIncludeVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ResolveIncludeVisitor.java index 11ad336a74..b298c5cfa1 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ResolveIncludeVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ResolveIncludeVisitor.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Set; +import nextflow.module.spi.RemoteModuleResolverProvider; import nextflow.script.ast.FunctionNode; import nextflow.script.ast.IncludeNode; import nextflow.script.ast.ScriptNode; @@ -48,6 +49,8 @@ public class ResolveIncludeVisitor extends ScriptVisitorSupport { private URI uri; + private Path projectDir; + private Compiler compiler; private Set changedUris; @@ -56,15 +59,16 @@ public class ResolveIncludeVisitor extends ScriptVisitorSupport { private boolean changed; - public ResolveIncludeVisitor(SourceUnit sourceUnit, Compiler compiler, Set changedUris) { + public ResolveIncludeVisitor(SourceUnit sourceUnit, Path projectDir, Compiler compiler, Set changedUris) { this.sourceUnit = sourceUnit; this.uri = sourceUnit.getSource().getURI(); this.compiler = compiler; this.changedUris = changedUris; + this.projectDir = projectDir; } - public ResolveIncludeVisitor(SourceUnit sourceUnit, Compiler compiler) { - this(sourceUnit, compiler, null); + public ResolveIncludeVisitor(SourceUnit sourceUnit, Path projectDir, Compiler compiler) { + this(sourceUnit, projectDir, compiler, null); } @Override @@ -86,9 +90,15 @@ public void visitInclude(IncludeNode node) { return; } - var isRemoteModule = ModuleResolver.isRemoteModule(source); - var parent = isRemoteModule ? Path.of("modules") : Path.of(uri).getParent(); - var includeUri = getIncludeUri(parent, source); + URI includeUri; + try { + includeUri = getIncludeUri(source); + } + catch( Exception e ) { + addError(e.getMessage(), node); + return; + } + if( !isIncludeStale(node, includeUri) ) return; changed = true; @@ -126,7 +136,20 @@ private static void setPlaceholderTargets(IncludeNode node) { } } - private static URI getIncludeUri(Path parent, String source) { + private URI getIncludeUri(String source) { + if( ModuleResolver.isRemoteModule(source) ) { + return RemoteModuleResolverProvider.getInstance() + .resolve(source, projectDir) + .normalize() + .toUri(); + } + else { + var parent = Path.of(uri).getParent(); + return getLocalIncludeUri(parent, source); + } + } + + private static URI getLocalIncludeUri(Path parent, String source) { Path includePath = parent.resolve(source); if( Files.isDirectory(includePath) ) includePath = includePath.resolve("main.nf"); diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptParser.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptParser.java index 88e9656b3e..62ba17a7a1 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptParser.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptParser.java @@ -16,6 +16,7 @@ package nextflow.script.control; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; @@ -33,12 +34,18 @@ */ public class ScriptParser { + private Path projectDir; private Compiler compiler; - public ScriptParser() { + public ScriptParser(Path projectDir) { + this.projectDir = projectDir; var config = getConfig(); var classLoader = new GroovyClassLoader(); - compiler = new Compiler(config, classLoader); + this.compiler = new Compiler(config, classLoader); + } + + public ScriptParser() { + this(null); } public Compiler compiler() { @@ -68,11 +75,11 @@ private void parse0(SourceUnit source) { public void analyze() { var sources = new ArrayList<>(compiler.getSources().values()); for( var source : sources ) { - new ModuleResolver(compiler()).resolve(source, (uri) -> compiler.createSourceUnit(new File(uri))); + new ModuleResolver(projectDir, compiler()).resolve(source, (uri) -> compiler.createSourceUnit(new File(uri))); } for( var source : compiler.getSources().values() ) { - var includeResolver = new ResolveIncludeVisitor(source, compiler); + var includeResolver = new ResolveIncludeVisitor(source, projectDir, compiler); includeResolver.visit(); for( var error : includeResolver.getErrors() ) source.getErrorCollector().addErrorAndContinue(error);