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 pluginClass = (Class) 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 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 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