Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package software.coley.recaf.services.analysis.entry;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.recaf.info.member.MethodMember;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.path.PathNodes;
import software.coley.recaf.services.inheritance.InheritanceGraph;
import software.coley.recaf.services.inheritance.InheritanceGraphService;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;

import java.util.ArrayList;
import java.util.List;

/**
* Discovery process for locating entry points in Bukkit plugins.
*
* @author Matt Coley
* @author TheWakz
* @see <a href="https://docs.papermc.io/paper/dev/how-do-plugins-work/#plugin-lifecycle">Bukkit plugin entry points</a>
*/
@ApplicationScoped
public class BukkitPluginEntryPointDiscovery implements EntryPointDiscovery {
private final InheritanceGraphService inheritanceGraphService;

@Inject
public BukkitPluginEntryPointDiscovery(@Nonnull InheritanceGraphService inheritanceGraphService) {
this.inheritanceGraphService = inheritanceGraphService;
}

@Nonnull
@Override
public EntryPointKind kind() {
return EntryPointKind.MC_BUKKIT_PLUGIN_INIT;
}

@Nonnull
@Override
public List<EntryPoint> findEntryPoints(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource) {
InheritanceGraph graph = inheritanceGraphService.getOrCreateInheritanceGraph(workspace);
List<EntryPoint> entries = new ArrayList<>();
resource.jvmAllClassBundleStream().forEach(bundle -> bundle.forEach(cls -> {
String className = cls.getName();
ClassPathNode classPath = null;
Boolean isPlugin = null;
for (MethodMember method : cls.getMethods()) {
// Must be a plugin enable or load method name.
String methodName = method.getName();
if (!methodName.equals("onEnable") && !methodName.equals("onLoad"))
continue;

// Lazily check if this class is a plugin subtype.
if (isPlugin == null)
isPlugin = isPlugin(graph, className);

// Skip methods if the class is not a valid plugin.
if (!isPlugin)
continue;

// Add the entry point.
if (classPath == null)
classPath = PathNodes.classPath(workspace, resource, bundle, cls);
entries.add(new EntryPoint(kind(), classPath, classPath.child(method)));
}
}));
return entries;
}

private static boolean isPlugin(@Nonnull InheritanceGraph graph, @Nonnull String className) {
return graph.isAssignableFrom("org/bukkit/plugin/java/JavaPlugin", className);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ public record EntryPointKind(@Nonnull String id, @Nonnull String displayName) {
public static final EntryPointKind JVM_MAIN_METHOD = new EntryPointKind("jvm-main-method", "Java main(String[])");
public static final EntryPointKind ANDROID_ACTIVITY = new EntryPointKind("android-activity", "Android activity");
public static final EntryPointKind MC_FABRIC_MOD_INIT = new EntryPointKind("mc.fabric", "Fabric mod initializer");
public static final EntryPointKind MC_FORGE_MOD_INIT = new EntryPointKind("mc.forge", "Fabric mod initializer");
public static final EntryPointKind MC_FORGE_MOD_INIT = new EntryPointKind("mc.forge", "Forge mod initializer");
public static final EntryPointKind MC_BUKKIT_PLUGIN_INIT = new EntryPointKind("mc.bukkit", "Bukkit plugin initializer");
public static final EntryPointKind MC_VELOCITY_PLUGIN_INIT = new EntryPointKind("mc.velocity", "Velocity plugin initializer");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package software.coley.recaf.services.analysis.entry;

import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import software.coley.collections.Sets;
import software.coley.recaf.info.annotation.AnnotationInfo;
import software.coley.recaf.info.member.MethodMember;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.path.PathNodes;
import software.coley.recaf.services.inheritance.InheritanceGraph;
import software.coley.recaf.services.inheritance.InheritanceGraphService;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Discovery process for locating entry points in Velocity plugins.
*
* @author Matt Coley
* @author TheWakz
* @see <a href="https://docs.papermc.io/velocity/dev/api-basics/#create-the-plugin-class">Velocity plugin entry points</a>
*/
@ApplicationScoped
public class VelocityPluginEntryPointDiscovery implements EntryPointDiscovery {
private final InheritanceGraphService inheritanceGraphService;

@Inject
public VelocityPluginEntryPointDiscovery(@Nonnull InheritanceGraphService inheritanceGraphService) {
this.inheritanceGraphService = inheritanceGraphService;
}

@Nonnull
@Override
public EntryPointKind kind() {
return EntryPointKind.MC_VELOCITY_PLUGIN_INIT;
}

@Nonnull
@Override
public List<EntryPoint> findEntryPoints(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource) {
InheritanceGraph graph = inheritanceGraphService.getOrCreateInheritanceGraph(workspace);
List<EntryPoint> entries = new ArrayList<>();
resource.jvmAllClassBundleStream().forEach(bundle -> bundle.forEach(cls -> {
ClassPathNode classPath = null;
EntryPoint classEntry = null;

// Check for @Plugin annotations
Set<String> annotations = Sets.combine(
cls.getAnnotations().stream().map(AnnotationInfo::getDescriptor).collect(Collectors.toSet()),
cls.getTypeAnnotations().stream().map(AnnotationInfo::getDescriptor).collect(Collectors.toSet())
);
for (String annotation : annotations) {
if ("Lcom/velocitypowered/api/plugin/Plugin;".equals(annotation)) {
classPath = PathNodes.classPath(workspace, resource, bundle, cls);
classEntry = new EntryPoint(kind(), classPath, null);
break;
}
}

// Check for initialization event receivers
boolean foundEventReceiver = false;
for (MethodMember method : cls.getMethods()) {
String desc = method.getDescriptor();
if ("(Lcom/velocitypowered/api/event/proxy/ProxyInitializeEvent;)V".equals(desc)
|| "(Lcom/velocitypowered/api/event/proxy/ProxyReloadEvent;)V".equals(desc)
) {
if (classPath == null)
classPath = PathNodes.classPath(workspace, resource, bundle, cls);
entries.add(new EntryPoint(kind(), classPath, classPath.child(method)));
foundEventReceiver = true;
}
}

// Only add the class as an entry point if it wasn't already added as an event receiver.
// The event receiver is more specific and the containing class shares the same path.
if (!foundEventReceiver && classEntry != null)
entries.add(classEntry);
}));
return entries;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,84 @@ void findsAndroidActivityEntryPoint() {
assertEquals(entry.classPath(), entry.targetPath());
}

@Test
void findsFabricModEntryPoint() {
JvmClassInfo fabricClass = createClass("test/TestFabricMod", node -> {
node.interfaces.add("net/fabricmc/api/ModInitializer");

MethodNode initMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onInitialize", "()V", null, null);
initMethod.visitCode();
initMethod.visitInsn(Opcodes.RETURN);
initMethod.visitEnd();
node.methods.add(initMethod);
});

Workspace workspace = fromBundle(fromClasses(fabricClass));
List<EntryPoint> results = service.findEntryPoints(workspace, workspace.getPrimaryResource());

assertEquals(1, results.size(), "Should find exactly one entry point");
assertEquals(EntryPointKind.MC_FABRIC_MOD_INIT, results.getFirst().kind(), "Should be Fabric entry point");
}

@Test
void findsForgeModEntryPoint() {
JvmClassInfo forgeModClass = createClass("test/TestForgeMod", node -> {
node.visitAnnotation("Lnet/minecraftforge/fml/common/Mod;", true).visitEnd();

MethodNode setupMethod = new MethodNode(Opcodes.ACC_PUBLIC, "setup",
"(Lnet/minecraftforge/fml/event/lifecycle/FMLCommonSetupEvent;)V", null, null);
setupMethod.visitCode();
setupMethod.visitInsn(Opcodes.RETURN);
setupMethod.visitEnd();
node.methods.add(setupMethod);
});

Workspace workspace = fromBundle(fromClasses(forgeModClass));
List<EntryPoint> results = service.findEntryPoints(workspace, workspace.getPrimaryResource());

assertEquals(1, results.size(), "Should find exactly one entry point");
assertEquals(EntryPointKind.MC_FORGE_MOD_INIT, results.getFirst().kind(), "Should be Forge entry point");
}

@Test
void findsBukkitPluginEntryPoint() {
JvmClassInfo bukkitPluginClass = createClass("test/TestBukkitPlugin", node -> {
node.superName = "org/bukkit/plugin/java/JavaPlugin";

MethodNode onEnableMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onEnable", "()V", null, null);
onEnableMethod.visitCode();
onEnableMethod.visitInsn(Opcodes.RETURN);
onEnableMethod.visitEnd();
node.methods.add(onEnableMethod);
});

Workspace workspace = fromBundle(fromClasses(bukkitPluginClass));
List<EntryPoint> results = service.findEntryPoints(workspace, workspace.getPrimaryResource());

assertEquals(1, results.size(), "Should find exactly one entry point");
assertEquals(EntryPointKind.MC_BUKKIT_PLUGIN_INIT, results.getFirst().kind(), "Should be Bukkit entry point");
}

@Test
void findsVelocityPluginEntryPoint() {
JvmClassInfo velocityPluginClass = createClass("test/TestVelocityPlugin", node -> {
node.visitAnnotation("Lcom/velocitypowered/api/plugin/Plugin;", true).visitEnd();

MethodNode initMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onInit",
"(Lcom/velocitypowered/api/event/proxy/ProxyInitializeEvent;)V", null, null);
initMethod.visitCode();
initMethod.visitInsn(Opcodes.RETURN);
initMethod.visitEnd();
node.methods.add(initMethod);
});

Workspace workspace = fromBundle(fromClasses(velocityPluginClass));
List<EntryPoint> results = service.findEntryPoints(workspace, workspace.getPrimaryResource());

assertEquals(1, results.size(), "Should find exactly one entry point");
assertEquals(EntryPointKind.MC_VELOCITY_PLUGIN_INIT, results.getFirst().kind(), "Should be Velocity entry point");
}

@Test
void supportsRuntimeDiscoveryRegistration() throws IOException {
Workspace workspace = fromBundle(fromClasses(HelloWorld.class));
Expand Down
2 changes: 2 additions & 0 deletions recaf-ui/src/main/resources/translations/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,8 @@ service.analysis.entry-points.kind.jvm-main-method=Java main(String[])
service.analysis.entry-points.kind.android-activity=Android activity
service.analysis.entry-points.kind.mc.fabric=Fabric mod initializer
service.analysis.entry-points.kind.mc.forge=Forge mod initializer
service.analysis.entry-points.kind.mc.bukkit=Bukkit plugin initializer
service.analysis.entry-points.kind.mc.velocity=Velocity plugin initializer
service.analysis.entry-points.none=No entries found
service.analysis.hashing=Hashing
service.analysis.hashing.type=Type
Expand Down
Loading