Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JADX decompiler #522

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ subprojects {
mavenLocal()
mavenCentral()
maven { url 'https://maven.fabricmc.net/' }
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' }
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public enum Decompiler {
VINEFLOWER("Vineflower", Decompilers.VINEFLOWER),
CFR("CFR", Decompilers.CFR),
JADX("JADX", Decompilers.JADX),
PROCYON("Procyon", Decompilers.PROCYON),
BYTECODE("Bytecode", Decompilers.BYTECODE);

Expand Down
10 changes: 10 additions & 0 deletions enigma/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ dependencies {
implementation 'net.fabricmc:cfr:0.2.2'
implementation 'org.vineflower:vineflower:1.10.0'

implementation ('io.github.skylot:jadx-core:1.5.0-20240408.212728-12') {
exclude group: 'com.android.tools.build', module: 'aapt2-proto'
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
implementation ('io.github.skylot:jadx-java-input:1.5.0-20240408.212728-12') {
exclude group: 'com.android.tools.build', module: 'aapt2-proto'
exclude group: 'io.github.skylot', module: 'raung-disasm'
}
implementation 'io.github.skylot:jadx-input-api:1.5.0-20240408.212728-12' // Pin version (would pull 1.5.0-SNAPSHOT otherwise)

proGuard 'com.guardsquare:proguard-base:7.4.0-beta02'

testImplementation 'com.google.jimfs:jimfs:1.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ private void registerEnumNamingService(EnigmaPluginContext ctx) {
private void registerDecompilerServices(EnigmaPluginContext ctx) {
ctx.registerService("enigma:vineflower", DecompilerService.TYPE, ctx1 -> Decompilers.VINEFLOWER);
ctx.registerService("enigma:cfr", DecompilerService.TYPE, ctx1 -> Decompilers.CFR);
ctx.registerService("enigma:jadx", DecompilerService.TYPE, ctx1 -> Decompilers.JADX);
ctx.registerService("enigma:procyon", DecompilerService.TYPE, ctx1 -> Decompilers.PROCYON);
ctx.registerService("enigma:bytecode", DecompilerService.TYPE, ctx1 -> Decompilers.BYTECODE);
}
Expand Down
2 changes: 2 additions & 0 deletions enigma/src/main/java/cuchaz/enigma/source/Decompilers.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import cuchaz.enigma.source.bytecode.BytecodeDecompiler;
import cuchaz.enigma.source.cfr.CfrDecompiler;
import cuchaz.enigma.source.jadx.JadxDecompiler;
import cuchaz.enigma.source.procyon.ProcyonDecompiler;
import cuchaz.enigma.source.vineflower.VineflowerDecompiler;

public class Decompilers {
public static final DecompilerService VINEFLOWER = VineflowerDecompiler::new;
public static final DecompilerService CFR = CfrDecompiler::new;
public static final DecompilerService JADX = JadxDecompiler::new;
public static final DecompilerService PROCYON = ProcyonDecompiler::new;
public static final DecompilerService BYTECODE = BytecodeDecompiler::new;
}
10 changes: 10 additions & 0 deletions enigma/src/main/java/cuchaz/enigma/source/jadx/CustomJadxArgs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cuchaz.enigma.source.jadx;

import jadx.api.JadxArgs;

import cuchaz.enigma.translation.mapping.EntryRemapper;

class CustomJadxArgs extends JadxArgs {
EntryRemapper mapper;
JadxHelper jadxHelper;
}
49 changes: 49 additions & 0 deletions enigma/src/main/java/cuchaz/enigma/source/jadx/JadxDecompiler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cuchaz.enigma.source.jadx;

import jadx.api.JadxArgs;
import jadx.api.impl.NoOpCodeCache;
import org.checkerframework.checker.nullness.qual.Nullable;

import cuchaz.enigma.classprovider.ClassProvider;
import cuchaz.enigma.source.Decompiler;
import cuchaz.enigma.source.Source;
import cuchaz.enigma.source.SourceSettings;
import cuchaz.enigma.translation.mapping.EntryRemapper;

public class JadxDecompiler implements Decompiler {
private final SourceSettings settings;
private final ClassProvider classProvider;

public JadxDecompiler(ClassProvider classProvider, SourceSettings sourceSettings) {
this.settings = sourceSettings;
this.classProvider = classProvider;
}

@Override
public Source getSource(String className, @Nullable EntryRemapper mapper) {
JadxHelper jadxHelper = new JadxHelper();

return new JadxSource(settings, mapperX -> createJadxArgs(mapperX, jadxHelper), classProvider.get(className), mapper, jadxHelper);
}

private JadxArgs createJadxArgs(EntryRemapper mapper, JadxHelper jadxHelper) {
CustomJadxArgs args = new CustomJadxArgs();
args.setCodeCache(NoOpCodeCache.INSTANCE);
args.setShowInconsistentCode(true);
args.setInlineAnonymousClasses(false);
args.setInlineMethods(false);
args.setRespectBytecodeAccModifiers(true);
args.setRenameValid(false);
args.setCodeIndentStr("\t");
args.setCodeNewLineStr("\n"); // JEditorPane is hardcoded to \n
args.mapper = mapper;
args.jadxHelper = jadxHelper;

if (settings.removeImports) {
// Commented out for now, since JADX would use full identifiers everywhere
// args.setUseImports(false);
}

return args;
}
}
68 changes: 68 additions & 0 deletions enigma/src/main/java/cuchaz/enigma/source/jadx/JadxHelper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cuchaz.enigma.source.jadx;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import jadx.api.ICodeInfo;
import jadx.api.metadata.annotations.VarNode;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;

import cuchaz.enigma.translation.representation.MethodDescriptor;
import cuchaz.enigma.translation.representation.TypeDescriptor;
import cuchaz.enigma.translation.representation.entry.ClassEntry;
import cuchaz.enigma.translation.representation.entry.FieldEntry;
import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
import cuchaz.enigma.translation.representation.entry.MethodEntry;

class JadxHelper {
private Map<ClassNode, String> internalNames = new HashMap<>();
private Map<ClassNode, ClassEntry> classMap = new HashMap<>();
private Map<FieldNode, FieldEntry> fieldMap = new HashMap<>();
private Map<MethodNode, MethodEntry> methodMap = new HashMap<>();
private Map<VarNode, LocalVariableEntry> varMap = new HashMap<>();
private Map<MethodNode, List<VarNode>> argMap = new HashMap<>();

private String internalNameOf(ClassNode cls) {
return internalNames.computeIfAbsent(cls, (unused) -> cls.getClassInfo().makeRawFullName().replace('.', '/'));
}

ClassEntry classEntryOf(ClassNode cls) {
if (cls == null) return null;
return classMap.computeIfAbsent(cls, (unused) -> new ClassEntry(internalNameOf(cls)));
}

FieldEntry fieldEntryOf(FieldNode fld) {
return fieldMap.computeIfAbsent(fld, (unused) ->
new FieldEntry(classEntryOf(fld.getParentClass()), fld.getName(), new TypeDescriptor(TypeGen.signature(fld.getType()))));
}

MethodEntry methodEntryOf(MethodNode mth) {
return methodMap.computeIfAbsent(mth, (unused) -> {
MethodInfo mthInfo = mth.getMethodInfo();
MethodDescriptor desc = new MethodDescriptor(mthInfo.getShortId().substring(mthInfo.getName().length()));
return new MethodEntry(classEntryOf(mth.getParentClass()), mthInfo.getName(), desc);
});
}

LocalVariableEntry paramEntryOf(VarNode param, ICodeInfo codeInfo) {
return varMap.computeIfAbsent(param, (unused) -> {
MethodEntry owner = methodEntryOf(param.getMth());
int index = param.getMth().collectArgsWithoutLoading().indexOf(param); // FIXME: This is just a placeholder (and obviously wrong), fix later
return new LocalVariableEntry(owner, index, param.getName(), true, null);
});
}

boolean isRecord(jadx.core.dex.nodes.ClassNode cls) {
if (cls.getSuperClass() == null || !cls.getSuperClass().isObject()) {
return false;
}

return Objects.equals(cls.getSuperClass().getObject(), "java/lang/Record");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package cuchaz.enigma.source.jadx;

import java.util.Collection;

import jadx.api.data.CommentStyle;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginContext;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.pass.JadxPassInfo;
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo;
import jadx.api.plugins.pass.types.JadxPreparePass;
import jadx.core.dex.attributes.nodes.NotificationAttrNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;

import cuchaz.enigma.translation.mapping.EntryMapping;
import cuchaz.enigma.translation.mapping.EntryRemapper;
import cuchaz.enigma.translation.representation.entry.Entry;
import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;

public class JadxJavadocProvider implements JadxPlugin {
public static final String PLUGIN_ID = "enigma-javadoc-provider";

@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo(PLUGIN_ID, "Enigma Javadoc Provider", "Applies Enigma-supplied Javadocs");
}

@SuppressWarnings("resource")
@Override
public void init(JadxPluginContext context) {
CustomJadxArgs args = (CustomJadxArgs) context.getArgs();

context.addPass(new JavadocProvidingPass(args.mapper, args.jadxHelper));
}

private static class JavadocProvidingPass implements JadxPreparePass {
private final EntryRemapper mapper;
private final JadxHelper jadxHelper;

private JavadocProvidingPass(EntryRemapper mapper, JadxHelper jadxHelper) {
this.mapper = mapper;
this.jadxHelper = jadxHelper;
}

@Override
public JadxPassInfo getInfo() {
return new OrderedJadxPassInfo("ApplyJavadocs", "Applies Enigma-supplied Javadocs")
.before("RenameVisitor");
}

@Override
public void init(RootNode root) {
process(root);
root.registerCodeDataUpdateListener(codeData -> process(root));
}

private void process(RootNode root) {
if (mapper == null) return;

for (ClassNode cls : root.getClasses()) {
processClass(cls);
}
}

private void processClass(ClassNode cls) {
EntryMapping mapping = mapper.getDeobfMapping(jadxHelper.classEntryOf(cls));

if (mapping.javadoc() != null && !mapping.javadoc().isBlank()) {
// TODO: Once JADX supports records, add @param tags for components
attachJavadoc(cls, mapping.javadoc());
}

for (FieldNode field : cls.getFields()) {
processField(field);
}

for (MethodNode method : cls.getMethods()) {
processMethod(method);
}
}

private void processField(FieldNode field) {
EntryMapping mapping = mapper.getDeobfMapping(jadxHelper.fieldEntryOf(field));

if (mapping.javadoc() != null && !mapping.javadoc().isBlank()) {
attachJavadoc(field, mapping.javadoc());
}
}

private void processMethod(MethodNode method) {
Entry<?> entry = jadxHelper.methodEntryOf(method);
EntryMapping mapping = mapper.getDeobfMapping(entry);
StringBuilder builder = new StringBuilder();
String javadoc = mapping.javadoc();

if (javadoc != null) {
builder.append(javadoc);
}

Collection<Entry<?>> children = mapper.getObfChildren(entry);
boolean addedLf = false;

if (children != null && !children.isEmpty()) {
for (Entry<?> child : children) {
if (child instanceof LocalVariableEntry) {
mapping = mapper.getDeobfMapping(child);
javadoc = mapping.javadoc();

if (javadoc != null) {
if (!addedLf) {
addedLf = true;
builder.append('\n');
}

builder.append(String.format("\n@param %s %s", mapping.targetName(), javadoc));
}
}
}
}

javadoc = builder.toString();

if (!javadoc.isBlank()) {
attachJavadoc(method, javadoc);
}
}

private void attachJavadoc(NotificationAttrNode target, String javadoc) {
target.addCodeComment(javadoc.trim(), CommentStyle.JAVADOC);
}
}
}
Loading
Loading