diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index 53d0f6e580d07cf7905403bed3f1749fd53e82c2..1100ca9f55ecf4666144d6b79c2c3d03159a2387 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -13,7 +13,7 @@ dependencies { } compile 'com.google.guava:guava:27.1-jre' - testCompile 'org.smali:baksmali:2.2.7' + compile 'org.smali:baksmali:2.2.7' testCompile 'org.apache.commons:commons-lang3:3.8.1' diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index de2a8498bb2ad54159dd239aaf557c0a736d38b0..08a41bdd4f839887861e86cf3f5c14869509e2f4 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -1,17 +1,35 @@ package jadx.api; +import java.io.BufferedWriter; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import org.jf.baksmali.Adaptors.ClassDefinition; +import org.jf.baksmali.Baksmali; +import org.jf.baksmali.BaksmaliOptions; +import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedClassDef; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.util.IndentingWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +44,8 @@ import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.SaveCode; import jadx.core.export.ExportGradleProject; import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.DexFile; +import jadx.core.utils.files.FileUtils; import jadx.core.utils.files.InputFile; import jadx.core.xmlgen.BinaryXMLParser; import jadx.core.xmlgen.ResourcesSaver; @@ -290,6 +310,31 @@ public final class JadxDecompiler { ProcessClass.process(cls, passes, true); } + void generateSmali(ClassNode cls) { + Path path = cls.dex().getDexFile().getPath(); + String className = cls.getAlias().makeRawFullName(); + className = 'L' + className.replace('.', '/') + ';'; + try (InputStream in = Files.newInputStream(path)) { + DexBackedDexFile dexFile = DexFileFactory.loadDexFile(path.toFile(), Opcodes.getDefault()); + boolean decompiled = false; + for (DexBackedClassDef classDef : dexFile.getClasses()) { + if (classDef.getType().equals(className)) { + ClassDefinition classDefinition = new ClassDefinition(new BaksmaliOptions(), classDef); + StringWriter sw = new StringWriter(); + classDefinition.writeTo(new IndentingWriter(sw)); + cls.setSmali(sw.toString()); + decompiled = true; + break; + } + } + if (!decompiled) { + LOG.error("Failed to find smali class {}", className); + } + } catch (IOException e) { + LOG.error("Error generating smali", e); + } + } + RootNode getRoot() { return root; } diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index 25034545fc3143b3e07e100c6c73550debb46afd..e9387b2fd00031e96f3ddb3505255cbc840bc814 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -64,6 +64,16 @@ public final class JavaClass implements JavaNode { } } + public synchronized String getSmali() { + if (decompiler == null) { + return null; + } + if (cls.getSmali() == null) { + decompiler.generateSmali(cls); + } + return cls.getSmali(); + } + public synchronized void unload() { cls.unload(); } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index f440dbd8ac7d9cc0f8661cb722e9c7508f3f9e1e..3ea9362716ca9d14de55ac836cddf5daeba7d3c3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -1,19 +1,22 @@ package jadx.core.dex.nodes; +import static jadx.core.dex.nodes.ProcessState.UNLOADED; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.android.dex.ClassData; import com.android.dex.ClassData.Field; import com.android.dex.ClassData.Method; import com.android.dex.ClassDef; import com.android.dex.Dex; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.codegen.CodeWriter; @@ -35,8 +38,6 @@ import jadx.core.dex.nodes.parser.StaticValuesParser; import jadx.core.utils.exceptions.DecodeException; import jadx.core.utils.exceptions.JadxRuntimeException; -import static jadx.core.dex.nodes.ProcessState.UNLOADED; - public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { private static final Logger LOG = LoggerFactory.getLogger(ClassNode.class); @@ -53,6 +54,8 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { // store decompiled code private CodeWriter code; + // store smali + private String smali; // store parent for inner classes or 'this' otherwise private ClassNode parentClass; @@ -482,6 +485,14 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { return code; } + public void setSmali(String smali) { + this.smali = smali; + } + + public String getSmali() { + return smali; + } + public ProcessState getState() { return state; } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/DexFile.java b/jadx-core/src/main/java/jadx/core/utils/files/DexFile.java index f5b9661f6440fe7e4d4caa3bafe484c305cd97d0..1b490f427fbe6f3a3f1ecb1b2e52e236506d7568 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/DexFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/DexFile.java @@ -1,16 +1,20 @@ package jadx.core.utils.files; +import java.nio.file.Path; + import com.android.dex.Dex; public class DexFile { private final InputFile inputFile; private final String name; private final Dex dexBuf; + private final Path path; - public DexFile(InputFile inputFile, String name, Dex dexBuf) { + public DexFile(InputFile inputFile, String name, Dex dexBuf, Path path) { this.inputFile = inputFile; this.name = name; this.dexBuf = dexBuf; + this.path = path; } public String getName() { @@ -21,6 +25,10 @@ public class DexFile { return dexBuf; } + public Path getPath() { + return path; + } + public InputFile getInputFile() { return inputFile; } diff --git a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java index 66f9629832e95886b457f873b50a374c27393925..0bc310a2de39ca9a194cbf819d11961f421f8042 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java @@ -54,7 +54,7 @@ public class InputFile { String fileName = file.getName(); if (fileName.endsWith(".dex")) { - addDexFile(new Dex(file)); + addDexFile(fileName, new Dex(file), file.toPath()); return; } if (fileName.endsWith(".smali")) { @@ -62,12 +62,12 @@ public class InputFile { SmaliOptions options = new SmaliOptions(); options.outputDexFile = output.toAbsolutePath().toString(); Smali.assemble(options, file.getAbsolutePath()); - addDexFile(new Dex(output.toFile())); + addDexFile("", new Dex(output.toFile()), output); return; } if (fileName.endsWith(".class")) { - for (Dex dex : loadFromClassFile(file)) { - addDexFile(dex); + for (Path path : loadFromClassFile(file)) { + addDexFile(path); } return; } @@ -81,8 +81,8 @@ public class InputFile { return; } if (fileName.endsWith(".jar")) { - for (Dex dex : loadFromJar(file.toPath())) { - addDexFile(dex); + for (Path path : loadFromJar(file.toPath())) { + addDexFile(path); } return; } @@ -98,12 +98,16 @@ public class InputFile { LOG.warn("No dex files found in {}", file); } - private void addDexFile(Dex dexBuf) { - addDexFile("", dexBuf); + private void addDexFile(Path path) throws IOException { + addDexFile("", path); } - private void addDexFile(String fileName, Dex dexBuf) { - dexFiles.add(new DexFile(this, fileName, dexBuf)); + private void addDexFile(String fileName, Path path) throws IOException { + addDexFile(fileName, new Dex(Files.readAllBytes(path)), path); + } + + private void addDexFile(String fileName, Dex dexBuf, Path path) { + dexFiles.add(new DexFile(this, fileName, dexBuf, path)); } private boolean loadFromZip(String ext) throws IOException, DecodeException { @@ -125,9 +129,9 @@ public class InputFile { || entryName.endsWith(instantRunDexSuffix)) { switch (ext) { case ".dex": - Dex dexBuf = makeDexBuf(entryName, inputStream); - if (dexBuf != null) { - addDexFile(entryName, dexBuf); + Path path = makeDexBuf(entryName, inputStream); + if (path != null) { + addDexFile(entryName, path); index++; } break; @@ -136,8 +140,8 @@ public class InputFile { index++; Path jarFile = FileUtils.createTempFile(entryName); Files.copy(inputStream, jarFile, StandardCopyOption.REPLACE_EXISTING); - for (Dex dex : loadFromJar(jarFile)) { - addDexFile(entryName, dex); + for (Path p : loadFromJar(jarFile)) { + addDexFile(entryName, p); } break; @@ -164,28 +168,26 @@ public class InputFile { } @Nullable - private Dex makeDexBuf(String entryName, InputStream inputStream) { + private Path makeDexBuf(String entryName, InputStream inputStream) { try { - return new Dex(inputStream); + Path path = FileUtils.createTempFile(".dex"); + Files.copy(inputStream, path, StandardCopyOption.REPLACE_EXISTING); + return path; } catch (Exception e) { LOG.error("Failed to load file: {}, error: {}", entryName, e.getMessage(), e); return null; } } - private static List loadFromJar(Path jar) throws DecodeException { + private static List loadFromJar(Path jar) throws DecodeException { JavaToDex j2d = new JavaToDex(); try { LOG.info("converting to dex: {} ...", jar.getFileName()); - List byteList = j2d.convert(jar); - if (byteList.isEmpty()) { + List pathList = j2d.convert(jar); + if (pathList.isEmpty()) { throw new JadxException("Empty dx output"); } - List dexList = new ArrayList<>(byteList.size()); - for (byte[] b : byteList) { - dexList.add(new Dex(b)); - } - return dexList; + return pathList; } catch (Exception e) { throw new DecodeException("java class to dex conversion error:\n " + e.getMessage(), e); } finally { @@ -195,7 +197,7 @@ public class InputFile { } } - private static List loadFromClassFile(File file) throws IOException, DecodeException { + private static List loadFromClassFile(File file) throws IOException, DecodeException { Path outFile = FileUtils.createTempFile(".jar"); try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(outFile))) { String clsName = AsmUtils.getNameFromClassFile(file); diff --git a/jadx-core/src/main/java/jadx/core/utils/files/JavaToDex.java b/jadx-core/src/main/java/jadx/core/utils/files/JavaToDex.java index ed2294e33bc38f5fa9c206271185ef4a15ea2ef7..83c75691d74ec6c7406df6e40ec0df1f6d7f2e97 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/JavaToDex.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/JavaToDex.java @@ -37,7 +37,7 @@ public class JavaToDex { private String dxErrors; - public List convert(Path jar) throws JadxException { + public List convert(Path jar) throws JadxException { try (ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream errOut = new ByteArrayOutputStream()) { DxContext context = new DxContext(out, errOut); @@ -51,14 +51,14 @@ public class JavaToDex { if (result != 0) { throw new JadxException("Java to dex conversion error, code: " + result); } - List list = new ArrayList<>(); + List list = new ArrayList<>(); try (DirectoryStream ds = Files.newDirectoryStream(dir)) { for (Path child : ds) { - list.add(Files.readAllBytes(child)); - Files.delete(child); + list.add(child); + child.toFile().deleteOnExit(); } } - Files.delete(dir); + dir.toFile().deleteOnExit(); return list; } catch (Exception e) { throw new JadxException("dx exception: " + e.getMessage(), e); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 8998a35412afb45d2f2590e5d62187eb2731f0eb..269f68b4b96ec4968ea52079f879ac633fb697ad 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -81,6 +81,11 @@ public class JClass extends JLoadableNode { return cls.getCode(); } + @Override + public String getSmali() { + return cls.getSmali(); + } + @Override public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_JAVA; diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index c8ada4e8997985dabf8aa4d59b1bee3e5cdfa9cc..40593c64ae0db3c9d2ad4fcc934fe83e28f8f767 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -28,6 +28,10 @@ public abstract class JNode extends DefaultMutableTreeNode { return null; } + public String getSmali() { + return null; + } + public String getSyntaxName() { return SyntaxConstants.SYNTAX_STYLE_NONE; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java index 24b5271f6ec8b1ae4f6bb68908227057e0e81e14..b1d824536a2c06dd7237942c3f87d74e31a8df2e 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodePanel.java @@ -1,15 +1,20 @@ package jadx.gui.ui.codearea; -import javax.swing.*; -import java.awt.*; +import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; +import javax.swing.AbstractAction; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.KeyStroke; + import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.ContentPanel; import jadx.gui.ui.TabbedPane; +import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; public final class CodePanel extends ContentPanel { @@ -17,22 +22,36 @@ public final class CodePanel extends ContentPanel { private final SearchBar searchBar; private final CodeArea codeArea; - private final JScrollPane scrollPane; + private final SmaliArea smaliArea; + private final JScrollPane codeScrollPane; + private final JScrollPane smaliScrollPane; + private JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); public CodePanel(TabbedPane panel, JNode jnode) { super(panel, jnode); codeArea = new CodeArea(this); + smaliArea = new SmaliArea(this); searchBar = new SearchBar(codeArea); - scrollPane = new JScrollPane(codeArea); + codeScrollPane = new JScrollPane(codeArea); + smaliScrollPane = new JScrollPane(smaliArea); initLineNumbers(); setLayout(new BorderLayout()); add(searchBar, BorderLayout.NORTH); - add(scrollPane); + + areaTabbedPane.add(codeScrollPane, NLS.str("tabs.code")); + areaTabbedPane.add(smaliScrollPane, NLS.str("tabs.smali")); + add(areaTabbedPane); KeyStroke key = KeyStroke.getKeyStroke(KeyEvent.VK_F, Utils.ctrlButton()); Utils.addKeyBinding(codeArea, key, "SearchAction", new SearchAction()); + + areaTabbedPane.addChangeListener(e -> { + if (areaTabbedPane.getSelectedComponent() == smaliScrollPane) { + smaliArea.load(); + } + }); } private void initLineNumbers() { @@ -40,7 +59,7 @@ public final class CodePanel extends ContentPanel { if (codeArea.getDocument().getLength() <= 100_000) { LineNumbers numbers = new LineNumbers(codeArea); numbers.setUseSourceLines(isUseSourceLines()); - scrollPane.setRowHeaderView(numbers); + codeScrollPane.setRowHeaderView(numbers); } } @@ -89,7 +108,4 @@ public final class CodePanel extends ContentPanel { return codeArea; } - JScrollPane getScrollPane() { - return scrollPane; - } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java new file mode 100644 index 0000000000000000000000000000000000000000..b11faa8b1eb8d30a1a5186bf4edb1b4974cd22cf --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -0,0 +1,23 @@ +package jadx.gui.ui.codearea; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import jadx.gui.treemodel.JNode; + +public final class SmaliArea extends RSyntaxTextArea { + private static final long serialVersionUID = 1334485631870306494L; + + private final JNode node; + + SmaliArea(CodePanel panel) { + node = panel.getNode(); + + setEditable(false); + } + + void load() { + if (getText().isEmpty()) { + setText(node.getSmali()); + } + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index bc1438ba5ca3ed355c0ad0fea2a8159f07a00c9d..905a6088db7e19da00a22c19e2c8a8a70e4b7507 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -44,6 +44,8 @@ tabs.copy_class_name=Copy Name tabs.close=Close tabs.closeOthers=Close Others tabs.closeAll=Close All +tabs.code=Code +tabs.smali=Smali nav.back=Back nav.forward=Forward diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 81a460d69ac5dc23c57a4c189cab23952c98b2c3..ab3faf6b4cebc72039898fa925e2c8bd3911ff70 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -44,6 +44,8 @@ tabs.copy_class_name=Copy Name tabs.close=Cerrar tabs.closeOthers=Cerrar otros tabs.closeAll=Cerrar todo +#tabs.code= +#tabs.smali= nav.back=Atrás nav.forward=Adelante diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index b6dc7fb83d2bc661013573d62bad155a44eb08b4..81a257f1dd1a6f3067ea74b83dacfca530916b3b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -44,6 +44,8 @@ tabs.copy_class_name=复制类名 tabs.close=关闭 tabs.closeOthers=关闭其他文件 tabs.closeAll=全部关闭 +#tabs.code= +#tabs.smali= nav.back=后退 nav.forward=前进