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/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/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/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/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 new file mode 100644 index 0000000..9d0f352 --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModule.java @@ -0,0 +1,14 @@ +package org.densy.scriptify.api.script.module; + +import org.jetbrains.annotations.NotNull; + +/** + * A base interface for all module types. + */ +public interface ScriptModule { + + /** + * Gets module name. For ES import "@densy/mymodule". + */ + @NotNull String getName(); +} \ 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..29feb9e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/ScriptModuleManager.java @@ -0,0 +1,63 @@ +package org.densy.scriptify.api.script.module; + +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Map; + +/** + * Manages all modules available to the script. + */ +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); + + /** + * Gets the internal global module. Exports added here are available globally without import. + */ + 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/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/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..c8bc28e --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolver.java @@ -0,0 +1,17 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +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 new file mode 100644 index 0000000..481c27f --- /dev/null +++ b/api/src/main/java/org/densy/scriptify/api/script/module/export/resolver/ScriptModuleExportResolverFactory.java @@ -0,0 +1,18 @@ +package org.densy.scriptify.api.script.module.export.resolver; + +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/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/AbstractScriptInternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java new file mode 100644 index 0000000..b9baf2b --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/AbstractScriptInternalModule.java @@ -0,0 +1,50 @@ +package org.densy.scriptify.core.script.module; + +import org.densy.scriptify.api.exception.ScriptModuleCopyException; +import org.densy.scriptify.api.script.module.ScriptInternalModule; +import org.densy.scriptify.api.script.module.export.ScriptExport; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; +import java.util.Collections; +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<>(); + + @Override + public void export(ScriptExport export) { + if (export == null) { + throw new IllegalArgumentException("Export cannot be null"); + } + exports.put(export.getName(), export); + } + + @Override + 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. + 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/ScriptInternalGlobalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java new file mode 100644 index 0000000..4f40e21 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/ScriptInternalGlobalModule.java @@ -0,0 +1,14 @@ +package org.densy.scriptify.core.script.module; + +import org.jetbrains.annotations.NotNull; + +/** + * A global internal module. All exports are available globally. + */ +public final class ScriptInternalGlobalModule extends AbstractScriptInternalModule { + + @Override + public @NotNull String getName() { + return "global"; + } +} 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..f435f7c --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptFileExternalModule.java @@ -0,0 +1,60 @@ +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; + +/** + * A script module loaded from an external source using file. + */ +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/SimpleScriptInternalModule.java b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java new file mode 100644 index 0000000..28efc4d --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/SimpleScriptInternalModule.java @@ -0,0 +1,21 @@ +package org.densy.scriptify.core.script.module; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * A simple implementation of an external script module. + */ +public class SimpleScriptInternalModule extends AbstractScriptInternalModule { + private final String name; + + public SimpleScriptInternalModule(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/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/module/export/ScriptConstantExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java new file mode 100644 index 0000000..ccf0872 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptConstantExport.java @@ -0,0 +1,23 @@ +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; + +/** + * A script module export for a constant. + */ +@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/ScriptFunctionDefinitionExport.java b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java new file mode 100644 index 0000000..e2ca1af --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionDefinitionExport.java @@ -0,0 +1,23 @@ +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; + +/** + * A script module export for a function with definition. + */ +@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 new file mode 100644 index 0000000..8b08e82 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/ScriptFunctionExport.java @@ -0,0 +1,23 @@ +package org.densy.scriptify.core.script.module.export; + +import lombok.Getter; +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 { + + private final ScriptFunction function; + + public ScriptFunctionExport(ScriptFunction function) { + this.function = function; + } + + @Override + public String 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..c931d22 --- /dev/null +++ b/core/src/main/java/org/densy/scriptify/core/script/module/export/resolver/MappedModuleExportResolver.java @@ -0,0 +1,29 @@ +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; + +/** + * A value resolver for export with simple mapping for each export. + */ +public abstract class MappedModuleExportResolver implements ScriptModuleExportResolver { + + private final Map, Function> resolvers = new HashMap<>(); + + public void mapping(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/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/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-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..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 @@ -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; @@ -8,33 +9,49 @@ 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.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.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; 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; +import java.util.function.Supplier; 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 +76,18 @@ 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<>(); + + 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 @@ -69,6 +98,14 @@ public CompiledScript compile(String script) throws ScriptException { object -> true, ScriptObject::getValue ) + .build()) + .allowIO(IOAccess.newBuilder() + .fileSystem(new VirtualModuleFileSystem( + moduleManager, + contextRef::get, + resolverSupplier, + securityManager.getPathAccessor() + )) .build()); // If security mode is enabled, search all exclusions @@ -80,16 +117,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 ScriptFunctionDefinitionExport(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 +135,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..f990ea8 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/GraalModuleManager.java @@ -0,0 +1,73 @@ +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.resolver.ScriptModuleExportResolver; +import org.densy.scriptify.api.script.module.export.resolver.ScriptModuleExportResolverFactory; +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; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public class GraalModuleManager implements ScriptModuleManager { + + private final ScriptInternalGlobalModule globalModule = new ScriptInternalGlobalModule(); + private final Map modules = new LinkedHashMap<>(); + private ScriptModuleExportResolverFactory moduleExportResolverFactory; + + public GraalModuleManager(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 + public ScriptInternalGlobalModule getGlobalModule() { + return globalModule; + } + + @Override + public Map getModules() { + return modules; + } + + @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"); + + 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); + } + } +} 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..d689810 --- /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.mapping(ScriptValueExport.class, export -> context.asValue(export.getValue())); + this.mapping(ScriptFunctionExport.class, export -> new JsFunction(script, script.getFunctionManager() + .getFunctionDefinitionFactory() + .create(export.getFunction()) + )); + this.mapping(ScriptFunctionDefinitionExport.class, export -> new JsFunction(script, export.getDefinition())); + this.mapping(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 new file mode 100644 index 0000000..d9cb76e --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/VirtualModuleFileSystem.java @@ -0,0 +1,225 @@ +package org.densy.scriptify.js.graalvm.script.module.fs; + +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; +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 Supplier resolverSupplier; + private final SecurityPathAccessor securityAccessor; + + private final Map modulePathCache = new HashMap<>(); + + public VirtualModuleFileSystem( + ScriptModuleManager moduleManager, + Supplier contextSupplier, + Supplier resolverSupplier, + SecurityPathAccessor securityAccessor + ) { + this.moduleManager = moduleManager; + this.contextSupplier = contextSupplier; + this.resolverSupplier = resolverSupplier; + this.securityAccessor = securityAccessor; + } + + @Override + public Path parsePath(String path) { + // internal java-module + if (moduleManager.getModule(path) instanceof ScriptInternalModule) { + return resolveVirtualPath(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 resolveVirtualPath(uri.getHost()); + } + + Path path = real.parsePath(uri); + if (!securityAccessor.isAccessible(path.toString())) { + throw new IllegalArgumentException("Access denied by security policy: " + uri); + } + + return path; + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + 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); + } + + @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; + } + + // 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); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + if (this.isVirtual(path)) { + String moduleName = getModuleName(path); + + 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); + } + } + + 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; + } + + // 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 (!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); + } + + private Path resolveVirtualPath(String moduleName) { + return modulePathCache.computeIfAbsent(moduleName, name -> Paths.get( + System.getProperty("java.io.tmpdir"), + "scriptify", + JsModuleSourceGenerator.encodeModuleName(name) + ".mjs" + )); + } + + 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); + } + + 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-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 new file mode 100644 index 0000000..bb557e4 --- /dev/null +++ b/script-js-graalvm/src/main/java/org/densy/scriptify/js/graalvm/script/module/fs/util/JsModuleSourceGenerator.java @@ -0,0 +1,57 @@ +package org.densy.scriptify.js.graalvm.script.module.fs.util; + +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; +import org.graalvm.polyglot.Context; + +import java.util.ArrayList; +import java.util.List; + +public final class JsModuleSourceGenerator { + + public static final String BRIDGE_PREFIX = "__scriptify_bridge_"; + + public static String generateModuleSource( + Context context, + ScriptInternalModule module, + ScriptModuleExportResolver resolver + ) { + StringBuilder builder = new StringBuilder(); + builder.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 && 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); + } + } + + builder.append("\nexport { ").append(String.join(", ", names)).append(" };\n"); + 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("/", "__"); + } + + public static String decodeModuleName(String encoded) { + return encoded.replace("_at_", "@").replace("__", "/"); + } +} 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..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 @@ -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,11 @@ public ScriptSecurityManager getSecurityManager() { return securityManager; } + @Override + public ScriptModuleManager getModuleManager() { + throw new UnsupportedOperationException("Rhino does not support a module system."); + } + @Override public ScriptFunctionManager getFunctionManager() { return functionManager;