diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b809ea327..553aa4ab8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -22,7 +22,7 @@ jlinker = "1.0.7"
jphantom = "1.4.4"
junit = "5.10.2"
jsvg = "1.4.0"
-llzip = "2.5.0"
+llzip = "2.6.0"
logback-classic = { strictly = "1.4.11" } # newer releases break in jar releases
mapping-io = "0.5.1"
mockito = "5.11.0"
diff --git a/recaf-core/src/main/java/software/coley/recaf/plugin/PluginContainer.java b/recaf-core/src/main/java/software/coley/recaf/plugin/PluginContainer.java
index e6157bc51..37e0e5853 100644
--- a/recaf-core/src/main/java/software/coley/recaf/plugin/PluginContainer.java
+++ b/recaf-core/src/main/java/software/coley/recaf/plugin/PluginContainer.java
@@ -13,7 +13,6 @@
* @see PluginLoader
*/
public interface PluginContainer
{
-
/**
* @return Plugin information.
*/
diff --git a/recaf-core/src/main/java/software/coley/recaf/plugin/PluginSource.java b/recaf-core/src/main/java/software/coley/recaf/plugin/PluginSource.java
index 2ab30c48d..c676c97bb 100644
--- a/recaf-core/src/main/java/software/coley/recaf/plugin/PluginSource.java
+++ b/recaf-core/src/main/java/software/coley/recaf/plugin/PluginSource.java
@@ -4,14 +4,16 @@
import software.coley.recaf.util.io.ByteSource;
/**
- * Plugin source.
+ * A functional mapping of internal paths (Like paths in a ZIP file) to the contents of the plugin.
*
* @author xDark
*/
public interface PluginSource {
/**
- * @param name Resource name.
+ * @param name
+ * Resource path name.
+ *
* @return Resource content or {@code null}, if not found.
*/
@Nullable
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/BasicPluginManager.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/BasicPluginManager.java
index 1dcae728d..9cf9a6286 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/BasicPluginManager.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/BasicPluginManager.java
@@ -5,12 +5,8 @@
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.recaf.cdi.EagerInitialization;
-import software.coley.recaf.plugin.ClassAllocator;
-import software.coley.recaf.plugin.Plugin;
-import software.coley.recaf.plugin.PluginContainer;
-import software.coley.recaf.plugin.PluginException;
-import software.coley.recaf.plugin.PluginLoader;
-import software.coley.recaf.services.plugin.discovery.DiscoveredPlugin;
+import software.coley.recaf.plugin.*;
+import software.coley.recaf.services.plugin.discovery.DiscoveredPluginSource;
import software.coley.recaf.services.plugin.discovery.PluginDiscoverer;
import software.coley.recaf.services.plugin.zip.ZipPluginLoader;
@@ -68,10 +64,10 @@ public void registerLoader(@Nonnull PluginLoader loader) {
@Nonnull
@Override
public Collection> loadPlugins(@Nonnull PluginDiscoverer discoverer) throws PluginException {
- List discoveredPlugins = discoverer.findAll();
+ List discoveredPlugins = discoverer.findSources();
List loaders = this.loaders;
List prepared = new ArrayList<>(discoveredPlugins.size());
- for (DiscoveredPlugin plugin : discoveredPlugins) {
+ for (DiscoveredPluginSource plugin : discoveredPlugins) {
for (PluginLoader loader : loaders) {
PreparedPlugin preparedPlugin = loader.prepare(plugin.source());
if (preparedPlugin == null)
@@ -84,8 +80,8 @@ public Collection> loadPlugins(@Nonnull PluginDiscoverer disc
@Nonnull
@Override
- public PluginUnloader unloadPlugin(@Nonnull String id) {
- return mainGraph.unload(id);
+ public PluginUnloader unloaderFor(@Nonnull String id) {
+ return mainGraph.unloaderFor(id);
}
@Override
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/CaseInsensitiveCharSequence.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/CaseInsensitiveCharSequence.java
deleted file mode 100644
index c90143c4a..000000000
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/CaseInsensitiveCharSequence.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package software.coley.recaf.services.plugin;
-
-final class CaseInsensitiveCharSequence {
- private final CharSequence csq;
-
- CaseInsensitiveCharSequence(CharSequence csq) {
- this.csq = csq;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof CaseInsensitiveCharSequence that))
- return false;
- CharSequence csq = this.csq;
- CharSequence thatCsq = that.csq;
- int length;
- if ((length = csq.length()) != thatCsq.length())
- return false;
- while (length != 0) {
- if (Character.toUpperCase(csq.charAt(--length))
- != Character.toUpperCase(thatCsq.charAt(length)))
- return false;
- }
- return true;
- }
-
- @Override
- public int hashCode() {
- int h = 1;
- CharSequence csq = this.csq;
- for (int i = csq.length(); i != 0; ) {
- h = h * 31 + Character.toUpperCase(csq.charAt(--i));
- }
- return h;
- }
-}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/CdiClassAllocator.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/CdiClassAllocator.java
index 264802deb..55412fc39 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/CdiClassAllocator.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/CdiClassAllocator.java
@@ -3,11 +3,8 @@
import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.spi.CreationalContext;
-import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.spi.*;
import jakarta.inject.Inject;
-import software.coley.recaf.Bootstrap;
-import software.coley.recaf.Recaf;
import software.coley.recaf.plugin.AllocationException;
import software.coley.recaf.plugin.ClassAllocator;
@@ -25,7 +22,7 @@ public class CdiClassAllocator implements ClassAllocator {
private final BeanManager beanManager;
@Inject
- public CdiClassAllocator(BeanManager beanManager) {
+ public CdiClassAllocator(@Nonnull BeanManager beanManager) {
this.beanManager = beanManager;
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/LoadedPlugin.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/LoadedPlugin.java
index 71ddf2b7b..fec425665 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/LoadedPlugin.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/LoadedPlugin.java
@@ -1,13 +1,31 @@
package software.coley.recaf.services.plugin;
+import jakarta.annotation.Nonnull;
+
import java.util.HashSet;
import java.util.Set;
final class LoadedPlugin {
- final Set dependencies = HashSet.newHashSet(4);
- final PluginContainerImpl> container;
+ private final Set dependencies = HashSet.newHashSet(4);
+ private final PluginContainerImpl> container;
- LoadedPlugin(PluginContainerImpl> container) {
+ LoadedPlugin(@Nonnull PluginContainerImpl> container) {
this.container = container;
}
+
+ /**
+ * @return Mutable set of dependencies this plugin relies on.
+ */
+ @Nonnull
+ public Set getDependencies() {
+ return dependencies;
+ }
+
+ /**
+ * @return
+ */
+ @Nonnull
+ public PluginContainerImpl> getContainer() {
+ return container;
+ }
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoader.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoader.java
index 50f4b3e5d..37389d5dc 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoader.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoader.java
@@ -10,17 +10,22 @@
public interface PluginClassLoader {
/**
- * @param name Resource path.
+ * @param name
+ * Resource path.
+ *
* @return Resource source or {@code null} if not found.
*/
@Nullable
ByteSource lookupResource(@Nonnull String name);
/**
- * @param name Class name.
+ * @param name
+ * Class name.
+ *
* @return Class.
+ *
* @throws ClassNotFoundException
- * If class was not found.
+ * If class was not found.
*/
@Nonnull
Class> lookupClass(@Nonnull String name) throws ClassNotFoundException;
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java
index 74f94894d..0fa88a2c2 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java
@@ -7,12 +7,7 @@
import java.io.IOException;
import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
+import java.net.*;
final class PluginClassLoaderImpl extends ClassLoader implements PluginClassLoader {
private final PluginGraph graph;
@@ -81,9 +76,9 @@ protected Class> findClass(String name) throws ClassNotFoundException {
Class> cls = lookupClassImpl(name);
if (cls != null)
return cls;
- var dependencies = graph.getDependencies(id);
- while (dependencies.hasNext()) {
- if ((cls = dependencies.next().findClass(name)) != null)
+ var dependencyLoaders = graph.getDependencyClassloaders(id);
+ while (dependencyLoaders.hasNext()) {
+ if ((cls = dependencyLoaders.next().findClass(name)) != null)
return cls;
}
throw new ClassNotFoundException(name);
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginContainerImpl.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginContainerImpl.java
index 580ad0049..28e39448c 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginContainerImpl.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginContainerImpl.java
@@ -5,12 +5,20 @@
import software.coley.recaf.plugin.PluginContainer;
import software.coley.recaf.plugin.PluginInfo;
+/**
+ * Plugin container implementation.
+ *
+ * @param
+ * Plugin instance type.
+ *
+ * @author xDark
+ */
final class PluginContainerImpl
implements PluginContainer
{
final PreparedPlugin preparedPlugin;
final PluginClassLoader classLoader;
P plugin;
- PluginContainerImpl(PreparedPlugin preparedPlugin, PluginClassLoader classLoader) {
+ PluginContainerImpl(@Nonnull PreparedPlugin preparedPlugin, @Nonnull PluginClassLoader classLoader) {
this.preparedPlugin = preparedPlugin;
this.classLoader = classLoader;
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginGraph.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginGraph.java
index db3482d0e..58ec7fa84 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginGraph.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginGraph.java
@@ -4,31 +4,39 @@
import com.google.common.collect.Iterators;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
-import software.coley.recaf.plugin.ClassAllocator;
-import software.coley.recaf.plugin.Plugin;
-import software.coley.recaf.plugin.PluginContainer;
-import software.coley.recaf.plugin.PluginException;
-import software.coley.recaf.plugin.PluginInfo;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import software.coley.recaf.plugin.*;
+
+import java.util.*;
import java.util.stream.Stream;
+/**
+ * Plugin dependency graph.
+ *
+ * @author xDark
+ */
final class PluginGraph {
final Map plugins = HashMap.newHashMap(16);
private final ClassAllocator classAllocator;
- PluginGraph(ClassAllocator classAllocator) {
+ /**
+ * @param classAllocator
+ * Allocator to construct plugin instances with.
+ */
+ PluginGraph(@Nonnull ClassAllocator classAllocator) {
this.classAllocator = classAllocator;
}
+ /**
+ * Primary plugin load action.
+ *
+ * @param preparedPlugins
+ * Intermediate plugin data to load.
+ *
+ * @return Collection of loaded plugin containers.
+ *
+ * @throws PluginException
+ * If the plugins could not be loaded for any reason.
+ */
@Nonnull
Collection> apply(@Nonnull List preparedPlugins) throws PluginException {
Map temp = LinkedHashMap.newLinkedHashMap(preparedPlugins.size());
@@ -46,7 +54,7 @@ Collection> apply(@Nonnull List preparedPlugi
}
}
for (LoadedPlugin plugin : temp.values()) {
- PluginInfo info = plugin.container.info();
+ PluginInfo info = plugin.getContainer().info();
for (String dependencyId : info.dependencies()) {
LoadedPlugin dep = temp.get(dependencyId);
if (dep == null) {
@@ -55,14 +63,14 @@ Collection> apply(@Nonnull List preparedPlugi
if (dep == null) {
throw new PluginException("Plugin %s is missing dependency %s".formatted(info.id(), dependencyId));
}
- plugin.dependencies.add(dep);
+ plugin.getDependencies().add(dep);
}
for (String dependencyId : info.softDependencies()) {
LoadedPlugin dep = temp.get(dependencyId);
if (dep == null && (dep = plugins.get(dependencyId)) == null) {
continue;
}
- plugin.dependencies.add(dep);
+ plugin.getDependencies().add(dep);
}
}
for (LoadedPlugin loadedPlugin : temp.values()) {
@@ -70,7 +78,7 @@ Collection> apply(@Nonnull List preparedPlugi
enable(loadedPlugin);
} catch (PluginException ex) {
for (LoadedPlugin pl : temp.values()) {
- PluginContainerImpl> container = pl.container;
+ PluginContainerImpl> container = pl.getContainer();
try {
try {
Plugin maybeEnabled = container.plugin;
@@ -88,11 +96,17 @@ Collection> apply(@Nonnull List preparedPlugi
}
}
plugins.putAll(temp);
- return Collections2.transform(temp.values(), input -> input.container);
+ return Collections2.transform(temp.values(), input -> input.getContainer());
}
+ /**
+ * @param id
+ * Plugin identifier.
+ *
+ * @return Plugin unload action.
+ */
@Nonnull
- PluginUnloader unload(@Nonnull String id) {
+ PluginUnloader unloaderFor(@Nonnull String id) {
LoadedPlugin plugin = plugins.get(id);
if (plugin == null) {
throw new IllegalStateException("Plugin %s is not loaded".formatted(id));
@@ -110,7 +124,7 @@ public void commit() throws PluginException {
@Nonnull
@Override
public PluginInfo unloadingPlugin() {
- return plugin.container.info();
+ return plugin.getContainer().info();
}
@Nonnull
@@ -119,13 +133,24 @@ public Stream dependants() {
return dependants.values()
.stream()
.flatMap(Collection::stream)
- .map(plugin -> plugin.container.info());
+ .map(plugin -> plugin.getContainer().info());
}
};
}
- private PluginException unload(LoadedPlugin plugin, Map> dependants) {
- String id = plugin.container.info().id();
+ /**
+ * Attempts to unload the given plugin, along with its dependants.
+ *
+ * @param plugin
+ * Plugin to unload.
+ * @param dependants
+ * Map of plugin dependents.
+ *
+ * @return Exception to be thrown if the plugin could not be unloaded.
+ */
+ @Nullable
+ private PluginException unload(@Nonnull LoadedPlugin plugin, @Nonnull Map> dependants) {
+ String id = plugin.getContainer().info().id();
if (!plugins.remove(id, plugin)) {
throw new IllegalStateException("Plugin %s was already removed, recursion?".formatted(id));
}
@@ -135,13 +160,13 @@ private PluginException unload(LoadedPlugin plugin, Map container = plugin.container;
+ PluginContainerImpl> container = plugin.getContainer();
try {
container.plugin().onDisable();
} finally {
@@ -160,56 +185,90 @@ private PluginException unload(LoadedPlugin plugin, Map> dependants) {
+ /**
+ * Iterates over which plugins depend on the given plugin, and stores them in the provided map.
+ *
+ * @param plugin
+ * Plugin to collect dependants of.
+ * @param dependants
+ * Map to store results in.
+ */
+ private void collectDependants(@Nonnull LoadedPlugin plugin, @Nonnull Map> dependants) {
Set dependantsSet = dependants.computeIfAbsent(plugin, __ -> HashSet.newHashSet(4));
for (LoadedPlugin pl : plugins.values()) {
if (plugin == pl) continue;
- if (pl.dependencies.contains(plugin)) {
+ if (pl.getDependencies().contains(plugin)) {
dependantsSet.add(pl);
collectDependants(pl, dependants);
}
}
}
+ /**
+ * @param id
+ * Plugin identifier.
+ *
+ * @return Plugin container if the item was found.
+ */
@Nullable
PluginContainer> getContainer(@Nonnull String id) {
LoadedPlugin plugin = plugins.get(id);
- if (plugin == null) {
+ if (plugin == null)
return null;
- }
- return plugin.container;
+ return plugin.getContainer();
}
+ /**
+ * @return Collection of plugin containers.
+ */
@Nonnull
Collection> plugins() {
- return Collections2.transform(plugins.values(), plugin -> plugin.container);
+ return Collections2.transform(plugins.values(), LoadedPlugin::getContainer);
}
+ /**
+ * @param id
+ * Plugin identifier.
+ *
+ * @return Iterator of dependency classloaders.
+ */
@Nonnull
- Iterator getDependencies(@Nonnull String id) {
+ Iterator getDependencyClassloaders(@Nonnull String id) {
var loaded = plugins.get(id);
if (loaded == null) {
return Collections.emptyIterator();
}
- return Iterators.transform(loaded.dependencies.iterator(), input -> ((PluginClassLoaderImpl) input.container.plugin().getClass().getClassLoader()));
+ return Iterators.transform(loaded.getDependencies().iterator(), input -> ((PluginClassLoaderImpl) input.getContainer().plugin().getClass().getClassLoader()));
}
- private void enable(LoadedPlugin loadedPlugin) throws PluginException {
- for (LoadedPlugin dependency : loadedPlugin.dependencies) {
+ /**
+ * Enables the given plugin, initializing if necessary.
+ *
+ * @param loadedPlugin
+ * Plugin to enable.
+ *
+ * @throws PluginException
+ * If the plugin could not be initialized or enabled.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private void enable(@Nonnull LoadedPlugin loadedPlugin) throws PluginException {
+ // Enable dependent plugins
+ for (LoadedPlugin dependency : loadedPlugin.getDependencies())
enable(dependency);
- }
- //noinspection rawtypes
- PluginContainerImpl container = loadedPlugin.container;
+
+ // Check if the plugin is already initialized.
+ PluginContainerImpl container = loadedPlugin.getContainer();
Plugin plugin = container.plugin;
- if (plugin != null) return;
+ if (plugin != null) return; // Already initialized, skip.
+
+ // Initialize and enable the plugin.
try {
- //noinspection unchecked
Class extends Plugin> pluginClass = (Class extends Plugin>) container.classLoader.lookupClass(container.preparedPlugin.pluginClassName());
plugin = classAllocator.instance(pluginClass);
plugin.onEnable();
container.plugin = plugin;
- } catch (Exception ex) {
- throw new PluginException(ex);
+ } catch (Throwable t) {
+ throw new PluginException(t);
}
}
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginId.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginId.java
index 6b4780ab9..30df2e2e0 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginId.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginId.java
@@ -1,3 +1,3 @@
package software.coley.recaf.services.plugin;
-record PluginId(String id) { }
+record PluginId(String id) {}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManager.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManager.java
index 34d643736..3ca81c053 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManager.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManager.java
@@ -3,11 +3,7 @@
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
-import software.coley.recaf.plugin.ClassAllocator;
-import software.coley.recaf.plugin.Plugin;
-import software.coley.recaf.plugin.PluginContainer;
-import software.coley.recaf.plugin.PluginException;
-import software.coley.recaf.plugin.PluginLoader;
+import software.coley.recaf.plugin.*;
import software.coley.recaf.services.Service;
import software.coley.recaf.services.plugin.discovery.PluginDiscoverer;
@@ -81,22 +77,28 @@ default Collection getPluginsOfType(@Nonnull Class type) {
void registerLoader(@Nonnull PluginLoader loader);
/**
- * @param discoverer Plugin discoverer.
+ * @param discoverer
+ * Plugin discoverer.
+ *
* @return Loaded plugins.
+ *
* @throws PluginException
- * If plugins fail to load.
+ * If plugins fail to load.
*/
@Nonnull
Collection> loadPlugins(@Nonnull PluginDiscoverer discoverer) throws PluginException;
/**
- * @param id ID of the plugin to be unloaded.
+ * @param id
+ * ID of the plugin to be unloaded.
+ *
* @return Plugin unload action.
- * @see PluginUnloader
+ *
* @throws IllegalStateException
- * If plugin to be unloaded was not found.
+ * If plugin to be unloaded was not found.
+ * @see PluginUnloader
*/
@Nonnull
- PluginUnloader unloadPlugin(@Nonnull String id);
+ PluginUnloader unloaderFor(@Nonnull String id);
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManagerConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManagerConfig.java
index de1034808..172670524 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManagerConfig.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginManagerConfig.java
@@ -1,6 +1,5 @@
package software.coley.recaf.services.plugin;
-import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.observables.ObservableBoolean;
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginUnloader.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginUnloader.java
index 83129955f..bc65c7b45 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginUnloader.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginUnloader.java
@@ -8,14 +8,15 @@
/**
* Plugin unload action.
+ *
* @author xDark
*/
public interface PluginUnloader {
-
/**
* Unloads the plugins.
+ *
* @throws PluginException
- * If any of the plugins failed to unload.
+ * If any of the plugins failed to unload.
*/
void commit() throws PluginException;
@@ -27,6 +28,7 @@ public interface PluginUnloader {
/**
* @return A collection of plugins that depend on the plugin to be unloaded.
+ *
* @apiNote These plugins will also get unloaded.
*/
@Nonnull
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PreparedPlugin.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PreparedPlugin.java
index c3c23d405..62452db99 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/PreparedPlugin.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/PreparedPlugin.java
@@ -1,14 +1,19 @@
package software.coley.recaf.services.plugin;
import jakarta.annotation.Nonnull;
-import software.coley.recaf.plugin.PluginSource;
+import software.coley.recaf.plugin.PluginContainer;
import software.coley.recaf.plugin.PluginException;
import software.coley.recaf.plugin.PluginInfo;
+import software.coley.recaf.plugin.PluginSource;
+import software.coley.recaf.services.plugin.discovery.DiscoveredPluginSource;
+import software.coley.recaf.services.plugin.discovery.PluginDiscoverer;
/**
- * Prepared plugin.
+ * Prepared plugin. Used as an intermediate step in plugin loading between {@link DiscoveredPluginSource}
+ * and {@link PluginContainer}.
*
* @author xDark
+ * @see BasicPluginManager#loadPlugins(PluginDiscoverer)
*/
public interface PreparedPlugin {
@@ -33,7 +38,8 @@ public interface PreparedPlugin {
/**
* Called if {@link PluginManager} rejects this plugin.
*
- * @throws PluginException If any exception occurs.
+ * @throws PluginException
+ * If any exception occurs.
*/
void reject() throws PluginException;
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DirectoryPluginDiscoverer.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DirectoryPluginDiscoverer.java
index e2641e4b3..1e02fec20 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DirectoryPluginDiscoverer.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DirectoryPluginDiscoverer.java
@@ -8,10 +8,19 @@
import java.nio.file.Path;
import java.util.stream.Stream;
+/**
+ * A plugin discoverer that searches a directory for plugin sources.
+ * Subdirectories are not searched.
+ *
+ * @author xDark
+ */
public final class DirectoryPluginDiscoverer extends PathPluginDiscoverer {
private final Path directory;
- public DirectoryPluginDiscoverer(Path directory) {
+ /**
+ * @param directory Directory to iterate over for plugins.
+ */
+ public DirectoryPluginDiscoverer(@Nonnull Path directory) {
this.directory = directory;
}
@@ -19,7 +28,8 @@ public DirectoryPluginDiscoverer(Path directory) {
@Override
protected Stream stream() throws PluginException {
try {
- return Files.find(directory, 1, (path, attributes) -> attributes.isRegularFile());
+ return Files.find(directory, 1,
+ (path, attributes) -> attributes.isRegularFile() && path.toString().toLowerCase().endsWith(".jar"));
} catch (IOException ex) {
throw new PluginException(ex);
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPlugin.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPluginSource.java
similarity index 50%
rename from recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPlugin.java
rename to recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPluginSource.java
index 3093ca36e..62c4d6c4c 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPlugin.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/DiscoveredPluginSource.java
@@ -4,12 +4,13 @@
import software.coley.recaf.util.io.ByteSource;
/**
- * Discovered plugin.
+ * Provider for a {@link ByteSource} to point to a newly discovered plugin file.
+ *
+ * @author xDark
*/
-public interface DiscoveredPlugin {
-
+public interface DiscoveredPluginSource {
/**
- * @return Plugin file.
+ * @return Source to load from the plugin file.
*/
@Nonnull
ByteSource source();
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PathPluginDiscoverer.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PathPluginDiscoverer.java
index a8964ff8f..7b33b7243 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PathPluginDiscoverer.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PathPluginDiscoverer.java
@@ -8,21 +8,26 @@
import java.util.List;
import java.util.stream.Stream;
+/**
+ * A common base for {@link Path} backed plugin discoverers.
+ *
+ * @author xDark
+ */
public abstract class PathPluginDiscoverer implements PluginDiscoverer {
-
@Nonnull
@Override
- public final List findAll() throws PluginException {
+ public final List findSources() throws PluginException {
try (Stream s = stream()) {
- return s
- .map(path -> (DiscoveredPlugin) () -> ByteSources.forPath(path))
+ return s.map(path -> (DiscoveredPluginSource) () -> ByteSources.forPath(path))
.toList();
}
}
/**
- * @return A stream of paths.
- * @throws PluginException If discovery fails.
+ * @return A stream of paths to treat as plugin sources.
+ *
+ * @throws PluginException
+ * If discovery fails.
*/
@Nonnull
protected abstract Stream stream() throws PluginException;
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PluginDiscoverer.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PluginDiscoverer.java
index 74125523c..3a2572acc 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PluginDiscoverer.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/discovery/PluginDiscoverer.java
@@ -6,16 +6,17 @@
import java.util.List;
/**
- * Plugin discoverer.
+ * Plugin source discoverer.
*
* @author xDark
*/
public interface PluginDiscoverer {
-
/**
- * @return A list of discovered plugins.
- * @throws PluginException If discovery fails.
+ * @return A list of discovered plugin sources.
+ *
+ * @throws PluginException
+ * If discovery fails.
*/
@Nonnull
- List findAll() throws PluginException;
+ List findSources() throws PluginException;
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/LocalFileHeaderStreamHandler.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/LocalFileHeaderStreamHandler.java
index 2329fcee5..de8ba7e83 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/LocalFileHeaderStreamHandler.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/LocalFileHeaderStreamHandler.java
@@ -31,7 +31,7 @@ public void connect() throws IOException {
public InputStream getInputStream() throws IOException {
InputStream in = this.in;
if (in == null) {
- if (archiveView.closed)
+ if (archiveView.isClosed())
throw new IOException("Archive is closed");
this.in = in = blackbox(header); // TODO Blocked on ll-java-zip.
}
@@ -40,5 +40,5 @@ public InputStream getInputStream() throws IOException {
};
}
- private static native T blackbox(Object value);
+ private static native T blackbox(Object value);
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipArchiveView.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipArchiveView.java
index a8e8e0f4e..8965abb92 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipArchiveView.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipArchiveView.java
@@ -1,5 +1,6 @@
package software.coley.recaf.services.plugin.zip;
+import jakarta.annotation.Nonnull;
import software.coley.lljzip.format.model.CentralDirectoryFileHeader;
import software.coley.lljzip.format.model.LocalFileHeader;
import software.coley.lljzip.format.model.ZipArchive;
@@ -9,14 +10,21 @@
import java.util.List;
import java.util.Map;
+/**
+ * Exposes a {@link ZipArchive}'s contents as a {@code Map}.
+ *
+ * @author xDark
+ */
final class ZipArchiveView implements AutoCloseable {
- final ZipArchive archive; // keep alive
- final Map names;
- volatile boolean closed;
+ private final ZipArchive archive; // keep alive
+ private final Map names;
+ private volatile boolean closed;
- ZipArchiveView(ZipArchive archive) {
+ ZipArchiveView(@Nonnull ZipArchive archive) {
this.archive = archive;
Map names;
+
+ // Populate view from authoritative central directory entries if possible.
List centralDirectories = archive.getCentralDirectories();
if (!centralDirectories.isEmpty()) {
names = HashMap.newHashMap(centralDirectories.size());
@@ -25,6 +33,7 @@ final class ZipArchiveView implements AutoCloseable {
names.putIfAbsent(name, cdf.getLinkedFileHeader());
}
} else {
+ // Fall back to local file entries id central directory entries do not exist.
List localFiles = archive.getLocalFiles();
names = HashMap.newHashMap(localFiles.size());
for (LocalFileHeader localFile : localFiles) {
@@ -41,8 +50,33 @@ public void close() throws IOException {
if (closed) return;
closed = true;
}
+
+ // Try-with to auto-close the archive when complete.
try (archive) {
names.clear();
}
}
+
+ /**
+ * @return {@code true} when the backing archive is released.
+ */
+ public boolean isClosed() {
+ return closed;
+ }
+
+ /**
+ * @return Backing archive.
+ */
+ @Nonnull
+ public ZipArchive getArchive() {
+ return archive;
+ }
+
+ /**
+ * @return Archive entries as a map of internal paths.
+ */
+ @Nonnull
+ public Map getEntries() {
+ return names;
+ }
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPluginLoader.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPluginLoader.java
index 4f30dddf8..b43578e68 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPluginLoader.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPluginLoader.java
@@ -12,16 +12,12 @@
import software.coley.cafedude.classfile.attribute.AnnotationsAttribute;
import software.coley.cafedude.classfile.attribute.Attribute;
import software.coley.cafedude.io.ClassFileReader;
+import software.coley.collections.Unchecked;
import software.coley.lljzip.ZipIO;
import software.coley.lljzip.format.model.ZipArchive;
-import software.coley.recaf.plugin.Plugin;
-import software.coley.recaf.plugin.PluginException;
-import software.coley.recaf.plugin.PluginInfo;
-import software.coley.recaf.plugin.PluginInformation;
-import software.coley.recaf.plugin.PluginLoader;
+import software.coley.recaf.plugin.*;
import software.coley.recaf.services.plugin.PreparedPlugin;
import software.coley.recaf.util.IOUtil;
-import software.coley.recaf.util.Unchecked;
import software.coley.recaf.util.io.ByteSource;
import java.io.BufferedReader;
@@ -64,6 +60,7 @@ public PreparedPlugin prepare(@Nonnull ByteSource source) throws PluginException
if (resPluginImplementation == null) {
throw new PluginException("Cannot find %s resource".formatted(SERVICE_PATH));
}
+
// Cannot use ServiceLoader here, it will attempt to instantiate the plugin,
// which is what we *don't* want at this point.
String pluginClassName;
@@ -77,11 +74,13 @@ public PreparedPlugin prepare(@Nonnull ByteSource source) throws PluginException
}
}
}
+
// Find plugin class.
ByteSource resPluginClass = zs.findResource(pluginClassName.replace('.', '/') + ".class");
if (resPluginClass == null) {
throw new PluginException("Plugin class %s doesn't exist".formatted(pluginClassName));
}
+
// Find @PluginInformation annotation.
ClassFile cf;
try (InputStream in = resPluginClass.openStream()) {
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPreparedPlugin.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPreparedPlugin.java
index da40f3b37..a577b476e 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPreparedPlugin.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipPreparedPlugin.java
@@ -1,19 +1,24 @@
package software.coley.recaf.services.plugin.zip;
import jakarta.annotation.Nonnull;
-import software.coley.recaf.plugin.PluginSource;
import software.coley.recaf.plugin.PluginException;
import software.coley.recaf.plugin.PluginInfo;
+import software.coley.recaf.plugin.PluginSource;
import software.coley.recaf.services.plugin.PreparedPlugin;
import java.io.IOException;
+/**
+ * ZIP backed prepared plugin implementation.
+ *
+ * @author xDark
+ */
final class ZipPreparedPlugin implements PreparedPlugin {
private final PluginInfo pluginInfo;
private final String pluginClassName;
private final ZipSource classLoader;
- ZipPreparedPlugin(PluginInfo pluginInfo, String pluginClassName, ZipSource classLoader) {
+ ZipPreparedPlugin(@Nonnull PluginInfo pluginInfo, @Nonnull String pluginClassName, @Nonnull ZipSource classLoader) {
this.pluginInfo = pluginInfo;
this.pluginClassName = pluginClassName;
this.classLoader = classLoader;
diff --git a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipSource.java b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipSource.java
index fa1bd3ad0..488943222 100644
--- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipSource.java
+++ b/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/ZipSource.java
@@ -1,5 +1,6 @@
package software.coley.recaf.services.plugin.zip;
+import jakarta.annotation.Nonnull;
import software.coley.lljzip.format.model.LocalFileHeader;
import software.coley.recaf.plugin.PluginSource;
import software.coley.recaf.util.io.ByteSource;
@@ -7,21 +8,26 @@
import java.io.IOException;
+/**
+ * ZIP backed plugin source.
+ *
+ * @author xDark
+ */
final class ZipSource implements PluginSource, AutoCloseable {
private final ZipArchiveView archiveView;
- ZipSource(ZipArchiveView archiveView) {
+ ZipSource(@Nonnull ZipArchiveView archiveView) {
this.archiveView = archiveView;
}
@Override
public ByteSource findResource(String name) {
ZipArchiveView archiveView = this.archiveView;
- if (archiveView.closed)
+ if (archiveView.isClosed())
return null;
synchronized (this) {
- if (archiveView.closed) return null;
- LocalFileHeader file = archiveView.names.get(name);
+ if (archiveView.isClosed()) return null;
+ LocalFileHeader file = archiveView.getEntries().get(name);
if (file == null) return null;
return new LocalFileHeaderSource(file);
}
diff --git a/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java
index 3173e8f6d..3f554d8dd 100644
--- a/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java
+++ b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java
@@ -1,11 +1,10 @@
package software.coley.recaf.util.io;
import jakarta.annotation.Nonnull;
-import software.coley.recaf.util.IOUtil;
+import software.coley.lljzip.util.MemorySegmentInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
@@ -55,158 +54,7 @@ public InputStream openStream() {
@Nonnull
@Override
- public MemorySegment mmap() throws IOException {
+ public MemorySegment mmap() {
return data;
}
-
- // TODO: Replace this class with the one from LL-J-Zip when the next release is made
- static final class MemorySegmentInputStream extends InputStream {
- private final MemorySegment data;
- private long read;
- private long markedOffset = -1;
- private long markedLimit;
- private volatile boolean closed;
-
- MemorySegmentInputStream(MemorySegment data) {
- this.data = data;
- }
-
- private void checkMarkLimit() {
- if (markedOffset > -1) {
- // Discard if we passed the read limit for our mark
- long diff = read - markedOffset;
- if (diff > markedLimit) {
- markedOffset = -1;
- }
- }
- }
-
- @Override
- public boolean markSupported() {
- return true;
- }
-
- @Override
- public synchronized void mark(int limit) {
- // Record current position and read-limit
- markedOffset = read;
- markedLimit = limit;
- }
-
- @Override
- public synchronized void reset() {
- // Revert read to marked position.
- read = markedOffset;
- }
-
- @Override
- public int read() throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- if (read >= data.byteSize()) {
- return -1;
- }
- byte b = data.get(ValueLayout.JAVA_BYTE, read++);
- checkMarkLimit();
- return b & 0xff;
- }
-
- @Override
- public int read(@Nonnull byte[] b, int off, int len) throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- long read = this.read;
- long length = data.byteSize();
- if (read >= length) {
- return -1;
- }
- long remaining = length - read;
- len = (int) Math.min(remaining, len);
- MemorySegment.copy(data, read, MemorySegment.ofArray(b), off, len);
- this.read += len;
- checkMarkLimit();
- return len;
- }
-
- @Override
- public byte[] readNBytes(int len) throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- long read = this.read;
- long length = data.byteSize();
- if (read >= length) {
- return new byte[0];
- }
- long remaining = length - read;
- len = (int) Math.min(remaining, len);
- byte[] buf = new byte[len];
- MemorySegment.copy(data, read, MemorySegment.ofArray(buf), 0, len);
- this.read += len;
- checkMarkLimit();
- return buf;
- }
-
- @Override
- public long skip(long n) throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- long read = this.read;
- long length = data.byteSize();
- if (read >= length) {
- return 0;
- }
- n = Math.min(n, length - read);
- this.read += n;
- checkMarkLimit();
- return n;
- }
-
- @Override
- public int available() throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- long length = data.byteSize();
- long read = this.read;
- if (read >= length) {
- return 0;
- }
- long remaining = length - read;
- if (remaining > Integer.MAX_VALUE)
- return Integer.MAX_VALUE;
- return (int) remaining;
- }
-
- @Override
- public void close() throws IOException {
- closed = true;
- }
-
- @Override
- public long transferTo(OutputStream out) throws IOException {
- ensureOpen();
- MemorySegment data = this.data;
- long length = data.byteSize();
- long read = this.read;
- if (read >= length) {
- return 0L;
- }
- long remaining = length - read;
- byte[] buffer = IOUtil.newByteBuffer();
- MemorySegment bufferSegment = MemorySegment.ofArray(buffer);
- while (read < length) {
- int copyable = (int) Math.min(buffer.length, length - read);
- MemorySegment.copy(data, read, bufferSegment, 0, copyable);
- out.write(buffer, 0, copyable);
- read += copyable;
- }
- this.read = length;
- checkMarkLimit();
- return remaining;
- }
-
- private void ensureOpen() throws IOException {
- if (closed)
- throw new IOException("Stream closed");
- }
- }
}
diff --git a/recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java b/recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java
index 55b668e97..50c9d2475 100644
--- a/recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java
+++ b/recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java
@@ -48,50 +48,10 @@ void testLoadAndUnload() throws IOException {
.intercept(FixedValue.originType())
.defineMethod("onDisable", void.class, Modifier.PUBLIC)
.intercept(FixedValue.originType())
- .annotateType(new PluginInformation() {
- @Override
- public Class extends Annotation> annotationType() {
- return PluginInformation.class;
- }
-
- @Override
- public String id() {
- return id;
- }
-
- @Override
- public String name() {
- return name;
- }
-
- @Override
- public String version() {
- return version;
- }
-
- @Override
- public String author() {
- return author;
- }
-
- @Override
- public String description() {
- return description;
- }
-
- @Override
- public String[] dependencies() {
- return new String[0];
- }
-
- @Override
- public String[] softDependencies() {
- return new String[0];
- }
- }).make();
+ .annotateType(new PluginInformationRecord(id, name, version, author, description)).make();
byte[] zip = ZipCreationUtils.createZip(Map.of(
"test/PluginTest.class", unloaded.getBytes(),
- ZipPluginLoader.SERVICE_PATH, className.getBytes(StandardCharsets.UTF_8)
+ ZipPluginLoader.SERVICE_PATH, className.getBytes(StandardCharsets.UTF_8)
));
try {
@@ -120,7 +80,7 @@ public String[] softDependencies() {
assertSame(container, pluginManager.getPlugin(id));
// Now unload it
- pluginManager.unloadPlugin(id).commit();
+ pluginManager.unloaderFor(id).commit();
// Assert the plugin is no longer active
assertEquals(0, pluginManager.getPlugins().size());
@@ -129,4 +89,28 @@ public String[] softDependencies() {
fail("Failed to load plugin", ex);
}
}
+
+ // TODO: Test plugin dependency resolving
+ // - "A depends on B, B depends on C"
+ // - Given 'A, B, C' try and load 'A' - should load the dependencies
+
+ @SuppressWarnings("all")
+ private record PluginInformationRecord(String id, String name, String version, String author,
+ String description) implements PluginInformation {
+ @Override
+ public Class extends Annotation> annotationType() {
+ return PluginInformation.class;
+ }
+
+
+ @Override
+ public String[] dependencies() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] softDependencies() {
+ return new String[0];
+ }
+ }
}
diff --git a/recaf-ui/src/main/java/software/coley/recaf/Main.java b/recaf-ui/src/main/java/software/coley/recaf/Main.java
index f0ad5625b..f7ab1ccd5 100644
--- a/recaf-ui/src/main/java/software/coley/recaf/Main.java
+++ b/recaf-ui/src/main/java/software/coley/recaf/Main.java
@@ -176,7 +176,6 @@ private static void initTranslations() {
* Load plugins.
*/
private static void initPlugins() {
- // Plugin loading is handled in the implementation's @PostConstruct handler
PluginManager pluginManager = recaf.get(PluginManager.class);
// Log the discovered plugins