diff --git a/pom.xml b/pom.xml index e2e717e..19e8889 100644 --- a/pom.xml +++ b/pom.xml @@ -51,16 +51,19 @@ UTF-8 - 5.12.1 - 2.10 + 5.13.0 + 2.10.1 - 5.9.1 - 3.23.1 + 5.9.3 + 3.24.2 - 3.10.1 - 2.22.2 + 3.11.0 + 3.1.2 + 3.5.0 + 3.2.1 + 1.8.0 true @@ -74,7 +77,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.4.1 + ${maven-javadoc-plugin.version} -Xdoclint:none @@ -90,7 +93,7 @@ org.apache.maven.plugins maven-source-plugin - 3.2.1 + ${maven-source-plugin.version} attach-source @@ -103,7 +106,7 @@ org.jreleaser jreleaser-maven-plugin - 1.3.1 + ${jreleaser-maven-plugin.version} @@ -183,12 +186,5 @@ ${assertj-core.version} test - - - uk.org.webcompere - model-assert - 1.0.0 - test - diff --git a/src/main/java/org/extism/sdk/CancelHandle.java b/src/main/java/org/extism/sdk/CancelHandle.java index b88e3b2..563e03c 100644 --- a/src/main/java/org/extism/sdk/CancelHandle.java +++ b/src/main/java/org/extism/sdk/CancelHandle.java @@ -6,7 +6,8 @@ * CancelHandle is used to cancel a running Plugin */ public class CancelHandle { - private Pointer handle; + + private final Pointer handle; public CancelHandle(Pointer handle) { this.handle = handle; @@ -15,7 +16,7 @@ public CancelHandle(Pointer handle) { /** * Cancel execution of the Plugin associated with the CancelHandle */ - boolean cancel() { + public boolean cancel() { return LibExtism.INSTANCE.extism_plugin_cancel(this.handle); } } diff --git a/src/main/java/org/extism/sdk/Extism.java b/src/main/java/org/extism/sdk/Extism.java index 8e106b3..458de6a 100644 --- a/src/main/java/org/extism/sdk/Extism.java +++ b/src/main/java/org/extism/sdk/Extism.java @@ -1,7 +1,9 @@ package org.extism.sdk; import org.extism.sdk.manifest.Manifest; +import org.extism.sdk.wasm.WasmSourceResolver; +import java.net.URI; import java.nio.file.Path; import java.util.Objects; @@ -10,25 +12,38 @@ */ public class Extism { + private static final WasmSourceResolver DEFAULT_RESOLVER = new WasmSourceResolver(); + /** - * Configure a log file with the given {@link Path} and configure the given {@link LogLevel}. - * + * Creates a {@link Manifest} from the given {@code path}. * @param path - * @param level - * - * @deprecated will be replaced with better logging API. + * @return */ - @Deprecated(forRemoval = true) - public static void setLogFile(Path path, LogLevel level) { - + public static Manifest manifestFromPath(Path path) { Objects.requireNonNull(path, "path"); - Objects.requireNonNull(level, "level"); + return new Manifest(DEFAULT_RESOLVER.resolve(path)); + } - var result = LibExtism.INSTANCE.extism_log_file(path.toString(), level.getLevel()); - if (!result) { - var error = String.format("Could not set extism logger to %s with level %s", path, level); - throw new ExtismException(error); - } + /** + * Creates a {@link Manifest} from the given {@code name} and {@code bytes}. + * @param name + * @param bytes + * @return + */ + public static Manifest manifestFromBytes(String name, byte[] bytes) { + Objects.requireNonNull(name, "name"); + Objects.requireNonNull(bytes, "bytes"); + return new Manifest(DEFAULT_RESOLVER.resolve(name, bytes)); + } + + /** + * Creates a {@link Manifest} from the given {@code url}. + * @param url + * @return + */ + public static Manifest manifestFromUrl(String url) { + Objects.requireNonNull(url, "url"); + return new Manifest(DEFAULT_RESOLVER.resolve(URI.create(url))); } /** @@ -46,30 +61,4 @@ public static String invokeFunction(Manifest manifest, String function, String i return plugin.call(function, input); } } - - /** - * Error levels for the Extism logging facility. - * - * @see Extism#setLogFile(Path, LogLevel) - */ - public enum LogLevel { - - INFO("info"), // - - DEBUG("debug"), // - - WARN("warn"), // - - TRACE("trace"); - - private final String level; - - LogLevel(String level) { - this.level = level; - } - - public String getLevel() { - return level; - } - } } diff --git a/src/main/java/org/extism/sdk/ExtismCurrentPlugin.java b/src/main/java/org/extism/sdk/ExtismCurrentPlugin.java index 8e6a327..b675c13 100644 --- a/src/main/java/org/extism/sdk/ExtismCurrentPlugin.java +++ b/src/main/java/org/extism/sdk/ExtismCurrentPlugin.java @@ -5,7 +5,8 @@ import java.nio.charset.StandardCharsets; public class ExtismCurrentPlugin { - public Pointer pointer; + + private final Pointer pointer; public ExtismCurrentPlugin(Pointer pointer) { this.pointer = pointer; diff --git a/src/main/java/org/extism/sdk/HostFunction.java b/src/main/java/org/extism/sdk/HostFunction.java index 7eebd30..a8bc2ae 100644 --- a/src/main/java/org/extism/sdk/HostFunction.java +++ b/src/main/java/org/extism/sdk/HostFunction.java @@ -1,82 +1,86 @@ package org.extism.sdk; import com.sun.jna.Pointer; -import com.sun.jna.PointerType; +import org.extism.sdk.LibExtism.ExtismVal; +import org.extism.sdk.LibExtism.ExtismValType; +import org.extism.sdk.LibExtism.InternalExtismFunction; import java.util.Arrays; import java.util.Optional; public class HostFunction { - private final LibExtism.InternalExtismFunction callback; + private final Pointer pointer; - public final Pointer pointer; + private final String name; - public final String name; + private final ExtismValType[] params; - public final LibExtism.ExtismValType[] params; + private final ExtismValType[] returns; - public final LibExtism.ExtismValType[] returns; + private final T userData; - public final Optional userData; + public HostFunction(String name, ExtismValType[] params, ExtismValType[] returns, ExtismFunction func) { + this(name, params, returns, func, null); + } - public HostFunction(String name, LibExtism.ExtismValType[] params, LibExtism.ExtismValType[] returns, ExtismFunction f, Optional userData) { + public HostFunction(String name, ExtismValType[] params, ExtismValType[] returns, ExtismFunction func, T userData) { this.name = name; this.params = params; this.returns = returns; this.userData = userData; - this.callback = (Pointer currentPlugin, - LibExtism.ExtismVal inputs, - int nInputs, - LibExtism.ExtismVal outs, - int nOutputs, - Pointer data) -> { - - LibExtism.ExtismVal[] outputs = (LibExtism.ExtismVal []) outs.toArray(nOutputs); - - f.invoke( - new ExtismCurrentPlugin(currentPlugin), - (LibExtism.ExtismVal []) inputs.toArray(nInputs), - outputs, - userData - ); - - for (LibExtism.ExtismVal output : outputs) { - convertOutput(output, output); - } - }; - this.pointer = LibExtism.INSTANCE.extism_function_new( - this.name, - Arrays.stream(this.params).mapToInt(r -> r.v).toArray(), - this.params.length, - Arrays.stream(this.returns).mapToInt(r -> r.v).toArray(), - this.returns.length, - this.callback, - userData.map(PointerType::getPointer).orElse(null), - null + int[] inputTypeValues = Arrays.stream(this.params).mapToInt(r -> r.v).toArray(); + int[] outputTypeValues = Arrays.stream(this.returns).mapToInt(r -> r.v).toArray(); + InternalExtismFunction callback = createCallbackFunction(func, userData); + Pointer userDataPointer = userData != null ? userData.getPointer() : null; + + this.pointer = LibExtism.INSTANCE.extism_function_new( // + name, // + inputTypeValues, // + inputTypeValues.length, // + outputTypeValues, // + outputTypeValues.length, // + callback, // + userDataPointer, // + null // ); } - void convertOutput(LibExtism.ExtismVal original, LibExtism.ExtismVal fromHostFunction) { - if (fromHostFunction.t != original.t) - throw new ExtismException(String.format("Output type mismatch, got %d but expected %d", fromHostFunction.t, original.t)); - - if (fromHostFunction.t == LibExtism.ExtismValType.I32.v) { - original.v.setType(Integer.TYPE); - original.v.i32 = fromHostFunction.v.i32; - } else if (fromHostFunction.t == LibExtism.ExtismValType.I64.v) { - original.v.setType(Long.TYPE); - original.v.i64 = fromHostFunction.v.i64; - } else if (fromHostFunction.t == LibExtism.ExtismValType.F32.v) { - original.v.setType(Float.TYPE); - original.v.f32 = fromHostFunction.v.f32; - } else if (fromHostFunction.t == LibExtism.ExtismValType.F64.v) { - original.v.setType(Double.TYPE); - original.v.f64 = fromHostFunction.v.f64; - } else - throw new ExtismException(String.format("Unsupported return type: %s", original.t)); + private InternalExtismFunction createCallbackFunction(ExtismFunction func, T userData) { + return (Pointer pluginPointer, ExtismVal inputs, int nInputs, ExtismVal outputs, int nOutputs, Pointer data) -> { + + var outputValues = (ExtismVal[]) outputs.toArray(nOutputs); + var inputValues = (ExtismVal[]) inputs.toArray(nInputs); + var currentPlugin = new ExtismCurrentPlugin(pluginPointer); + + func.invoke(currentPlugin, inputValues, outputValues, Optional.ofNullable(userData)); + + for (ExtismVal output : outputValues) { + coerceType(output); + } + }; + } + + void coerceType(ExtismVal value) { + + switch (value.t) { + case ExtismValType.I32_KEY: + value.v.setType(Integer.TYPE); + break; + case ExtismValType.I64_KEY: + value.v.setType(Long.TYPE); + break; + case ExtismValType.F32_KEY: + value.v.setType(Float.TYPE); + break; + case ExtismValType.F64_KEY: + value.v.setType(Double.TYPE); + break; + default: + throw new ExtismException(String.format("Unsupported return type: %s", value.t)); + } } public void setNamespace(String name) { @@ -85,8 +89,28 @@ public void setNamespace(String name) { } } - HostFunction withNamespace(String name) { + public HostFunction withNamespace(String name) { this.setNamespace(name); return this; } + + public Optional params() { + return Optional.ofNullable(params).map(ExtismValType[]::clone); + } + + public Optional returns() { + return Optional.ofNullable(returns).map(ExtismValType[]::clone); + } + + public Optional userData() { + return Optional.ofNullable(userData); + } + + public String name() { + return name; + } + + /* package scoped */ Pointer getPointer() { + return pointer; + } } diff --git a/src/main/java/org/extism/sdk/HostUserData.java b/src/main/java/org/extism/sdk/HostUserData.java index bbd31f4..f0ab773 100644 --- a/src/main/java/org/extism/sdk/HostUserData.java +++ b/src/main/java/org/extism/sdk/HostUserData.java @@ -3,5 +3,4 @@ import com.sun.jna.PointerType; public class HostUserData extends PointerType { - } \ No newline at end of file diff --git a/src/main/java/org/extism/sdk/LibExtism.java b/src/main/java/org/extism/sdk/LibExtism.java index b05fa98..f7f3810 100644 --- a/src/main/java/org/extism/sdk/LibExtism.java +++ b/src/main/java/org/extism/sdk/LibExtism.java @@ -1,6 +1,11 @@ package org.extism.sdk; -import com.sun.jna.*; +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.Union; /** * Wrapper around the Extism library. @@ -13,23 +18,36 @@ public interface LibExtism extends Library { */ LibExtism INSTANCE = Native.load("extism", LibExtism.class); + /** + * Host function callback + */ interface InternalExtismFunction extends Callback { - void invoke( - Pointer currentPlugin, - ExtismVal inputs, - int nInputs, - ExtismVal outputs, - int nOutputs, - Pointer data - ); + + /** + * Host function implementation. + * + * @param plugin + * @param inputs + * @param nInputs + * @param outputs + * @param nOutputs + * @param data + */ + void invoke(Pointer plugin, ExtismVal inputs, int nInputs, ExtismVal outputs, int nOutputs, Pointer data); } + /** + * `Holds the type and value of a function argument/return. + */ @Structure.FieldOrder({"t", "v"}) class ExtismVal extends Structure { public int t; public ExtismValUnion v; } + /** + * A union type for host function argument/return values. + */ class ExtismValUnion extends Union { public int i32; public long i64; @@ -37,22 +55,81 @@ class ExtismValUnion extends Union { public double f64; } + /** + * An enumeration of all possible value types in WebAssembly. + */ enum ExtismValType { - I32(0), - I64(1), - F32(2), - F64(3), - V128(4), - FuncRef(5), - ExternRef(6); + + /** + * Signed 32 bit integer. + */ + I32(ExtismValType.I32_KEY), + + /** + * Signed 64 bit integer. + */ + I64(ExtismValType.I64_KEY), + + /** + * Floating point 32 bit. + */ + F32(ExtismValType.F32_KEY), + + /** + * Floating point 64 bit. + */ + F64(ExtismValType.F64_KEY), + + /** + * A 128 bit number + */ + V128(ExtismValType.V128_KEY), + + /** + * A reference to a Wasm function. + */ + FuncRef(ExtismValType.FUNC_REF_KEY), + + /** + * A reference to opaque data in the Wasm instance. + */ + ExternRef(ExtismValType.FUNC_EXTERN_REF_KEY); public final int v; + public static final int I32_KEY = 0; + + public static final int I64_KEY = 1; + + public static final int F32_KEY = 2; + + public static final int F64_KEY = 3; + + public static final int V128_KEY = 4; + + public static final int FUNC_REF_KEY = 6; + + public static final int FUNC_EXTERN_REF_KEY = 6; + + ExtismValType(int value) { this.v = value; } } + /** + * Create a new host function. + * + * @param name function name + * @param inputs argument types + * @param nInputs number of argument types + * @param outputs return types + * @param nOutputs number of return types + * @param func the function to call + * @param userData a pointer that will be passed to the function when it's called + * @param freeUserData a callback to release the `user_data` value when the resulting ExtismFunction is freed. + * @return Returns a pointer to a new ExtismFunction or {@literal null} if the {@code name} argument is invalid. + */ Pointer extism_function_new(String name, int[] inputs, int nInputs, @@ -162,8 +239,32 @@ Pointer extism_function_new(String name, * @return {@literal true} if update was successful */ boolean extism_plugin_config(Pointer pluginPointer, byte[] json, int jsonLength); + + /** + * Get a handle for plugin cancellation + * @param pluginPointer + * @return a Pointer to a cancellation handle + */ Pointer extism_plugin_cancel_handle(Pointer pluginPointer); + + /** + * Cancel a running plugin. + * @param cancelHandle + * @return {@literal true} if cancellation was successful + */ boolean extism_plugin_cancel(Pointer cancelHandle); - void extism_function_set_namespace(Pointer p, String name); - int strlen(Pointer s); + + /** + * Set the namespace of an `ExtismFunction` + * @param pluginPointer + * @param namespace + */ + void extism_function_set_namespace(Pointer pluginPointer, String namespace); + + /** + * Helper function to get the length of a string represented by the pointer s. + * @param stringPointer + * @return + */ + int strlen(Pointer stringPointer); } diff --git a/src/main/java/org/extism/sdk/Plugin.java b/src/main/java/org/extism/sdk/Plugin.java index 7a5500c..6997ae0 100644 --- a/src/main/java/org/extism/sdk/Plugin.java +++ b/src/main/java/org/extism/sdk/Plugin.java @@ -22,23 +22,27 @@ public class Plugin implements AutoCloseable { * @param functions The Host functions for th eplugin * @param withWASI Set to true to enable WASI */ - public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) { + public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) { Objects.requireNonNull(manifestBytes, "manifestBytes"); - Pointer[] ptrArr = new Pointer[functions == null ? 0 : functions.length]; - - if (functions != null) + int funcCount = functions == null ? 0 : functions.length; + Pointer[] ptrArr = new Pointer[funcCount]; + if (funcCount > 0) { for (int i = 0; i < functions.length; i++) { - ptrArr[i] = functions[i].pointer; + ptrArr[i] = functions[i].getPointer(); } + } Pointer[] errormsg = new Pointer[1]; - Pointer p = LibExtism.INSTANCE.extism_plugin_new(manifestBytes, manifestBytes.length, + Pointer p = LibExtism.INSTANCE.extism_plugin_new( + manifestBytes, + manifestBytes.length, ptrArr, - functions == null ? 0 : functions.length, + funcCount, withWASI, errormsg); + if (p == null) { int errlen = LibExtism.INSTANCE.strlen(errormsg[0]); byte[] msg = new byte[errlen]; @@ -50,10 +54,18 @@ public Plugin(byte[] manifestBytes, boolean withWASI, HostFunction[] functions) this.pluginPointer = p; } - public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) { + public Plugin(Manifest manifest, boolean withWASI, HostFunction[] functions) { this(serialize(manifest), withWASI, functions); } + public Plugin(Manifest manifest, boolean withWASI) { + this(manifest, withWASI, null); + } + + public Plugin(Manifest manifest) { + this(manifest, false); + } + private static byte[] serialize(Manifest manifest) { Objects.requireNonNull(manifest, "manifest"); @@ -120,8 +132,8 @@ public void free() { /** * Update plugin config values, this will merge with the existing values. * - * @param json - * @return + * @param json json string of the config + * @return true if the config could be applied */ public boolean updateConfig(String json) { Objects.requireNonNull(json, "json"); @@ -131,7 +143,7 @@ public boolean updateConfig(String json) { /** * Update plugin config values, this will merge with the existing values. * - * @param jsonBytes + * @param jsonBytes bytes of the config json * @return {@literal true} if update was successful */ public boolean updateConfig(byte[] jsonBytes) { @@ -151,7 +163,6 @@ public void close() { * Return a new `CancelHandle`, which can be used to cancel a running Plugin */ public CancelHandle cancelHandle() { - Pointer handle = LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer); - return new CancelHandle(handle); + return new CancelHandle(LibExtism.INSTANCE.extism_plugin_cancel_handle(this.pluginPointer)); } } diff --git a/src/main/java/org/extism/sdk/manifest/Manifest.java b/src/main/java/org/extism/sdk/manifest/Manifest.java index 37f4c61..87ec53d 100644 --- a/src/main/java/org/extism/sdk/manifest/Manifest.java +++ b/src/main/java/org/extism/sdk/manifest/Manifest.java @@ -8,24 +8,47 @@ import java.util.List; import java.util.Map; +/** + * The `Manifest` type is used to configure the runtime and specify how to load modules. + */ public class Manifest { + /** + * Holds the WebAssembly modules, the `main` module should be named `main` or listed last. + */ @SerializedName("wasm") private final List sources; + /** + * Holds the memory options + */ @SerializedName("memory") private final MemoryOptions memoryOptions; + /** + * Specifies which hosts may be accessed via HTTP, if this is empty then no hosts may be accessed. Wildcards may be used. + */ // FIXME remove this and related stuff if not supported in java-sdk @SerializedName("allowed_hosts") private final List allowedHosts; + /** + * Specifies which paths should be made available on disk when using WASI. + * This is a mapping from the path on disk to the path it should be available inside the plugin. + * For example, `".": "/tmp"` would mount the current directory as `/tmp` inside the module + */ @SerializedName("allowed_paths") private final Map allowedPaths; + /** + * Config values are made accessible using the PDK `extism_config_get` function + */ @SerializedName("config") private final Map config; + /** + * Creates a new Manifest with an empty list of wasm sources. + */ public Manifest() { this(new ArrayList<>(), null, null, null, null); } @@ -58,8 +81,63 @@ public Manifest(List sources, MemoryOptions memoryOptions, Map(this.sources); + sources.add(source); + + return new Manifest(sources, memoryOptions, config, allowedHosts, allowedPaths); + } + + /** + * Returns a new {@link Manifest} with the given {@link MemoryOptions}. + * @param memoryOptions + * @return + */ + public Manifest withMemoryOptions(MemoryOptions memoryOptions) { + return new Manifest(sources, memoryOptions, config, allowedHosts, allowedPaths); + } + + /** + * Returns a new {@link Manifest} with the given config. + * @param config + * @return + */ + public Manifest withConfig(Map config) { + return new Manifest(sources, memoryOptions, config, allowedHosts, allowedPaths); + } + + /** + * Returns a new {@link Manifest} with the given allowed hosts. + * @param allowedHosts + * @return + */ + public Manifest withAllowedHosts(List allowedHosts) { + return new Manifest(sources, memoryOptions, config, allowedHosts, allowedPaths); + } + + /** + * Returns a new {@link Manifest} with the given allowed paths. + * + * @param allowedPaths + * @return + */ + public Manifest withAllowedPaths(Map allowedPaths) { + return new Manifest(sources, memoryOptions, config, allowedHosts, allowedPaths); } public List getSources() { diff --git a/src/main/java/org/extism/sdk/manifest/MemoryOptions.java b/src/main/java/org/extism/sdk/manifest/MemoryOptions.java index f350893..4702b61 100644 --- a/src/main/java/org/extism/sdk/manifest/MemoryOptions.java +++ b/src/main/java/org/extism/sdk/manifest/MemoryOptions.java @@ -5,10 +5,12 @@ /** * Configures memory for the Wasm runtime. * Memory is described in units of pages (64KB) and represent contiguous chunks of addressable memory. - * - * @param max Max number of pages. */ public class MemoryOptions { + + /** + * The max number of WebAssembly pages that should be allocated for the module. + */ @SerializedName("max") private final Integer max; diff --git a/src/main/java/org/extism/sdk/support/Hashing.java b/src/main/java/org/extism/sdk/support/Hashing.java index 57c72b6..dd49acc 100644 --- a/src/main/java/org/extism/sdk/support/Hashing.java +++ b/src/main/java/org/extism/sdk/support/Hashing.java @@ -2,7 +2,11 @@ import java.security.MessageDigest; -public class Hashing { +public final class Hashing { + + private Hashing() { + // prevent instantiation + } public static String sha256HexDigest(byte[] input) { try { diff --git a/src/main/java/org/extism/sdk/support/JsonSerde.java b/src/main/java/org/extism/sdk/support/JsonSerde.java index 7669572..b0e7704 100644 --- a/src/main/java/org/extism/sdk/support/JsonSerde.java +++ b/src/main/java/org/extism/sdk/support/JsonSerde.java @@ -1,17 +1,23 @@ package org.extism.sdk.support; -import com.google.gson.*; +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import org.extism.sdk.manifest.Manifest; import java.io.IOException; -import java.lang.reflect.Type; -import java.nio.charset.StandardCharsets; import java.util.Base64; -public class JsonSerde { +public final class JsonSerde { + + private JsonSerde() { + // prevent instantiation + } private static final Gson GSON; diff --git a/src/main/java/org/extism/sdk/support/Logging.java b/src/main/java/org/extism/sdk/support/Logging.java new file mode 100644 index 0000000..24b8220 --- /dev/null +++ b/src/main/java/org/extism/sdk/support/Logging.java @@ -0,0 +1,66 @@ +package org.extism.sdk.support; + +import org.extism.sdk.ExtismException; +import org.extism.sdk.LibExtism; + +import java.nio.file.Path; +import java.util.Objects; + +public final class Logging { + + private Logging() { + // prevent instantiation + } + + /** + * Configure a log file with the given {@link Path} and configure the given {@link LogLevel}. + * + * @param path + * @param level + * + * @deprecated will be replaced with better logging API. + */ + @Deprecated(forRemoval = true) + public static void setLogFile(Path path, LogLevel level) { + + Objects.requireNonNull(path, "path"); + Objects.requireNonNull(level, "level"); + + var result = LibExtism.INSTANCE.extism_log_file(path.toString(), level.getLevel()); + if (!result) { + var error = String.format("Could not set extism logger to %s with level %s", path, level); + throw new ExtismException(error); + } + } + + /** + * Error levels for the Extism logging facility. + * + * @see Logging#setLogFile(Path, LogLevel) + */ + public enum LogLevel { + + INFO("info"), // + + DEBUG("debug"), // + + WARN("warn"), // + + TRACE("trace"), // + + ERROR("error"), // + + OFF("off"), // + ; + + private final String level; + + LogLevel(String level) { + this.level = level; + } + + public String getLevel() { + return level; + } + } +} diff --git a/src/main/java/org/extism/sdk/wasm/UrlWasmSource.java b/src/main/java/org/extism/sdk/wasm/UrlWasmSource.java index 9398fdc..5422700 100644 --- a/src/main/java/org/extism/sdk/wasm/UrlWasmSource.java +++ b/src/main/java/org/extism/sdk/wasm/UrlWasmSource.java @@ -1,5 +1,7 @@ package org.extism.sdk.wasm; +import java.net.URI; + /** * WASM Source represented by a url. */ @@ -21,6 +23,10 @@ public static UrlWasmSource fromUrl(String url) { return new UrlWasmSource(null, url, null); } + public static UrlWasmSource fromUrl(URI uri) { + return fromUrl(uri.toString()); + } + /** * Constructor * @param name diff --git a/src/main/java/org/extism/sdk/wasm/WasmSourceResolver.java b/src/main/java/org/extism/sdk/wasm/WasmSourceResolver.java index 8b6ab82..54db429 100644 --- a/src/main/java/org/extism/sdk/wasm/WasmSourceResolver.java +++ b/src/main/java/org/extism/sdk/wasm/WasmSourceResolver.java @@ -4,9 +4,10 @@ import org.extism.sdk.support.Hashing; import java.io.IOException; -import java.net.URL; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Objects; /** @@ -32,6 +33,14 @@ public ByteArrayWasmSource resolve(String name, byte[] bytes) { return new ByteArrayWasmSource(name, bytes, hash(bytes)); } + public ByteArrayWasmSource resolve(byte[] bytes) { + return resolve("wasm@" + Arrays.hashCode(bytes), bytes); + } + + public UrlWasmSource resolve(URI uri) { + return UrlWasmSource.fromUrl(uri); + } + protected String hash(Path wasmFile) { try { return hash(Files.readAllBytes(wasmFile)); diff --git a/src/test/java/org/extism/sdk/ManifestTests.java b/src/test/java/org/extism/sdk/ManifestTests.java index 3c24c4f..df2ce41 100644 --- a/src/test/java/org/extism/sdk/ManifestTests.java +++ b/src/test/java/org/extism/sdk/ManifestTests.java @@ -1,44 +1,48 @@ package org.extism.sdk; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import org.extism.sdk.manifest.Manifest; import org.extism.sdk.manifest.MemoryOptions; import org.extism.sdk.support.JsonSerde; import org.junit.jupiter.api.Test; import java.util.List; -import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.extism.sdk.TestWasmSources.CODE; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static uk.org.webcompere.modelassert.json.JsonAssertions.assertJson; public class ManifestTests { @Test public void shouldSerializeManifestWithWasmSourceToJson() { - var paths = new HashMap(); - paths.put("/tmp/foo", "/tmp/extism-plugins/foo"); + var externalPath = "/tmp/foo"; + var internalPath = "/tmp/extism-plugins/foo"; + var paths = Map.of(externalPath, internalPath); var manifest = new Manifest(List.of(CODE.pathWasmSource()), null, null, null, paths); - var json = JsonSerde.toJson(manifest); - assertNotNull(json); - - assertJson(json).at("/wasm").isArray(); - assertJson(json).at("/wasm").hasSize(1); - assertJson(json).at("/allowed_paths").isObject(); - assertJson(json).at("/allowed_paths").hasSize(1); + var jsonString = JsonSerde.toJson(manifest); + assertNotNull(jsonString); + + var json = parseAsJsonObject(jsonString); + assertThat(json.get("wasm").isJsonArray()).isTrue(); + assertThat(json.get("wasm").getAsJsonArray()).hasSize(1); + assertThat(json.get("allowed_paths").isJsonObject()).isTrue(); + assertThat(json.get("allowed_paths").getAsJsonObject().get(externalPath).getAsString()).isEqualTo(internalPath); } @Test public void shouldSerializeManifestWithWasmSourceAndMemoryOptionsToJson() { var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(4)); - var json = JsonSerde.toJson(manifest); - assertNotNull(json); + var jsonString = JsonSerde.toJson(manifest); + assertNotNull(jsonString); - assertJson(json).at("/wasm").isArray(); - assertJson(json).at("/wasm").hasSize(1); - assertJson(json).at("/memory/max").isEqualTo(4); + var json = parseAsJsonObject(jsonString); + assertThat(json.get("wasm").isJsonArray()).isTrue(); + assertThat(json.get("wasm").getAsJsonArray()).hasSize(1); + assertThat(json.get("memory").getAsJsonObject().get("max").getAsInt()).isEqualTo(4); } @Test @@ -49,4 +53,8 @@ public void codeWasmFromFileAndBytesShouldProduceTheSameHash() { assertThat(byteHash).isEqualTo(fileHash); } + + private static JsonObject parseAsJsonObject(String json) { + return JsonParser.parseString(json).getAsJsonObject(); + } } diff --git a/src/test/java/org/extism/sdk/PluginTests.java b/src/test/java/org/extism/sdk/PluginTests.java index 6ace3fe..cf1d704 100644 --- a/src/test/java/org/extism/sdk/PluginTests.java +++ b/src/test/java/org/extism/sdk/PluginTests.java @@ -1,13 +1,14 @@ package org.extism.sdk; -import com.sun.jna.Pointer; import org.extism.sdk.manifest.Manifest; import org.extism.sdk.manifest.MemoryOptions; import org.extism.sdk.wasm.UrlWasmSource; import org.extism.sdk.wasm.WasmSourceResolver; import org.junit.jupiter.api.Test; -import java.util.*; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.extism.sdk.TestWasmSources.CODE; @@ -21,7 +22,7 @@ public class PluginTests { @Test public void shouldInvokeFunctionWithMemoryOptions() { - var manifest = new Manifest(List.of(CODE.pathWasmSource()), new MemoryOptions(0)); + var manifest = new Manifest(CODE.pathWasmSource()).withMemoryOptions(new MemoryOptions(0)); assertThrows(ExtismException.class, () -> { Extism.invokeFunction(manifest, "count_vowels", "Hello World"); }); @@ -31,7 +32,7 @@ public void shouldInvokeFunctionWithMemoryOptions() { public void shouldInvokeFunctionWithConfig() { //FIXME check if config options are available in wasm call var config = Map.of("key1", "value1"); - var manifest = new Manifest(List.of(CODE.pathWasmSource()), null, config); + var manifest = new Manifest(CODE.pathWasmSource()).withConfig(config); var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); assertThat(output).isEqualTo("{\"count\": 3}"); } @@ -47,10 +48,13 @@ public void shouldInvokeFunctionFromFileWasmSource() { public void shouldInvokeFunctionFromUrlWasmSource() { var url = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"; var config = Map.of("vowels", "aeiouyAEIOUY"); - var manifest = new Manifest(List.of(UrlWasmSource.fromUrl(url)), null, config); - var plugin = new Plugin(manifest, false, null); - var output = plugin.call("count_vowels", "Yellow, World!"); - assertThat(output).isEqualTo("{\"count\":4,\"total\":4,\"vowels\":\"aeiouyAEIOUY\"}"); + var manifest = Extism.manifestFromUrl(url).withConfig(config); + + try (var plugin = new Plugin(manifest)) { + String output = plugin.call("count_vowels", "Yellow, World!"); + assertThat(output).isEqualTo("{\"count\":4,\"total\":4,\"vowels\":\"aeiouyAEIOUY\"}"); + } + } // @Test @@ -105,12 +109,12 @@ public void shouldInvokeFunctionFromUrlWasmSource() { // var output = plugin.call("count_vowels", "Hello, World!"); // } - @Test - public void shouldInvokeFunctionFromByteArrayWasmSource() { - var manifest = new Manifest(CODE.byteArrayWasmSource()); - var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); - assertThat(output).isEqualTo("{\"count\": 3}"); - } + @Test + public void shouldInvokeFunctionFromByteArrayWasmSource() { + var manifest = new Manifest(CODE.byteArrayWasmSource()); + var output = Extism.invokeFunction(manifest, "count_vowels", "Hello World"); + assertThat(output).isEqualTo("{\"count\": 3}"); + } @Test public void shouldFailToInvokeUnknownFunction() { @@ -123,13 +127,12 @@ public void shouldFailToInvokeUnknownFunction() { @Test public void shouldAllowInvokeFunctionFromFileWasmSourceApiUsageExample() { - var wasmSourceResolver = new WasmSourceResolver(); - var manifest = new Manifest(wasmSourceResolver.resolve(CODE.getWasmFilePath())); + var manifest = Extism.manifestFromPath(CODE.getWasmFilePath()); var functionName = "count_vowels"; var input = "Hello World"; - try (var plugin = new Plugin(manifest, false, null)) { + try (var plugin = new Plugin(manifest)) { var output = plugin.call(functionName, input); assertThat(output).isEqualTo("{\"count\": 3}"); } @@ -141,7 +144,7 @@ public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() { var functionName = "count_vowels"; var input = "Hello World"; - try (var plugin = new Plugin(manifest, false, null)) { + try (var plugin = new Plugin(manifest)) { var output = plugin.call(functionName, input); assertThat(output).isEqualTo("{\"count\": 3}"); @@ -152,108 +155,101 @@ public void shouldAllowInvokeFunctionFromFileWasmSourceMultipleTimes() { @Test public void shouldAllowInvokeHostFunctionFromPDK() { - var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; - var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; class MyUserData extends HostUserData { - private String data1; - private int data2; + + private final String data1; + + private final int data2; public MyUserData(String data1, int data2) { - super(); this.data1 = data1; this.data2 = data2; } } - ExtismFunction helloWorldFunction = (ExtismFunction) (plugin, params, returns, data) -> { + ExtismFunction func = (plugin, params, returns, data) -> { System.out.println("Hello from Java Host Function!"); - System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0]))); + System.out.printf("Input string received from plugin, %s%n", plugin.inputString(params[0])); - int offs = plugin.alloc(4); - Pointer mem = plugin.memory(); - mem.write(offs, "test".getBytes(), 0, 4); - returns[0].v.i64 = offs; + data.ifPresent(d -> { + System.out.printf("Host user data, %s, %d%n", d.data1, d.data2); - data.ifPresent(d -> System.out.println(String.format("Host user data, %s, %d", d.data1, d.data2))); + plugin.returnString(returns[0], d.data1.toUpperCase(Locale.US)); + }); }; - HostFunction helloWorld = new HostFunction<>( + var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; + var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; + + var hostFunc = new HostFunction( "hello_world", parametersTypes, resultsTypes, - helloWorldFunction, - Optional.of(new MyUserData("test", 2)) + func, + new MyUserData("test", 2) ); - HostFunction[] functions = {helloWorld}; + var functions = new HostFunction[]{hostFunc}; - Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); + Manifest manifest = new Manifest(CODE.pathWasmFunctionsSource()); String functionName = "count_vowels"; try (var plugin = new Plugin(manifest, true, functions)) { var output = plugin.call(functionName, "this is a test"); - assertThat(output).isEqualTo("test"); + assertThat(output).isEqualTo("TEST"); } } @Test public void shouldAllowInvokeHostFunctionWithoutUserData() { - var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; - var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; - - ExtismFunction helloWorldFunction = (plugin, params, returns, data) -> { + ExtismFunction func = (plugin, params, returns, data) -> { System.out.println("Hello from Java Host Function!"); - System.out.println(String.format("Input string received from plugin, %s", plugin.inputString(params[0]))); + System.out.printf("Input string received from plugin, %s%n", plugin.inputString(params[0])); - int offs = plugin.alloc(4); - Pointer mem = plugin.memory(); - mem.write(offs, "test".getBytes(), 0, 4); - returns[0].v.i64 = offs; + plugin.returnString(returns[0], "fromHostFunction"); - assertThat(data.isEmpty()); + assertThat(data.isEmpty()).isTrue(); }; - HostFunction f = new HostFunction<>( + var parametersTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; + var resultsTypes = new LibExtism.ExtismValType[]{LibExtism.ExtismValType.I64}; + + var hostFuncEnv = new HostFunction<>( "hello_world", parametersTypes, resultsTypes, - helloWorldFunction, - Optional.empty() - ) - .withNamespace("env"); + func + ).withNamespace("env"); - HostFunction g = new HostFunction<>( + var hostFuncTest = new HostFunction<>( "hello_world", parametersTypes, resultsTypes, - helloWorldFunction, - Optional.empty() - ) - .withNamespace("test"); + func + ).withNamespace("test"); - HostFunction[] functions = {f,g}; + var functions = new HostFunction[]{hostFuncEnv, hostFuncTest}; - Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); + Manifest manifest = new Manifest(CODE.pathWasmFunctionsSource()); String functionName = "count_vowels"; try (var plugin = new Plugin(manifest, true, functions)) { var output = plugin.call(functionName, "this is a test"); - assertThat(output).isEqualTo("test"); + assertThat(output).isEqualTo("fromHostFunction"); } } @Test public void shouldFailToInvokeUnknownHostFunction() { - Manifest manifest = new Manifest(Arrays.asList(CODE.pathWasmFunctionsSource())); + Manifest manifest = new Manifest(CODE.pathWasmFunctionsSource()); String functionName = "count_vowels"; - try { - var plugin = new Plugin(manifest, true, null); + try (var plugin = new Plugin(manifest, true)) { plugin.call(functionName, "this is a test"); - } catch (ExtismException e) { + } catch (ExtismException e) { assertThat(e.getMessage()).contains("unknown import: `env::hello_world` has not been defined"); } } diff --git a/src/test/java/org/extism/sdk/TestWasmSources.java b/src/test/java/org/extism/sdk/TestWasmSources.java index 20e0f16..acc82e0 100644 --- a/src/test/java/org/extism/sdk/TestWasmSources.java +++ b/src/test/java/org/extism/sdk/TestWasmSources.java @@ -38,7 +38,7 @@ public PathWasmSource pathWasmFunctionsSource() { public ByteArrayWasmSource byteArrayWasmSource() { try { byte[] wasmBytes = Files.readAllBytes(getWasmFilePath()); - return new WasmSourceResolver().resolve("wasm@" + Arrays.hashCode(wasmBytes), wasmBytes); + return new WasmSourceResolver().resolve(wasmBytes); } catch (IOException ioe) { throw new RuntimeException(ioe); }