diff --git a/build.gradle b/build.gradle index bab265ae0..56ece4d38 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ subprojects { mavenLocal() mavenCentral() maven { url 'https://maven.fabricmc.net/' } + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } } dependencies { diff --git a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java index 41ee2547c..48c16f2fc 100644 --- a/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java +++ b/enigma-swing/src/main/java/cuchaz/enigma/gui/config/Decompiler.java @@ -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); diff --git a/enigma/build.gradle b/enigma/build.gradle index 44f26808a..af4e398a7 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -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' diff --git a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java index 10bc436a2..2687fa53d 100644 --- a/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java +++ b/enigma/src/main/java/cuchaz/enigma/analysis/BuiltinPlugin.java @@ -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); } diff --git a/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java index 121903028..d14f930df 100644 --- a/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java +++ b/enigma/src/main/java/cuchaz/enigma/source/Decompilers.java @@ -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; } diff --git a/enigma/src/main/java/cuchaz/enigma/source/jadx/CustomJadxArgs.java b/enigma/src/main/java/cuchaz/enigma/source/jadx/CustomJadxArgs.java new file mode 100644 index 000000000..2e2086b65 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/jadx/CustomJadxArgs.java @@ -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; +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxDecompiler.java b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxDecompiler.java new file mode 100644 index 000000000..13f886c80 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxDecompiler.java @@ -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; + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxHelper.java b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxHelper.java new file mode 100644 index 000000000..80730eb7a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxHelper.java @@ -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 internalNames = new HashMap<>(); + private Map classMap = new HashMap<>(); + private Map fieldMap = new HashMap<>(); + private Map methodMap = new HashMap<>(); + private Map varMap = new HashMap<>(); + private Map> 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"); + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxJavadocProvider.java b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxJavadocProvider.java new file mode 100644 index 000000000..3e491830a --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxJavadocProvider.java @@ -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> 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); + } + } +} diff --git a/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxSource.java b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxSource.java new file mode 100644 index 000000000..c791cf7d0 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/source/jadx/JadxSource.java @@ -0,0 +1,147 @@ +package cuchaz.enigma.source.jadx; + +import java.util.function.Function; + +import javax.annotation.Nullable; + +import jadx.api.ICodeInfo; +import jadx.api.JadxArgs; +import jadx.api.JadxDecompiler; +import jadx.api.JavaClass; +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.api.metadata.annotations.VarNode; +import jadx.api.metadata.annotations.VarRef; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.plugins.input.java.JavaInputPlugin; +import org.objectweb.asm.tree.ClassNode; + +import cuchaz.enigma.source.Source; +import cuchaz.enigma.source.SourceIndex; +import cuchaz.enigma.source.SourceSettings; +import cuchaz.enigma.source.Token; +import cuchaz.enigma.translation.mapping.EntryRemapper; +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; +import cuchaz.enigma.utils.AsmUtil; + +public class JadxSource implements Source { + private final SourceSettings settings; + private final Function jadxArgsFactory; + private final ClassNode classNode; + private final EntryRemapper mapper; + private final JadxHelper jadxHelper; + private SourceIndex index; + + public JadxSource(SourceSettings settings, Function jadxArgsFactory, ClassNode classNode, @Nullable EntryRemapper mapper, JadxHelper jadxHelper) { + this.settings = settings; + this.jadxArgsFactory = jadxArgsFactory; + this.classNode = classNode; + this.mapper = mapper; + this.jadxHelper = jadxHelper; + } + + @Override + public Source withJavadocs(EntryRemapper mapper) { + return new JadxSource(settings, jadxArgsFactory, classNode, mapper, jadxHelper); + } + + @Override + public SourceIndex index() { + ensureDecompiled(); + return index; + } + + @Override + public String asString() { + ensureDecompiled(); + return index.getSource(); + } + + private void ensureDecompiled() { + if (index != null) { + return; + } + + try (JadxDecompiler jadx = new JadxDecompiler(jadxArgsFactory.apply(mapper))) { + jadx.addCustomCodeLoader(JavaInputPlugin.loadSingleClass(AsmUtil.nodeToBytes(classNode), classNode.name)); + jadx.load(); + JavaClass cls = jadx.getClasses().get(0); + + // Cache decompilation result to prevent https://github.com/skylot/jadx/issues/2141 + ICodeInfo codeInfo = cls.getCodeInfo(); + index = new SourceIndex(codeInfo.getCodeStr()); + + // Tokens + codeInfo.getCodeMetadata().searchDown(0, (pos, ann) -> { + processAnnotatedElement(pos, ann, codeInfo); + return null; + }); + } + } + + private void processAnnotatedElement(int pos, ICodeAnnotation ann, ICodeInfo codeInfo) { + if (ann == null) return; + + if (ann instanceof NodeDeclareRef ref) { + processAnnotatedElement(pos, ref.getNode(), codeInfo); + } else if (ann instanceof jadx.core.dex.nodes.ClassNode cls) { + Token token = new Token(pos, pos + cls.getShortName().length(), cls.getShortName()); + + if (pos == cls.getDefPosition()) { + index.addDeclaration(token, classEntryOf(cls)); + } else { + index.addReference(token, classEntryOf(cls), classEntryOf(cls.getParentClass())); + } + } else if (ann instanceof FieldNode fld) { + Token token = new Token(pos, pos + fld.getName().length(), fld.getName()); + + if (pos == fld.getDefPosition()) { + index.addDeclaration(token, fieldEntryOf(fld)); + } else { + index.addReference(token, fieldEntryOf(fld), classEntryOf(fld.getParentClass())); + } + } else if (ann instanceof MethodNode mth) { + if (mth.getName().equals("")) return; + Token token = new Token(pos, pos + mth.getName().length(), mth.getName()); + + if (mth.isConstructor()) { + processAnnotatedElement(pos, mth.getTopParentClass(), codeInfo); + } else if (pos == mth.getDefPosition()) { + index.addDeclaration(token, methodEntryOf(mth)); + } else { + index.addReference(token, methodEntryOf(mth), classEntryOf(mth.getParentClass())); + } + } else if (ann instanceof VarNode var) { + if (!var.getMth().collectArgNodes().contains(var)) return; + Token token = new Token(pos, pos + var.getName().length(), var.getName()); + + if (pos == var.getDefPosition()) { + index.addDeclaration(token, paramEntryOf(var, codeInfo)); + } else { + index.addReference(token, paramEntryOf(var, codeInfo), methodEntryOf(var.getMth())); + } + } else if (ann instanceof VarRef varRef) { + processAnnotatedElement(pos, codeInfo.getCodeMetadata().getAt(varRef.getRefPos()), codeInfo); + } + } + + private ClassEntry classEntryOf(jadx.core.dex.nodes.ClassNode cls) { + return jadxHelper.classEntryOf(cls); + } + + private FieldEntry fieldEntryOf(FieldNode fld) { + return jadxHelper.fieldEntryOf(fld); + } + + private MethodEntry methodEntryOf(MethodNode mth) { + return jadxHelper.methodEntryOf(mth); + } + + private LocalVariableEntry paramEntryOf(VarNode param, ICodeInfo codeInfo) { + return jadxHelper.paramEntryOf(param, codeInfo); + } +} diff --git a/enigma/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/enigma/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 000000000..9add613b9 --- /dev/null +++ b/enigma/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +cuchaz.enigma.source.jadx.JadxJavadocProvider