From 8c28a8530e0fb2d2caba930b38f0f936f118d490 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:32:36 +0100 Subject: [PATCH] fix(gui): improve dot graph viewer, CFG generation simplified --- jadx-cli/src/main/resources/logback.xml | 1 + .../src/main/java/jadx/core/ProcessClass.java | 39 ++ .../java/jadx/core/dex/nodes/MethodNode.java | 14 +- .../visitors/finaly/MarkFinallyVisitor.java | 17 +- .../core/dex/visitors/usage/UsageInfo.java | 2 +- .../java/jadx/core/utils/DotGraphUtils.java | 11 +- .../java/jadx/gui/ui/action/ActionModel.java | 46 +- .../gui/ui/action/ViewCallGraphAction.java | 36 +- .../ViewClassInheritanceGraphAction.java | 19 +- .../ui/action/ViewClassMethodGraphAction.java | 19 +- .../ui/action/ViewControlFlowGraphAction.java | 47 +- .../action/ViewRawControlFlowGraphAction.java | 58 --- .../ViewRegionControlFlowGraphAction.java | 58 --- .../java/jadx/gui/ui/codearea/CodeArea.java | 11 +- .../jadx/gui/ui/codearea/CommentAction.java | 3 +- .../gui/ui/codearea/ConvertNumberAction.java | 157 +++---- .../gui/ui/codearea/JNodePopupBuilder.java | 27 +- .../jadx/gui/ui/dialog/CommentDialog.java | 2 +- .../gui/ui/dialog/ControlFlowGraphDialog.java | 52 --- .../java/jadx/gui/ui/dialog/GraphDialog.java | 417 ------------------ .../{dialog => graphs}/CallGraphDialog.java | 64 +-- .../ClassInheritanceGraphDialog.java | 39 +- .../ClassMethodGraphDialog.java | 80 +--- .../gui/ui/graphs/ControlFlowGraphDialog.java | 171 +++++++ .../main/java/jadx/gui/ui/graphs/Edge.java | 33 ++ .../java/jadx/gui/ui/graphs/GraphDialog.java | 170 +++++++ .../java/jadx/gui/ui/graphs/GraphPanel.java | 201 +++++++++ .../gui/utils/ui/MouseListenerAdapter.java | 26 ++ .../resources/i18n/Messages_de_DE.properties | 15 +- .../resources/i18n/Messages_en_US.properties | 17 +- .../resources/i18n/Messages_es_ES.properties | 17 +- .../resources/i18n/Messages_id_ID.properties | 17 +- .../resources/i18n/Messages_ko_KR.properties | 17 +- .../resources/i18n/Messages_pt_BR.properties | 17 +- .../resources/i18n/Messages_ru_RU.properties | 17 +- .../resources/i18n/Messages_zh_CN.properties | 15 +- .../resources/i18n/Messages_zh_TW.properties | 17 +- .../ui/codearea/ConvertNumberActionTest.java | 105 +---- 38 files changed, 902 insertions(+), 1172 deletions(-) delete mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java delete mode 100644 jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java rename jadx-gui/src/main/java/jadx/gui/ui/{dialog => graphs}/CallGraphDialog.java (91%) rename jadx-gui/src/main/java/jadx/gui/ui/{dialog => graphs}/ClassInheritanceGraphDialog.java (96%) rename jadx-gui/src/main/java/jadx/gui/ui/{dialog => graphs}/ClassMethodGraphDialog.java (75%) create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/graphs/ControlFlowGraphDialog.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/graphs/Edge.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphDialog.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphPanel.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/ui/MouseListenerAdapter.java diff --git a/jadx-cli/src/main/resources/logback.xml b/jadx-cli/src/main/resources/logback.xml index 58785b3e5..8325b3e38 100644 --- a/jadx-cli/src/main/resources/logback.xml +++ b/jadx-cli/src/main/resources/logback.xml @@ -9,6 +9,7 @@ + diff --git a/jadx-core/src/main/java/jadx/core/ProcessClass.java b/jadx-core/src/main/java/jadx/core/ProcessClass.java index ff602ad28..e62bbce58 100644 --- a/jadx-core/src/main/java/jadx/core/ProcessClass.java +++ b/jadx-core/src/main/java/jadx/core/ProcessClass.java @@ -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 getPasses() { return passes; diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index ba6fe4a7a..48de30684 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -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 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 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() { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java index 24aa5b076..7892605fa 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java @@ -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); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java index 2acd8bc8c..41f8c0c96 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java @@ -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 diff --git a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java index 4574cf9c4..23a1fd43c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java @@ -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); - } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java index 6a5114322..9c36a1a66 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java @@ -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 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); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java index 8369d85ad..d69bc916c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java @@ -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); + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java index a48a64d8f..647c6ab2c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java @@ -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; - } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java index 29252a610..80caf2644 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java @@ -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; - } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java index 4df8f9987..7151d9c79 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java @@ -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(); - } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java deleted file mode 100644 index 7b481b725..000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java +++ /dev/null @@ -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(); - } - -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java deleted file mode 100644 index 291993b41..000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java +++ /dev/null @@ -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(); - } - -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index e34d44b88..bf49b3135 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -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)); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java index edfe339c5..0b952a24b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java index 4f4581865..23b1bb436 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java @@ -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 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> updater) { - try { - JadxProject project = codeArea.getProject(); - JadxCodeData codeData = project.getCodeData(); - if (codeData == null) { - codeData = new JadxCodeData(); - } - List 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 getConversionsFromWord(String word) { - - List conversions = new ArrayList<>(); - - if (word == null || word.isEmpty()) { - return null; + static List 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 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; + } } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java index ff361763f..3864cff83 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java index 2acf05f94..cdbb5feab 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java @@ -44,7 +44,7 @@ public class CommentDialog extends CommonDialog { dialog.setVisible(true); } - private static void updateCommentsData(CodeArea codeArea, Consumer> updater) { + public static void updateCommentsData(CodeArea codeArea, Consumer> updater) { try { JadxProject project = codeArea.getProject(); JadxCodeData codeData = project.getCodeData(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java deleted file mode 100644 index a83880d0d..000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java +++ /dev/null @@ -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; - } - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java deleted file mode 100644 index 8e335852c..000000000 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java +++ /dev/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; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/CallGraphDialog.java similarity index 91% rename from jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java rename to jadx-gui/src/main/java/jadx/gui/ui/graphs/CallGraphDialog.java index 92d292dfe..aec696454 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/CallGraphDialog.java @@ -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 methodToNodeID; private Map unresolvedMethodToNodeID; private Set 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 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 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 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); - } - } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java similarity index 96% rename from jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java rename to jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java index 08446eaee..d7a19ed0b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassInheritanceGraphDialog.java @@ -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> 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 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; } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassMethodGraphDialog.java similarity index 75% rename from jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java rename to jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassMethodGraphDialog.java index ce37d5bda..5cc3e0d8a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ClassMethodGraphDialog.java @@ -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 methodToNodeID; private Set edges; private List 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 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 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); - } - } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/graphs/ControlFlowGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ControlFlowGraphDialog.java new file mode 100644 index 000000000..0bdb24133 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/ControlFlowGraphDialog.java @@ -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 presetsCB; + private JComboBox 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 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 passList = getPassList(); + int size = passList.size(); + Map 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 getPassList() { + return mth.root().getProcessClasses().getPasses(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/graphs/Edge.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/Edge.java new file mode 100644 index 000000000..052ae5165 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/Edge.java @@ -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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphDialog.java new file mode 100644 index 000000000..01bc2cdd2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphDialog.java @@ -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 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; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphPanel.java new file mode 100644 index 000000000..e0df48c31 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/graphs/GraphPanel.java @@ -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(); + } + } + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/MouseListenerAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/MouseListenerAdapter.java new file mode 100644 index 000000000..d688f7c1b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/MouseListenerAdapter.java @@ -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) { + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index aa75bc3f2..efdc1f00c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index a060a0e1d..c7b90bd51 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 2c4b6c2c6..cbbd7e1ac 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 584621ed1..a605a5f86 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index c0220779c..175325a50 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index df1b8b8dd..e8c07f26a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index a1dde29af..e039a9dd4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index ee8b59497..17311ad9b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 5a803b1df..346832651 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -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 diff --git a/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java b/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java index 8abb0ca0e..f2565777f 100644 --- a/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java +++ b/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java @@ -20,167 +20,105 @@ public class ConvertNumberActionTest { @Test public void simpleDecimalToHex() { - - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x7b"); expected.add("0b01111011"); expected.add("'{'"); List result = ConvertNumberAction.getConversionsFromWord("123"); - - assertThat(result).isNotEmpty(); - - expected.removeAll(result); - assertThat(expected).isEmpty(); + assertThat(result).isEqualTo(expected); } @Test public void negativeDecimalToHex() { - - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0xffffff85"); expected.add("0b11111111111111111111111110000101"); List result = ConvertNumberAction.getConversionsFromWord("-123"); - - assertThat(result).isNotEmpty(); - - expected.removeAll(result); - assertThat(expected).isEmpty(); + assertThat(result).isEqualTo(expected); } @Test public void negativeLongDecimalToHex() { - - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0xFFFFFFE8B7891800".toLowerCase()); expected.add("0b1111111111111111111111111110100010110111100010010001100000000000"); List result = ConvertNumberAction.getConversionsFromWord("-100000000000"); - - assertThat(result).isNotEmpty(); - - expected.removeAll(result); - assertThat(expected).isEmpty(); + assertThat(result).isEqualTo(expected); } @Test public void simpleHexToDecimal() { - - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("123"); expected.add("0b01111011"); expected.add("'{'"); List result = ConvertNumberAction.getConversionsFromWord("0x7b"); - - assertThat(result).isNotEmpty(); - - expected.removeAll(result); - assertThat(expected).isEmpty(); + assertThat(result).isEqualTo(expected); } @Test - public void zeroToHex() { - - List expected = new ArrayList(); - expected.add("0x0"); - expected.add("0b00000000"); - + public void zero() { List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(0)); - - assertThat(result).isNotEmpty(); - - expected.removeAll(result); - assertThat(expected).isEmpty(); - + assertThat(result).isEmpty(); } @Test public void minIntToHex() { - - List expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x80000000"); expected.add("0b10000000000000000000000000000000"); List 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 expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x7fffffff"); expected.add("0b01111111111111111111111111111111"); List 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 expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x8000000000000000"); expected.add("0b1000000000000000000000000000000000000000000000000000000000000000"); List 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 expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x7fffffffffffffff"); expected.add("0b0111111111111111111111111111111111111111111111111111111111111111"); List 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 expected = new ArrayList(); + List expected = new ArrayList<>(); expected.add("0x7b"); expected.add("0b01111011"); expected.add("'{'"); List 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); } } - }