Skip to content

Commit

Permalink
Add test for plugin reading resource from its container
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Jun 16, 2024
1 parent 41583f2 commit 5c44049
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,15 +32,16 @@ protected URL findResource(String name) {
return null;

Check warning on line 32 in recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java#L32

Added line #L32 was not covered by tests
}
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
}

Check warning on line 45 in recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/plugin/PluginClassLoaderImpl.java#L45

Added line #L45 was not covered by tests

@Override
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand All @@ -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 <V extends ElementValue, R> R extractValue(ElementValue value, Function<V, R> extractor, V... typeHint) throws PluginException {
private static <V extends ElementValue, R> R extractValue(@Nonnull ElementValue value, Function<V, R> extractor, V... typeHint) throws PluginException {
Class<?> type = typeHint.getClass().getComponentType();
V v;
try {
Expand All @@ -153,11 +155,13 @@ private static <V extends ElementValue, R> 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<String> stringSet(ElementValue value) throws PluginException {
@Nonnull
private static Set<String> stringSet(@Nonnull ElementValue value) throws PluginException {
return extractValue(value, (ArrayElementValue array) -> array
.getArray()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
Expand Down Expand Up @@ -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, "<init>", "()V", null, null);
mv.visitCode();
mv.visitLabel(new Label());
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()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", "<init>", "([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", "<init>", "(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);

Check warning on line 305 in recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/test/java/software/coley/recaf/services/plugin/PluginManagerTest.java#L304-L305

Added lines #L304 - L305 were not covered by tests
}
}

@SuppressWarnings("all")
private record PluginInformationRecord(String id, String name, String version, String author, String[] dependencies,
String description) implements PluginInformation {
Expand Down

0 comments on commit 5c44049

Please sign in to comment.