From 363b76feee840533b05f0301f75b764150150765 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Wed, 22 Apr 2026 18:47:22 +0200 Subject: [PATCH 1/9] feat: introduce module system --- .../densy/scriptify/api/script/Script.java | 3 + .../api/script/module/ScriptModule.java | 26 ++++ .../script/module/ScriptModuleManager.java | 19 +++ .../script/module/export/ScriptExport.java | 8 + .../module/export/ScriptValueExport.java | 33 +++++ build.gradle.kts | 2 +- .../script/module/AbstractScriptModule.java | 26 ++++ .../script/module/ScriptGlobalModule.java | 10 ++ .../script/module/SimpleScriptModule.java | 18 +++ .../module/export/ScriptConstantExport.java | 21 +++ .../module/export/ScriptFunctionExport.java | 21 +++ script-js-graalvm/build.gradle.kts | 1 + .../scriptify/js/graalvm/script/JsScript.java | 44 ++++-- .../script/module/GraalModuleManager.java | 71 +++++++++ .../module/GraalModuleSourceGenerator.java | 50 +++++++ .../module/fs/VirtualModuleFileSystem.java | 138 ++++++++++++++++++ .../module/fs/util/ByteArrayChannel.java | 60 ++++++++ .../scriptify/js/rhino/script/JsScript.java | 7 + 18 files changed, 547 insertions(+), 11 deletions(-) create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java diff --git a/api/src/main/java/org/densy/scriptify/api/script/Script.java b/api/src/main/java/org/densy/scriptify/api/script/Script.java index 7db08a5..7bbba65 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/Script.java +++ b/api/src/main/java/org/densy/scriptify/api/script/Script.java @@ -4,6 +4,7 @@ import org.densy.scriptify.api.exception.ScriptFunctionException; import org.densy.scriptify.api.script.constant.ScriptConstantManager; import org.densy.scriptify.api.script.function.ScriptFunctionManager; +import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.security.ScriptSecurityManager; /** @@ -21,6 +22,8 @@ public interface Script { */ ScriptSecurityManager getSecurityManager(); + ScriptModuleManager getModuleManager(); + /** * Retrieves the function manager associated with this script. * diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java new file mode 100644 index 0000000..34e83c5 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -0,0 +1,26 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; + +/** + * A module that exports elements to the script environment. + * Named modules (non-null name) are accessible via ES import. + * The global module (null name) injects exports directly into global scope. + */ +public interface ScriptModule { + + /** + * Module name for ES import, e.g. "@densy/mymodule". + */ + @NotNull + String getName(); + + @UnmodifiableView + Collection getExports(); + + void export(ScriptExport export); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java new file mode 100644 index 0000000..6e9eb5e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -0,0 +1,19 @@ +package org.densy.scriptify.api.script.module; + +/** + * Manages all modules available to the script. + * The global module is always present and created automatically. + */ +public interface ScriptModuleManager { + + /** + * Exports added here are available globally without import + */ + ScriptModule getGlobalModule(); + + ScriptModule getModule(String name); + + void addModule(ScriptModule module); + + void removeModule(String name); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java new file mode 100644 index 0000000..26ae21e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptExport.java @@ -0,0 +1,8 @@ +package org.densy.scriptify.api.script.module.export; + +/** + * Represents any exportable element from a module. + */ +public interface ScriptExport { + String getName(); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java new file mode 100644 index 0000000..dfc9ff2 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/ScriptValueExport.java @@ -0,0 +1,33 @@ +package org.densy.scriptify.api.script.module.export; + +import lombok.Getter; + +/** + * Universal wrapper for exporting Java values and classes. + * + *
+ *   new ScriptValueExport("PI", 3.14)               - PI available as a number
+ *   new ScriptValueExport("MyClass", MyClass.class) - new MyClass() in JS
+ *   new ScriptValueExport("service", myService)     - access to instance methods
+ * 
+ */ +@Getter +public class ScriptValueExport implements ScriptExport { + + private final String name; + private final Object value; + + public ScriptValueExport(String name, Object value) { + this.name = name; + this.value = value; + } + + @Override + public String getName() { + return name; + } + + public boolean isClass() { + return value instanceof Class; + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 21db491..4f88e4d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java new file mode 100644 index 0000000..104c857 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java @@ -0,0 +1,26 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.ScriptModule; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public abstract class AbstractScriptModule implements ScriptModule { + private final Map exports = new LinkedHashMap<>(); + + @Override + public void export(ScriptExport export) { + if (export == null) { + throw new IllegalArgumentException("Export cannot be null"); + } + exports.put(export.getName(), export); + } + + @Override + public Collection getExports() { + return Collections.unmodifiableCollection(exports.values()); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java new file mode 100644 index 0000000..4b56373 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java @@ -0,0 +1,10 @@ +package org.densy.scriptify.core.script.module; + +import org.jetbrains.annotations.NotNull; + +public class ScriptGlobalModule extends AbstractScriptModule { + @Override + public @NotNull String getName() { + return "global"; + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java new file mode 100644 index 0000000..b8bd872 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java @@ -0,0 +1,18 @@ +package org.densy.scriptify.core.script.module; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class SimpleScriptModule extends AbstractScriptModule { + private final String name; + + public SimpleScriptModule(String name) { + this.name = Objects.requireNonNull(name, "Module name cannot be null"); + } + + @Override + public @NotNull String getName() { + return name; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java new file mode 100644 index 0000000..3ece777 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java @@ -0,0 +1,21 @@ +package org.densy.scriptify.core.script.module.export; + +import lombok.Getter; +import org.densy.scriptify.api.script.constant.ScriptConstant; +import org.densy.scriptify.api.script.module.export.ScriptExport; + +@Getter +public final class ScriptConstantExport implements ScriptExport { + + private final ScriptConstant constant; + + public ScriptConstantExport(ScriptConstant constant) { + this.constant = constant; + } + + @Override + public String getName() { + return constant.getName(); + } + +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java new file mode 100644 index 0000000..0cf39d4 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java @@ -0,0 +1,21 @@ +package org.densy.scriptify.core.script.module.export; + +import lombok.Getter; +import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.module.export.ScriptExport; + +@Getter +public final class ScriptFunctionExport implements ScriptExport { + + private final ScriptFunctionDefinition definition; + + public ScriptFunctionExport(ScriptFunctionDefinition definition) { + this.definition = definition; + } + + @Override + public String getName() { + return definition.getFunction().getName(); + } + +} diff --git a/script-js-graalvm/build.gradle.kts b/script-js-graalvm/build.gradle.kts index 6c13dec..ede57fd 100644 --- a/script-js-graalvm/build.gradle.kts +++ b/script-js-graalvm/build.gradle.kts @@ -8,6 +8,7 @@ repositories { dependencies { api(project(":core")) + api(project(":common")) api("org.graalvm.polyglot:polyglot:24.1.1") api("org.graalvm.polyglot:js:24.1.1") } \ No newline at end of file diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java index 929f912..2ce084a 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java @@ -8,33 +8,47 @@ import org.densy.scriptify.api.script.constant.ScriptConstantManager; import org.densy.scriptify.api.script.function.ScriptFunctionManager; import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.security.ScriptSecurityManager; import org.densy.scriptify.core.script.constant.StandardConstantManager; import org.densy.scriptify.core.script.function.StandardFunctionManager; +import org.densy.scriptify.core.script.module.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; import org.densy.scriptify.core.script.security.StandardSecurityManager; +import org.densy.scriptify.js.graalvm.script.module.GraalModuleManager; +import org.densy.scriptify.js.graalvm.script.module.fs.VirtualModuleFileSystem; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.Source; import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.io.IOAccess; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; public class JsScript implements Script { private final ScriptSecurityManager securityManager = new StandardSecurityManager(); + private final GraalModuleManager moduleManager = new GraalModuleManager(this); private ScriptFunctionManager functionManager = new StandardFunctionManager(); private ScriptConstantManager constantManager = new StandardConstantManager(); private final List extraScript = new ArrayList<>(); @Override - public ScriptConstantManager getConstantManager() { - return constantManager; + public ScriptSecurityManager getSecurityManager() { + return securityManager; } @Override - public ScriptSecurityManager getSecurityManager() { - return securityManager; + public ScriptModuleManager getModuleManager() { + return moduleManager; + } + + @Override + public ScriptConstantManager getConstantManager() { + return constantManager; } @Override @@ -59,6 +73,10 @@ public void addExtraScript(String script) { @Override public CompiledScript compile(String script) throws ScriptException { + // A context reference, so that once it has been created, + // we can access it in the file system + AtomicReference contextRef = new AtomicReference<>(); + Context.Builder builder = Context.newBuilder("js") .allowHostAccess(HostAccess.newBuilder(HostAccess.ALL) // Mapping for the ScriptObject class required @@ -69,6 +87,9 @@ public CompiledScript compile(String script) throws ScriptException { object -> true, ScriptObject::getValue ) + .build()) + .allowIO(IOAccess.newBuilder() + .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get)) .build()); // If security mode is enabled, search all exclusions @@ -80,16 +101,15 @@ public CompiledScript compile(String script) throws ScriptException { } Context context = builder.build(); - - Value bindings = context.getBindings("js"); + contextRef.set(context); for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) { - bindings.putMember(definition.getFunction().getName(), new JsFunction(this, definition)); + moduleManager.getGlobalModule().export(new ScriptFunctionExport(definition)); } - for (ScriptConstant constant : constantManager.getConstants().values()) { - bindings.putMember(constant.getName(), constant.getValue()); + moduleManager.getGlobalModule().export(new ScriptConstantExport(constant)); } + moduleManager.applyTo(context); // Building full script including extra script code StringBuilder fullScript = new StringBuilder(); @@ -99,7 +119,11 @@ public CompiledScript compile(String script) throws ScriptException { fullScript.append(script); try { - return new JsCompiledScript(context, context.eval("js", fullScript.toString())); + Source source = Source.newBuilder("js", fullScript.toString(), "script.mjs") + .mimeType("application/javascript+module") + .build(); + + return new JsCompiledScript(context, context.eval(source)); } catch (Exception e) { throw new ScriptException(e); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java new file mode 100644 index 0000000..07ec3a6 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -0,0 +1,71 @@ +package org.densy.scriptify.js.graalvm.script.module; + +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.core.script.module.ScriptGlobalModule; +import org.densy.scriptify.core.script.module.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public class GraalModuleManager implements ScriptModuleManager { + + private final Script script; + private final ScriptGlobalModule globalModule = new ScriptGlobalModule(); + private final Map modules = new LinkedHashMap<>(); + + public GraalModuleManager(Script script) { + this.script = script; + } + + @Override + public ScriptGlobalModule getGlobalModule() { + return globalModule; + } + + @Override + public ScriptModule getModule(String name) { + return modules.get(name); + } + + @Override + public void addModule(ScriptModule module) { + Objects.requireNonNull(module, "module cannot be null"); + Objects.requireNonNull(module.getName(), "module name cannot be null"); + modules.put(module.getName(), module); + } + + @Override + public void removeModule(String name) { + modules.remove(name); + } + + public void applyTo(Context context) { + Value bindings = context.getBindings("js"); + + for (ScriptExport export : globalModule.getExports()) { + bindings.putMember(export.getName(), resolveValue(context, export)); + } + } + + private Object resolveValue(Context context, ScriptExport export) { + if (export instanceof ScriptValueExport valueExport) { + return context.asValue(valueExport.getValue()); + } + if (export instanceof ScriptFunctionExport functionExport) { + return new JsFunction(script, functionExport.getDefinition()); + } + if (export instanceof ScriptConstantExport constantExport) { + return constantExport.getConstant().getValue(); + } + throw new UnsupportedOperationException("Unsupported export type: " + export.getClass().getName() ); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java new file mode 100644 index 0000000..9bdddfe --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java @@ -0,0 +1,50 @@ +package org.densy.scriptify.js.graalvm.script.module; + +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.graalvm.polyglot.Context; + +import java.util.ArrayList; +import java.util.List; + +public final class GraalModuleSourceGenerator { + + public static final String BRIDGE_PREFIX = "__scriptify_bridge_"; + + public static String generateModuleSource(Context context, ScriptModule module) { + StringBuilder sb = new StringBuilder(); + sb.append("// @generated module: ").append(module.getName()).append("\n"); + + List names = new ArrayList<>(); + + for (ScriptExport export : module.getExports()) { + String name = export.getName(); + names.add(name); + + if (export instanceof ScriptValueExport valueExport) { + if (valueExport.isClass()) { + Class valueClass = (Class) valueExport.getValue(); + sb.append("const ").append(name) + .append(" = Java.type('").append(valueClass.getName()).append("');\n"); + } else { + String bridge = BRIDGE_PREFIX + name; + context.getBindings("js").putMember(bridge, context.asValue(valueExport.getValue())); + sb.append("const ").append(name) + .append(" = globalThis.").append(bridge).append(";\n"); + } + } + } + + sb.append("\nexport { ").append(String.join(", ", names)).append(" };\n"); + return sb.toString(); + } + + public static String encodeModuleName(String name) { + return name.replace("@", "_at_").replace("/", "__"); + } + + public static String decodeModuleName(String encoded) { + return encoded.replace("_at_", "@").replace("__", "/"); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java new file mode 100644 index 0000000..13736ec --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -0,0 +1,138 @@ +package org.densy.scriptify.js.graalvm.script.module.fs; + +import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.js.graalvm.script.module.GraalModuleSourceGenerator; +import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.io.FileSystem; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +public class VirtualModuleFileSystem implements FileSystem { + + private static final String SCHEME = "scriptify"; + + private final FileSystem real = FileSystem.newDefaultFileSystem(); + private final ScriptModuleManager moduleManager; + private final Supplier contextSupplier; + + private final Map modulePathCache = new HashMap<>(); + + public VirtualModuleFileSystem(ScriptModuleManager moduleManager, Supplier contextSupplier) { + this.moduleManager = moduleManager; + this.contextSupplier = contextSupplier; + } + + @Override + public Path parsePath(String path) { + if (moduleManager.getModule(path) != null) { + return this.resolveVirtualPath(path); + } + return real.parsePath(path); + } + + @Override + public Path parsePath(URI uri) { + if (SCHEME.equals(uri.getScheme())) { + return this.resolveVirtualPath(uri.getHost()); + } + return real.parsePath(uri); + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + if (this.isVirtual(path)) { + return; + } + real.checkAccess(path, modes, linkOptions); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + if (this.isVirtual(path)) { + Map attrs = new HashMap<>(); + attrs.put("isRegularFile", true); + attrs.put("isDirectory", false); + attrs.put("size", 0L); + attrs.put("lastModifiedTime", FileTime.fromMillis(0)); + attrs.put("creationTime", FileTime.fromMillis(0)); + attrs.put("lastAccessTime", FileTime.fromMillis(0)); + return attrs; + } + return real.readAttributes(path, attributes, options); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + if (this.isVirtual(path)) { + String moduleName = getModuleName(path); + ScriptModule module = moduleManager.getModule(moduleName); + if (module == null) { + throw new IOException("Scriptify module not found: " + moduleName); + } + byte[] source = GraalModuleSourceGenerator + .generateModuleSource(contextSupplier.get(), module) + .getBytes(StandardCharsets.UTF_8); + return new ByteArrayChannel(source); + } + return real.newByteChannel(path, options, attrs); + } + + @Override + public Path toAbsolutePath(Path path) { + if (isVirtual(path)) return path; + return real.toAbsolutePath(path); + } + + @Override + public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { + if (isVirtual(path)) return path; + return real.toRealPath(path, linkOptions); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + return real.newDirectoryStream(dir, filter); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + real.createDirectory(dir, attrs); + } + + @Override + public void delete(Path path) throws IOException { + real.delete(path); + } + + private Path resolveVirtualPath(String moduleName) { + return modulePathCache.computeIfAbsent(moduleName, name -> Paths.get( + System.getProperty("java.io.tmpdir"), + "scriptify", + GraalModuleSourceGenerator.encodeModuleName(name) + ".mjs" + )); + } + + private boolean isVirtual(Path path) { + return modulePathCache.containsValue(path); + } + + private String getModuleName(Path path) { + return modulePathCache.entrySet().stream() + .filter(e -> e.getValue().equals(path)) + .map(Map.Entry::getKey) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Unknown virtual path: " + path)); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java new file mode 100644 index 0000000..f9a50fe --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/ByteArrayChannel.java @@ -0,0 +1,60 @@ +package org.densy.scriptify.js.graalvm.script.module.fs.util; + +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +public class ByteArrayChannel implements SeekableByteChannel { + + private final byte[] data; + private int position = 0; + private boolean open = true; + + public ByteArrayChannel(byte[] data) { + this.data = data; + } + + @Override + public int read(ByteBuffer dst) { + if (position >= data.length) return -1; + int toRead = Math.min(dst.remaining(), data.length - position); + dst.put(data, position, toRead); + position += toRead; + return toRead; + } + + @Override + public SeekableByteChannel position(long pos) { + position = (int) pos; + return this; + } + + @Override + public long position() { + return position; + } + + @Override + public long size() { + return data.length; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() { + open = false; + } + + @Override + public int write(ByteBuffer src) { + throw new UnsupportedOperationException(); + } + + @Override + public SeekableByteChannel truncate(long size) { + throw new UnsupportedOperationException(); + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java index 81c2e48..1638a46 100644 --- a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java @@ -7,6 +7,7 @@ import org.densy.scriptify.api.script.constant.ScriptConstantManager; import org.densy.scriptify.api.script.function.ScriptFunctionManager; import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.security.ScriptSecurityManager; import org.densy.scriptify.core.script.constant.StandardConstantManager; import org.densy.scriptify.core.script.function.StandardFunctionManager; @@ -30,6 +31,12 @@ public ScriptSecurityManager getSecurityManager() { return securityManager; } + @Override + public ScriptModuleManager getModuleManager() { + // TODO: implement + return null; + } + @Override public ScriptFunctionManager getFunctionManager() { return functionManager; From a61b74f24378ab296ea3efc1ccc581027c04daf1 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Wed, 22 Apr 2026 18:53:03 +0200 Subject: [PATCH 2/9] fix: rollback java version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4f88e4d..21db491 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } From 9beb116848fd6f4b469f28865f41091e38c8aa3e Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Thu, 23 Apr 2026 15:49:04 +0200 Subject: [PATCH 3/9] feat: module exports copying + rename GraalModuleSourceGenerator to JsModuleSourceGenerator --- .../exception/ScriptModuleCopyException.java | 35 +++++++++++++++++++ .../api/script/module/ScriptModule.java | 27 ++++++++++---- .../script/module/ScriptModuleManager.java | 11 +++++- .../script/module/AbstractScriptModule.java | 25 +++++++++++-- .../script/module/ScriptGlobalModule.java | 3 +- script-js-graalvm/build.gradle.kts | 1 - .../script/module/GraalModuleManager.java | 4 +-- .../module/fs/VirtualModuleFileSystem.java | 6 ++-- .../util/JsModuleSourceGenerator.java} | 16 ++++----- 9 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java rename script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/{GraalModuleSourceGenerator.java => fs/util/JsModuleSourceGenerator.java} (75%) diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java new file mode 100644 index 0000000..f414ed7 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleCopyException.java @@ -0,0 +1,35 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors while script copy process. + */ +public class ScriptModuleCopyException extends RuntimeException { + + /** + * Creates a new ScriptModuleCopyException with the specified message. + * + * @param message the detail message + */ + public ScriptModuleCopyException(String message) { + super(message); + } + + /** + * Creates a new ScriptModuleCopyException with the specified message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public ScriptModuleCopyException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new ScriptModuleCopyException with the specified cause. + * + * @param cause the cause of the exception + */ + public ScriptModuleCopyException(Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java index 34e83c5..7818789 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -7,20 +7,33 @@ import java.util.Collection; /** - * A module that exports elements to the script environment. - * Named modules (non-null name) are accessible via ES import. - * The global module (null name) injects exports directly into global scope. + * A module that exports elements to the script environment. Modules can be accessed via ES import (in JavaScript). */ public interface ScriptModule { /** * Module name for ES import, e.g. "@densy/mymodule". */ - @NotNull - String getName(); + @NotNull String getName(); - @UnmodifiableView - Collection getExports(); + /** + * Gets collection of all exports in module. + * + * @return Collection with ScriptExport + */ + @UnmodifiableView Collection getExports(); + /** + * Adds export to the module. + * + * @param export export to add + */ void export(ScriptExport export); + + /** + * Copies all exports from the target module that are not present in the current module. + * + * @param module target module + */ + void copy(ScriptModule module); } \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java index 6e9eb5e..131d82f 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -1,5 +1,10 @@ package org.densy.scriptify.api.script.module; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Map; + /** * Manages all modules available to the script. * The global module is always present and created automatically. @@ -11,7 +16,11 @@ public interface ScriptModuleManager { */ ScriptModule getGlobalModule(); - ScriptModule getModule(String name); + @UnmodifiableView Map getModules(); + + default @Nullable ScriptModule getModule(String name) { + return this.getModules().get(name); + } void addModule(ScriptModule module); diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java index 104c857..970a3e1 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java @@ -1,7 +1,9 @@ package org.densy.scriptify.core.script.module; -import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.exception.ScriptModuleCopyException; import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.UnmodifiableView; import java.util.Collection; import java.util.Collections; @@ -20,7 +22,26 @@ public void export(ScriptExport export) { } @Override - public Collection getExports() { + public void copy(ScriptModule module) { + // We need to verify that the module from which we want to copy exports + // does not contain any exports with the same name as in the current + // module but with a different hash. + boolean conflicts = module.getExports().stream().anyMatch(e -> + exports.values().stream().anyMatch(existing -> + existing.getName().equals(e.getName()) && + existing.hashCode() != e.hashCode() + ) + ); + + if (conflicts) { + throw new ScriptModuleCopyException("The copy operation cannot be performed: both modules contain different exports with the same name"); + } + + module.getExports().forEach(this::export); + } + + @Override + public @UnmodifiableView Collection getExports() { return Collections.unmodifiableCollection(exports.values()); } } \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java index 4b56373..14546be 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java @@ -2,7 +2,8 @@ import org.jetbrains.annotations.NotNull; -public class ScriptGlobalModule extends AbstractScriptModule { +public final class ScriptGlobalModule extends AbstractScriptModule { + @Override public @NotNull String getName() { return "global"; diff --git a/script-js-graalvm/build.gradle.kts b/script-js-graalvm/build.gradle.kts index ede57fd..6c13dec 100644 --- a/script-js-graalvm/build.gradle.kts +++ b/script-js-graalvm/build.gradle.kts @@ -8,7 +8,6 @@ repositories { dependencies { api(project(":core")) - api(project(":common")) api("org.graalvm.polyglot:polyglot:24.1.1") api("org.graalvm.polyglot:js:24.1.1") } \ No newline at end of file diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java index 07ec3a6..990104a 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -32,8 +32,8 @@ public ScriptGlobalModule getGlobalModule() { } @Override - public ScriptModule getModule(String name) { - return modules.get(name); + public Map getModules() { + return modules; } @Override diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java index 13736ec..dd432b9 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -2,8 +2,8 @@ import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.ScriptModuleManager; -import org.densy.scriptify.js.graalvm.script.module.GraalModuleSourceGenerator; import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; +import org.densy.scriptify.js.graalvm.script.module.fs.util.JsModuleSourceGenerator; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.io.FileSystem; @@ -81,7 +81,7 @@ public SeekableByteChannel newByteChannel(Path path, Set o if (module == null) { throw new IOException("Scriptify module not found: " + moduleName); } - byte[] source = GraalModuleSourceGenerator + byte[] source = JsModuleSourceGenerator .generateModuleSource(contextSupplier.get(), module) .getBytes(StandardCharsets.UTF_8); return new ByteArrayChannel(source); @@ -120,7 +120,7 @@ private Path resolveVirtualPath(String moduleName) { return modulePathCache.computeIfAbsent(moduleName, name -> Paths.get( System.getProperty("java.io.tmpdir"), "scriptify", - GraalModuleSourceGenerator.encodeModuleName(name) + ".mjs" + JsModuleSourceGenerator.encodeModuleName(name) + ".mjs" )); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java similarity index 75% rename from script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java rename to script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java index 9bdddfe..cc078a1 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleSourceGenerator.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -1,4 +1,4 @@ -package org.densy.scriptify.js.graalvm.script.module; +package org.densy.scriptify.js.graalvm.script.module.fs.util; import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.export.ScriptExport; @@ -8,13 +8,13 @@ import java.util.ArrayList; import java.util.List; -public final class GraalModuleSourceGenerator { +public final class JsModuleSourceGenerator { public static final String BRIDGE_PREFIX = "__scriptify_bridge_"; public static String generateModuleSource(Context context, ScriptModule module) { - StringBuilder sb = new StringBuilder(); - sb.append("// @generated module: ").append(module.getName()).append("\n"); + StringBuilder builder = new StringBuilder(); + builder.append("// @generated module: ").append(module.getName()).append("\n"); List names = new ArrayList<>(); @@ -25,19 +25,19 @@ public static String generateModuleSource(Context context, ScriptModule module) if (export instanceof ScriptValueExport valueExport) { if (valueExport.isClass()) { Class valueClass = (Class) valueExport.getValue(); - sb.append("const ").append(name) + builder.append("const ").append(name) .append(" = Java.type('").append(valueClass.getName()).append("');\n"); } else { String bridge = BRIDGE_PREFIX + name; context.getBindings("js").putMember(bridge, context.asValue(valueExport.getValue())); - sb.append("const ").append(name) + builder.append("const ").append(name) .append(" = globalThis.").append(bridge).append(";\n"); } } } - sb.append("\nexport { ").append(String.join(", ", names)).append(" };\n"); - return sb.toString(); + builder.append("\nexport { ").append(String.join(", ", names)).append(" };\n"); + return builder.toString(); } public static String encodeModuleName(String name) { From e51f2f8ba48551b8b32d5bccf77a34b4af24851a Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Thu, 23 Apr 2026 18:35:34 +0200 Subject: [PATCH 4/9] feat: module export resolvers --- .../ScriptModuleWrongContextException.java | 17 +++++++++ .../script/module/ScriptModuleManager.java | 6 +++ .../resolver/ScriptModuleExportResolver.java | 7 ++++ .../ScriptModuleExportResolverFactory.java | 7 ++++ .../module/export/ScriptConstantExport.java | 1 - .../ScriptFunctionDefinitionExport.java | 20 ++++++++++ .../module/export/ScriptFunctionExport.java | 11 +++--- .../resolver/MappedModuleExportResolver.java | 26 +++++++++++++ .../scriptify/js/graalvm/script/JsScript.java | 17 +++++++-- .../script/module/GraalModuleManager.java | 38 +++++++++++-------- .../resolver/GraalModuleExportResolver.java | 23 +++++++++++ .../GraalModuleExportResolverFactory.java | 24 ++++++++++++ .../module/fs/VirtualModuleFileSystem.java | 11 +++++- .../fs/util/JsModuleSourceGenerator.java | 31 +++++++++------ 14 files changed, 200 insertions(+), 39 deletions(-) create mode 100644 api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java create mode 100644 script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java new file mode 100644 index 0000000..5e3695c --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleWrongContextException.java @@ -0,0 +1,17 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors when script module context mismatch occurs. + */ +public class ScriptModuleWrongContextException extends ScriptException { + + /** + * Creates a new ScriptModuleWrongContextException with the specified message. + * + * @param expected the expected context + * @param actual the given context + */ + public ScriptModuleWrongContextException(Class expected, Class actual) { + super("Expected context of type " + expected.getName() + " but got " + actual.getName()); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java index 131d82f..45cc96c 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -1,5 +1,7 @@ package org.densy.scriptify.api.script.module; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; @@ -11,6 +13,10 @@ */ public interface ScriptModuleManager { + ScriptModuleExportResolverFactory getModuleExportResolver(); + + void setModuleExportResolver(ScriptModuleExportResolverFactory factory); + /** * Exports added here are available globally without import */ diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java new file mode 100644 index 0000000..6ca46b5 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java @@ -0,0 +1,7 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +import org.densy.scriptify.api.script.module.export.ScriptExport; + +public interface ScriptModuleExportResolver { + Object resolve(ScriptExport export); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java new file mode 100644 index 0000000..d11bfce --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java @@ -0,0 +1,7 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; + +public interface ScriptModuleExportResolverFactory { + ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException; +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java index 3ece777..9f0a759 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java @@ -17,5 +17,4 @@ public ScriptConstantExport(ScriptConstant constant) { public String getName() { return constant.getName(); } - } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java new file mode 100644 index 0000000..66c5c14 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java @@ -0,0 +1,20 @@ +package org.densy.scriptify.core.script.module.export; + +import lombok.Getter; +import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.module.export.ScriptExport; + +@Getter +public final class ScriptFunctionDefinitionExport implements ScriptExport { + + private final ScriptFunctionDefinition definition; + + public ScriptFunctionDefinitionExport(ScriptFunctionDefinition definition) { + this.definition = definition; + } + + @Override + public String getName() { + return definition.getFunction().getName(); + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java index 0cf39d4..7caf3ea 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java @@ -1,21 +1,20 @@ package org.densy.scriptify.core.script.module.export; import lombok.Getter; -import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; +import org.densy.scriptify.api.script.function.ScriptFunction; import org.densy.scriptify.api.script.module.export.ScriptExport; @Getter public final class ScriptFunctionExport implements ScriptExport { - private final ScriptFunctionDefinition definition; + private final ScriptFunction function; - public ScriptFunctionExport(ScriptFunctionDefinition definition) { - this.definition = definition; + public ScriptFunctionExport(ScriptFunction function) { + this.function = function; } @Override public String getName() { - return definition.getFunction().getName(); + return function.getName(); } - } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java new file mode 100644 index 0000000..e869fc5 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java @@ -0,0 +1,26 @@ +package org.densy.scriptify.core.script.module.export.resolver; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public abstract class MappedModuleExportResolver implements ScriptModuleExportResolver { + + private final Map, Function> resolvers = new HashMap<>(); + + public void map(Class type, Function resolver) { + resolvers.put(type, export -> resolver.apply(type.cast(export))); + } + + @Override + public Object resolve(ScriptExport export) { + Function resolver = resolvers.get(export.getClass()); + if (resolver == null) { + throw new UnsupportedOperationException("No resolver found for export " + export.getClass()); + } + return resolver.apply(export); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java index 2ce084a..e504db2 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java @@ -1,6 +1,7 @@ package org.densy.scriptify.js.graalvm.script; import org.densy.scriptify.api.exception.ScriptException; +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; import org.densy.scriptify.api.script.CompiledScript; import org.densy.scriptify.api.script.Script; import org.densy.scriptify.api.script.ScriptObject; @@ -9,11 +10,12 @@ import org.densy.scriptify.api.script.function.ScriptFunctionManager; import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.api.script.security.ScriptSecurityManager; import org.densy.scriptify.core.script.constant.StandardConstantManager; import org.densy.scriptify.core.script.function.StandardFunctionManager; import org.densy.scriptify.core.script.module.export.ScriptConstantExport; -import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionDefinitionExport; import org.densy.scriptify.core.script.security.StandardSecurityManager; import org.densy.scriptify.js.graalvm.script.module.GraalModuleManager; import org.densy.scriptify.js.graalvm.script.module.fs.VirtualModuleFileSystem; @@ -27,6 +29,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; public class JsScript implements Script { @@ -77,6 +80,14 @@ public CompiledScript compile(String script) throws ScriptException { // we can access it in the file system AtomicReference contextRef = new AtomicReference<>(); + Supplier resolverSupplier = () -> { + try { + return moduleManager.getModuleExportResolver().create(contextRef.get()); + } catch (ScriptModuleWrongContextException e) { + throw new RuntimeException(e); + } + }; + Context.Builder builder = Context.newBuilder("js") .allowHostAccess(HostAccess.newBuilder(HostAccess.ALL) // Mapping for the ScriptObject class required @@ -89,7 +100,7 @@ public CompiledScript compile(String script) throws ScriptException { ) .build()) .allowIO(IOAccess.newBuilder() - .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get)) + .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get, resolverSupplier)) .build()); // If security mode is enabled, search all exclusions @@ -104,7 +115,7 @@ public CompiledScript compile(String script) throws ScriptException { contextRef.set(context); for (ScriptFunctionDefinition definition : functionManager.getFunctions().values()) { - moduleManager.getGlobalModule().export(new ScriptFunctionExport(definition)); + moduleManager.getGlobalModule().export(new ScriptFunctionDefinitionExport(definition)); } for (ScriptConstant constant : constantManager.getConstants().values()) { moduleManager.getGlobalModule().export(new ScriptConstantExport(constant)); diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java index 990104a..912bea9 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -1,14 +1,18 @@ package org.densy.scriptify.js.graalvm.script.module; +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; import org.densy.scriptify.api.script.Script; import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; import org.densy.scriptify.core.script.module.ScriptGlobalModule; import org.densy.scriptify.core.script.module.export.ScriptConstantExport; import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.densy.scriptify.js.graalvm.script.module.export.resolver.GraalModuleExportResolverFactory; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; @@ -21,9 +25,21 @@ public class GraalModuleManager implements ScriptModuleManager { private final Script script; private final ScriptGlobalModule globalModule = new ScriptGlobalModule(); private final Map modules = new LinkedHashMap<>(); + private ScriptModuleExportResolverFactory moduleExportResolverFactory; public GraalModuleManager(Script script) { this.script = script; + this.setModuleExportResolver(new GraalModuleExportResolverFactory(script)); + } + + @Override + public ScriptModuleExportResolverFactory getModuleExportResolver() { + return moduleExportResolverFactory; + } + + @Override + public void setModuleExportResolver(ScriptModuleExportResolverFactory moduleExportResolverFactory) { + this.moduleExportResolverFactory = Objects.requireNonNull(moduleExportResolverFactory, "moduleExportResolverFactory cannot be null"); } @Override @@ -51,21 +67,13 @@ public void removeModule(String name) { public void applyTo(Context context) { Value bindings = context.getBindings("js"); - for (ScriptExport export : globalModule.getExports()) { - bindings.putMember(export.getName(), resolveValue(context, export)); - } - } - - private Object resolveValue(Context context, ScriptExport export) { - if (export instanceof ScriptValueExport valueExport) { - return context.asValue(valueExport.getValue()); - } - if (export instanceof ScriptFunctionExport functionExport) { - return new JsFunction(script, functionExport.getDefinition()); - } - if (export instanceof ScriptConstantExport constantExport) { - return constantExport.getConstant().getValue(); + try { + ScriptModuleExportResolver resolver = moduleExportResolverFactory.create(context); + for (ScriptExport export : globalModule.getExports()) { + bindings.putMember(export.getName(), resolver.resolve(export)); + } + } catch (ScriptModuleWrongContextException e) { + throw new RuntimeException(e); } - throw new UnsupportedOperationException("Unsupported export type: " + export.getClass().getName() ); } } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java new file mode 100644 index 0000000..6948d46 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java @@ -0,0 +1,23 @@ +package org.densy.scriptify.js.graalvm.script.module.export.resolver; + +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.core.script.module.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionDefinitionExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.core.script.module.export.resolver.MappedModuleExportResolver; +import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.graalvm.polyglot.Context; + +public final class GraalModuleExportResolver extends MappedModuleExportResolver { + + public GraalModuleExportResolver(Script script, Context context) { + this.map(ScriptValueExport.class, export -> context.asValue(export.getValue())); + this.map(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(export.getFunction()) + )); + this.map(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); + this.map(ScriptConstantExport.class, export -> export.getConstant().getValue()); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java new file mode 100644 index 0000000..ea782df --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolverFactory.java @@ -0,0 +1,24 @@ +package org.densy.scriptify.js.graalvm.script.module.export.resolver; + +import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; +import org.densy.scriptify.api.script.Script; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +import org.graalvm.polyglot.Context; + +public final class GraalModuleExportResolverFactory implements ScriptModuleExportResolverFactory { + + private final Script script; + + public GraalModuleExportResolverFactory(Script script) { + this.script = script; + } + + @Override + public ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException { + if (!(context instanceof Context graalContext)) { + throw new ScriptModuleWrongContextException(Context.class, context.getClass()); + } + return new GraalModuleExportResolver(script, graalContext); + } +} diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java index dd432b9..822854e 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -2,6 +2,7 @@ import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; import org.densy.scriptify.js.graalvm.script.module.fs.util.JsModuleSourceGenerator; import org.graalvm.polyglot.Context; @@ -26,12 +27,18 @@ public class VirtualModuleFileSystem implements FileSystem { private final FileSystem real = FileSystem.newDefaultFileSystem(); private final ScriptModuleManager moduleManager; private final Supplier contextSupplier; + private final Supplier resolverSupplier; private final Map modulePathCache = new HashMap<>(); - public VirtualModuleFileSystem(ScriptModuleManager moduleManager, Supplier contextSupplier) { + public VirtualModuleFileSystem( + ScriptModuleManager moduleManager, + Supplier contextSupplier, + Supplier resolverSupplier + ) { this.moduleManager = moduleManager; this.contextSupplier = contextSupplier; + this.resolverSupplier = resolverSupplier; } @Override @@ -82,7 +89,7 @@ public SeekableByteChannel newByteChannel(Path path, Set o throw new IOException("Scriptify module not found: " + moduleName); } byte[] source = JsModuleSourceGenerator - .generateModuleSource(contextSupplier.get(), module) + .generateModuleSource(contextSupplier.get(), module, resolverSupplier.get()) .getBytes(StandardCharsets.UTF_8); return new ByteArrayChannel(source); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java index cc078a1..c273451 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -3,6 +3,7 @@ import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.graalvm.polyglot.Context; import java.util.ArrayList; @@ -12,7 +13,11 @@ public final class JsModuleSourceGenerator { public static final String BRIDGE_PREFIX = "__scriptify_bridge_"; - public static String generateModuleSource(Context context, ScriptModule module) { + public static String generateModuleSource( + Context context, + ScriptModule module, + ScriptModuleExportResolver resolver + ) { StringBuilder builder = new StringBuilder(); builder.append("// @generated module: ").append(module.getName()).append("\n"); @@ -22,17 +27,13 @@ public static String generateModuleSource(Context context, ScriptModule module) String name = export.getName(); names.add(name); - if (export instanceof ScriptValueExport valueExport) { - if (valueExport.isClass()) { - Class valueClass = (Class) valueExport.getValue(); - builder.append("const ").append(name) - .append(" = Java.type('").append(valueClass.getName()).append("');\n"); - } else { - String bridge = BRIDGE_PREFIX + name; - context.getBindings("js").putMember(bridge, context.asValue(valueExport.getValue())); - builder.append("const ").append(name) - .append(" = globalThis.").append(bridge).append(";\n"); - } + if (export instanceof ScriptValueExport valueExport && valueExport.isClass()) { + Class valueClass = (Class) valueExport.getValue(); + builder.append("const ").append(name) + .append(" = Java.type('").append(valueClass.getName()).append("');\n"); + } else { + Object resolved = resolver.resolve(export); + putBridge(context, builder, name, resolved); } } @@ -40,6 +41,12 @@ public static String generateModuleSource(Context context, ScriptModule module) return builder.toString(); } + private static void putBridge(Context context, StringBuilder sb, String name, Object value) { + String bridge = BRIDGE_PREFIX + name; + context.getBindings("js").putMember(bridge, value); + sb.append("const ").append(name).append(" = globalThis.").append(bridge).append(";\n"); + } + public static String encodeModuleName(String name) { return name.replace("@", "_at_").replace("/", "__"); } From c5cf7b6e4e6a2d2a15d55572e065f46031b910a6 Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Fri, 24 Apr 2026 16:40:54 +0200 Subject: [PATCH 5/9] feat: internal and external modules + rework security path accessor & make virtual file system secure --- .../exception/ScriptModuleLoadException.java | 35 ++++++ .../script/module/ScriptExternalModule.java | 19 +++ .../module/ScriptFileExternalModule.java | 19 +++ .../script/module/ScriptInternalModule.java | 34 ++++++ .../api/script/module/ScriptModule.java | 29 +---- .../script/module/ScriptModuleManager.java | 37 +++++- .../module/ScriptStreamExternalModule.java | 8 ++ ...java => AbstractScriptInternalModule.java} | 6 +- ...e.java => ScriptInternalGlobalModule.java} | 2 +- .../SimpleScriptFileExternalModule.java | 57 +++++++++ ...e.java => SimpleScriptInternalModule.java} | 4 +- .../SimpleScriptStreamExternalModule.java | 47 ++++++++ .../security/SecurityPathAccessorImpl.java | 50 ++------ .../scriptify/js/graalvm/script/JsScript.java | 7 +- .../script/module/GraalModuleManager.java | 12 +- .../module/fs/VirtualModuleFileSystem.java | 114 +++++++++++++++--- .../fs/util/JsModuleSourceGenerator.java | 4 +- 17 files changed, 378 insertions(+), 106 deletions(-) create mode 100644 api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java rename core/src/main/java/org/densy/scriptify/core/script/module/{AbstractScriptModule.java => AbstractScriptInternalModule.java} (88%) rename core/src/main/java/org/densy/scriptify/core/script/module/{ScriptGlobalModule.java => ScriptInternalGlobalModule.java} (67%) create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java rename core/src/main/java/org/densy/scriptify/core/script/module/{SimpleScriptModule.java => SimpleScriptInternalModule.java} (70%) create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java new file mode 100644 index 0000000..3897c5e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java @@ -0,0 +1,35 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors while script load process. + */ +public class ScriptModuleLoadException extends ScriptException { + + /** + * Creates a new ScriptModuleLoadException with the specified message. + * + * @param message the detail message + */ + public ScriptModuleLoadException(String message) { + super(message); + } + + /** + * Creates a new ScriptModuleLoadException with the specified message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public ScriptModuleLoadException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new ScriptModuleLoadException with the specified cause. + * + * @param cause the cause of the exception + */ + public ScriptModuleLoadException(Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java new file mode 100644 index 0000000..f81428d --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java @@ -0,0 +1,19 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; + +/** + * A script module loaded from an external source. + */ +public interface ScriptExternalModule extends ScriptModule { + + /** + * Loads the module source as bytes. + */ + byte[] load() throws ScriptModuleLoadException; + + /** + * Gets external module source name. + */ + String getSourceName(); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java new file mode 100644 index 0000000..be5f46b --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java @@ -0,0 +1,19 @@ +package org.densy.scriptify.api.script.module; + +import java.nio.file.Path; + +/** + * An external module backed by a file or directory on disk. + *

+ * If path points to a directory, entry point is resolved automatically. + */ +public interface ScriptFileExternalModule extends ScriptExternalModule { + Path getPath(); + + /** + * Gets entry point filename when path is a directory. + * */ + default String getEntryPoint() { + return "index.mjs"; + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java new file mode 100644 index 0000000..6abd6be --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java @@ -0,0 +1,34 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; + +/** + * A module that exports elements to the script environment. Modules can be accessed via ES import (in JavaScript). + */ +public interface ScriptInternalModule extends ScriptModule { + + /** + * Gets collection of all exports in module. + * + * @return Collection with ScriptExport + */ + @UnmodifiableView Collection getExports(); + + /** + * Adds export to the module. + * + * @param export export to add + */ + void export(ScriptExport export); + + /** + * Copies all exports from the target module that are not present in the current module. + * + * @param module target module + */ + void copy(ScriptInternalModule module); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java index 7818789..9d0f352 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -1,39 +1,14 @@ package org.densy.scriptify.api.script.module; -import org.densy.scriptify.api.script.module.export.ScriptExport; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.Collection; /** - * A module that exports elements to the script environment. Modules can be accessed via ES import (in JavaScript). + * A base interface for all module types. */ public interface ScriptModule { /** - * Module name for ES import, e.g. "@densy/mymodule". + * Gets module name. For ES import "@densy/mymodule". */ @NotNull String getName(); - - /** - * Gets collection of all exports in module. - * - * @return Collection with ScriptExport - */ - @UnmodifiableView Collection getExports(); - - /** - * Adds export to the module. - * - * @param export export to add - */ - void export(ScriptExport export); - - /** - * Copies all exports from the target module that are not present in the current module. - * - * @param module target module - */ - void copy(ScriptModule module); } \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java index 45cc96c..29feb9e 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -1,6 +1,5 @@ package org.densy.scriptify.api.script.module; -import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; @@ -9,26 +8,56 @@ /** * Manages all modules available to the script. - * The global module is always present and created automatically. */ public interface ScriptModuleManager { + /** + * Gets a factory for module export values. + * + * @return ScriptModuleExportResolverFactory + */ ScriptModuleExportResolverFactory getModuleExportResolver(); + /** + * Sets a custom factory for module export values. + * + * @param factory ScriptModuleExportResolverFactory to set + */ void setModuleExportResolver(ScriptModuleExportResolverFactory factory); /** - * Exports added here are available globally without import + * Gets the internal global module. Exports added here are available globally without import. */ - ScriptModule getGlobalModule(); + ScriptInternalModule getGlobalModule(); + /** + * Gets all registered modules. + * + * @return Map + */ @UnmodifiableView Map getModules(); + /** + * Gets registered module by name. + * + * @param name the module name + * @return found module or null + */ default @Nullable ScriptModule getModule(String name) { return this.getModules().get(name); } + /** + * Adds module to the script. + * + * @param module module to add + */ void addModule(ScriptModule module); + /** + * Removes registered module from the script. + * + * @param name the name of module to remove + */ void removeModule(String name); } \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java new file mode 100644 index 0000000..c2927cd --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java @@ -0,0 +1,8 @@ +package org.densy.scriptify.api.script.module; + +/** + * An external module loaded from an arbitrary byte source. + */ +public interface ScriptStreamExternalModule extends ScriptExternalModule { + // do nothing here +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java similarity index 88% rename from core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java index 970a3e1..3ce0046 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java @@ -1,7 +1,7 @@ package org.densy.scriptify.core.script.module; import org.densy.scriptify.api.exception.ScriptModuleCopyException; -import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptInternalModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.jetbrains.annotations.UnmodifiableView; @@ -10,7 +10,7 @@ import java.util.LinkedHashMap; import java.util.Map; -public abstract class AbstractScriptModule implements ScriptModule { +public abstract class AbstractScriptInternalModule implements ScriptInternalModule { private final Map exports = new LinkedHashMap<>(); @Override @@ -22,7 +22,7 @@ public void export(ScriptExport export) { } @Override - public void copy(ScriptModule module) { + public void copy(ScriptInternalModule module) { // We need to verify that the module from which we want to copy exports // does not contain any exports with the same name as in the current // module but with a different hash. diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java similarity index 67% rename from core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java index 14546be..a579664 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java @@ -2,7 +2,7 @@ import org.jetbrains.annotations.NotNull; -public final class ScriptGlobalModule extends AbstractScriptModule { +public final class ScriptInternalGlobalModule extends AbstractScriptInternalModule { @Override public @NotNull String getName() { diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java new file mode 100644 index 0000000..08e46bc --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java @@ -0,0 +1,57 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.ScriptFileExternalModule; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +public class SimpleScriptFileExternalModule implements ScriptFileExternalModule { + + private final String name; + private final Path path; + private final String entryPoint; + + public SimpleScriptFileExternalModule(String name, Path path) { + this(name, path, "index.mjs"); + } + + public SimpleScriptFileExternalModule(String name, Path path, String entryPoint) { + this.name = Objects.requireNonNull(name); + this.path = Objects.requireNonNull(path); + this.entryPoint = Objects.requireNonNull(entryPoint); + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public String getEntryPoint() { + return entryPoint; + } + + @Override + public String getSourceName() { + return path.getFileName().toString(); + } + + @Override + public byte[] load() throws ScriptModuleLoadException { + Path target = Files.isDirectory(path) ? path.resolve(entryPoint) : path; + try { + return Files.readAllBytes(target); + } catch (IOException e) { + throw new ScriptModuleLoadException("Failed to load external module " + name + "' from: " + target, e); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java similarity index 70% rename from core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java index b8bd872..52c3fe6 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java @@ -4,10 +4,10 @@ import java.util.Objects; -public class SimpleScriptModule extends AbstractScriptModule { +public class SimpleScriptInternalModule extends AbstractScriptInternalModule { private final String name; - public SimpleScriptModule(String name) { + public SimpleScriptInternalModule(String name) { this.name = Objects.requireNonNull(name, "Module name cannot be null"); } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java new file mode 100644 index 0000000..4f409fb --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java @@ -0,0 +1,47 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.ScriptStreamExternalModule; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Stream-based external module. Source supplier is called on every load() - allows dynamic reloading. + */ +public class SimpleScriptStreamExternalModule implements ScriptStreamExternalModule { + + private final String name; + private final String sourceName; + private final Supplier sourceSupplier; + + public SimpleScriptStreamExternalModule(String name, String sourceName, byte[] source) { + this(name, sourceName, () -> source); + } + + public SimpleScriptStreamExternalModule(String name, String sourceName, Supplier sourceSupplier) { + this.name = Objects.requireNonNull(name); + this.sourceName = Objects.requireNonNull(sourceName); + this.sourceSupplier = Objects.requireNonNull(sourceSupplier); + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public String getSourceName() { + return sourceName; + } + + @Override + public byte[] load() throws ScriptModuleLoadException { + try { + return sourceSupplier.get(); + } catch (Exception e) { + throw new ScriptModuleLoadException("Failed to load stream module " + name, e); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java b/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java index 2355ded..91094cd 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java +++ b/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java @@ -58,37 +58,24 @@ public void setBasePath(Path basePath) { /** * Returns a path that is safe to access according to security rules. - * If the path is accessible via exclusions, returns the normalized path. - * If the path is not accessible, creates a safe path within basePath by cleaning the path from invalid characters. * * @param path The path string to be checked and possibly modified - * @return A Path object representing the accessible path or a path within base directory + * @return a Path object representing the accessible path or a path within base directory + * @throws SecurityException if path cannot be accessed */ @Override public Path getAccessiblePath(String path) { if (this.isAccessible(path)) { - // Path is in exclusions - return it normalized - return Paths.get(path).normalize().toAbsolutePath(); + return basePath.resolve(path).normalize().toAbsolutePath(); } - - // Path is not accessible - create safe path within basePath - // We need to manually combine paths because resolve() ignores basePath for absolute paths - Path safePath = Paths.get(basePath.toString(), path.replace(":", "")).normalize(); - - // CRITICAL: Ensure the result stays within basePath boundaries - if (!safePath.startsWith(basePath)) { - // If path tries to escape basePath (e.g., "../"), return basePath itself - return basePath; - } - - return safePath; + throw new SecurityException("Access denied by security policy: " + path); } /** * Checks if the given path is accessible based on the current security settings. * - * @param path The path to check for access permission - * @return true if the path is accessible, false otherwise + * @param path the path to check for access permission + * @return true if the path is accessible, otherwise false */ @Override public boolean isAccessible(String path) { @@ -96,35 +83,18 @@ public boolean isAccessible(String path) { return true; } - // Normalize the path to resolve .. and . components to prevent path traversal Path normalizedPath; try { - normalizedPath = Paths.get(path).normalize().toAbsolutePath(); + normalizedPath = basePath.resolve(path).normalize().toAbsolutePath(); } catch (Exception e) { return false; } - // Check both original and normalized path against exclusions for compatibility - String normalizedPathString = normalizedPath.toString(); + String normalizedStr = normalizedPath.toString().replace('\\', '/'); - // Search all exclusions and check that the path is excluded for (SecurityExclude exclude : securityManager.getExcludes()) { - if (exclude instanceof PathSecurityExclude) { - // Check original path first - if (exclude.isExcluded(path)) { - return true; - } - - // Check normalized path - if (exclude.isExcluded(normalizedPathString)) { - return true; - } - - // Check with forward slashes for cross-platform compatibility - String pathWithForwardSlashes = normalizedPathString.replace('\\', '/'); - if (exclude.isExcluded(pathWithForwardSlashes)) { - return true; - } + if (exclude.isExcluded(path) || exclude.isExcluded(normalizedStr)) { + return true; } } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java index e504db2..f9c0d99 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java @@ -100,7 +100,12 @@ public CompiledScript compile(String script) throws ScriptException { ) .build()) .allowIO(IOAccess.newBuilder() - .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get, resolverSupplier)) + .fileSystem(new VirtualModuleFileSystem( + moduleManager, + contextRef::get, + resolverSupplier, + securityManager.getPathAccessor() + )) .build()); // If security mode is enabled, search all exclusions diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java index 912bea9..f990ea8 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -5,13 +5,9 @@ import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.module.export.ScriptExport; -import org.densy.scriptify.api.script.module.export.ScriptValueExport; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; -import org.densy.scriptify.core.script.module.ScriptGlobalModule; -import org.densy.scriptify.core.script.module.export.ScriptConstantExport; -import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; -import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.densy.scriptify.core.script.module.ScriptInternalGlobalModule; import org.densy.scriptify.js.graalvm.script.module.export.resolver.GraalModuleExportResolverFactory; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; @@ -22,13 +18,11 @@ public class GraalModuleManager implements ScriptModuleManager { - private final Script script; - private final ScriptGlobalModule globalModule = new ScriptGlobalModule(); + private final ScriptInternalGlobalModule globalModule = new ScriptInternalGlobalModule(); private final Map modules = new LinkedHashMap<>(); private ScriptModuleExportResolverFactory moduleExportResolverFactory; public GraalModuleManager(Script script) { - this.script = script; this.setModuleExportResolver(new GraalModuleExportResolverFactory(script)); } @@ -43,7 +37,7 @@ public void setModuleExportResolver(ScriptModuleExportResolverFactory moduleExpo } @Override - public ScriptGlobalModule getGlobalModule() { + public ScriptInternalGlobalModule getGlobalModule() { return globalModule; } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java index 822854e..f12fd29 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -1,8 +1,9 @@ package org.densy.scriptify.js.graalvm.script.module.fs; -import org.densy.scriptify.api.script.module.ScriptModule; -import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.*; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.security.SecurityPathAccessor; import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; import org.densy.scriptify.js.graalvm.script.module.fs.util.JsModuleSourceGenerator; import org.graalvm.polyglot.Context; @@ -28,33 +29,50 @@ public class VirtualModuleFileSystem implements FileSystem { private final ScriptModuleManager moduleManager; private final Supplier contextSupplier; private final Supplier resolverSupplier; + private final SecurityPathAccessor securityAccessor; private final Map modulePathCache = new HashMap<>(); public VirtualModuleFileSystem( ScriptModuleManager moduleManager, Supplier contextSupplier, - Supplier resolverSupplier + Supplier resolverSupplier, + SecurityPathAccessor securityAccessor ) { this.moduleManager = moduleManager; this.contextSupplier = contextSupplier; this.resolverSupplier = resolverSupplier; + this.securityAccessor = securityAccessor; } @Override public Path parsePath(String path) { - if (moduleManager.getModule(path) != null) { - return this.resolveVirtualPath(path); + // internal java-module + if (moduleManager.getModule(path) instanceof ScriptInternalModule) { + return resolveVirtualPath(path); } - return real.parsePath(path); + + // external module + if (moduleManager.getModule(path) instanceof ScriptExternalModule external) { + return resolveExternalPath(path, external); + } + + // real file — resolve via security accessor (relative to basePath if needed) + return securityAccessor.getAccessiblePath(path); } @Override public Path parsePath(URI uri) { if (SCHEME.equals(uri.getScheme())) { - return this.resolveVirtualPath(uri.getHost()); + return resolveVirtualPath(uri.getHost()); + } + + Path path = real.parsePath(uri); + if (!securityAccessor.isAccessible(path.toString())) { + throw new IllegalArgumentException("Access denied by security policy: " + uri); } - return real.parsePath(uri); + + return path; } @Override @@ -62,6 +80,12 @@ public void checkAccess(Path path, Set modes, LinkOption.. if (this.isVirtual(path)) { return; } + + // check the access to real path via security path accessor + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + real.checkAccess(path, modes, linkOptions); } @@ -77,6 +101,12 @@ public Map readAttributes(Path path, String attributes, LinkOpti attrs.put("lastAccessTime", FileTime.fromMillis(0)); return attrs; } + + // check the access to real path via security path accessor + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + return real.readAttributes(path, attributes, options); } @@ -84,42 +114,79 @@ public Map readAttributes(Path path, String attributes, LinkOpti public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { if (this.isVirtual(path)) { String moduleName = getModuleName(path); - ScriptModule module = moduleManager.getModule(moduleName); - if (module == null) { - throw new IOException("Scriptify module not found: " + moduleName); + + if (moduleManager.getModule(moduleName) instanceof ScriptInternalModule internal) { + byte[] source = JsModuleSourceGenerator + .generateModuleSource( + contextSupplier.get(), + internal, + resolverSupplier.get() + ) + .getBytes(StandardCharsets.UTF_8); + return new ByteArrayChannel(source); + } + + if (moduleManager.getModule(moduleName) instanceof ScriptExternalModule external) { + try { + return new ByteArrayChannel(external.load()); + } catch (ScriptModuleLoadException e) { + throw new IOException("Failed to load external module: " + moduleName, e); + } } - byte[] source = JsModuleSourceGenerator - .generateModuleSource(contextSupplier.get(), module, resolverSupplier.get()) - .getBytes(StandardCharsets.UTF_8); - return new ByteArrayChannel(source); + + throw new IOException("Script module not found: " + moduleName); } return real.newByteChannel(path, options, attrs); } @Override public Path toAbsolutePath(Path path) { - if (isVirtual(path)) return path; + if (isVirtual(path)) { + return path; + } + + // resolve the path relative to basePath + if (!path.isAbsolute()) { + return securityAccessor.getBasePath().resolve(path).normalize(); + } + return real.toAbsolutePath(path); } @Override public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { - if (isVirtual(path)) return path; + if (isVirtual(path)) { + return path; + } + + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + return real.toRealPath(path, linkOptions); } @Override public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + if (!securityAccessor.isAccessible(dir.toString())) { + throw new AccessDeniedException(dir.toString(), null, "Access denied by security policy"); + } return real.newDirectoryStream(dir, filter); } @Override public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + if (!securityAccessor.isAccessible(dir.toString())) { + throw new AccessDeniedException(dir.toString(), null, "Access denied by security policy"); + } real.createDirectory(dir, attrs); } @Override public void delete(Path path) throws IOException { + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } real.delete(path); } @@ -131,6 +198,19 @@ private Path resolveVirtualPath(String moduleName) { )); } + private Path resolveExternalPath(String moduleName, ScriptExternalModule external) { + if (external instanceof ScriptFileExternalModule fileModule) { + Path target = Files.isDirectory(fileModule.getPath()) + ? fileModule.getPath().resolve(fileModule.getEntryPoint()) + : fileModule.getPath(); + if (!securityAccessor.isAccessible(target.toString())) { + throw new IllegalArgumentException("Access denied by security policy: " + target); + } + return target; + } + return resolveVirtualPath(moduleName); + } + private boolean isVirtual(Path path) { return modulePathCache.containsValue(path); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java index c273451..bb557e4 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -1,6 +1,6 @@ package org.densy.scriptify.js.graalvm.script.module.fs.util; -import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptInternalModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.densy.scriptify.api.script.module.export.ScriptValueExport; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; @@ -15,7 +15,7 @@ public final class JsModuleSourceGenerator { public static String generateModuleSource( Context context, - ScriptModule module, + ScriptInternalModule module, ScriptModuleExportResolver resolver ) { StringBuilder builder = new StringBuilder(); From 951ef3d910bf8c94d511f287f3ba503f0b36345f Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Fri, 24 Apr 2026 16:40:54 +0200 Subject: [PATCH 6/9] feat: internal and external modules + rework security path accessor & make virtual file system secure --- .../exception/ScriptModuleLoadException.java | 35 ++++++ .../script/module/ScriptExternalModule.java | 19 +++ .../module/ScriptFileExternalModule.java | 19 +++ .../script/module/ScriptInternalModule.java | 34 ++++++ .../api/script/module/ScriptModule.java | 29 +---- .../script/module/ScriptModuleManager.java | 37 +++++- .../module/ScriptStreamExternalModule.java | 8 ++ ...java => AbstractScriptInternalModule.java} | 6 +- ...e.java => ScriptInternalGlobalModule.java} | 2 +- .../SimpleScriptFileExternalModule.java | 57 +++++++++ ...e.java => SimpleScriptInternalModule.java} | 4 +- .../SimpleScriptStreamExternalModule.java | 47 ++++++++ .../security/SecurityPathAccessorImpl.java | 50 ++------ .../scriptify/js/graalvm/script/JsScript.java | 7 +- .../script/module/GraalModuleManager.java | 12 +- .../module/fs/VirtualModuleFileSystem.java | 114 +++++++++++++++--- .../fs/util/JsModuleSourceGenerator.java | 4 +- 17 files changed, 378 insertions(+), 106 deletions(-) create mode 100644 api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java create mode 100644 api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java rename core/src/main/java/org/densy/scriptify/core/script/module/{AbstractScriptModule.java => AbstractScriptInternalModule.java} (88%) rename core/src/main/java/org/densy/scriptify/core/script/module/{ScriptGlobalModule.java => ScriptInternalGlobalModule.java} (67%) create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java rename core/src/main/java/org/densy/scriptify/core/script/module/{SimpleScriptModule.java => SimpleScriptInternalModule.java} (70%) create mode 100644 core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java diff --git a/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java new file mode 100644 index 0000000..3897c5e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/exception/ScriptModuleLoadException.java @@ -0,0 +1,35 @@ +package org.densy.scriptify.api.exception; + +/** + * Custom exception for errors while script load process. + */ +public class ScriptModuleLoadException extends ScriptException { + + /** + * Creates a new ScriptModuleLoadException with the specified message. + * + * @param message the detail message + */ + public ScriptModuleLoadException(String message) { + super(message); + } + + /** + * Creates a new ScriptModuleLoadException with the specified message and cause. + * + * @param message the detail message + * @param cause the cause of the exception + */ + public ScriptModuleLoadException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Creates a new ScriptModuleLoadException with the specified cause. + * + * @param cause the cause of the exception + */ + public ScriptModuleLoadException(Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java new file mode 100644 index 0000000..f81428d --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptExternalModule.java @@ -0,0 +1,19 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; + +/** + * A script module loaded from an external source. + */ +public interface ScriptExternalModule extends ScriptModule { + + /** + * Loads the module source as bytes. + */ + byte[] load() throws ScriptModuleLoadException; + + /** + * Gets external module source name. + */ + String getSourceName(); +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java new file mode 100644 index 0000000..be5f46b --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptFileExternalModule.java @@ -0,0 +1,19 @@ +package org.densy.scriptify.api.script.module; + +import java.nio.file.Path; + +/** + * An external module backed by a file or directory on disk. + *

+ * If path points to a directory, entry point is resolved automatically. + */ +public interface ScriptFileExternalModule extends ScriptExternalModule { + Path getPath(); + + /** + * Gets entry point filename when path is a directory. + * */ + default String getEntryPoint() { + return "index.mjs"; + } +} diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java new file mode 100644 index 0000000..6abd6be --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptInternalModule.java @@ -0,0 +1,34 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; + +/** + * A module that exports elements to the script environment. Modules can be accessed via ES import (in JavaScript). + */ +public interface ScriptInternalModule extends ScriptModule { + + /** + * Gets collection of all exports in module. + * + * @return Collection with ScriptExport + */ + @UnmodifiableView Collection getExports(); + + /** + * Adds export to the module. + * + * @param export export to add + */ + void export(ScriptExport export); + + /** + * Copies all exports from the target module that are not present in the current module. + * + * @param module target module + */ + void copy(ScriptInternalModule module); +} \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java index 7818789..9d0f352 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -1,39 +1,14 @@ package org.densy.scriptify.api.script.module; -import org.densy.scriptify.api.script.module.export.ScriptExport; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; - -import java.util.Collection; /** - * A module that exports elements to the script environment. Modules can be accessed via ES import (in JavaScript). + * A base interface for all module types. */ public interface ScriptModule { /** - * Module name for ES import, e.g. "@densy/mymodule". + * Gets module name. For ES import "@densy/mymodule". */ @NotNull String getName(); - - /** - * Gets collection of all exports in module. - * - * @return Collection with ScriptExport - */ - @UnmodifiableView Collection getExports(); - - /** - * Adds export to the module. - * - * @param export export to add - */ - void export(ScriptExport export); - - /** - * Copies all exports from the target module that are not present in the current module. - * - * @param module target module - */ - void copy(ScriptModule module); } \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java index 45cc96c..29feb9e 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -1,6 +1,5 @@ package org.densy.scriptify.api.script.module; -import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnmodifiableView; @@ -9,26 +8,56 @@ /** * Manages all modules available to the script. - * The global module is always present and created automatically. */ public interface ScriptModuleManager { + /** + * Gets a factory for module export values. + * + * @return ScriptModuleExportResolverFactory + */ ScriptModuleExportResolverFactory getModuleExportResolver(); + /** + * Sets a custom factory for module export values. + * + * @param factory ScriptModuleExportResolverFactory to set + */ void setModuleExportResolver(ScriptModuleExportResolverFactory factory); /** - * Exports added here are available globally without import + * Gets the internal global module. Exports added here are available globally without import. */ - ScriptModule getGlobalModule(); + ScriptInternalModule getGlobalModule(); + /** + * Gets all registered modules. + * + * @return Map + */ @UnmodifiableView Map getModules(); + /** + * Gets registered module by name. + * + * @param name the module name + * @return found module or null + */ default @Nullable ScriptModule getModule(String name) { return this.getModules().get(name); } + /** + * Adds module to the script. + * + * @param module module to add + */ void addModule(ScriptModule module); + /** + * Removes registered module from the script. + * + * @param name the name of module to remove + */ void removeModule(String name); } \ No newline at end of file diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java new file mode 100644 index 0000000..c2927cd --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptStreamExternalModule.java @@ -0,0 +1,8 @@ +package org.densy.scriptify.api.script.module; + +/** + * An external module loaded from an arbitrary byte source. + */ +public interface ScriptStreamExternalModule extends ScriptExternalModule { + // do nothing here +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java similarity index 88% rename from core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java index 970a3e1..3ce0046 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java @@ -1,7 +1,7 @@ package org.densy.scriptify.core.script.module; import org.densy.scriptify.api.exception.ScriptModuleCopyException; -import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptInternalModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.jetbrains.annotations.UnmodifiableView; @@ -10,7 +10,7 @@ import java.util.LinkedHashMap; import java.util.Map; -public abstract class AbstractScriptModule implements ScriptModule { +public abstract class AbstractScriptInternalModule implements ScriptInternalModule { private final Map exports = new LinkedHashMap<>(); @Override @@ -22,7 +22,7 @@ public void export(ScriptExport export) { } @Override - public void copy(ScriptModule module) { + public void copy(ScriptInternalModule module) { // We need to verify that the module from which we want to copy exports // does not contain any exports with the same name as in the current // module but with a different hash. diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java similarity index 67% rename from core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java index 14546be..a579664 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptGlobalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java @@ -2,7 +2,7 @@ import org.jetbrains.annotations.NotNull; -public final class ScriptGlobalModule extends AbstractScriptModule { +public final class ScriptInternalGlobalModule extends AbstractScriptInternalModule { @Override public @NotNull String getName() { diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java new file mode 100644 index 0000000..08e46bc --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java @@ -0,0 +1,57 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.ScriptFileExternalModule; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +public class SimpleScriptFileExternalModule implements ScriptFileExternalModule { + + private final String name; + private final Path path; + private final String entryPoint; + + public SimpleScriptFileExternalModule(String name, Path path) { + this(name, path, "index.mjs"); + } + + public SimpleScriptFileExternalModule(String name, Path path, String entryPoint) { + this.name = Objects.requireNonNull(name); + this.path = Objects.requireNonNull(path); + this.entryPoint = Objects.requireNonNull(entryPoint); + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public Path getPath() { + return path; + } + + @Override + public String getEntryPoint() { + return entryPoint; + } + + @Override + public String getSourceName() { + return path.getFileName().toString(); + } + + @Override + public byte[] load() throws ScriptModuleLoadException { + Path target = Files.isDirectory(path) ? path.resolve(entryPoint) : path; + try { + return Files.readAllBytes(target); + } catch (IOException e) { + throw new ScriptModuleLoadException("Failed to load external module " + name + "' from: " + target, e); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java similarity index 70% rename from core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java rename to core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java index b8bd872..52c3fe6 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java @@ -4,10 +4,10 @@ import java.util.Objects; -public class SimpleScriptModule extends AbstractScriptModule { +public class SimpleScriptInternalModule extends AbstractScriptInternalModule { private final String name; - public SimpleScriptModule(String name) { + public SimpleScriptInternalModule(String name) { this.name = Objects.requireNonNull(name, "Module name cannot be null"); } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java new file mode 100644 index 0000000..4f409fb --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptStreamExternalModule.java @@ -0,0 +1,47 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.ScriptStreamExternalModule; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.function.Supplier; + +/** + * Stream-based external module. Source supplier is called on every load() - allows dynamic reloading. + */ +public class SimpleScriptStreamExternalModule implements ScriptStreamExternalModule { + + private final String name; + private final String sourceName; + private final Supplier sourceSupplier; + + public SimpleScriptStreamExternalModule(String name, String sourceName, byte[] source) { + this(name, sourceName, () -> source); + } + + public SimpleScriptStreamExternalModule(String name, String sourceName, Supplier sourceSupplier) { + this.name = Objects.requireNonNull(name); + this.sourceName = Objects.requireNonNull(sourceName); + this.sourceSupplier = Objects.requireNonNull(sourceSupplier); + } + + @Override + public @NotNull String getName() { + return name; + } + + @Override + public String getSourceName() { + return sourceName; + } + + @Override + public byte[] load() throws ScriptModuleLoadException { + try { + return sourceSupplier.get(); + } catch (Exception e) { + throw new ScriptModuleLoadException("Failed to load stream module " + name, e); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java b/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java index 2355ded..91094cd 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java +++ b/core/src/main/java/org/densy/scriptify/core/script/security/SecurityPathAccessorImpl.java @@ -58,37 +58,24 @@ public void setBasePath(Path basePath) { /** * Returns a path that is safe to access according to security rules. - * If the path is accessible via exclusions, returns the normalized path. - * If the path is not accessible, creates a safe path within basePath by cleaning the path from invalid characters. * * @param path The path string to be checked and possibly modified - * @return A Path object representing the accessible path or a path within base directory + * @return a Path object representing the accessible path or a path within base directory + * @throws SecurityException if path cannot be accessed */ @Override public Path getAccessiblePath(String path) { if (this.isAccessible(path)) { - // Path is in exclusions - return it normalized - return Paths.get(path).normalize().toAbsolutePath(); + return basePath.resolve(path).normalize().toAbsolutePath(); } - - // Path is not accessible - create safe path within basePath - // We need to manually combine paths because resolve() ignores basePath for absolute paths - Path safePath = Paths.get(basePath.toString(), path.replace(":", "")).normalize(); - - // CRITICAL: Ensure the result stays within basePath boundaries - if (!safePath.startsWith(basePath)) { - // If path tries to escape basePath (e.g., "../"), return basePath itself - return basePath; - } - - return safePath; + throw new SecurityException("Access denied by security policy: " + path); } /** * Checks if the given path is accessible based on the current security settings. * - * @param path The path to check for access permission - * @return true if the path is accessible, false otherwise + * @param path the path to check for access permission + * @return true if the path is accessible, otherwise false */ @Override public boolean isAccessible(String path) { @@ -96,35 +83,18 @@ public boolean isAccessible(String path) { return true; } - // Normalize the path to resolve .. and . components to prevent path traversal Path normalizedPath; try { - normalizedPath = Paths.get(path).normalize().toAbsolutePath(); + normalizedPath = basePath.resolve(path).normalize().toAbsolutePath(); } catch (Exception e) { return false; } - // Check both original and normalized path against exclusions for compatibility - String normalizedPathString = normalizedPath.toString(); + String normalizedStr = normalizedPath.toString().replace('\\', '/'); - // Search all exclusions and check that the path is excluded for (SecurityExclude exclude : securityManager.getExcludes()) { - if (exclude instanceof PathSecurityExclude) { - // Check original path first - if (exclude.isExcluded(path)) { - return true; - } - - // Check normalized path - if (exclude.isExcluded(normalizedPathString)) { - return true; - } - - // Check with forward slashes for cross-platform compatibility - String pathWithForwardSlashes = normalizedPathString.replace('\\', '/'); - if (exclude.isExcluded(pathWithForwardSlashes)) { - return true; - } + if (exclude.isExcluded(path) || exclude.isExcluded(normalizedStr)) { + return true; } } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java index e504db2..f9c0d99 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/JsScript.java @@ -100,7 +100,12 @@ public CompiledScript compile(String script) throws ScriptException { ) .build()) .allowIO(IOAccess.newBuilder() - .fileSystem(new VirtualModuleFileSystem(moduleManager, contextRef::get, resolverSupplier)) + .fileSystem(new VirtualModuleFileSystem( + moduleManager, + contextRef::get, + resolverSupplier, + securityManager.getPathAccessor() + )) .build()); // If security mode is enabled, search all exclusions diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java index 912bea9..f990ea8 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -5,13 +5,9 @@ import org.densy.scriptify.api.script.module.ScriptModule; import org.densy.scriptify.api.script.module.ScriptModuleManager; import org.densy.scriptify.api.script.module.export.ScriptExport; -import org.densy.scriptify.api.script.module.export.ScriptValueExport; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; -import org.densy.scriptify.core.script.module.ScriptGlobalModule; -import org.densy.scriptify.core.script.module.export.ScriptConstantExport; -import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; -import org.densy.scriptify.js.graalvm.script.JsFunction; +import org.densy.scriptify.core.script.module.ScriptInternalGlobalModule; import org.densy.scriptify.js.graalvm.script.module.export.resolver.GraalModuleExportResolverFactory; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; @@ -22,13 +18,11 @@ public class GraalModuleManager implements ScriptModuleManager { - private final Script script; - private final ScriptGlobalModule globalModule = new ScriptGlobalModule(); + private final ScriptInternalGlobalModule globalModule = new ScriptInternalGlobalModule(); private final Map modules = new LinkedHashMap<>(); private ScriptModuleExportResolverFactory moduleExportResolverFactory; public GraalModuleManager(Script script) { - this.script = script; this.setModuleExportResolver(new GraalModuleExportResolverFactory(script)); } @@ -43,7 +37,7 @@ public void setModuleExportResolver(ScriptModuleExportResolverFactory moduleExpo } @Override - public ScriptGlobalModule getGlobalModule() { + public ScriptInternalGlobalModule getGlobalModule() { return globalModule; } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java index 822854e..d9cb76e 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -1,8 +1,9 @@ package org.densy.scriptify.js.graalvm.script.module.fs; -import org.densy.scriptify.api.script.module.ScriptModule; -import org.densy.scriptify.api.script.module.ScriptModuleManager; +import org.densy.scriptify.api.exception.ScriptModuleLoadException; +import org.densy.scriptify.api.script.module.*; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.security.SecurityPathAccessor; import org.densy.scriptify.js.graalvm.script.module.fs.util.ByteArrayChannel; import org.densy.scriptify.js.graalvm.script.module.fs.util.JsModuleSourceGenerator; import org.graalvm.polyglot.Context; @@ -28,33 +29,50 @@ public class VirtualModuleFileSystem implements FileSystem { private final ScriptModuleManager moduleManager; private final Supplier contextSupplier; private final Supplier resolverSupplier; + private final SecurityPathAccessor securityAccessor; private final Map modulePathCache = new HashMap<>(); public VirtualModuleFileSystem( ScriptModuleManager moduleManager, Supplier contextSupplier, - Supplier resolverSupplier + Supplier resolverSupplier, + SecurityPathAccessor securityAccessor ) { this.moduleManager = moduleManager; this.contextSupplier = contextSupplier; this.resolverSupplier = resolverSupplier; + this.securityAccessor = securityAccessor; } @Override public Path parsePath(String path) { - if (moduleManager.getModule(path) != null) { - return this.resolveVirtualPath(path); + // internal java-module + if (moduleManager.getModule(path) instanceof ScriptInternalModule) { + return resolveVirtualPath(path); } - return real.parsePath(path); + + // external module + if (moduleManager.getModule(path) instanceof ScriptExternalModule external) { + return resolveExternalPath(path, external); + } + + // real file - resolve via security accessor (relative to basePath if needed) + return securityAccessor.getAccessiblePath(path); } @Override public Path parsePath(URI uri) { if (SCHEME.equals(uri.getScheme())) { - return this.resolveVirtualPath(uri.getHost()); + return resolveVirtualPath(uri.getHost()); + } + + Path path = real.parsePath(uri); + if (!securityAccessor.isAccessible(path.toString())) { + throw new IllegalArgumentException("Access denied by security policy: " + uri); } - return real.parsePath(uri); + + return path; } @Override @@ -62,6 +80,12 @@ public void checkAccess(Path path, Set modes, LinkOption.. if (this.isVirtual(path)) { return; } + + // check the access to real path via security path accessor + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + real.checkAccess(path, modes, linkOptions); } @@ -77,6 +101,12 @@ public Map readAttributes(Path path, String attributes, LinkOpti attrs.put("lastAccessTime", FileTime.fromMillis(0)); return attrs; } + + // check the access to real path via security path accessor + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + return real.readAttributes(path, attributes, options); } @@ -84,42 +114,79 @@ public Map readAttributes(Path path, String attributes, LinkOpti public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { if (this.isVirtual(path)) { String moduleName = getModuleName(path); - ScriptModule module = moduleManager.getModule(moduleName); - if (module == null) { - throw new IOException("Scriptify module not found: " + moduleName); + + if (moduleManager.getModule(moduleName) instanceof ScriptInternalModule internal) { + byte[] source = JsModuleSourceGenerator + .generateModuleSource( + contextSupplier.get(), + internal, + resolverSupplier.get() + ) + .getBytes(StandardCharsets.UTF_8); + return new ByteArrayChannel(source); + } + + if (moduleManager.getModule(moduleName) instanceof ScriptExternalModule external) { + try { + return new ByteArrayChannel(external.load()); + } catch (ScriptModuleLoadException e) { + throw new IOException("Failed to load external module: " + moduleName, e); + } } - byte[] source = JsModuleSourceGenerator - .generateModuleSource(contextSupplier.get(), module, resolverSupplier.get()) - .getBytes(StandardCharsets.UTF_8); - return new ByteArrayChannel(source); + + throw new IOException("Script module not found: " + moduleName); } return real.newByteChannel(path, options, attrs); } @Override public Path toAbsolutePath(Path path) { - if (isVirtual(path)) return path; + if (isVirtual(path)) { + return path; + } + + // resolve the path relative to basePath + if (!path.isAbsolute()) { + return securityAccessor.getBasePath().resolve(path).normalize(); + } + return real.toAbsolutePath(path); } @Override public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { - if (isVirtual(path)) return path; + if (isVirtual(path)) { + return path; + } + + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } + return real.toRealPath(path, linkOptions); } @Override public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + if (!securityAccessor.isAccessible(dir.toString())) { + throw new AccessDeniedException(dir.toString(), null, "Access denied by security policy"); + } return real.newDirectoryStream(dir, filter); } @Override public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + if (!securityAccessor.isAccessible(dir.toString())) { + throw new AccessDeniedException(dir.toString(), null, "Access denied by security policy"); + } real.createDirectory(dir, attrs); } @Override public void delete(Path path) throws IOException { + if (!securityAccessor.isAccessible(path.toString())) { + throw new AccessDeniedException(path.toString(), null, "Access denied by security policy"); + } real.delete(path); } @@ -131,6 +198,19 @@ private Path resolveVirtualPath(String moduleName) { )); } + private Path resolveExternalPath(String moduleName, ScriptExternalModule external) { + if (external instanceof ScriptFileExternalModule fileModule) { + Path target = Files.isDirectory(fileModule.getPath()) + ? fileModule.getPath().resolve(fileModule.getEntryPoint()) + : fileModule.getPath(); + if (!securityAccessor.isAccessible(target.toString())) { + throw new IllegalArgumentException("Access denied by security policy: " + target); + } + return target; + } + return resolveVirtualPath(moduleName); + } + private boolean isVirtual(Path path) { return modulePathCache.containsValue(path); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java index c273451..bb557e4 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -1,6 +1,6 @@ package org.densy.scriptify.js.graalvm.script.module.fs.util; -import org.densy.scriptify.api.script.module.ScriptModule; +import org.densy.scriptify.api.script.module.ScriptInternalModule; import org.densy.scriptify.api.script.module.export.ScriptExport; import org.densy.scriptify.api.script.module.export.ScriptValueExport; import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolver; @@ -15,7 +15,7 @@ public final class JsModuleSourceGenerator { public static String generateModuleSource( Context context, - ScriptModule module, + ScriptInternalModule module, ScriptModuleExportResolver resolver ) { StringBuilder builder = new StringBuilder(); From 9c17f7b281e44b2864716c7b31cb2818a7f14b4f Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sat, 25 Apr 2026 17:40:46 +0200 Subject: [PATCH 7/9] feat: replace CommonConstantManager and CommonFunctionManager by StandardScriptModule --- build.gradle.kts | 2 +- .../constant/CommonConstantManager.java | 4 + .../function/CommonFunctionManager.java | 4 + .../script/module/StandardScriptModule.java | 79 +++++++++++++++++++ .../resolver/MappedModuleExportResolver.java | 2 +- .../resolver/GraalModuleExportResolver.java | 8 +- 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 common/src/main/java/org/densy/scriptify/common/script/module/StandardScriptModule.java diff --git a/build.gradle.kts b/build.gradle.kts index 21db491..d42a104 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,7 @@ java { allprojects { group = "org.densy.scriptify" - version = "1.5.0-SNAPSHOT" + version = "1.6.0-SNAPSHOT" } subprojects { diff --git a/common/src/main/java/org/densy/scriptify/common/script/constant/CommonConstantManager.java b/common/src/main/java/org/densy/scriptify/common/script/constant/CommonConstantManager.java index 4730b93..5d6cdda 100644 --- a/common/src/main/java/org/densy/scriptify/common/script/constant/CommonConstantManager.java +++ b/common/src/main/java/org/densy/scriptify/common/script/constant/CommonConstantManager.java @@ -4,6 +4,10 @@ import org.densy.scriptify.core.script.constant.impl.ScriptConstantBaseDir; import org.densy.scriptify.core.script.constant.impl.ScriptConstantOsName; +/** + * @deprecated this class is marked as deprecated in version 1.6. Modules have replaced managers. + */ +@Deprecated(forRemoval = true) public class CommonConstantManager extends StandardConstantManager { public CommonConstantManager() { diff --git a/common/src/main/java/org/densy/scriptify/common/script/function/CommonFunctionManager.java b/common/src/main/java/org/densy/scriptify/common/script/function/CommonFunctionManager.java index febfb95..c442cfb 100644 --- a/common/src/main/java/org/densy/scriptify/common/script/function/CommonFunctionManager.java +++ b/common/src/main/java/org/densy/scriptify/common/script/function/CommonFunctionManager.java @@ -16,6 +16,10 @@ import org.densy.scriptify.common.script.function.impl.zip.ScriptFunctionZipFile; import org.densy.scriptify.core.script.function.StandardFunctionManager; +/** + * @deprecated this class is marked as deprecated in version 1.6. Modules have replaced managers. + */ +@Deprecated(forRemoval = true) public class CommonFunctionManager extends StandardFunctionManager { public CommonFunctionManager() { diff --git a/common/src/main/java/org/densy/scriptify/common/script/module/StandardScriptModule.java b/common/src/main/java/org/densy/scriptify/common/script/module/StandardScriptModule.java new file mode 100644 index 0000000..9a2ae64 --- /dev/null +++ b/common/src/main/java/org/densy/scriptify/common/script/module/StandardScriptModule.java @@ -0,0 +1,79 @@ +package org.densy.scriptify.common.script.module; + +import org.densy.scriptify.api.script.constant.ScriptConstant; +import org.densy.scriptify.api.script.function.ScriptFunction; +import org.densy.scriptify.common.script.function.impl.crypto.ScriptFunctionBase64Decode; +import org.densy.scriptify.common.script.function.impl.crypto.ScriptFunctionBase64Encode; +import org.densy.scriptify.common.script.function.impl.crypto.ScriptFunctionMD5; +import org.densy.scriptify.common.script.function.impl.crypto.ScriptFunctionSHA256; +import org.densy.scriptify.common.script.function.impl.file.*; +import org.densy.scriptify.common.script.function.impl.os.ScriptFunctionEnv; +import org.densy.scriptify.common.script.function.impl.os.ScriptFunctionExecCommand; +import org.densy.scriptify.common.script.function.impl.random.*; +import org.densy.scriptify.common.script.function.impl.util.*; +import org.densy.scriptify.common.script.function.impl.zip.ScriptFunctionSmartUnzipFile; +import org.densy.scriptify.common.script.function.impl.zip.ScriptFunctionSmartZipFile; +import org.densy.scriptify.common.script.function.impl.zip.ScriptFunctionUnzipFile; +import org.densy.scriptify.common.script.function.impl.zip.ScriptFunctionZipFile; +import org.densy.scriptify.core.script.constant.impl.ScriptConstantBaseDir; +import org.densy.scriptify.core.script.constant.impl.ScriptConstantOsName; +import org.densy.scriptify.core.script.module.AbstractScriptInternalModule; +import org.densy.scriptify.core.script.module.export.ScriptConstantExport; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.jetbrains.annotations.NotNull; + +public final class StandardScriptModule extends AbstractScriptInternalModule { + + public StandardScriptModule() { + // exports for functions + this.export(new ScriptFunctionPrint()); + this.export(new ScriptFunctionExistsFile()); + this.export(new ScriptFunctionDeleteFile()); + this.export(new ScriptFunctionMoveFile()); + this.export(new ScriptFunctionListFiles()); + this.export(new ScriptFunctionReadFile()); + this.export(new ScriptFunctionWriteFile()); + this.export(new ScriptFunctionZipFile()); + this.export(new ScriptFunctionUnzipFile()); + this.export(new ScriptFunctionSmartZipFile()); + this.export(new ScriptFunctionSmartUnzipFile()); + this.export(new ScriptFunctionNormalizePath()); + this.export(new ScriptFunctionBase64Encode()); + this.export(new ScriptFunctionBase64Decode()); + this.export(new ScriptFunctionDownloadFromUrl()); + this.export(new ScriptFunctionJoinPath()); + this.export(new ScriptFunctionRandomUUID()); + this.export(new ScriptFunctionRandomInteger()); + this.export(new ScriptFunctionRandomLong()); + this.export(new ScriptFunctionRandomFloat()); + this.export(new ScriptFunctionRandomDouble()); + this.export(new ScriptFunctionRandomBoolean()); + this.export(new ScriptFunctionMD5()); + this.export(new ScriptFunctionSHA256()); + this.export(new ScriptFunctionExecCommand()); + this.export(new ScriptFunctionEnv()); + this.export(new ScriptFunctionShuffleArray()); + this.export(new ScriptFunctionListOf()); + this.export(new ScriptFunctionSetOf()); + this.export(new ScriptFunctionArrayOf()); + this.export(new ScriptFunctionRegexPattern()); + this.export(new ScriptFunctionRegexMatch()); + + // exports for constants + this.export(new ScriptConstantOsName()); + this.export(new ScriptConstantBaseDir()); + } + + @Override + public @NotNull String getName() { + return "standard"; + } + + private void export(ScriptFunction function) { + this.export(new ScriptFunctionExport(function)); + } + + private void export(ScriptConstant constant) { + this.export(new ScriptConstantExport(constant)); + } +} diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java index e869fc5..8472fcd 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java @@ -11,7 +11,7 @@ public abstract class MappedModuleExportResolver implements ScriptModuleExportRe private final Map, Function> resolvers = new HashMap<>(); - public void map(Class type, Function resolver) { + public void mapping(Class type, Function resolver) { resolvers.put(type, export -> resolver.apply(type.cast(export))); } diff --git a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java index 6948d46..d689810 100644 --- a/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/export/resolver/GraalModuleExportResolver.java @@ -12,12 +12,12 @@ public final class GraalModuleExportResolver extends MappedModuleExportResolver { public GraalModuleExportResolver(Script script, Context context) { - this.map(ScriptValueExport.class, export -> context.asValue(export.getValue())); - this.map(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() + this.mapping(ScriptValueExport.class, export -> context.asValue(export.getValue())); + this.mapping(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() .getFunctionDefinitionFactory() .create(export.getFunction()) )); - this.map(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); - this.map(ScriptConstantExport.class, export -> export.getConstant().getValue()); + this.mapping(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); + this.mapping(ScriptConstantExport.class, export -> export.getConstant().getValue()); } } From 0b343e7fe0d0ea86141562edb93fc10807ba760f Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 15:23:54 +0200 Subject: [PATCH 8/9] feat: complete javadoc --- .../export/resolver/ScriptModuleExportResolver.java | 10 ++++++++++ .../resolver/ScriptModuleExportResolverFactory.java | 11 +++++++++++ .../script/module/AbstractScriptInternalModule.java | 3 +++ .../script/module/ScriptInternalGlobalModule.java | 3 +++ .../script/module/SimpleScriptFileExternalModule.java | 3 +++ .../script/module/SimpleScriptInternalModule.java | 3 +++ .../script/module/export/ScriptConstantExport.java | 3 +++ .../module/export/ScriptFunctionDefinitionExport.java | 3 +++ .../script/module/export/ScriptFunctionExport.java | 3 +++ .../export/resolver/MappedModuleExportResolver.java | 3 +++ 10 files changed, 45 insertions(+) diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java index 6ca46b5..c8bc28e 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java @@ -2,6 +2,16 @@ import org.densy.scriptify.api.script.module.export.ScriptExport; +/** + * A value resolver for export. Used to determine which value in the script will be exported. + */ public interface ScriptModuleExportResolver { + + /** + * Resolves the value of export. + * + * @param export target export + * @return resolved value + */ Object resolve(ScriptExport export); } diff --git a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java index d11bfce..481c27f 100644 --- a/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java @@ -2,6 +2,17 @@ import org.densy.scriptify.api.exception.ScriptModuleWrongContextException; +/** + * A factory for creating an export resolver. + */ public interface ScriptModuleExportResolverFactory { + + /** + * Creates a resolver. + * + * @param context Engine context. Depends on the specific implementation. + * @return created ScriptModuleExportResolver + * @throws ScriptModuleWrongContextException if an incorrect context is passed to a specific implementation of the engine. + */ ScriptModuleExportResolver create(Object context) throws ScriptModuleWrongContextException; } diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java index 3ce0046..b9baf2b 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java @@ -10,6 +10,9 @@ import java.util.LinkedHashMap; import java.util.Map; +/** + * An abstract class for an internal module. + */ public abstract class AbstractScriptInternalModule implements ScriptInternalModule { private final Map exports = new LinkedHashMap<>(); diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java index a579664..4f40e21 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java @@ -2,6 +2,9 @@ import org.jetbrains.annotations.NotNull; +/** + * A global internal module. All exports are available globally. + */ public final class ScriptInternalGlobalModule extends AbstractScriptInternalModule { @Override diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java index 08e46bc..f435f7c 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java @@ -9,6 +9,9 @@ import java.nio.file.Path; import java.util.Objects; +/** + * A script module loaded from an external source using file. + */ public class SimpleScriptFileExternalModule implements ScriptFileExternalModule { private final String name; diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java index 52c3fe6..28efc4d 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java @@ -4,6 +4,9 @@ import java.util.Objects; +/** + * A simple implementation of an external script module. + */ public class SimpleScriptInternalModule extends AbstractScriptInternalModule { private final String name; diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java index 9f0a759..ccf0872 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java @@ -4,6 +4,9 @@ import org.densy.scriptify.api.script.constant.ScriptConstant; import org.densy.scriptify.api.script.module.export.ScriptExport; +/** + * A script module export for a constant. + */ @Getter public final class ScriptConstantExport implements ScriptExport { diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java index 66c5c14..e2ca1af 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java @@ -4,6 +4,9 @@ import org.densy.scriptify.api.script.function.definition.ScriptFunctionDefinition; import org.densy.scriptify.api.script.module.export.ScriptExport; +/** + * A script module export for a function with definition. + */ @Getter public final class ScriptFunctionDefinitionExport implements ScriptExport { diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java index 7caf3ea..8b08e82 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java @@ -4,6 +4,9 @@ import org.densy.scriptify.api.script.function.ScriptFunction; import org.densy.scriptify.api.script.module.export.ScriptExport; +/** + * A script module export for a function. + */ @Getter public final class ScriptFunctionExport implements ScriptExport { diff --git a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java index 8472fcd..c931d22 100644 --- a/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java @@ -7,6 +7,9 @@ import java.util.Map; import java.util.function.Function; +/** + * A value resolver for export with simple mapping for each export. + */ public abstract class MappedModuleExportResolver implements ScriptModuleExportResolver { private final Map, Function> resolvers = new HashMap<>(); From a996698e8da40ed8ca61b16b0340f7350644ecce Mon Sep 17 00:00:00 2001 From: MEFRREEX Date: Sun, 26 Apr 2026 15:52:41 +0200 Subject: [PATCH 9/9] feat: make http module + improve http api & mark JsScript#getModuleManager as unsupported in Rhino --- http/build.gradle.kts | 1 + .../http/script/function/data/HttpMethod.java | 12 ++++++++ .../script/function/data/HttpRequest.java | 28 ++++++++++--------- .../http/script/function/data/OutputType.java | 6 ++++ .../impl/ScriptFunctionCreateHttpRequest.java | 9 ++++-- .../http/script/module/HttpScriptModule.java | 25 +++++++++++++++++ .../scriptify/js/rhino/script/JsScript.java | 3 +- 7 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 http/src/main/java/org/densy/scriptify/http/script/function/data/HttpMethod.java create mode 100644 http/src/main/java/org/densy/scriptify/http/script/function/data/OutputType.java create mode 100644 http/src/main/java/org/densy/scriptify/http/script/module/HttpScriptModule.java diff --git a/http/build.gradle.kts b/http/build.gradle.kts index 3c06619..ba00cae 100644 --- a/http/build.gradle.kts +++ b/http/build.gradle.kts @@ -8,5 +8,6 @@ repositories { dependencies { api(project(":api")) + api(project(":core")) api("com.squareup.okhttp3:okhttp:4.12.0") } \ No newline at end of file diff --git a/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpMethod.java b/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpMethod.java new file mode 100644 index 0000000..c7edad5 --- /dev/null +++ b/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpMethod.java @@ -0,0 +1,12 @@ +package org.densy.scriptify.http.script.function.data; + +public enum HttpMethod { + GET, + PUT, + POST, + DELETE, + PATCH, + HEAD, + OPTIONS, + TRACE +} diff --git a/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpRequest.java b/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpRequest.java index c5c42cf..3d82d5f 100644 --- a/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpRequest.java +++ b/http/src/main/java/org/densy/scriptify/http/script/function/data/HttpRequest.java @@ -9,14 +9,14 @@ public class HttpRequest { private final String url; - private final String method; + private final HttpMethod method; private final Map headers = new HashMap<>(); private String body = ""; private String mediaType = ""; - public HttpRequest(String url, String method) { + public HttpRequest(String url, HttpMethod method) { this.url = url; - this.method = method.toUpperCase(); + this.method = method; } public void setBody(String body, String mediaType) { @@ -29,6 +29,10 @@ public void addHeader(String key, String value) { } public Object send(String outputType) { + return this.send(OutputType.valueOf(outputType)); + } + + public Object send(OutputType outputType) { Request.Builder requestBuilder = new Request.Builder() .url(url); @@ -36,16 +40,15 @@ public Object send(String outputType) { requestBuilder.addHeader(header.getKey(), header.getValue()); } - if (method.equals("POST") || method.equals("PUT")) { - + if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT)) { if (body != null && !body.isEmpty()) { RequestBody requestBody = RequestBody.create(body, MediaType.get(mediaType)); - requestBuilder.method(method, requestBody); + requestBuilder.method(method.name(), requestBody); } else { - requestBuilder.method(method, RequestBody.create(new byte[0], null)); + requestBuilder.method(method.name(), RequestBody.create(new byte[0], null)); } } else { - requestBuilder.method(method, null); + requestBuilder.method(method.name(), null); } OkHttpClient client = new OkHttpClient(); @@ -53,11 +56,10 @@ public Object send(String outputType) { try (Response response = client.newCall(requestBuilder.build()).execute()) { ResponseBody responseBody = response.body(); if (responseBody != null) { - return ScriptObject.of(switch(outputType.toUpperCase()) { - case "STRING" -> responseBody.string(); - case "BYTES" -> responseBody.bytes(); - default -> throw new IllegalArgumentException("Unsupported output type: " + outputType); - }); + return switch(outputType) { + case STRING -> responseBody.string(); + case BYTES -> responseBody.bytes(); + }; } return null; } catch (IOException e) { diff --git a/http/src/main/java/org/densy/scriptify/http/script/function/data/OutputType.java b/http/src/main/java/org/densy/scriptify/http/script/function/data/OutputType.java new file mode 100644 index 0000000..55fc939 --- /dev/null +++ b/http/src/main/java/org/densy/scriptify/http/script/function/data/OutputType.java @@ -0,0 +1,6 @@ +package org.densy.scriptify.http.script.function.data; + +public enum OutputType { + STRING, + BYTES +} diff --git a/http/src/main/java/org/densy/scriptify/http/script/function/impl/ScriptFunctionCreateHttpRequest.java b/http/src/main/java/org/densy/scriptify/http/script/function/impl/ScriptFunctionCreateHttpRequest.java index bf8a0d5..e327e5e 100644 --- a/http/src/main/java/org/densy/scriptify/http/script/function/impl/ScriptFunctionCreateHttpRequest.java +++ b/http/src/main/java/org/densy/scriptify/http/script/function/impl/ScriptFunctionCreateHttpRequest.java @@ -3,6 +3,7 @@ import org.densy.scriptify.api.script.function.ScriptFunction; import org.densy.scriptify.api.script.function.annotation.Argument; import org.densy.scriptify.api.script.function.annotation.ExecuteAt; +import org.densy.scriptify.http.script.function.data.HttpMethod; import org.densy.scriptify.http.script.function.data.HttpRequest; import org.jetbrains.annotations.NotNull; @@ -19,8 +20,12 @@ public class ScriptFunctionCreateHttpRequest implements ScriptFunction { @ExecuteAt public HttpRequest execute( @Argument(name = "url") String url, - @Argument(name = "method") String method + @Argument(name = "method") Object rawMethod ) { - return new HttpRequest(url, method); + if (rawMethod instanceof HttpMethod method) { + return new HttpRequest(url, method); + } else { + return new HttpRequest(url, HttpMethod.valueOf(String.valueOf(rawMethod))); + } } } diff --git a/http/src/main/java/org/densy/scriptify/http/script/module/HttpScriptModule.java b/http/src/main/java/org/densy/scriptify/http/script/module/HttpScriptModule.java new file mode 100644 index 0000000..f7da16a --- /dev/null +++ b/http/src/main/java/org/densy/scriptify/http/script/module/HttpScriptModule.java @@ -0,0 +1,25 @@ +package org.densy.scriptify.http.script.module; + +import org.densy.scriptify.api.script.module.export.ScriptValueExport; +import org.densy.scriptify.core.script.module.AbstractScriptInternalModule; +import org.densy.scriptify.core.script.module.export.ScriptFunctionExport; +import org.densy.scriptify.http.script.function.data.HttpMethod; +import org.densy.scriptify.http.script.function.data.HttpRequest; +import org.densy.scriptify.http.script.function.data.OutputType; +import org.densy.scriptify.http.script.function.impl.ScriptFunctionCreateHttpRequest; +import org.jetbrains.annotations.NotNull; + +public class HttpScriptModule extends AbstractScriptInternalModule { + + public HttpScriptModule() { + this.export(new ScriptValueExport("HttpRequest", HttpRequest.class)); + this.export(new ScriptValueExport("HttpMethod", HttpMethod.class)); + this.export(new ScriptValueExport("OutputType", OutputType.class)); + this.export(new ScriptFunctionExport(new ScriptFunctionCreateHttpRequest())); + } + + @Override + public @NotNull String getName() { + return "http"; + } +} diff --git a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java index 1638a46..ae36861 100644 --- a/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java +++ b/script-js-rhino/src/main/java/org/densy/scriptify/js/rhino/script/JsScript.java @@ -33,8 +33,7 @@ public ScriptSecurityManager getSecurityManager() { @Override public ScriptModuleManager getModuleManager() { - // TODO: implement - return null; + throw new UnsupportedOperationException("Rhino does not support a module system."); } @Override