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