Skip to content

Commit

Permalink
feat(gui): support for renaming methods, classes and fields (PR #794 #…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
S-trace authored and skylot committed Dec 10, 2019
1 parent 78eed86 commit e3055b9
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 3 deletions.
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/api/JadxDecompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ public void printErrorsReport() {
root.getErrorsCounter().printReport();
}

RootNode getRoot() {
public RootNode getRoot() {
return root;
}

Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/api/JavaField.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public int getDecompiledLine() {
return field.getDecompiledLine();
}

FieldNode getFieldNode() {
public FieldNode getFieldNode() {
return field;
}

Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/api/JavaMethod.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public int getDecompiledLine() {
return mth.getDecompiledLine();
}

MethodNode getMethodNode() {
public MethodNode getMethodNode() {
return mth;
}

Expand Down
233 changes: 233 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/ui/RenameDialog.java
Original file line number Diff line number Diff line change
@@ -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<DexNode> 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<String> 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<String> readAndUpdateDeobfMap(Path deobfMapPath, String alias) throws IOException {
List<String> 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);
}
}
3 changes: 3 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ public void load() {
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) {
Expand Down
37 changes: 37 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/ui/codearea/RenameAction.java
Original file line number Diff line number Diff line change
@@ -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<JNode> {
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);
}
}
1 change: 1 addition & 0 deletions jadx-gui/src/main/resources/i18n/Messages_en_US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
1 change: 1 addition & 0 deletions jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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你想替换它吗?
Expand Down

0 comments on commit e3055b9

Please sign in to comment.