fix(gui): improve dot graph viewer, CFG generation simplified

This commit is contained in:
Skylot
2026-06-02 20:32:36 +01:00
parent 6775cc5a93
commit 8c28a8530e
38 changed files with 902 additions and 1172 deletions
+1
View File
@@ -9,6 +9,7 @@
<!-- jadx-gui -->
<logger name="com.pinterest.ktlint" level="INFO"/>
<logger name="guru.nidi.graphviz" level="WARN"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
@@ -19,6 +19,7 @@ import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.LoadStage;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
@@ -207,6 +208,44 @@ public class ProcessClass {
}
}
public boolean processMethodUntilVisitor(MethodNode mth, String visitorName, boolean includeVisitor) {
IDexTreeVisitor foundPass = null;
IDexTreeVisitor prevPass = null;
for (IDexTreeVisitor pass : passes) {
if (pass.getName().equals(visitorName)) {
if (includeVisitor) {
foundPass = pass;
} else {
foundPass = prevPass;
}
break;
}
prevPass = pass;
}
if (foundPass == null) {
return false;
}
return processMethodToVisitor(mth, foundPass);
}
public boolean processMethodToVisitor(MethodNode mth, IDexTreeVisitor lastPassToProcess) {
synchronized (mth.getTopParentClass().getClassInfo()) {
try {
mth.unload();
mth.load();
for (IDexTreeVisitor pass : passes) {
DepthTraversal.visit(pass, mth);
if (pass == lastPassToProcess) {
return true;
}
}
} catch (Exception e) {
throw new JadxRuntimeException("Failed to process method to visitor: " + lastPassToProcess, e);
}
return false;
}
}
// TODO: make passes list private and not visible
public List<IDexTreeVisitor> getPasses() {
return passes;
@@ -131,6 +131,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
sVars = Collections.emptyList();
instructions = null;
blocks = null;
blocksMaxCId = 0;
enterBlock = null;
exitBlock = null;
region = null;
@@ -713,10 +714,9 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return codeReader;
}
// Cannot modify through get, use setUseIn
@Override
public List<MethodNode> getUseIn() {
return Collections.unmodifiableList(useIn);
return useIn;
}
// Do not modify passed list after setting
@@ -740,7 +740,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
}
public Set<MethodNode> getUsed() {
this.removeInavlidMethodsUsed();
this.removeInvalidMethodsUsed();
return methodsUsed;
}
@@ -762,12 +762,8 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
// Remove any methods from the list of used methods (calees) if this method (caller) has been
// removed from the calee's list of callers
private void removeInavlidMethodsUsed() {
for (MethodNode methodUsed : new ArrayList<>(methodsUsed)) {
if (!methodUsed.getUseIn().contains(this)) {
methodsUsed.remove(methodUsed);
}
}
private void removeInvalidMethodsUsed() {
methodsUsed.removeIf(methodUsed -> !methodUsed.getUseIn().contains(this));
}
public JavaMethod getJavaNode() {
@@ -27,8 +27,6 @@ import jadx.core.dex.trycatch.TryEdge;
import jadx.core.dex.trycatch.TryEdgeScopeGroupMap;
import jadx.core.dex.visitors.AbstractVisitor;
import jadx.core.dex.visitors.ConstInlineVisitor;
import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.finaly.traverser.TraverserController;
import jadx.core.dex.visitors.finaly.traverser.TraverserException;
@@ -37,7 +35,6 @@ import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Pair;
import jadx.core.utils.exceptions.DecodeException;
import jadx.core.utils.exceptions.JadxRuntimeException;
/**
@@ -474,18 +471,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
*/
private static void undoFinallyVisitor(MethodNode mth) {
try {
// TODO: make more common and less hacky
mth.unload();
mth.load();
for (IDexTreeVisitor visitor : mth.root().getPasses()) {
if (visitor instanceof MarkFinallyVisitor) {
break;
// All visitors after MarkFinally will be invoked as usual after this because
// the original decompilation request will proceed.
}
DepthTraversal.visit(visitor, mth);
}
} catch (DecodeException e) {
mth.root().getProcessClasses().processMethodUntilVisitor(mth, "MarkFinallyVisitor", false);
} catch (Exception e) {
mth.addError("Undo finally extract failed", e);
}
}
@@ -54,7 +54,7 @@ public class UsageInfo implements IUsageInfoData {
mthUsage.visit((mth, methods) -> mth.setUseIn(resolveMthList(sortedList(methods))));
mthUses.visit((mth, methods) -> mth.setUsed(resolveMthList(sortedList(methods))));
unresolvedMthUsage.visit((mth, unresolvedMethods) -> mth.setUnresolvedUsed(new ArrayList<>(unresolvedMethods)));
selfCalls.forEach((mth, selfCall) -> mth.setCallsSelf(selfCall));
selfCalls.forEach(MethodNode::setCallsSelf);
}
@Override
@@ -9,6 +9,8 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeWriter;
import jadx.api.JavaMethod;
import jadx.api.impl.SimpleCodeWriter;
@@ -89,8 +91,7 @@ public class DotGraphUtils {
.resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs")
.resolve(fileName)
.toFile();
file = FileUtils.cutFileName(file);
return file;
return FileUtils.cutFileName(file);
}
public void dumpToFile(MethodNode mth) {
@@ -100,16 +101,14 @@ public class DotGraphUtils {
public void dumpToFile(MethodNode mth, File dir) {
String graph = dumpToString(mth);
if (graph == null) {
return;
}
File file = getFullFile(mth, dir);
SaveCode.save(graph, file);
}
public String dumpToString(MethodNode mth) {
public @Nullable String dumpToString(MethodNode mth) {
dot.startLine("digraph \"CFG for");
dot.add(escape(mth.getMethodInfo().getFullId()));
dot.add("\" {");
@@ -144,9 +143,7 @@ public class DotGraphUtils {
// region will already process all it's containing blocks.
continue;
}
processBlock(mth, block);
}
}
@@ -4,10 +4,14 @@ import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.swing.ImageIcon;
import org.jetbrains.annotations.Nullable;
import jadx.core.utils.Utils;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.shortcut.Shortcut;
@@ -81,21 +85,14 @@ public enum ActionModel {
OPEN_DEVICE(MENU_TOOLBAR, "debugger.process_selector", "debugger.process_selector", "ui/startDebugger",
Shortcut.none()),
FIND_USAGE(CODE_AREA, "popup.find_usage", "popup.find_usage", null,
Shortcut.keyboard(KeyEvent.VK_X)),
FIND_USAGE_PLUS(CODE_AREA, "popup.usage_dialog_plus", "popup.usage_dialog_plus", null,
Shortcut.keyboard(KeyEvent.VK_C)),
GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", "popup.go_to_declaration", null,
Shortcut.keyboard(KeyEvent.VK_D)),
CONVERT_NUMBER(CODE_AREA, "popup.convert_number", "popup.convert_number", null, Shortcut.none()),
VIEW_CLASS_INHERITANCE_GRAPH(CODE_AREA, "popup.view_class_graph", "popup.view_class_graph_description", null,
Shortcut.none()),
VIEW_CLASS_METHOD_GRAPH(CODE_AREA, "popup.view_class_method_graph", "popup.view_class_method_graph_description",
null, Shortcut.none()),
VIEW_CALL_GRAPH(CODE_AREA, "popup.view_call_graph", "popup.view_call_graph_description", null, Shortcut.none()),
VIEW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_cfg", "popup.view_cfg_description", null, Shortcut.none()),
VIEW_RAW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_raw_cfg", "popup.view_raw_cfg_description", null, Shortcut.none()),
VIEW_REGION_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_region_cfg", "popup.view_region_cfg_description", null, Shortcut.none()),
FIND_USAGE(CODE_AREA, "popup.find_usage", null, null, Shortcut.keyboard(KeyEvent.VK_X)),
FIND_USAGE_PLUS(CODE_AREA, "popup.usage_dialog_plus", null, null, Shortcut.keyboard(KeyEvent.VK_C)),
GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", null, null, Shortcut.keyboard(KeyEvent.VK_D)),
CONVERT_NUMBER(CODE_AREA, "popup.convert_number", null, null, null),
VIEW_CLASS_INHERITANCE_GRAPH(CODE_AREA, "popup.view_class_graph", "popup.view_class_graph_description", null, null),
VIEW_CLASS_METHOD_GRAPH(CODE_AREA, "popup.view_class_method_graph", "popup.view_class_method_graph_description", null, null),
VIEW_CALL_GRAPH(CODE_AREA, "popup.view_call_graph", "popup.view_call_graph_description", null, null),
VIEW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_cfg", null, null, null),
CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null,
Shortcut.keyboard(KeyEvent.VK_SEMICOLON)),
@@ -131,15 +128,16 @@ public enum ActionModel {
private final ActionCategory category;
private final String nameRes;
private final String descRes;
private final String iconPath;
private final @Nullable String iconPath;
private final Shortcut defaultShortcut;
ActionModel(ActionCategory category, String nameRes, String descRes, String iconPath, Shortcut defaultShortcut) {
this.category = category;
this.nameRes = nameRes;
this.descRes = descRes;
ActionModel(ActionCategory category, String nameRes, @Nullable String descRes, @Nullable String iconPath,
@Nullable Shortcut defaultShortcut) {
this.category = Objects.requireNonNull(category);
this.nameRes = Objects.requireNonNull(nameRes);
this.descRes = Utils.getOrElse(descRes, nameRes);
this.iconPath = iconPath;
this.defaultShortcut = defaultShortcut;
this.defaultShortcut = defaultShortcut != null ? defaultShortcut : Shortcut.none();
}
public static List<ActionModel> select(ActionCategory category) {
@@ -152,7 +150,7 @@ public enum ActionModel {
return category;
}
public String getName() {
public @Nullable String getName() {
if (nameRes != null) {
String name = NLS.str(nameRes);
if (name().endsWith("_V")) {
@@ -163,14 +161,14 @@ public enum ActionModel {
return null;
}
public String getDescription() {
public @Nullable String getDescription() {
if (descRes != null) {
return NLS.str(descRes);
}
return null;
}
public ImageIcon getIcon() {
public @Nullable ImageIcon getIcon() {
if (iconPath != null) {
return UiUtils.openSvgIcon(iconPath);
}
@@ -5,44 +5,34 @@ import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.CallGraphDialog;
import jadx.gui.ui.graphs.CallGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewCallGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewCallGraphAction.class);
private static final long serialVersionUID = -11122327621269039L;
private static final Logger LOG = LoggerFactory.getLogger(ViewCallGraphAction.class);
public ViewCallGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CALL_GRAPH, codeArea);
}
@Override
public void runAction(JNode node) {
try {
JMethod methodNode;
if (node instanceof JMethod) {
methodNode = (JMethod) node;
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
CallGraphDialog.open(getCodeArea().getMainWindow(), methodNode);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod;
}
@Override
public void runAction(JNode node) {
try {
CallGraphDialog.open(getCodeArea().getMainWindow(), (JMethod) node);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
}
@@ -11,23 +11,27 @@ import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.ClassInheritanceGraphDialog;
import jadx.gui.ui.graphs.ClassInheritanceGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewClassInheritanceGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewClassInheritanceGraphAction.class);
private static final long serialVersionUID = -331826691076655264L;
private static final Logger LOG = LoggerFactory.getLogger(ViewClassInheritanceGraphAction.class);
public ViewClassInheritanceGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CLASS_INHERITANCE_GRAPH, codeArea);
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
@Override
public void runAction(JNode node) {
try {
JClass classNode;
if (node instanceof JMethod) {
classNode = node.getJParent();
} else if (node instanceof JField) {
@@ -37,7 +41,6 @@ public final class ViewClassInheritanceGraphAction extends JNodeAction {
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
ClassInheritanceGraphDialog.open(getCodeArea().getMainWindow(), classNode);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
@@ -45,10 +48,4 @@ public final class ViewClassInheritanceGraphAction extends JNodeAction {
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
}
@@ -11,23 +11,27 @@ import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.ClassMethodGraphDialog;
import jadx.gui.ui.graphs.ClassMethodGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewClassMethodGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewClassMethodGraphAction.class);
private static final long serialVersionUID = -331826691076655264L;
private static final Logger LOG = LoggerFactory.getLogger(ViewClassMethodGraphAction.class);
public ViewClassMethodGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CLASS_METHOD_GRAPH, codeArea);
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
@Override
public void runAction(JNode node) {
try {
JClass classNode;
if (node instanceof JMethod) {
classNode = node.getJParent();
} else if (node instanceof JField) {
@@ -37,7 +41,6 @@ public final class ViewClassMethodGraphAction extends JNodeAction {
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
ClassMethodGraphDialog.open(getCodeArea().getMainWindow(), classNode);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
@@ -45,10 +48,4 @@ public final class ViewClassMethodGraphAction extends JNodeAction {
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
}
@@ -1,58 +1,41 @@
package jadx.gui.ui.action;
import java.io.File;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.ControlFlowGraphDialog;
import jadx.gui.ui.graphs.ControlFlowGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewControlFlowGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewControlFlowGraphAction.class);
private static final long serialVersionUID = -490213655L;
public ViewControlFlowGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CONTROL_FLOW_GRAPH, codeArea);
private static final Logger LOG = LoggerFactory.getLogger(ViewControlFlowGraphAction.class);
public ViewControlFlowGraphAction(ActionModel actionModel, CodeArea codeArea) {
super(actionModel, codeArea);
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod;
}
@Override
public void runAction(JNode node) {
try {
JMethod methodNode;
if (node instanceof JMethod) {
methodNode = (JMethod) node;
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, false);
ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), (JMethod) node);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.showMessageDialog(
getCodeArea().getMainWindow(),
e.getLocalizedMessage(),
NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
if (!(node instanceof JMethod)) {
return false;
}
MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode();
File file = new DotGraphUtils(false, false).getFullFile(mth);
return file.exists() && !file.isDirectory();
}
}
@@ -1,58 +0,0 @@
package jadx.gui.ui.action;
import java.io.File;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.ControlFlowGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewRawControlFlowGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewRawControlFlowGraphAction.class);
private static final long serialVersionUID = -535703386523657L;
public ViewRawControlFlowGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_RAW_CONTROL_FLOW_GRAPH, codeArea);
}
@Override
public void runAction(JNode node) {
try {
JMethod methodNode;
if (node instanceof JMethod) {
methodNode = (JMethod) node;
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, true);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
if (!(node instanceof JMethod)) {
return false;
}
MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode();
File file = new DotGraphUtils(false, true).getFullFile(mth);
return file.exists() && !file.isDirectory();
}
}
@@ -1,58 +0,0 @@
package jadx.gui.ui.action;
import java.io.File;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.dialog.ControlFlowGraphDialog;
import jadx.gui.utils.NLS;
public final class ViewRegionControlFlowGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewRegionControlFlowGraphAction.class);
private static final long serialVersionUID = -14970352087936L;
public ViewRegionControlFlowGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_REGION_CONTROL_FLOW_GRAPH, codeArea);
}
@Override
public void runAction(JNode node) {
try {
JMethod methodNode;
if (node instanceof JMethod) {
methodNode = (JMethod) node;
} else {
throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null"));
}
ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, true, false);
} catch (Exception e) {
LOG.error("Failed to view graph", e);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
if (!(node instanceof JMethod)) {
return false;
}
MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode();
File file = new DotGraphUtils(true, false).getFullFile(mth);
return file.exists() && !file.isDirectory();
}
}
@@ -36,20 +36,18 @@ import jadx.gui.treemodel.JLoadableNode;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.action.ActionModel;
import jadx.gui.ui.action.CommentSearchAction;
import jadx.gui.ui.action.CopyReferenceAction;
import jadx.gui.ui.action.FindUsageAction;
import jadx.gui.ui.action.FridaAction;
import jadx.gui.ui.action.GoToDeclarationAction;
import jadx.gui.ui.action.JNodeAction;
import jadx.gui.ui.action.JsonPrettifyAction;
import jadx.gui.ui.action.RenameAction;
import jadx.gui.ui.action.ViewCallGraphAction;
import jadx.gui.ui.action.ViewClassInheritanceGraphAction;
import jadx.gui.ui.action.ViewClassMethodGraphAction;
import jadx.gui.ui.action.ViewControlFlowGraphAction;
import jadx.gui.ui.action.ViewRawControlFlowGraphAction;
import jadx.gui.ui.action.ViewRegionControlFlowGraphAction;
import jadx.gui.ui.action.XposedAction;
import jadx.gui.ui.codearea.mode.JCodeMode;
import jadx.gui.ui.codearea.sync.CodeAreaSyncee;
@@ -61,7 +59,6 @@ import jadx.gui.utils.CaretPositionFix;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.JNodeCache;
import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.shortcut.ShortcutsController;
@@ -202,11 +199,7 @@ public final class CodeArea extends AbstractCodeArea implements CodeAreaSyncerAb
popup.add(new ViewClassInheritanceGraphAction(this));
popup.add(new ViewClassMethodGraphAction(this));
popup.add(new ViewCallGraphAction(this));
popup.addSubmenu(new JNodeAction[] {
new ViewControlFlowGraphAction(this),
new ViewRawControlFlowGraphAction(this),
new ViewRegionControlFlowGraphAction(this),
}, NLS.str("popup.cfg_submenu"));
popup.add(new ViewControlFlowGraphAction(ActionModel.VIEW_CONTROL_FLOW_GRAPH, this));
popup.addSeparator();
popup.add(new ConvertNumberAction(this));
@@ -127,8 +127,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
*
* @return blank code comment object (comment string empty)
*/
@Nullable
protected ICodeComment getCommentRef(int pos) {
protected @Nullable ICodeComment getCommentRef(int pos) {
if (pos == -1) {
return null;
}
@@ -4,55 +4,45 @@ import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import javax.swing.event.PopupMenuEvent;
import javax.swing.text.BadLocationException;
import org.fife.ui.rsyntaxtextarea.Token;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.data.CommentStyle;
import jadx.api.data.ICodeComment;
import jadx.api.data.impl.JadxCodeComment;
import jadx.api.data.impl.JadxCodeData;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.action.ActionModel;
import jadx.gui.ui.dialog.CommentDialog;
import jadx.gui.utils.NLS;
public class ConvertNumberAction extends CommentAction {
private static final String DEFAULT_TEXT = NLS.str("popup.convert_number");
private static final String TOOLTIP_TEXT = NLS.str("popup.convert_number_tooltip");
private static final Logger LOG = LoggerFactory.getLogger(ConvertNumberAction.class);
private static final String DEFAULT_TEXT = "";
private final String tooltipText;
private @Nullable String codeComment;
public ConvertNumberAction(CodeArea codeArea) {
super(ActionModel.CONVERT_NUMBER, codeArea);
tooltipText = NLS.str("popup.convert_number");
// default menu item to disabled
setEnabled(false);
setNameAndDesc(DEFAULT_TEXT);
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
if (codeArea.getNode() instanceof JClass) {
// try parse number from word under caret
// and set text of popup menu dynamically
String word = getWordByPosition(codeArea.getCaretPosition());
List<String> conversions = getConversionsFromWord(word);
if (conversions != null && !conversions.isEmpty()) {
String joined = String.join(" | ", conversions);
setName(joined);
setShortDescription(tooltipText);
if (!conversions.isEmpty()) {
codeComment = String.join(" | ", conversions);
setName(DEFAULT_TEXT + ": " + codeComment);
setShortDescription(TOOLTIP_TEXT);
setEnabled(true);
}
}
@@ -63,59 +53,29 @@ public class ConvertNumberAction extends CommentAction {
// reset menu to disabled on cancel
setEnabled(false);
setNameAndDesc(DEFAULT_TEXT);
codeComment = null;
}
@Override
public void actionPerformed(ActionEvent e) {
if (!super.enabled) {
if (!enabled) {
return;
}
String newText = e.getActionCommand();
String newText = codeComment;
if (newText == null) {
return;
}
ICodeComment comment = getCommentRef(codeArea.getCaretPosition());
if (comment == null) {
return;
}
ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(), comment.getCodeRef(), newText, CommentStyle.LINE);
updateCommentsData(codeArea, list -> list.add(newComment));
CommentDialog.updateCommentsData(codeArea, list -> list.add(newComment));
}
/**
* Adds comments to project file and code area
*/
private static void updateCommentsData(CodeArea codeArea, Consumer<List<ICodeComment>> updater) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
if (codeData == null) {
codeData = new JadxCodeData();
}
List<ICodeComment> list = new ArrayList<>(codeData.getComments());
updater.accept(list);
Collections.sort(list);
codeData.setComments(list);
project.setCodeData(codeData);
codeArea.getMainWindow().getWrapper().reloadCodeData();
} catch (Exception e) {
LOG.error("Comment action failed", e);
}
try {
// refresh code
codeArea.backgroundRefreshClass();
} catch (Exception e) {
LOG.error("Failed to reload code", e);
}
}
/**
* similar to AbstractCodeArea::getWordByPosition
* Similar to AbstractCodeArea::getWordByPosition
* but includes "-" for negative numbers
*/
public @Nullable String getWordByPosition(int offset) {
@@ -123,19 +83,15 @@ public class ConvertNumberAction extends CommentAction {
if (token == null) {
return null;
}
String str = token.getLexeme();
try {
String prev = codeArea.getText(token.getOffset() - 1, 1);
if (prev.equals("-")) {
str = "-" + str;
}
} catch (BadLocationException e) {
// ignore
}
int len = str.length();
if (len > 2 && str.startsWith("\"") && str.endsWith("\"")) {
return str.substring(1, len - 1);
@@ -143,53 +99,52 @@ public class ConvertNumberAction extends CommentAction {
return str;
}
private static final Pattern NUMBER_FORMAT = Pattern.compile("(-?\\d+L?|0x[0-9A-Fa-f]+)");
/**
* Tries to parse a number from input string,
* returns list of strings of the number converted to different formats.
* e.g. if input number is in hex, converts to decimal and binary.
*/
static @Nullable List<String> getConversionsFromWord(String word) {
List<String> conversions = new ArrayList<>();
if (word == null || word.isEmpty()) {
return null;
static List<String> getConversionsFromWord(@Nullable String word) {
if (word == null || word.isEmpty() || word.equals("0") || word.equals("0L")) {
return Collections.emptyList();
}
int i32 = 0;
long i64 = 0;
int radix = 10;
if (!NUMBER_FORMAT.matcher(word).matches()) {
return Collections.emptyList();
}
int i32;
long i64;
int radix;
boolean parsedLong = false;
// handle hex
if (word.startsWith("0x")) {
word = word.substring(2);
radix = 16;
} else {
radix = 10;
}
// handle long int syntax like "12345L"
if (word.endsWith("L")) {
word = word.substring(0, word.length() - 1);
i64 = tryParseLong(word, radix);
i32 = (int) i64;
parsedLong = true;
}
// try parse int
try {
i32 = Integer.parseInt(word, radix);
i64 = i32;
} catch (NumberFormatException e) {
// try parse long
try {
i64 = Long.parseLong(word, radix);
if (i64 == 0) {
return Collections.emptyList();
}
} else {
i32 = tryParseInt(word, radix);
if (i32 != 0) {
i64 = i32;
} else {
i64 = tryParseLong(word, radix);
parsedLong = true;
} catch (NumberFormatException ignore) {
return null;
if (i64 == 0) {
return Collections.emptyList();
}
}
}
List<String> conversions = new ArrayList<>();
// if we parsed decimal, output hex and vice versa
if (radix == 10) {
if (parsedLong) {
@@ -197,13 +152,11 @@ public class ConvertNumberAction extends CommentAction {
} else {
conversions.add(String.format("0x%x", i32));
}
} else if (radix == 16) {
conversions.add(String.format("%d", i32));
} else {
conversions.add(parsedLong ? Long.toString(i64) : Integer.toString(i32));
}
// pad binary in 8-bit groups
// int leadingZeros = parsed_long ? : Integer.numberOfLeadingZeros(i32);
int padBits = (int) Math.ceil((64 - Long.numberOfLeadingZeros(i64)) / 8.0) * 8;
if (padBits < 8) {
padBits = 8;
@@ -211,17 +164,31 @@ public class ConvertNumberAction extends CommentAction {
if (!parsedLong && padBits > 32) {
padBits = 32;
}
// format padded binary
String binaryString = parsedLong ? Long.toBinaryString(i64) : Integer.toBinaryString(i32);
String fmt = String.format("0b%%%ds", padBits);
conversions.add(String.format(fmt, binaryString).replace(' ', '0'));
// format printable ascii chars
// format printable ASCII chars
if (i32 >= ' ' && i32 <= '~') {
conversions.add(String.format("'%c'", (int) i32));
conversions.add(String.format("'%c'", i32));
}
return conversions;
}
return conversions; // no match
private static int tryParseInt(String str, int radix) {
try {
return Integer.parseInt(str, radix);
} catch (NumberFormatException e) {
return 0;
}
}
private static long tryParseLong(String str, int radix) {
try {
return Long.parseLong(str, radix);
} catch (NumberFormatException e) {
return 0;
}
}
}
@@ -38,21 +38,6 @@ public class JNodePopupBuilder {
popupListener.addActions(nodeAction);
}
public void addSubmenu(JNodeAction[] nodeActions, String name) {
JMenu submenu = new JMenu(name);
for (JNodeAction nodeAction : nodeActions) {
if (nodeAction.getActionModel() != null) {
shortcutsController.bindImmediate(nodeAction);
}
submenu.add(nodeAction);
popupListener.addActions(nodeAction);
}
menu.add(submenu);
}
public void add(JadxGuiAction action) {
if (action.getActionModel() != null) {
shortcutsController.bindImmediate(action);
@@ -63,6 +48,18 @@ public class JNodePopupBuilder {
}
}
public void addSubmenu(String name, JNodeAction... nodeActions) {
JMenu submenu = new JMenu(name);
for (JNodeAction nodeAction : nodeActions) {
if (nodeAction.getActionModel() != null) {
shortcutsController.bindImmediate(nodeAction);
}
submenu.add(nodeAction);
popupListener.addActions(nodeAction);
}
menu.add(submenu);
}
public JPopupMenu getMenu() {
return menu;
}
@@ -44,7 +44,7 @@ public class CommentDialog extends CommonDialog {
dialog.setVisible(true);
}
private static void updateCommentsData(CodeArea codeArea, Consumer<List<ICodeComment>> updater) {
public static void updateCommentsData(CodeArea codeArea, Consumer<List<ICodeComment>> updater) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
@@ -1,52 +0,0 @@
package jadx.gui.ui.dialog;
import java.io.File;
import java.util.Scanner;
import javax.swing.SwingUtilities;
import jadx.api.JavaMethod;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.gui.treemodel.JMethod;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
public class ControlFlowGraphDialog extends GraphDialog {
private static final long serialVersionUID = -68749445239697710L;
public ControlFlowGraphDialog(MainWindow mainWindow, String method) {
super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.cfg.title"), method));
}
public static void open(MainWindow window, JMethod method, boolean useRegions, boolean rawInsn) {
JavaMethod javaMethod = method.getJavaMethod();
GraphDialog graphDialog = new ControlFlowGraphDialog(window, DotGraphUtils.methodFormatName(javaMethod, false));
graphDialog.addMenuBar();
graphDialog.setVisible(true);
SwingUtilities.invokeLater(() -> {
String graph = generateGraph(javaMethod, useRegions, rawInsn);
if (graph != null) {
graphDialog.getPanel().setGraph(graph);
} else {
graphDialog.getPanel().invalidateImage(graphError(NLS.str("graph_viewer.file_not_found_error")));
}
});
}
private static String generateGraph(JavaMethod javaMethod, boolean useRegions, boolean rawInsn) {
MethodNode mth = javaMethod.getMethodNode();
File file = new DotGraphUtils(useRegions, rawInsn).getFullFile(mth);
try (Scanner reader = new Scanner(file)) {
String contents = reader.useDelimiter("\\Z").next();
return contents;
} catch (Exception e) {
return null;
}
}
}
@@ -1,417 +0,0 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import guru.nidi.graphviz.engine.Format;
import guru.nidi.graphviz.engine.Graphviz;
import guru.nidi.graphviz.model.MutableGraph;
import guru.nidi.graphviz.parse.Parser;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
public abstract class GraphDialog extends JFrame {
private static final long serialVersionUID = 5840390965763493590L;
private static final Logger LOG = LoggerFactory.getLogger(GraphDialog.class);
private final MainWindow mainWindow;
private GraphPanel panel;
private static final Dimension MIN_WINDOW_SIZE = new Dimension(800, 500);
private JMenuBar menuBar = null;
public static JTextArea graphError() {
return graphError(NLS.str("graph_viewer.default_error"));
}
public static JTextArea graphError(String errorMessage) {
JTextArea errorText = new JTextArea();
errorText.setText(errorMessage);
errorText.setVisible(true);
errorText.setEditable(false);
errorText.setLineWrap(false);
return errorText;
}
public static JTextArea graphError(Exception error) {
JTextArea errorText = new JTextArea();
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
stringWriter.write(NLS.str("graph_viewer.default_error"));
stringWriter.write(": ");
error.printStackTrace(printWriter);
errorText.setText(stringWriter.toString());
errorText.setVisible(true);
errorText.setEditable(false);
errorText.setLineWrap(false);
return errorText;
}
public GraphDialog(MainWindow mainWindow) {
this(mainWindow, NLS.str("graph_viewer.default_title"));
}
public JMenuBar addMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setLayout(new WrapLayout(FlowLayout.LEFT));
add(menuBar, BorderLayout.PAGE_START);
this.menuBar = menuBar;
JFileChooser fileChooser = new JFileChooser();
JButton saveButton = new JButton(NLS.str("graph_viewer.save_graph"));
saveButton.setEnabled(false);
saveButton.addActionListener(e -> {
try {
int option = fileChooser.showSaveDialog(this);
if (option == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
getPanel().renderer.render(Format.SVG).toFile(file);
}
} catch (Exception ex) {
LOG.error("Failed to save file: ", ex);
JOptionPane.showMessageDialog(this, NLS.str("graph_viewer.file_failure"),
NLS.str("graph_viewer.file_failure"),
JOptionPane.INFORMATION_MESSAGE);
}
});
// Assemble menubar panel
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.add(saveButton);
// Add menubar panel to menuBar
menuBar.add(menuBarPanel);
return menuBar;
}
private void enableMenu() {
JMenuBar menu = this.menuBar;
setAllEnabled(true, menu);
}
private void disableMenu() {
JMenuBar menu = this.menuBar;
setAllEnabled(false, menu);
}
private void setAllEnabled(boolean isEnabled, JComponent component) {
component.setEnabled(isEnabled);
Component[] components = component.getComponents();
for (Component subComponent : components) {
if (subComponent instanceof JComponent) {
setAllEnabled(isEnabled, (JComponent) subComponent);
} else {
subComponent.setEnabled(isEnabled);
}
}
}
public GraphDialog(MainWindow mainWindow, String title) {
super(title);
this.mainWindow = mainWindow;
setMinimumSize(MIN_WINDOW_SIZE);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
UiUtils.addEscapeShortCutToDispose(this);
setLocationRelativeTo(null);
loadWindowPos();
LOG.debug("Dialog w: {} h: {}", getWidth(), getHeight());
LOG.debug("cwd: {}", System.getProperty("user.dir"));
panel = new GraphPanel(this);
panel.setFocusable(true);
panel.addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
requestFocusInWindow();
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
});
setLayout(new BorderLayout());
add(panel, BorderLayout.CENTER);
}
public void loadWindowPos() {
if (!mainWindow.getSettings().loadWindowPos(this)) {
setPreferredSize(MIN_WINDOW_SIZE);
}
}
@Override
public void dispose() {
try {
mainWindow.getSettings().saveWindowPos(this);
} catch (Exception e) {
LOG.warn("Failed to save window size and position", e);
}
super.dispose();
}
class GraphPanel extends JPanel {
private Dimension fullImageSize = new Dimension();
private double scale = 1.0;
private double minimumScale = 0.01;
private double maximumScale = 7.0;
private double translateX = 0;
private double translateY = 0;
private Point lastDragPoint = null;
private BufferedImage image;
private Graphviz renderer;
private final GraphDialog parentDialog;
public GraphPanel(GraphDialog parentDialog) {
this.parentDialog = parentDialog;
MouseAdapter ma = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
lastDragPoint = e.getPoint();
}
@Override
public void mouseDragged(MouseEvent e) {
if (image != null) {
Point p = e.getPoint();
translateX += (p.x - lastDragPoint.x) / scale;
translateY += (p.y - lastDragPoint.y) / scale;
lastDragPoint = p;
repaint();
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (image != null) {
double prevScale = scale;
scale *= Math.pow(1.1, -e.getWheelRotation());
if (scale > maximumScale) {
scale = maximumScale;
}
if (scale < minimumScale) {
scale = minimumScale;
}
if (scale != prevScale) {
Point p = e.getPoint();
double px = (p.x - translateX * prevScale) / prevScale;
double py = (p.y - translateY * prevScale) / prevScale;
translateX = (p.x / scale) - px;
translateY = (p.y / scale) - py;
LOG.debug("Rescaling {}%", scale * 100);
renderGraphScaled();
if (image == null) {
return;
}
repaint();
}
}
}
};
addMouseListener(ma);
addMouseMotionListener(ma);
addMouseWheelListener(ma);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
Graphics2D g2d = (Graphics2D) g;
AffineTransform transform = new AffineTransform();
transform.translate(translateX * scale, translateY * scale);
g2d.drawImage(image, transform, null);
}
}
public void setGraph(File dotString) {
try {
LOG.debug("Parsing DOT file: {} ", dotString.getAbsolutePath());
setGraph(new Parser().read(dotString));
} catch (Exception e) {
LOG.error("Error parsing DOT file", e);
invalidateImage(graphError(e));
}
}
public void setGraph(String dotString) {
try {
setGraph(new Parser().read(dotString));
} catch (Exception e) {
LOG.error("Error parsing DOT string", e);
invalidateImage(graphError(e));
}
}
public void setGraph(MutableGraph g) {
renderer = Graphviz.fromGraph(g);
parentDialog.enableMenu();
scale = 1.0;
// set initial image scale and posiition
Runnable doCenter = new Runnable() {
public void run() {
renderGraphFullSize();
if (image == null) {
return;
}
LOG.debug("full image w {} h {}", fullImageSize.width, fullImageSize.height);
// scale required to fit image to window width or height
double heightScale = (double) getHeight() / (double) fullImageSize.height;
double widthScale = (double) getWidth() / (double) fullImageSize.width;
if (widthScale < heightScale) {
scale = widthScale;
LOG.debug("scaling to fit width {}/{} {}", getWidth(), fullImageSize.width, scale);
} else {
scale = heightScale;
LOG.debug("scaling to fit height {}/{} {}", getHeight(), fullImageSize.height, scale);
}
scale = scale * 0.95;
maximumScale = Math.sqrt(Integer.MAX_VALUE / (fullImageSize.width * fullImageSize.height)) / 8;
minimumScale = Math.min(scale, maximumScale);
renderGraphScaled();
if (image == null) {
return;
}
// center image in window
translateY = (getHeight() / 2 - (fullImageSize.height * scale) / 2) / scale;
translateX = (getWidth() / 2 - (fullImageSize.width * scale) / 2) / scale;
repaint();
}
};
SwingUtilities.invokeLater(doCenter);
}
private void renderGraphFullSize() {
try {
image = null;
image = renderer.render(Format.SVG).toImage();
if (image.getWidth() == 0 || image.getHeight() == 0) {
// If rendered image is too small, calculating the scale would later cause a
// division by zero
LOG.error("Graph render failed, image too small");
invalidateImage(graphError(NLS.str("graph_viewer.image_too_small")));
return;
}
fullImageSize.setSize(image.getWidth(), image.getHeight());
} catch (IllegalArgumentException illegalArgumentException) {
// If rendered image is too large, a Dimension object is passed invalid arguments
LOG.error("Graph render failed, illegal arguments: ", illegalArgumentException);
invalidateImage(graphError(NLS.str("graph_viewer.image_too_large")));
} catch (Exception e) {
// A large image may cause a number of other other exception types caught here along with other
// failure cases
LOG.error("Graph render failed: ", e);
invalidateImage(graphError(e));
}
}
private void renderGraphScaled() {
try {
if (fullImageSize.width * scale * fullImageSize.height * scale >= Integer.MAX_VALUE) {
scale = maximumScale;
}
image = renderer.width((int) (fullImageSize.width * scale)).render(Format.SVG).toImage();
} catch (Exception e) {
LOG.error("Graph render failed: ", e);
invalidateImage(graphError(e));
}
}
public void invalidateImage(JTextArea errorMsg) {
this.add(errorMsg);
image = null;
this.parentDialog.disableMenu();
this.revalidate();
repaint();
}
}
protected GraphPanel getPanel() {
return this.panel;
}
}
@@ -1,4 +1,4 @@
package jadx.gui.ui.dialog;
package jadx.gui.ui.graphs;
import java.awt.BorderLayout;
import java.awt.Color;
@@ -9,7 +9,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.swing.Box;
@@ -24,9 +23,6 @@ import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.plugins.input.data.IMethodRef;
@@ -38,19 +34,17 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
public class CallGraphDialog extends GraphDialog {
private static final long serialVersionUID = -850803763322590708L;
private static final Logger LOG = LoggerFactory.getLogger(CallGraphDialog.class);
private static final String FONT = "fontname=\"Courier\" fontsize=12";
private final JavaMethod javaMethod;
private int callerDepthLimit = 3;
private int calleeDepthLimit = 3;
private int nextNodeID;
private Map<JavaMethod, Integer> methodToNodeID;
private Map<IMethodRef, Integer> unresolvedMethodToNodeID;
private Set<Edge> edges;
private JavaMethod javaMethod;
private boolean longNames = false;
public CallGraphDialog(MainWindow mainWindow, JavaMethod javaMethod) {
@@ -59,6 +53,7 @@ public class CallGraphDialog extends GraphDialog {
this.javaMethod = javaMethod;
}
@Override
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
@@ -128,7 +123,6 @@ public class CallGraphDialog extends GraphDialog {
}
public static void open(MainWindow window, JMethod method) {
JavaMethod javaMethod = method.getJavaMethod();
CallGraphDialog graphDialog = new CallGraphDialog(window, javaMethod);
graphDialog.addMenuBar();
@@ -141,13 +135,11 @@ public class CallGraphDialog extends GraphDialog {
SwingUtilities.invokeLater(() -> {
String graph = generateGraph(javaMethod);
getPanel().setGraph(graph);
});
}
private String generateGraph(JavaMethod javaMethod) {
StringBuilder sb = new StringBuilder();
Color themeBackground = UIManager.getColor("Panel.background");
Color themeForeground = UIManager.getColor("Label.foreground");
Color themeHighlight = UIManager.getColor("Component.focusedBorderColor");
@@ -167,7 +159,6 @@ public class CallGraphDialog extends GraphDialog {
String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue());
try (Formatter f = new Formatter(sb)) {
// graph header
f.format("digraph G {\n");
f.format("%s\n", bgColor);
@@ -180,16 +171,12 @@ public class CallGraphDialog extends GraphDialog {
edges = new HashSet<>();
addNode(f, javaMethod, highlightColor);
// add caller relationships
addCallers(0, f, javaMethod);
// add calee relationships
addCallees(0, f, javaMethod);
// close graph
f.format("}");
return f.toString();
}
}
@@ -198,7 +185,6 @@ public class CallGraphDialog extends GraphDialog {
if (depth >= callerDepthLimit) {
return;
}
List<JavaNode> uses = javaMethod.getUseIn();
// add "calls" relationships
@@ -207,10 +193,8 @@ public class CallGraphDialog extends GraphDialog {
continue;
}
JavaMethod caller = (JavaMethod) node;
int nodeID = addNode(f, caller);
addEdge(f, nodeID, methodToNodeID.get(javaMethod));
addCallers(depth + 1, f, caller);
}
}
@@ -219,7 +203,6 @@ public class CallGraphDialog extends GraphDialog {
if (depth >= calleeDepthLimit) {
return;
}
List<JavaNode> used = javaMethod.getUsed();
// add "calls" relationships
@@ -228,10 +211,8 @@ public class CallGraphDialog extends GraphDialog {
continue;
}
JavaMethod callee = (JavaMethod) node;
int nodeID = addNode(f, callee);
addEdge(f, methodToNodeID.get(javaMethod), nodeID);
addCallees(depth + 1, f, callee);
}
addUnresolvedCallees(depth, f, javaMethod);
@@ -241,7 +222,6 @@ public class CallGraphDialog extends GraphDialog {
if (depth >= calleeDepthLimit) {
return;
}
List<IMethodRef> used = javaMethod.getUnresolvedUsed();
// add "calls" relationships
@@ -250,7 +230,6 @@ public class CallGraphDialog extends GraphDialog {
if (name == null) {
continue;
}
int nodeID = addNode(f, callee);
addEdge(f, methodToNodeID.get(javaMethod), nodeID);
}
@@ -260,7 +239,9 @@ public class CallGraphDialog extends GraphDialog {
return addNode(f, method, "");
}
// Add a node representing method to the graph in f. Returns the ID of the new node
/**
* Add a node representing method to the graph in f. Returns the ID of the new node
*/
private int addNode(Formatter f, JavaMethod method, String extra) {
int nodeID;
if (methodToNodeID.containsKey(method)) {
@@ -270,14 +251,11 @@ public class CallGraphDialog extends GraphDialog {
nextNodeID++;
methodToNodeID.put(method, nodeID);
}
String name = DotGraphUtils.methodFormatName(method, longNames);
f.format("Node_%d [ label=\"{%s}\" %s]\n", nodeID, UiUtils.toDotNodeName(name), extra);
if (javaMethod.callsSelf()) {
addEdge(f, nodeID, nodeID);
}
return nodeID;
}
@@ -285,7 +263,9 @@ public class CallGraphDialog extends GraphDialog {
return addNode(f, method, "");
}
// Add a node representing an unresolved method to the graph in f. Returns the ID of the new node
/**
* Add a node representing an unresolved method to the graph in f. Returns the ID of the new node
*/
private int addNode(Formatter f, IMethodRef method, String extra) {
int nodeID;
if (unresolvedMethodToNodeID.containsKey(method)) {
@@ -315,28 +295,4 @@ public class CallGraphDialog extends GraphDialog {
edges.add(edge);
}
}
private static class Edge {
public int source;
public int dest;
public Edge(int source, int dest) {
this.source = source;
this.dest = dest;
}
@Override
public boolean equals(Object otherObject) {
if (!(otherObject instanceof Edge)) {
return false;
}
Edge other = (Edge) otherObject;
return (this.source == other.source) && (this.dest == other.dest);
}
@Override
public int hashCode() {
return Objects.hash(source, dest);
}
}
}
@@ -1,4 +1,4 @@
package jadx.gui.ui.dialog;
package jadx.gui.ui.graphs;
import java.awt.BorderLayout;
import java.awt.Color;
@@ -15,9 +15,6 @@ import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.android.apksig.internal.util.Pair;
import jadx.core.dex.attributes.AType;
@@ -34,13 +31,11 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
public class ClassInheritanceGraphDialog extends GraphDialog {
private static final long serialVersionUID = 938883901412562913L;
private static final Logger LOG = LoggerFactory.getLogger(ClassInheritanceGraphDialog.class);
private static final String FONT = "fontname=\"Courier\" fontsize=12";
private ClassNode cls;
private final ClassNode cls;
private boolean longNames = false;
private boolean overrides = false;
@@ -53,6 +48,7 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
this.cls = cls;
}
@Override
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
@@ -85,12 +81,9 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
}
public static void open(MainWindow window, JClass node) {
ClassNode cls = node.getCls().getClassNode();
ClassInheritanceGraphDialog graphDialog = new ClassInheritanceGraphDialog(window, cls);
graphDialog.addMenuBar();
graphDialog.setVisible(true);
graphDialog.reload();
}
@@ -103,9 +96,6 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
}
private String generateGraph(ClassNode rootClass) {
StringBuilder sb = new StringBuilder();
ClassNode cls = rootClass;
objectToNodeID = new HashMap<>();
@@ -126,8 +116,8 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
themeHighlight.getBlue());
String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue());
StringBuilder sb = new StringBuilder();
try (Formatter f = new Formatter(sb)) {
// graph header
f.format("digraph G {\n");
f.format("%s\n", bgColor);
@@ -135,11 +125,9 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor);
// add nodes
processClass(f, cls, highlightColor);
processClass(f, rootClass, highlightColor);
// close graph
f.format("}");
return f.toString();
}
}
@@ -175,7 +163,6 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
// add superclass relationship
ArgType superClass = cls.getSuperClass();
if (superClass != ArgType.OBJECT) {
int superClsID;
cls = cls.root().resolveClass(superClass);
@@ -185,7 +172,6 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
} else {
superClsID = addNode(f, superClass);
}
f.format("Node_%d -> Node_%d [label=\"extends\" ]\n", classID, superClsID);
}
return classID;
@@ -205,14 +191,11 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
nextNodeID++;
objectToNodeID.put(cls, nodeID);
}
if (cls.getAccessFlags().isInterface()) {
extra += " style=\"dashed, filled\"";
}
String name = DotGraphUtils.classFormatName(cls, longNames);
f.format("Node_%d [ label=\"{%s\\ ", nodeID, UiUtils.toDotNodeName(name));
if (overrides) {
f.format("|");
List<Pair<String, String>> table = new ArrayList<>();
@@ -227,30 +210,24 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
String baseClassName = DotGraphUtils.classFormatName(baseMthDetails.getMethodInfo().getDeclClass(), longNames);
details.format("%s, ", baseClassName);
}
String detailsString = details.toString();
// Remove trailing ', '
detailsString = detailsString.substring(0, detailsString.length() - 2);
table.add(Pair.of(methodName, detailsString));
details.close();
}
}
}
if (!table.isEmpty()) {
int longestLength = table.stream().map(Pair::getFirst).map(String::length).max((a, b) -> a - b).get();
for (Pair<String, String> entry : table) {
f.format("%-" + longestLength + "s %s\\l", entry.getFirst(), entry.getSecond());
}
} else {
f.format("No overrides.");
}
}
f.format("}\" %s]\n", extra);
return nodeID;
}
@@ -268,15 +245,11 @@ public class ClassInheritanceGraphDialog extends GraphDialog {
nextNodeID++;
objectToNodeID.put(argType, nodeID);
}
Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor");
String outOfFocus =
String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(), themeOutOfFocus.getBlue());
String name = DotGraphUtils.interfaceFormatName(argType, cls, longNames);
f.format("Node_%d [ label=\"{%s}\" %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra);
return nodeID;
}
}
@@ -1,4 +1,4 @@
package jadx.gui.ui.dialog;
package jadx.gui.ui.graphs;
import java.awt.BorderLayout;
import java.awt.Color;
@@ -9,7 +9,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -31,16 +30,16 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
public class ClassMethodGraphDialog extends GraphDialog {
private static final long serialVersionUID = -850803763322590708L;
private static final String FONT = "fontname=\"Courier\" fontsize=12";
private int callerDepthLimit = 10;
private static final int CALLER_DEPTH_LIMIT = 10;
private final ClassNode cls;
private int nextNodeID = 0;
private Map<JavaMethod, Integer> methodToNodeID;
private Set<Edge> edges;
private List<JavaMethod> javaMethods = Collections.emptyList();
private ClassNode cls;
private boolean longNames = false;
public ClassMethodGraphDialog(MainWindow mainWindow, ClassNode cls) {
@@ -48,6 +47,7 @@ public class ClassMethodGraphDialog extends GraphDialog {
this.cls = cls;
}
@Override
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
@@ -71,41 +71,28 @@ public class ClassMethodGraphDialog extends GraphDialog {
}
public static void open(MainWindow window, JClass node) {
ClassNode cls = node.getCls().getClassNode();
ClassMethodGraphDialog graphDialog = new ClassMethodGraphDialog(window, cls);
graphDialog.addMenuBar();
graphDialog.setVisible(true);
graphDialog.reload();
}
public void reload() {
SwingUtilities.invokeLater(() -> {
String graph = generateGraph(cls);
getPanel().setGraph(graph);
});
SwingUtilities.invokeLater(() -> getPanel().setGraph(generateGraph(cls)));
}
private String generateGraph(ClassNode classNode) {
StringBuilder sb = new StringBuilder();
Color themeBackground = UIManager.getColor("Panel.background");
Color themeForeground = UIManager.getColor("Label.foreground");
Color themeShade = UIManager.getColor("TextArea.background");
String bgColor =
String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(),
themeBackground.getBlue());
String lineColor =
String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue());
String fontColor =
String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(),
themeForeground.getBlue());
String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue());
try (Formatter f = new Formatter(sb)) {
String bgColor = "bgcolor=" + formatColor(themeBackground);
String lineColor = "color=" + formatColor(themeForeground);
String fontColor = "fontcolor=" + formatColor(themeForeground);
String shadeColor = "fillcolor=" + formatColor(themeShade);
try (Formatter f = new Formatter(new StringBuilder())) {
// graph header
f.format("digraph G {\n");
f.format("%s\n", bgColor);
@@ -117,45 +104,39 @@ public class ClassMethodGraphDialog extends GraphDialog {
edges = new HashSet<>();
List<MethodNode> methods = classNode.getMethods();
javaMethods = methods.stream().map(method -> method.getJavaNode()).collect(Collectors.toList());
javaMethods = methods.stream().map(MethodNode::getJavaNode).collect(Collectors.toList());
for (JavaMethod javaMethod : javaMethods) {
addNode(f, javaMethod);
// add caller relationships
addCallers(0, f, javaMethod);
}
// close graph
f.format("}");
return f.toString();
}
}
private static String formatColor(Color color) {
return String.format("\"#%02x%02x%02x\"", color.getRed(), color.getGreen(), color.getBlue());
}
private void addCallers(int depth, Formatter f, JavaMethod javaMethod) {
if (depth >= callerDepthLimit) {
if (depth >= CALLER_DEPTH_LIMIT) {
return;
}
List<JavaNode> uses = javaMethod.getUseIn();
// add "calls" relationships
for (JavaNode node : uses) {
if (!(node instanceof JavaMethod)) {
continue;
}
JavaMethod caller = (JavaMethod) node;
// Do not process callers that are not methods from the class
if (!javaMethods.contains(node)) {
continue;
}
int nodeID = addNode(f, caller);
addEdge(f, nodeID, methodToNodeID.get(javaMethod));
addCallers(depth + 1, f, caller);
}
}
@@ -170,14 +151,11 @@ public class ClassMethodGraphDialog extends GraphDialog {
nextNodeID++;
methodToNodeID.put(method, nodeID);
}
String name = DotGraphUtils.methodFormatName(method, longNames);
f.format("Node_%d [ label=\"{%s}\"]\n", nodeID, UiUtils.toDotNodeName(name));
if (method.callsSelf()) {
addEdge(f, nodeID, nodeID);
}
return nodeID;
}
@@ -189,28 +167,4 @@ public class ClassMethodGraphDialog extends GraphDialog {
edges.add(edge);
}
}
private static class Edge {
public int source;
public int dest;
public Edge(int source, int dest) {
this.source = source;
this.dest = dest;
}
@Override
public boolean equals(Object otherObject) {
if (!(otherObject instanceof Edge)) {
return false;
}
Edge other = (Edge) otherObject;
return (this.source == other.source) && (this.dest == other.dest);
}
@Override
public int hashCode() {
return Objects.hash(source, dest);
}
}
}
@@ -0,0 +1,171 @@
package jadx.gui.ui.graphs;
import java.awt.FlowLayout;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.Box;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import org.jetbrains.annotations.Nullable;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.utils.DotGraphUtils;
import jadx.gui.treemodel.JMethod;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
public class ControlFlowGraphDialog extends GraphDialog {
private static final long serialVersionUID = -68749445239697710L;
public static void open(MainWindow window, JMethod jMth) {
ControlFlowGraphDialog graphDialog = new ControlFlowGraphDialog(window, jMth);
graphDialog.addMenuBar();
graphDialog.setVisible(true);
graphDialog.usePreset(GraphPreset.NORMAL);
}
private static final String[] PRESETS_NSL = NLS.str("graph_viewer.cfg.preset_names").split("\\|");
private enum GraphPreset {
RAW(PRESETS_NSL[0], true, false, "SSATransform"),
NORMAL(PRESETS_NSL[1], false, false, "RegionMakerVisitor"),
REGION(PRESETS_NSL[2], false, true, "PrepareForCodeGen");
final boolean useRawInsns;
final boolean useRegions;
final String beforePass;
final String nlsStr;
GraphPreset(String nlsStr, boolean useRawInsns, boolean useRegions, String beforePass) {
this.nlsStr = nlsStr;
this.useRawInsns = useRawInsns;
this.useRegions = useRegions;
this.beforePass = beforePass;
}
@Override
public String toString() {
return nlsStr;
}
}
private final MethodNode mth;
private @Nullable GraphPreset graphPreset;
private JComboBox<GraphPreset> presetsCB;
private JComboBox<String> passesCB;
private String[] passNames;
private int currentPassIdx;
private ControlFlowGraphDialog(MainWindow mainWindow, JMethod jMth) {
super(mainWindow);
mth = jMth.getJavaMethod().getMethodNode();
String mthName = DotGraphUtils.methodFormatName(jMth.getJavaMethod(), false);
setTitle(String.format("%s: %s", NLS.str("graph_viewer.cfg.title"), mthName));
}
private void usePreset(@Nullable GraphPreset graphPreset) {
if (graphPreset == null || this.graphPreset == graphPreset) {
return;
}
this.graphPreset = graphPreset;
this.currentPassIdx = selectPassBefore(graphPreset.beforePass);
presetsCB.setSelectedItem(graphPreset);
passesCB.setSelectedItem(passNames[currentPassIdx]);
reloadGraph();
}
private int selectPassBefore(String beforePass) {
List<IDexTreeVisitor> passes = getPassList();
for (int i = 1, passesSize = passes.size(); i < passesSize; i++) {
if (passes.get(i).getName().equals(beforePass)) {
return i - 1;
}
}
return passes.size() - 1;
}
@Override
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
presetsCB = new JComboBox<>(GraphPreset.values());
presetsCB.addActionListener(e -> usePreset((GraphPreset) presetsCB.getSelectedItem()));
List<IDexTreeVisitor> passList = getPassList();
int size = passList.size();
Map<String, Integer> passMap = new HashMap<>();
passNames = new String[size];
for (int i = 0; i < size; i++) {
IDexTreeVisitor pass = passList.get(i);
String name = i + ": " + pass.getName();
passMap.put(name, i);
passNames[i] = name;
}
passesCB = new JComboBox<>(passNames);
passesCB.addActionListener(e -> {
String newValue = (String) passesCB.getSelectedItem();
if (newValue != null) {
Integer newIdx = passMap.get(newValue);
if (newIdx != null && newIdx != currentPassIdx) {
currentPassIdx = newIdx;
reloadGraph();
}
}
});
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
menuBarPanel.add(new JLabel(NLS.str("graph_viewer.cfg.preset_selector_label")));
menuBarPanel.add(presetsCB);
menuBarPanel.add(Box.createHorizontalBox());
menuBarPanel.add(new JLabel(NLS.str("graph_viewer.cfg.pass_selector_label")));
menuBarPanel.add(passesCB);
menuBar.add(menuBarPanel);
return menuBar;
}
@Override
protected void disableMenu() {
// don't disable menu if invalid combination of insn type and pass selected
}
private void reloadGraph() {
UiUtils.uiRun(() -> {
String graph = generateGraph();
if (graph != null) {
getPanel().setGraph(graph);
} else {
getPanel().invalidateImage(graphError(NLS.str("graph_viewer.default_error")));
}
});
}
private @Nullable String generateGraph() {
GraphPreset preset = graphPreset;
if (preset == null) {
return null;
}
try {
IDexTreeVisitor pass = getPassList().get(currentPassIdx);
boolean success = mth.root().getProcessClasses().processMethodToVisitor(mth, pass);
if (!success) {
return null;
}
return new DotGraphUtils(preset.useRegions, preset.useRawInsns).dumpToString(mth);
} finally {
mth.unload();
}
}
private List<IDexTreeVisitor> getPassList() {
return mth.root().getProcessClasses().getPasses();
}
}
@@ -0,0 +1,33 @@
package jadx.gui.ui.graphs;
class Edge {
private final int source;
private final int dest;
public Edge(int source, int dest) {
this.source = source;
this.dest = dest;
}
public int getDest() {
return dest;
}
public int getSource() {
return source;
}
@Override
public boolean equals(Object otherObject) {
if (!(otherObject instanceof Edge)) {
return false;
}
Edge other = (Edge) otherObject;
return this.source == other.source && this.dest == other.dest;
}
@Override
public int hashCode() {
return source + 31 * dest;
}
}
@@ -0,0 +1,170 @@
package jadx.gui.ui.graphs;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.WindowConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.ListUtils;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.ui.MouseListenerAdapter;
public abstract class GraphDialog extends JFrame {
private static final long serialVersionUID = 5840390965763493590L;
private static final Logger LOG = LoggerFactory.getLogger(GraphDialog.class);
private static final Dimension MIN_WINDOW_SIZE = new Dimension(800, 500);
private final MainWindow mainWindow;
private final GraphPanel panel;
private JMenuBar menuBar = null;
public static JTextArea graphError(String errorMessage) {
JTextArea errorText = new JTextArea();
errorText.setText(errorMessage);
errorText.setVisible(true);
errorText.setEditable(false);
errorText.setLineWrap(false);
return errorText;
}
public static JTextArea graphError(Exception error) {
JTextArea errorText = new JTextArea();
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
stringWriter.write(NLS.str("graph_viewer.default_error"));
stringWriter.write(": ");
error.printStackTrace(printWriter);
errorText.setText(stringWriter.toString());
errorText.setVisible(true);
errorText.setEditable(false);
errorText.setLineWrap(false);
return errorText;
}
protected GraphDialog(MainWindow mainWindow) {
this(mainWindow, NLS.str("graph_viewer.default_title"));
}
public GraphDialog(MainWindow mainWindow, String title) {
super(title);
this.mainWindow = mainWindow;
setMinimumSize(MIN_WINDOW_SIZE);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
UiUtils.addEscapeShortCutToDispose(this);
setLocationRelativeTo(null);
loadWindowPos();
panel = new GraphPanel(this);
panel.setFocusable(true);
panel.addMouseListener(new MouseListenerAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
requestFocusInWindow();
}
});
setLayout(new BorderLayout());
add(panel, BorderLayout.CENTER);
}
public JMenuBar addMenuBar() {
JMenuBar menuBar = new JMenuBar();
menuBar.setLayout(new WrapLayout(FlowLayout.LEFT));
add(menuBar, BorderLayout.PAGE_START);
this.menuBar = menuBar;
JButton saveButton = new JButton(NLS.str("graph_viewer.save_graph"));
saveButton.setEnabled(false);
saveButton.addActionListener(e -> {
try {
FileDialogWrapper fileDialog = new FileDialogWrapper(mainWindow, FileOpenMode.CUSTOM_SAVE);
fileDialog.setTitle(NLS.str("graph_viewer.save_graph"));
fileDialog.setFileExtList(Collections.singletonList("svg"));
fileDialog.setSelectionMode(JFileChooser.FILES_ONLY);
List<Path> savePaths = fileDialog.show();
if (!savePaths.isEmpty()) {
File saveFile = ListUtils.first(savePaths).toFile();
getPanel().exportSVG(saveFile);
}
} catch (Exception ex) {
LOG.error("Failed to save file: ", ex);
JOptionPane.showMessageDialog(this, NLS.str("graph_viewer.file_failure"),
NLS.str("graph_viewer.file_failure"),
JOptionPane.INFORMATION_MESSAGE);
}
});
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.add(saveButton);
menuBar.add(menuBarPanel);
return menuBar;
}
protected void enableMenu() {
JMenuBar menu = this.menuBar;
setAllEnabled(true, menu);
}
protected void disableMenu() {
JMenuBar menu = this.menuBar;
setAllEnabled(false, menu);
}
private void setAllEnabled(boolean isEnabled, JComponent component) {
component.setEnabled(isEnabled);
Component[] components = component.getComponents();
for (Component subComponent : components) {
if (subComponent instanceof JComponent) {
setAllEnabled(isEnabled, (JComponent) subComponent);
} else {
subComponent.setEnabled(isEnabled);
}
}
}
public void loadWindowPos() {
if (!mainWindow.getSettings().loadWindowPos(this)) {
setPreferredSize(MIN_WINDOW_SIZE);
}
}
@Override
public void dispose() {
try {
mainWindow.getSettings().saveWindowPos(this);
} catch (Exception e) {
LOG.warn("Failed to save window size and position", e);
}
super.dispose();
}
protected GraphPanel getPanel() {
return this.panel;
}
}
@@ -0,0 +1,201 @@
package jadx.gui.ui.graphs;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URI;
import javax.swing.*;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.kitfox.svg.SVGDiagram;
import com.kitfox.svg.SVGUniverse;
import guru.nidi.graphviz.engine.Format;
import guru.nidi.graphviz.engine.Graphviz;
import guru.nidi.graphviz.engine.Renderer;
import jadx.core.utils.exceptions.JadxRuntimeException;
import static java.awt.RenderingHints.*;
public class GraphPanel extends JPanel {
private static final Logger LOG = LoggerFactory.getLogger(GraphPanel.class);
private final GraphDialog parentDialog;
private final Dimension fullImageSize = new Dimension();
private final double minimumScale = 0.1;
private final double maximumScale = 10.0;
private double scale = 1.0;
private double translateX = 0;
private double translateY = 0;
private Point lastDragPoint = null;
private @Nullable BufferedImage image;
private Renderer renderer;
private SVGDiagram svgDiagram;
public GraphPanel(GraphDialog parentDialog) {
this.parentDialog = parentDialog;
MouseAdapter ma = new GraphPanelMouseAdapter();
addMouseListener(ma);
addMouseMotionListener(ma);
addMouseWheelListener(ma);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (image != null) {
Graphics2D g2d = (Graphics2D) g;
AffineTransform transform = new AffineTransform();
transform.translate(translateX * scale, translateY * scale);
g2d.drawImage(image, transform, null);
}
}
public void setGraph(String dotString) {
try {
init(dotString);
} catch (Exception e) {
LOG.error("Error parsing DOT string", e);
invalidateImage(GraphDialog.graphError(e));
}
}
public void exportSVG(File svgImage) {
try {
renderer.toFile(svgImage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void init(String dotString) {
image = null;
byte[] bytes;
try (ByteArrayOutputStream bout = new ByteArrayOutputStream()) {
renderer = Graphviz.fromString(dotString).width(getWidth()).render(Format.SVG);
renderer.toOutputStream(bout);
bytes = bout.toByteArray();
} catch (Exception e) {
throw new JadxRuntimeException("Failed to render graph", e);
}
try (InputStream in = new ByteArrayInputStream(bytes)) {
SVGUniverse universe = new SVGUniverse();
URI uri = universe.loadSVG(in, "//graph/");
svgDiagram = universe.getDiagram(uri);
svgDiagram.setIgnoringClipHeuristic(true);
fullImageSize.setSize(svgDiagram.getWidth(), svgDiagram.getHeight());
} catch (Exception e) {
throw new JadxRuntimeException("Failed to process SVG image", e);
}
parentDialog.enableMenu();
removeAll();
SwingUtilities.invokeLater(() -> {
double width = getWidth();
double height = getHeight();
if (scale == 1.0) {
// fit in window on dialog open (keep on graph changes)
scale = Math.min(width / (double) fullImageSize.width, height / (double) fullImageSize.height);
}
renderGraphScaled();
if (image == null) {
return;
}
// center image in window
translateX = (width / 2. - fullImageSize.width * scale / 2.) / scale;
translateY = (height / 2. - fullImageSize.height * scale / 2.) / scale;
repaint();
});
}
private void renderGraphScaled() {
try {
if (fullImageSize.width * scale * fullImageSize.height * scale >= Integer.MAX_VALUE) {
scale = maximumScale;
}
image = new BufferedImage(
(int) (fullImageSize.width * scale),
(int) (fullImageSize.height * scale),
BufferedImage.TYPE_INT_ARGB);
Graphics2D imgG2d = image.createGraphics();
configGraphics(imgG2d);
AffineTransform transform = new AffineTransform();
transform.scale(scale, scale);
imgG2d.setTransform(transform);
svgDiagram.render(imgG2d);
} catch (Exception e) {
LOG.error("Graph render failed: ", e);
invalidateImage(GraphDialog.graphError(e));
}
}
private void configGraphics(Graphics2D graphics) {
graphics.setRenderingHint(KEY_ALPHA_INTERPOLATION, VALUE_ALPHA_INTERPOLATION_QUALITY);
graphics.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
graphics.setRenderingHint(KEY_COLOR_RENDERING, VALUE_COLOR_RENDER_QUALITY);
graphics.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC);
graphics.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY);
graphics.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON);
}
public void invalidateImage(JTextArea errorMsg) {
removeAll();
add(errorMsg);
image = null;
parentDialog.disableMenu();
revalidate();
repaint();
}
private class GraphPanelMouseAdapter extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
lastDragPoint = e.getPoint();
}
@Override
public void mouseDragged(MouseEvent e) {
if (image != null) {
Point p = e.getPoint();
translateX += (p.x - lastDragPoint.x) / scale;
translateY += (p.y - lastDragPoint.y) / scale;
lastDragPoint = p;
repaint();
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
if (image != null) {
double prevScale = scale;
scale *= Math.pow(1.1, -e.getWheelRotation());
if (scale > maximumScale) {
scale = maximumScale;
}
if (scale < minimumScale) {
scale = minimumScale;
}
if (scale != prevScale) {
Point p = e.getPoint();
double px = (p.x - translateX * prevScale) / prevScale;
double py = (p.y - translateY * prevScale) / prevScale;
translateX = p.x / scale - px;
translateY = p.y / scale - py;
renderGraphScaled();
if (image == null) {
return;
}
repaint();
}
}
}
}
}
@@ -0,0 +1,26 @@
package jadx.gui.utils.ui;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public abstract class MouseListenerAdapter implements MouseListener {
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
}
@@ -375,19 +375,14 @@ popup.go_to_declaration=Zur Erklärung gehen
popup.exclude=Ausschließen
popup.exclude_packages=Pakete ausschließen
popup.convert_number=Conversion als Kommentar hinzufügen
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=Kommentar
popup.update_comment=Kommentar aktualisieren
popup.search_comment=Kommentar suchen
@@ -547,9 +542,6 @@ action_category.plugin_script=Plugin-Skript
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ action_category.plugin_script=Plugin-Skript
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=Find Usage
popup.go_to_declaration=Go to declaration
popup.exclude=Exclude
popup.exclude_packages=Exclude packages
popup.convert_number=Add conversion as comment
popup.convert_number=Convert number
popup.convert_number_tooltip=Add other number formats as comment
popup.view_call_graph=View call graph
popup.view_call_graph_description=Show call chains to this function
popup.view_class_graph=View inheritance graph
popup.view_class_graph_description=Show inheritance tree for this class
popup.view_class_method_graph=View methods graph
popup.view_class_method_graph_description=Show all methods for this class
popup.cfg_submenu=View control flow graph
popup.view_cfg=Regular
popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
popup.view_raw_cfg=Raw
popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
popup.view_region_cfg=Region
popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
popup.view_cfg=View method control flow graph
popup.add_comment=Comment
popup.update_comment=Update comment
popup.search_comment=Search comments
@@ -547,9 +542,6 @@ graph_viewer.overrides=Show overrides
graph_viewer.callee_depth=Down depth
graph_viewer.caller_depth=Up depth
graph_viewer.default_error=Failed to view graph
graph_viewer.file_not_found_error=Failed to load graph file
graph_viewer.image_too_large=Failed to render graph: graph too large
graph_viewer.image_too_small=Failed to render graph: graph too small
graph_viewer.file_failure=Error in File Operation
graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ graph_viewer.method_graph.title=Methods Graph
graph_viewer.call_graph.title=Call Graph
graph_viewer.inheritance_graph.title=Inheritance Graph
graph_viewer.cfg.title=Control Flow Graph
graph_viewer.cfg.preset_selector_label=CFG preset
graph_viewer.cfg.preset_names=Raw|Normal|Region
graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.xposed=Copiar como fragmento de xposed
#popup.go_to_declaration=Go to declaration
#popup.exclude=Exclude
#popup.exclude_packages=Exclude packages
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
#popup.add_comment=Comment
#popup.update_comment=Update comment
#popup.search_comment=Search comments
@@ -547,9 +542,6 @@ certificate.serialPubKeyY=Y
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ certificate.serialPubKeyY=Y
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=Cari Penggunaan
popup.go_to_declaration=Pergi ke Deklarasi
popup.exclude=Kecualikan
popup.exclude_packages=Kecualikan paket
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=Komentar
#popup.update_comment=Update comment
popup.search_comment=Cari komentar
@@ -547,9 +542,6 @@ action_category.plugin_script=Plugin Script
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ action_category.plugin_script=Plugin Script
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=사용 찾기
popup.go_to_declaration=선언문으로 이동
popup.exclude=제외
popup.exclude_packages=패키지 제외
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=주석
#popup.update_comment=Update comment
popup.search_comment=주석 검색
@@ -547,9 +542,6 @@ adb_dialog.starting_debugger=디버거 시작 중 ...
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ adb_dialog.starting_debugger=디버거 시작 중 ...
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=Buscar uso
popup.go_to_declaration=Ir para declaração
popup.exclude=Ignorar
popup.exclude_packages=Pacotes ignorados
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=Comentar
#popup.update_comment=Update comment
popup.search_comment=Buscar comentários
@@ -547,9 +542,6 @@ adb_dialog.starting_debugger=Iniciando depurador...
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ adb_dialog.starting_debugger=Iniciando depurador...
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=Найти использования
popup.go_to_declaration=Перейти к объявлению
popup.exclude=Исключить
popup.exclude_packages=Исключить пакеты
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=Комментарий
#popup.update_comment=Update comment
popup.search_comment=Поиск комментариев
@@ -547,9 +542,6 @@ action_category.plugin_script=Скрипты и плагины
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ action_category.plugin_script=Скрипты и плагины
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -375,19 +375,14 @@ popup.go_to_declaration=跳到声明
popup.exclude=排除此包
popup.exclude_packages=排除包
popup.convert_number=将数值转换添加为注释
#popup.convert_number_tooltip=Add other number formats as comment
popup.view_call_graph=查看调用图
popup.view_call_graph_description=显示到此函数的调用链
popup.view_class_graph=查看继承图
popup.view_class_graph_description=显示该类的继承树
popup.view_class_method_graph=查看方法图
popup.view_class_method_graph_description=显示该类的所有方法
popup.cfg_submenu=查看控制图
popup.view_cfg=常规
popup.view_cfg_description=显示该函数的常规控制流图(可在 文件->首选项->其他 中启用)
popup.view_raw_cfg=原始
popup.view_raw_cfg_description=显示该函数包含原始指令的控制流图(可在 文件->首选项->其他 中启用)
popup.view_region_cfg=区域
popup.view_region_cfg_description=显示该函数的区域化控制流图(可在 文件->首选项->其他 中启用)
#popup.view_cfg=View method control flow graph
popup.add_comment=添加注释
popup.update_comment=更新注释
popup.search_comment=搜索注释
@@ -547,9 +542,6 @@ graph_viewer.overrides=显示重写关系
graph_viewer.callee_depth=向下深度
graph_viewer.caller_depth=向上深度
graph_viewer.default_error=图形显示失败
graph_viewer.file_not_found_error=图形文件加载失败
graph_viewer.image_too_large=图形渲染失败:图过大
graph_viewer.image_too_small=图形渲染失败:图过小
graph_viewer.file_failure=文件操作错误
graph_viewer.save_graph=保存图形
@@ -558,3 +550,6 @@ graph_viewer.method_graph.title=方法图
graph_viewer.call_graph.title=调用图
graph_viewer.inheritance_graph.title=继承图
graph_viewer.cfg.title=控制图
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -374,20 +374,15 @@ popup.find_usage=尋找使用情況
popup.go_to_declaration=前往宣告
popup.exclude=排除
popup.exclude_packages=排除套件
#popup.convert_number=Add conversion as comment
#popup.convert_number=Convert number
#popup.convert_number_tooltip=Add other number formats as comment
#popup.view_call_graph=View call graph
#popup.view_call_graph_description=Show call chains to this function
#popup.view_class_graph=View inheritance graph
#popup.view_class_graph_description=Show inheritance tree for this class
#popup.view_class_method_graph=View methods graph
#popup.view_class_method_graph_description=Show all methods for this class
#popup.cfg_submenu=View control flow graph
#popup.view_cfg=Regular
#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other)
#popup.view_raw_cfg=Raw
#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other)
#popup.view_region_cfg=Region
#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other)
#popup.view_cfg=View method control flow graph
popup.add_comment=註解
popup.update_comment=更新註解
popup.search_comment=搜尋註解
@@ -547,9 +542,6 @@ action_category.plugin_script=外掛程式腳本
#graph_viewer.callee_depth=Down depth
#graph_viewer.caller_depth=Up depth
#graph_viewer.default_error=Failed to view graph
#graph_viewer.file_not_found_error=Failed to load graph file
#graph_viewer.image_too_large=Failed to render graph: graph too large
#graph_viewer.image_too_small=Failed to render graph: graph too small
#graph_viewer.file_failure=Error in File Operation
#graph_viewer.save_graph=Save graph
@@ -558,3 +550,6 @@ action_category.plugin_script=外掛程式腳本
#graph_viewer.call_graph.title=Call Graph
#graph_viewer.inheritance_graph.title=Inheritance Graph
#graph_viewer.cfg.title=Control Flow Graph
#graph_viewer.cfg.preset_selector_label=CFG preset
#graph_viewer.cfg.preset_names=Raw|Normal|Region
#graph_viewer.cfg.pass_selector_label=View after pass
@@ -20,167 +20,105 @@ public class ConvertNumberActionTest {
@Test
public void simpleDecimalToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x7b");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("123");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void negativeDecimalToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0xffffff85");
expected.add("0b11111111111111111111111110000101");
List<String> result = ConvertNumberAction.getConversionsFromWord("-123");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void negativeLongDecimalToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0xFFFFFFE8B7891800".toLowerCase());
expected.add("0b1111111111111111111111111110100010110111100010010001100000000000");
List<String> result = ConvertNumberAction.getConversionsFromWord("-100000000000");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void simpleHexToDecimal() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("123");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("0x7b");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void zeroToHex() {
List<String> expected = new ArrayList<String>();
expected.add("0x0");
expected.add("0b00000000");
public void zero() {
List<String> result = ConvertNumberAction.getConversionsFromWord(Integer.toString(0));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEmpty();
}
@Test
public void minIntToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x80000000");
expected.add("0b10000000000000000000000000000000");
List<String> result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MIN_VALUE));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void maxIntToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x7fffffff");
expected.add("0b01111111111111111111111111111111");
List<String> result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MAX_VALUE));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void minLongToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x8000000000000000");
expected.add("0b1000000000000000000000000000000000000000000000000000000000000000");
List<String> result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MIN_VALUE));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void maxLongToHex() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x7fffffffffffffff");
expected.add("0b0111111111111111111111111111111111111111111111111111111111111111");
List<String> result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MAX_VALUE));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void simpleLongSuffix() {
List<String> expected = new ArrayList<String>();
List<String> expected = new ArrayList<>();
expected.add("0x7b");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("123L");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
assertThat(result).isEqualTo(expected);
}
@Test
public void binaryPadding() {
assertThat(ConvertNumberAction.getConversionsFromWord("0")).containsOnlyOnce("0b00000000");
assertThat(ConvertNumberAction.getConversionsFromWord("1")).containsOnlyOnce("0b00000001");
assertThat(ConvertNumberAction.getConversionsFromWord("127")).containsOnlyOnce("0b01111111");
assertThat(ConvertNumberAction.getConversionsFromWord("0xff")).containsOnlyOnce("0b11111111");
@@ -197,16 +135,13 @@ public class ConvertNumberActionTest {
assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffffffff"))
.containsOnlyOnce("0b0111111111111111111111111111111111111111111111111111111111111111");
}
@Test
public void printableAscii() {
for (int i = 32; i < 127; i++) {
String printed = String.format("'%c'", i);
assertThat(ConvertNumberAction.getConversionsFromWord(Integer.toString(i))).containsOnlyOnce(printed);
}
}
}