diff --git a/recaf-core/src/main/java/software/coley/recaf/plugin/Plugin.java b/recaf-core/src/main/java/software/coley/recaf/plugin/Plugin.java index bf7ed26e0..2a8d794b4 100644 --- a/recaf-core/src/main/java/software/coley/recaf/plugin/Plugin.java +++ b/recaf-core/src/main/java/software/coley/recaf/plugin/Plugin.java @@ -2,6 +2,7 @@ /** * Base interface that all plugins must inherit from. + * Classes that implement this type should also be annotated with {@link PluginInformation}. * * @author xDark */ 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 0fa88a2c2..6645520ec 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 @@ -9,12 +9,17 @@ import java.io.InputStream; import java.net.*; +/** + * Classloader for plugin content. + * + * @author xDark + */ final class PluginClassLoaderImpl extends ClassLoader implements PluginClassLoader { private final PluginGraph graph; private final PluginSource source; private final String id; - PluginClassLoaderImpl(PluginGraph graph, PluginSource source, String id) { + PluginClassLoaderImpl(@Nonnull PluginGraph graph, @Nonnull PluginSource source, @Nonnull String id) { this.graph = graph; this.source = source; this.id = id; @@ -27,15 +32,16 @@ protected URL findResource(String name) { return null; } try { - URI uri = new URI("recaf", "", name); + URI uri = new URI("recaf", "/", name); return URL.of(uri, new URLStreamHandler() { @Override - protected URLConnection openConnection(URL u) throws IOException { + protected URLConnection openConnection(URL u) { return new URLConnection(u) { InputStream in; @Override - public void connect() throws IOException { + public void connect() { + // no-op } @Override 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 deleted file mode 100644 index de8ba7e83..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/services/plugin/zip/LocalFileHeaderStreamHandler.java +++ /dev/null @@ -1,44 +0,0 @@ -package software.coley.recaf.services.plugin.zip; - -import software.coley.lljzip.format.model.LocalFileHeader; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; - -final class LocalFileHeaderStreamHandler extends URLStreamHandler { - private final ZipArchiveView archiveView; // keep alive - final LocalFileHeader header; - - LocalFileHeaderStreamHandler(ZipArchiveView archiveView, LocalFileHeader header) { - this.archiveView = archiveView; - this.header = header; - } - - @Override - protected URLConnection openConnection(URL u) throws IOException { - return new URLConnection(u) { - InputStream in; - - @Override - public void connect() throws IOException { - - } - - @Override - public InputStream getInputStream() throws IOException { - InputStream in = this.in; - if (in == null) { - if (archiveView.isClosed()) - throw new IOException("Archive is closed"); - this.in = in = blackbox(header); // TODO Blocked on ll-java-zip. - } - return in; - } - }; - } - - private static native T blackbox(Object value); -} 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 b43578e68..287377396 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 @@ -62,7 +62,7 @@ public PreparedPlugin prepare(@Nonnull ByteSource source) throws PluginException } // Cannot use ServiceLoader here, it will attempt to instantiate the plugin, - // which is what we *don't* want at this point. + // which is what we *don't* want at this point. String pluginClassName; try (BufferedReader reader = IOUtil.toBufferedReader(resPluginImplementation.openStream())) { pluginClassName = reader.readLine(); @@ -117,7 +117,8 @@ public PreparedPlugin prepare(@Nonnull ByteSource source) throws PluginException } } - private static PluginInfo parsePluginInfo(Annotation annotation) throws PluginException { + @Nonnull + private static PluginInfo parsePluginInfo(@Nonnull Annotation annotation) throws PluginException { PluginInfo info = PluginInfo.empty(); for (var e : annotation.getValues().entrySet()) { String name = e.getKey().getText(); @@ -136,12 +137,13 @@ private static PluginInfo parsePluginInfo(Annotation annotation) throws PluginEx return info; } + @Nonnull private static String servicePath() { return "META-INF/services/%s".formatted(Plugin.class.getName()); } @SafeVarargs - private static R extractValue(ElementValue value, Function extractor, V... typeHint) throws PluginException { + private static R extractValue(@Nonnull ElementValue value, Function extractor, V... typeHint) throws PluginException { Class type = typeHint.getClass().getComponentType(); V v; try { @@ -153,11 +155,13 @@ private static R extractValue(ElementValue value, Fu return extractor.apply(v); } - private static String string(ElementValue value) throws PluginException { + @Nonnull + private static String string(@Nonnull ElementValue value) throws PluginException { return extractValue(value, (Utf8ElementValue elem) -> elem.getValue().getText()); } - private static Set stringSet(ElementValue value) throws PluginException { + @Nonnull + private static Set stringSet(@Nonnull ElementValue value) throws PluginException { return extractValue(value, (ArrayElementValue array) -> array .getArray() .stream() 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 41734f02f..77b2380e7 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 @@ -6,6 +6,10 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; import software.coley.recaf.plugin.*; import software.coley.recaf.services.plugin.discovery.DiscoveredPluginSource; import software.coley.recaf.services.plugin.discovery.PluginDiscoverer; @@ -24,6 +28,7 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.*; +import static org.objectweb.asm.Opcodes.*; /** * Tests for {@link PluginManager} @@ -147,6 +152,160 @@ void testDependentChain() throws IOException { } } + @Test + void testPluginWithResourceLoading() throws IOException { + String className = "DummyPluginBody"; + byte[] classBytes; + { + /* + @PluginInformation(id = "dummy", name = "dummy", version = "1.0") + public class DummyPluginBody implements Plugin { + @Override + public void onEnable() { + try (var s = getClass().getResourceAsStream("file.txt")) { + System.out.println(new String(s.readAllBytes())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public void onDisable() {} + } + */ + + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + cw.visit(V22, ACC_PUBLIC | ACC_SUPER, className, null, "java/lang/Object", new String[]{"software/coley/recaf/plugin/Plugin"}); + AnnotationVisitor av = cw.visitAnnotation("Lsoftware/coley/recaf/plugin/PluginInformation;", true); + av.visit("id", "dummy"); + av.visit("name", "dummy"); + av.visit("version", "1.0"); + av.visitEnd(); + MethodVisitor mv; + { + mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitLabel(new Label()); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false); + mv.visitInsn(RETURN); + mv.visitLabel(new Label()); + mv.visitMaxs(1, 1); + mv.visitEnd(); + } + { + mv = cw.visitMethod(ACC_PUBLIC, "onEnable", "()V", null, null); + mv.visitCode(); + Label label0 = new Label(); + Label label1 = new Label(); + Label label2 = new Label(); + mv.visitTryCatchBlock(label0, label1, label2, "java/lang/Throwable"); + Label label3 = new Label(); + Label label4 = new Label(); + Label label5 = new Label(); + mv.visitTryCatchBlock(label3, label4, label5, "java/lang/Throwable"); + Label label6 = new Label(); + Label label7 = new Label(); + Label label8 = new Label(); + mv.visitTryCatchBlock(label6, label7, label8, "java/lang/Exception"); + mv.visitLabel(label6); + mv.visitLineNumber(10, label6); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false); + mv.visitLdcInsn("file.txt"); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getResourceAsStream", "(Ljava/lang/String;)Ljava/io/InputStream;", false); + mv.visitVarInsn(ASTORE, 1); + mv.visitLabel(label0); + mv.visitLineNumber(11, label0); + mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); + mv.visitTypeInsn(NEW, "java/lang/String"); + mv.visitInsn(DUP); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/InputStream", "readAllBytes", "()[B", false); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/String", "", "([B)V", false); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); + mv.visitLabel(label1); + mv.visitLineNumber(12, label1); + mv.visitVarInsn(ALOAD, 1); + mv.visitJumpInsn(IFNULL, label7); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/InputStream", "close", "()V", false); + mv.visitJumpInsn(GOTO, label7); + mv.visitLabel(label2); + mv.visitLineNumber(10, label2); + mv.visitVarInsn(ASTORE, 2); + mv.visitVarInsn(ALOAD, 1); + Label label9 = new Label(); + mv.visitJumpInsn(IFNULL, label9); + mv.visitLabel(label3); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/InputStream", "close", "()V", false); + mv.visitLabel(label4); + mv.visitJumpInsn(GOTO, label9); + mv.visitLabel(label5); + mv.visitVarInsn(ASTORE, 3); + mv.visitVarInsn(ALOAD, 2); + mv.visitVarInsn(ALOAD, 3); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Throwable", "addSuppressed", "(Ljava/lang/Throwable;)V", false); + mv.visitLabel(label9); + mv.visitVarInsn(ALOAD, 2); + mv.visitInsn(ATHROW); + mv.visitLabel(label7); + mv.visitLineNumber(14, label7); + Label label10 = new Label(); + mv.visitJumpInsn(GOTO, label10); + mv.visitLabel(label8); + mv.visitLineNumber(12, label8); + mv.visitVarInsn(ASTORE, 1); + Label label11 = new Label(); + mv.visitLabel(label11); + mv.visitLineNumber(13, label11); + mv.visitTypeInsn(NEW, "java/lang/RuntimeException"); + mv.visitInsn(DUP); + mv.visitVarInsn(ALOAD, 1); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "", "(Ljava/lang/Throwable;)V", false); + mv.visitInsn(ATHROW); + mv.visitLabel(label10); + mv.visitLineNumber(15, label10); + mv.visitInsn(RETURN); + Label label12 = new Label(); + mv.visitLabel(label12); + mv.visitLocalVariable("s", "Ljava/io/InputStream;", null, label0, label7, 1); + mv.visitLocalVariable("e", "Ljava/lang/Exception;", null, label11, label10, 1); + mv.visitLocalVariable("this", "Lsoftware/coley/recaf/services/plugin/DummyPluginBody;", null, label6, label12, 0); + mv.visitMaxs(4, 4); + mv.visitEnd(); + } + { + mv = cw.visitMethod(ACC_PUBLIC, "onDisable", "()V", null, null); + mv.visitCode(); + mv.visitLabel(new Label()); + mv.visitInsn(RETURN); + mv.visitLabel(new Label()); + mv.visitMaxs(0, 1); + mv.visitEnd(); + } + cw.visitEnd(); + classBytes = cw.toByteArray(); + } + byte[] zip = ZipCreationUtils.createZip(Map.of( + className + ".class", classBytes, + ZipPluginLoader.SERVICE_PATH, className.getBytes(StandardCharsets.UTF_8), + "file.txt", "Dummy plugin says 'hello world'!".getBytes(StandardCharsets.UTF_8) + )); + + try { + // Load the plugin + ByteSource pluginSource = ByteSources.wrap(zip); + PluginDiscoverer discoverer = () -> List.of(() -> pluginSource); + PluginContainer container = pluginManager.loadPlugins(discoverer).iterator().next(); + + // Now unload it + pluginManager.unloaderFor("dummy").commit(); + } catch (PluginException ex) { + fail("Failed to load plugin", ex); + } + } + @SuppressWarnings("all") private record PluginInformationRecord(String id, String name, String version, String author, String[] dependencies, String description) implements PluginInformation {