From e3055b95f6cb108f0a836f31bdd16658c527f67b Mon Sep 17 00:00:00 2001 From: Soul Trace Date: Tue, 10 Dec 2019 22:08:27 +0300 Subject: [PATCH] feat(gui): support for renaming methods, classes and fields (PR #794 #791) * Add getRealFullName() to ClassNode and JavaClass and searchJavaClassByRealName() to JadxWrapper Those methods is like getFullName() and searchJavaClassByClassName(), but for class names without aliases. It is necessary for renaming classes/methods/fields. * core: Make getFieldNode(), getMethodNode() and getRoot() public This is necessary for renaming functionality * jadx-gui: Add Rename popup menu entry (renames classes, methods and fields) It allows user to rename classes, methods and fields. It updates deobfuscation map and reload file. This may be suboptimal, and maybe some RenameVisitor should be added. Deobfuscation should be enabled in order to allow this. --- .../main/java/jadx/api/JadxDecompiler.java | 2 +- .../src/main/java/jadx/api/JavaField.java | 2 +- .../src/main/java/jadx/api/JavaMethod.java | 2 +- .../main/java/jadx/gui/ui/RenameDialog.java | 233 ++++++++++++++++++ .../java/jadx/gui/ui/codearea/CodeArea.java | 3 + .../jadx/gui/ui/codearea/RenameAction.java | 37 +++ .../resources/i18n/Messages_en_US.properties | 1 + .../resources/i18n/Messages_es_ES.properties | 1 + .../resources/i18n/Messages_zh_CN.properties | 1 + 9 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 9253789f7..3bcdbe574 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -281,7 +281,7 @@ public final class JadxDecompiler { root.getErrorsCounter().printReport(); } - RootNode getRoot() { + public RootNode getRoot() { return root; } diff --git a/jadx-core/src/main/java/jadx/api/JavaField.java b/jadx-core/src/main/java/jadx/api/JavaField.java index fa7ec75f4..81e85bb71 100644 --- a/jadx-core/src/main/java/jadx/api/JavaField.java +++ b/jadx-core/src/main/java/jadx/api/JavaField.java @@ -46,7 +46,7 @@ public final class JavaField implements JavaNode { return field.getDecompiledLine(); } - FieldNode getFieldNode() { + public FieldNode getFieldNode() { return field; } diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java index 811b3ca7b..d2f0cd98a 100644 --- a/jadx-core/src/main/java/jadx/api/JavaMethod.java +++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java @@ -74,7 +74,7 @@ public final class JavaMethod implements JavaNode { return mth.getDecompiledLine(); } - MethodNode getMethodNode() { + public MethodNode getMethodNode() { return mth; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java new file mode 100644 index 000000000..1480eb3c3 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java @@ -0,0 +1,233 @@ +package jadx.gui.ui; + +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; + +import javax.swing.*; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.JavaClass; +import jadx.api.JavaField; +import jadx.api.JavaMethod; +import jadx.api.JavaNode; +import jadx.core.dex.nodes.DexNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.files.InputFile; +import jadx.gui.treemodel.*; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.utils.NLS; +import jadx.gui.utils.TextStandardActions; + +public class RenameDialog extends JDialog { + private static final long serialVersionUID = -3269715644416902410L; + + private static final Logger LOG = LoggerFactory.getLogger(RenameDialog.class); + + protected final transient MainWindow mainWindow; + + private final transient JNode node; + + private JTextField renameField; + + private CodeArea codeArea; + + public RenameDialog(CodeArea codeArea, JNode node) { + super(codeArea.getMainWindow()); + mainWindow = codeArea.getMainWindow(); + this.codeArea = codeArea; + this.node = node; + initUI(); + loadWindowPos(); + } + + private void loadWindowPos() { + mainWindow.getSettings().loadWindowPos(this); + } + + @Override + public void dispose() { + mainWindow.getSettings().saveWindowPos(this); + super.dispose(); + } + + private Path getDeobfMapPath(RootNode root) { + List dexNodes = root.getDexNodes(); + if (dexNodes.isEmpty()) { + return null; + } + InputFile firstInputFile = dexNodes.get(0).getDexFile().getInputFile(); + Path inputFilePath = firstInputFile.getFile().getAbsoluteFile().toPath(); + + String inputName = inputFilePath.getFileName().toString(); + String baseName = inputName.substring(0, inputName.lastIndexOf('.')); + return inputFilePath.getParent().resolve(baseName + ".jobf"); + } + + private String getNodeAlias(String renameText) { + String type = ""; + String id = ""; + if (node instanceof JMethod) { + JavaMethod javaMethod = (JavaMethod) node.getJavaNode(); + type = "m"; + id = javaMethod.getMethodNode().getMethodInfo().getRawFullId(); + } else if (node instanceof JField) { + JavaField javaField = (JavaField) node.getJavaNode(); + type = "f"; + id = javaField.getFieldNode().getFieldInfo().getRawFullId(); + } else if (node instanceof JClass) { + type = "c"; + JavaNode javaNode = node.getJavaNode(); + id = javaNode.getFullName(); + if (javaNode instanceof JavaClass) { + JavaClass javaClass = (JavaClass) javaNode; + id = javaClass.getRealFullName(); + } + + } else if (node instanceof JPackage) { + type = "p"; + id = node.getJavaNode().getFullName(); + } + return String.format("%s %s = %s", type, id, renameText); + } + + private boolean updateDeobfMap(String renameText, RootNode root) { + Path deobfMapPath = getDeobfMapPath(root); + if (deobfMapPath == null) { + LOG.error("rename(): Failed deofbMapFile is null"); + return false; + } + String alias = getNodeAlias(renameText); + LOG.info("rename(): " + alias); + + try { + List deobfMap = readAndUpdateDeobfMap(deobfMapPath, alias); + File tmpFile = File.createTempFile("deobf_tmp_", ".txt"); + FileOutputStream fileOut = new FileOutputStream(tmpFile); + for (String entry : deobfMap) { + fileOut.write(entry.getBytes()); + fileOut.write(System.lineSeparator().getBytes()); + } + fileOut.close(); + File oldMap = File.createTempFile("deobf_bak_", ".txt"); + Files.copy(deobfMapPath, oldMap.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(tmpFile.toPath(), deobfMapPath, StandardCopyOption.REPLACE_EXISTING); + Files.delete(oldMap.toPath()); + + } catch (IOException e) { + LOG.error("rename(): Failed to write deofbMapFile {}", deobfMapPath); + e.printStackTrace(); + return false; + } + return true; + } + + private List readAndUpdateDeobfMap(Path deobfMapPath, String alias) throws IOException { + List deobfMap = Files.readAllLines(deobfMapPath, StandardCharsets.UTF_8); + String id = alias.split("=")[0]; + LOG.info("Id = " + id); + int i = 0; + while (i < deobfMap.size()) { + if (deobfMap.get(i).startsWith(id)) { + LOG.info("Removing entry " + deobfMap.get(i)); + deobfMap.remove(i); + } else { + i++; + } + } + deobfMap.add(alias); + return deobfMap; + } + + private void rename() { + String renameText = renameField.getText(); + if (renameText == null || renameText.length() == 0 || codeArea.getText() == null) { + return; + } + RootNode root = mainWindow.getWrapper().getDecompiler().getRoot(); + if (node == null) { + LOG.error("rename(): rootNode is null!"); + dispose(); + return; + } + if (!updateDeobfMap(renameText, root)) { + LOG.error("rename(): updateDeobfMap() failed"); + dispose(); + return; + } + mainWindow.reOpenFile(); + dispose(); + } + + private void initCommon() { + KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + getRootPane().registerKeyboardAction(e -> dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + @NotNull + private JPanel initButtonsPanel() { + JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); + cancelButton.addActionListener(event -> dispose()); + JButton renameBtn = new JButton(NLS.str("popup.rename")); + renameBtn.addActionListener(event -> rename()); + getRootPane().setDefaultButton(renameBtn); + + JPanel buttonPane = new JPanel(); + buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); + buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); + buttonPane.add(Box.createHorizontalGlue()); + buttonPane.add(renameBtn); + buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); + buttonPane.add(cancelButton); + return buttonPane; + } + + private void initUI() { + JLabel lbl = new JLabel(NLS.str("popup.rename")); + JLabel nodeLabel = new JLabel(this.node.makeLongString(), this.node.getIcon(), SwingConstants.LEFT); + lbl.setLabelFor(nodeLabel); + + renameField = new JTextField(40); + renameField.addActionListener(e -> rename()); + renameField.setText(node.getName()); + renameField.selectAll(); + new TextStandardActions(renameField); + + JPanel renamePane = new JPanel(); + renamePane.setLayout(new FlowLayout(FlowLayout.LEFT)); + renamePane.add(lbl); + renamePane.add(nodeLabel); + renamePane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + JPanel textPane = new JPanel(); + textPane.setLayout(new FlowLayout(FlowLayout.LEFT)); + textPane.add(renameField); + textPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + initCommon(); + JPanel buttonPane = initButtonsPanel(); + + Container contentPane = getContentPane(); + contentPane.add(renamePane, BorderLayout.PAGE_START); + contentPane.add(textPane, BorderLayout.CENTER); + contentPane.add(buttonPane, BorderLayout.PAGE_END); + + setTitle(NLS.str("popup.rename")); + pack(); + setSize(800, 80); + setLocationRelativeTo(null); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setModalityType(ModalityType.MODELESS); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index ffa62db9d..8e75db620 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -54,13 +54,16 @@ public final class CodeArea extends AbstractCodeArea { private void addMenuItems() { FindUsageAction findUsage = new FindUsageAction(this); GoToDeclarationAction goToDeclaration = new GoToDeclarationAction(this); + RenameAction rename = new RenameAction(this); JPopupMenu popup = getPopupMenu(); popup.addSeparator(); popup.add(findUsage); popup.add(goToDeclaration); + popup.add(rename); popup.addPopupMenuListener(findUsage); popup.addPopupMenuListener(goToDeclaration); + popup.addPopupMenuListener(rename); } public int adjustOffsetForToken(@Nullable Token token) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java new file mode 100644 index 000000000..80697a2c1 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java @@ -0,0 +1,37 @@ +package jadx.gui.ui.codearea; + +import java.awt.event.ActionEvent; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.RenameDialog; +import jadx.gui.utils.NLS; + +public final class RenameAction extends JNodeMenuAction { + private static final long serialVersionUID = -4680872086148463289L; + + private static final Logger LOG = LoggerFactory.getLogger(RenameAction.class); + + public RenameAction(CodeArea codeArea) { + super(NLS.str("popup.rename"), codeArea); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (node == null) { + LOG.info("node == null!"); + return; + } + RenameDialog renameDialog = new RenameDialog(codeArea, node); + renameDialog.setVisible(true); + } + + @Nullable + @Override + public JNode getNodeByOffset(int offset) { + return codeArea.getJNodeAtOffset(offset); + } +} 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 c8ee1f5d1..c86609c0a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -145,6 +145,7 @@ popup.select_all=Select All popup.find_usage=Find Usage popup.go_to_declaration=Go to declaration popup.exclude=Exclude +popup.rename=Rename confirm.save_as_title=Confirm Save as confirm.save_as_message=%s already exists.\nDo you want to replace it? 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 acaa0a61a..37c10bb1c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -145,6 +145,7 @@ popup.select_all=Seleccionar todo #popup.find_usage= #popup.go_to_declaration= #popup.exclude= +popup.rename=Nimeta ümber #confirm.save_as_title= #confirm.save_as_message= 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 28770b7a2..95fd49e68 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -145,6 +145,7 @@ popup.select_all=全选 popup.find_usage=查找用例 popup.go_to_declaration=跳到声明 popup.exclude=排除 +popup.rename=改名 confirm.save_as_title=确认另存为 confirm.save_as_message=%s 已存在。\n你想替换它吗?