feat: graph views, code pane sync, and more (PR #2784)

* snapshot 219

* revert non-working string searcher

* fix(gui): fix illegal ':' character in path when exporting resources.arsc/res

* fix(gui): use resource short name when exporting a folder via context menu

* fix(gui): use new resource class for files in arsc (#2771)

* fix(gui): limit tabs title length, fix tooltips (#2771)

* resolve issues with script code area after merge

---------

Co-authored-by: Jan S. <jpstotz@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
gordon-f0
2026-02-13 19:02:36 +00:00
committed by GitHub
parent 2a2806ebd7
commit be8b96280e
184 changed files with 12718 additions and 1011 deletions
+6 -1
View File
@@ -50,6 +50,11 @@ dependencies {
implementation("org.exbin.auxiliary:binary_data:$bined")
implementation("org.exbin.auxiliary:binary_data-array:$bined")
// Library for rendering GraphViz DOT files
implementation("guru.nidi:graphviz-java:0.18.1")
implementation("com.eclipsesource.j2v8:j2v8_linux_x86_64:4.6.0")
implementation("com.eclipsesource.j2v8:j2v8_win32_x86_64:4.6.0")
testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output)
}
@@ -160,7 +165,7 @@ fun escapeJVMOptions(): List<String> {
}
runtime {
addOptions("--strip-debug", "--compress", "zip-9", "--no-header-files", "--no-man-pages")
addOptions("--strip-debug", "--no-header-files", "--no-man-pages")
addModules(
"java.desktop",
"java.naming",
@@ -0,0 +1,67 @@
package jadx.gui.cache.usage;
import java.util.List;
import jadx.api.plugins.input.data.IMethodRef;
public class CachedMethodRef implements IMethodRef {
private String parentClassType;
private String name;
private String returnType;
private List<String> argTypes;
public CachedMethodRef(String parentClassType, String name, String returnType, List<String> argTypes) {
this.parentClassType = parentClassType;
this.name = name;
this.returnType = returnType;
this.argTypes = argTypes;
}
@Override
public String getParentClassType() {
return parentClassType;
}
public void setParentClassType(String parentClassType) {
this.parentClassType = parentClassType;
}
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getReturnType() {
return returnType;
}
public void setReturnType(String returnType) {
this.returnType = returnType;
}
@Override
public List<String> getArgTypes() {
return argTypes;
}
public void setArgTypes(List<String> argTypes) {
this.argTypes = argTypes;
}
@Override
public int getUniqId() {
throw new UnsupportedOperationException("Unimplemented method 'getUniqId'");
}
@Override
public void load() {
throw new UnsupportedOperationException("Unimplemented method 'load'");
}
}
@@ -2,6 +2,7 @@ package jadx.gui.cache.usage;
import java.util.List;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.usage.IUsageInfoVisitor;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
@@ -40,6 +41,21 @@ final class CollectUsageData implements IUsageInfoVisitor {
data.getMethodData(mth).setUsage(mthNodesRef(methods));
}
@Override
public void visitMethodsUses(MethodNode mth, List<MethodNode> methods) {
data.getMethodData(mth).setUses(mthNodesRef(methods));
}
@Override
public void visitUnresolvedMethodsUsage(MethodNode mth, List<IMethodRef> methods) {
data.getMethodData(mth).setUnresolvedUsage(methods);
}
@Override
public void visitIsSelfCall(MethodNode mth, boolean isSelfCall) {
data.getMethodData(mth).setCallsSelf(isSelfCall);
}
@Override
public void visitComplete() {
data.collectClassesWithoutData();
@@ -2,9 +2,14 @@ package jadx.gui.cache.usage;
import java.util.List;
import jadx.api.plugins.input.data.IMethodRef;
final class MthUsageData {
private final MthRef mthRef;
private List<MthRef> usage;
private List<MthRef> uses;
private List<IMethodRef> unresolvedUsage;
private boolean callsSelf;
public MthUsageData(MthRef mthRef) {
this.mthRef = mthRef;
@@ -21,4 +26,28 @@ final class MthUsageData {
public void setUsage(List<MthRef> usage) {
this.usage = usage;
}
public List<MthRef> getUses() {
return uses;
}
public void setUses(List<MthRef> uses) {
this.uses = uses;
}
public List<IMethodRef> getUnresolvedUsage() {
return unresolvedUsage;
}
public void setUnresolvedUsage(List<IMethodRef> unresolvedUsage) {
this.unresolvedUsage = unresolvedUsage;
}
public boolean callsSelf() {
return callsSelf;
}
public void setCallsSelf(boolean callsSelf) {
this.callsSelf = callsSelf;
}
}
@@ -59,6 +59,9 @@ class UsageData implements IUsageInfoData {
MthUsageData mthUsageData = mthUsage.get(mth.getMethodInfo().getShortId());
if (mthUsageData != null) {
mth.setUseIn(resolveMthList(mthUsageData.getUsage()));
mth.setUsed(resolveMthList(mthUsageData.getUses()));
mth.setUnresolvedUsed(mthUsageData.getUnresolvedUsage());
mth.setCallsSelf(mthUsageData.callsSelf());
}
}
Map<String, FldUsageData> fldUsage = clsUsageData.getFldUsage();
@@ -11,16 +11,20 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.input.data.IMethodRef;
import jadx.api.usage.IUsageInfoData;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -104,6 +108,7 @@ public class UsageFileAdapter extends DataAdapterHelper {
int clsCount = readUVInt(in);
int clsWithoutDataCount = readUVInt(in);
// Class information
String[] clsNames = new String[clsCount + clsWithoutDataCount];
ClsUsageData[] classes = new ClsUsageData[clsCount];
int c = 0;
@@ -115,6 +120,8 @@ public class UsageFileAdapter extends DataAdapterHelper {
for (int i = 0; i < clsWithoutDataCount; i++) {
clsNames[c++] = in.readUTF();
}
// Method information
int mthCount = readUVInt(in);
MthRef[] methods = new MthRef[mthCount];
for (int i = 0; i < mthCount; i++) {
@@ -125,6 +132,24 @@ public class UsageFileAdapter extends DataAdapterHelper {
cls.getMthUsage().put(mthShortId, new MthUsageData(mthRef));
methods[i] = mthRef;
}
// Unresolved method information
int uMthCount = readUVInt(in);
IMethodRef[] unresolvedMethods = new IMethodRef[uMthCount];
for (int i = 0; i < uMthCount; i++) {
String name = in.readUTF();
String parentClassType = in.readUTF();
String returnType = in.readUTF();
int argCount = in.readInt();
String[] args = new String[argCount];
for (int j = 0; j < argCount; j++) {
args[j] = in.readUTF();
}
IMethodRef iMethodRef = new CachedMethodRef(parentClassType, name, returnType, Arrays.asList(args));
unresolvedMethods[i] = iMethodRef;
}
// Usage data
for (int i = 0; i < clsCount; i++) {
ClsUsageData cls = data.getClassData(clsNames[i]);
cls.setClsDeps(readClsList(in, clsNames));
@@ -134,8 +159,11 @@ public class UsageFileAdapter extends DataAdapterHelper {
int mCount = readUVInt(in);
for (int m = 0; m < mCount; m++) {
MthRef mthRef = methods[readUVInt(in)];
cls.getMthUsage().get(mthRef.getShortId())
.setUsage(readMthList(in, methods));
MthUsageData mthUsageData = cls.getMthUsage().get(mthRef.getShortId());
mthUsageData.setUsage(readMthList(in, methods));
mthUsageData.setUses(readMthList(in, methods));
mthUsageData.setUnresolvedUsage(readUnresolvedMthList(in, unresolvedMethods));
mthUsageData.setCallsSelf(in.readBoolean());
}
int fCount = readUVInt(in);
for (int f = 0; f < fCount; f++) {
@@ -151,11 +179,13 @@ public class UsageFileAdapter extends DataAdapterHelper {
private static void writeData(DataOutputStream out, RawUsageData usageData) throws IOException {
Map<String, Integer> clsMap = new HashMap<>();
Map<MthRef, Integer> mthMap = new HashMap<>();
Map<IMethodRef, Integer> uMthMap = new HashMap<>();
Map<String, ClsUsageData> clsDataMap = usageData.getClsMap();
List<String> classes = new ArrayList<>(clsDataMap.keySet());
Collections.sort(classes);
List<String> classesWithoutData = usageData.getClassesWithoutData();
// Class information
writeUVInt(out, classes.size());
writeUVInt(out, classesWithoutData.size());
int i = 0;
@@ -167,6 +197,8 @@ public class UsageFileAdapter extends DataAdapterHelper {
out.writeUTF(cls);
clsMap.put(cls, i++);
}
// Method information
List<MthRef> methods = clsDataMap.values().stream()
.flatMap(c -> c.getMthUsage().values().stream())
.map(MthUsageData::getMthRef)
@@ -178,6 +210,38 @@ public class UsageFileAdapter extends DataAdapterHelper {
out.writeUTF(mth.getShortId());
mthMap.put(mth, j++);
}
// Unresolved method information
Set<IMethodRef> unresolvedMethods = clsDataMap.values().stream()
.flatMap(classUsageData -> classUsageData.getMthUsage().values().stream())
.flatMap(methodUsageData -> {
List<IMethodRef> unresolvedUsageList = methodUsageData.getUnresolvedUsage();
return (unresolvedUsageList == null) ? null : unresolvedUsageList.stream();
})
.filter(Objects::nonNull)
.collect(Collectors.toSet());
writeUVInt(out, unresolvedMethods.size());
int k = 0;
for (IMethodRef uMth : unresolvedMethods) {
String name = uMth.getName();
out.writeUTF((name == null) ? "" : name);
String parentClassType = uMth.getParentClassType();
out.writeUTF((parentClassType == null) ? "" : parentClassType);
String returnType = uMth.getReturnType();
out.writeUTF((returnType == null) ? "" : returnType);
List<String> argTypes = uMth.getArgTypes();
if (argTypes == null) {
out.writeInt(0);
} else {
out.writeInt(argTypes.size());
for (String arg : argTypes) {
out.writeUTF(arg);
}
}
uMthMap.put(uMth, k++);
}
// Usage data
for (String cls : classes) {
ClsUsageData clsData = clsDataMap.get(cls);
writeClsList(out, clsMap, clsData.getClsDeps());
@@ -188,6 +252,9 @@ public class UsageFileAdapter extends DataAdapterHelper {
for (MthUsageData mthData : clsData.getMthUsage().values()) {
writeUVInt(out, mthMap.get(mthData.getMthRef()));
writeMthList(out, mthMap, mthData.getUsage());
writeMthList(out, mthMap, mthData.getUses());
writeUnresolvedMthList(out, uMthMap, mthData.getUnresolvedUsage());
out.writeBoolean(mthData.callsSelf());
}
writeUVInt(out, clsData.getFldUsage().size());
@@ -248,6 +315,30 @@ public class UsageFileAdapter extends DataAdapterHelper {
}
}
private static List<IMethodRef> readUnresolvedMthList(DataInputStream in, IMethodRef[] methods) throws IOException {
int count = readUVInt(in);
if (count == 0) {
return Collections.emptyList();
}
List<IMethodRef> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
list.add(methods[readUVInt(in)]);
}
return list;
}
private static void writeUnresolvedMthList(DataOutputStream out, Map<IMethodRef, Integer> uMthMap, List<IMethodRef> mthList)
throws IOException {
if (Utils.isEmpty(mthList)) {
writeUVInt(out, 0);
return;
}
writeUVInt(out, mthList.size());
for (IMethodRef mth : mthList) {
writeUVInt(out, uMthMap.get(mth));
}
}
private static String buildInputsHash(List<File> inputs) {
List<Path> paths = inputs.stream()
.filter(f -> !f.getName().endsWith(".jadx.kts"))
@@ -19,6 +19,7 @@ import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.android.AppAttribute;
import jadx.core.utils.android.ApplicationParams;
import jadx.gui.device.debugger.smali.Smali;
import jadx.gui.device.debugger.smali.SmaliMethodNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
@@ -53,6 +54,21 @@ public class DbgUtils {
return null;
}
@Nullable
public static SmaliMethodNode getSmaliMethodNode(JClass cls, String mthRawFullID) {
Smali smali = getSmali(cls.getCls().getClassNode().getTopParentClass());
if (smali != null) {
return smali.getMethodNode(mthRawFullID);
}
return null;
}
public static void printSmaliLineMapping(SmaliMethodNode smn) {
for (Map.Entry<Integer, Integer> lineToCodeOffset : smn.getLineMapping().entrySet()) {
LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue());
}
}
public static String[] sepClassAndMthSig(String fullSig) {
int pos = fullSig.indexOf('(');
if (pos != -1) {
@@ -108,6 +108,11 @@ public class Smali {
return -1;
}
@Nullable
public SmaliMethodNode getMethodNode(String mthFullRawID) {
return insnMap.get(mthFullRawID);
}
public int getRegCount(String mthFullRawID) {
SmaliMethodNode info = insnMap.get(mthFullRawID);
if (info != null) {
@@ -10,7 +10,7 @@ import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
import jadx.core.dex.nodes.InsnNode;
class SmaliMethodNode {
public class SmaliMethodNode {
private Map<Long, InsnNode> nodes; // codeOffset: InsnNode
private List<SmaliRegister> regList;
private int[] insnPos;
@@ -27,6 +27,13 @@ class SmaliMethodNode {
return this.regCount;
}
/**
* Returns the line mapping of the:
* 'output disassembled smali file line index' to 'dex instruction position code offset'
* The value is the same as {@link InsnNode#getOffset()}
*
* @return the line mapping
*/
public Map<Integer, Integer> getLineMapping() {
return lineMapping;
}
@@ -60,9 +60,10 @@ public class RenameService {
private void process(NodeRenamedByUser event) {
try {
LOG.debug("Applying rename event: {}", event);
long timeStarted = System.nanoTime();
JRenameNode node = getRenameNode(event);
updateCodeRenames(set -> processRename(node, event, set));
refreshState(node);
refreshState(node, timeStarted);
} catch (Exception e) {
LOG.error("Rename failed", e);
UiUtils.errorMessage(mainWindow, "Rename failed:\n" + Utils.getStackTrace(e));
@@ -109,7 +110,7 @@ public class RenameService {
project.setCodeData(codeData);
}
private void refreshState(JRenameNode node) {
private void refreshState(JRenameNode node, long timeStarted) {
List<JavaNode> toUpdate = new ArrayList<>();
node.addUpdateNodes(toUpdate);
@@ -128,8 +129,18 @@ public class RenameService {
mainWindow.getBackgroundExecutor().execute("Refreshing",
() -> {
mainWindow.getWrapper().reloadCodeData();
// Reload all the classes in the background process, rather than using the UI thread for
// decompilation. We don't just use codeArea.backgroundRefreshClass because it would spawn a
// separate background process, whereas we would like it to happen in this one.
for (ContentPanel tab : mainWindow.getTabbedPane().getTabs()) {
JClass rootClass = tab.getNode().getRootClass();
if (updatedTopClasses.contains(rootClass)) {
rootClass.reload(mainWindow.getCacheObject());
}
}
UiUtils.uiRunAndWait(() -> refreshTabs(mainWindow.getTabbedPane(), updatedTopClasses));
refreshClasses(updatedTopClasses);
LOG.debug("Finished rename, took " + (System.nanoTime() - timeStarted) + " ns");
},
(status) -> {
if (status == TaskStatus.CANCEL_BY_MEMORY) {
@@ -171,7 +182,7 @@ public class RenameService {
if (updatedClasses.remove(rootClass)) {
ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) tab;
CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea();
codeArea.refreshClass();
codeArea.refreshClass(true);
}
}
}
@@ -896,4 +896,5 @@ public class JadxSettings {
public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) {
settingsData.setSmaliAreaShowBytecode(smaliAreaShowBytecode);
}
}
@@ -501,4 +501,5 @@ public class JadxSettingsData extends JadxGUIArgs {
public void setXposedCodegenLanguage(XposedCodegenLanguage xposedCodegenLanguage) {
this.xposedCodegenLanguage = xposedCodegenLanguage;
}
}
@@ -35,6 +35,7 @@ public class HeapUsageBar extends JProgressBar {
private final double maxGB;
private final long limit;
private long peakUsed;
private final String labelTemplate;
private transient Disposable timer;
@@ -45,6 +46,7 @@ public class HeapUsageBar extends JProgressBar {
setStringPainted(true);
long maxMemory = runtime.maxMemory();
peakUsed = 0;
maxGB = maxMemory / GB;
limit = maxMemory - UiUtils.MIN_FREE_MEMORY;
labelTemplate = NLS.str("heapUsage.text");
@@ -102,8 +104,11 @@ public class HeapUsageBar extends JProgressBar {
}
UpdateData updateData = new UpdateData();
long used = runtime.totalMemory() - runtime.freeMemory();
if (used > peakUsed) {
peakUsed = used;
}
updateData.value = (int) (used / 1024);
updateData.label = String.format(labelTemplate, used / GB, maxGB);
updateData.label = String.format(labelTemplate, used / GB, maxGB, peakUsed / GB);
updateData.color = used > limit ? RED : GREEN;
return updateData;
}
@@ -45,6 +45,7 @@ public enum ActionModel {
Shortcut.keyboard(KeyEvent.VK_T, UiUtils.ctrlButton())),
TEXT_SEARCH(MENU_TOOLBAR, "menu.text_search", "menu.text_search", "ui/find",
Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)),
CLASS_SEARCH(MENU_TOOLBAR, "menu.class_search", "menu.class_search", "ui/ejbFinderMethod",
Shortcut.keyboard(KeyEvent.VK_N, UiUtils.ctrlButton())),
COMMENT_SEARCH(MENU_TOOLBAR, "menu.comment_search", "menu.comment_search", "ui/usagesFinder",
@@ -53,7 +54,8 @@ public enum ActionModel {
Shortcut.keyboard(KeyEvent.VK_M, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)),
GO_TO_APPLICATION(MENU_TOOLBAR, "menu.go_to_application", "menu.go_to_application", "ui/application",
Shortcut.keyboard(KeyEvent.VK_A, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)),
GO_TO_ANDROID_MANIFEST(MENU_TOOLBAR, "menu.go_to_android_manifest", "menu.go_to_android_manifest", "ui/androidManifest",
GO_TO_ANDROID_MANIFEST(MENU_TOOLBAR, "menu.go_to_android_manifest", "menu.go_to_android_manifest",
"ui/androidManifest",
Shortcut.none()),
PREVIEW_TAB(MENU_TOOLBAR, "menu.enable_preview_tab", "menu.enable_preview_tab", "ui/editorPreview",
Shortcut.none()),
@@ -85,6 +87,16 @@ public enum ActionModel {
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()),
CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null,
Shortcut.keyboard(KeyEvent.VK_SEMICOLON)),
CODE_COMMENT_SEARCH(CODE_AREA, "popup.search_comment", "popup.search_comment", null,
@@ -0,0 +1,48 @@
package jadx.gui.ui.action;
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.utils.NLS;
public final class ViewCallGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewCallGraphAction.class);
private static final long serialVersionUID = -11122327621269039L;
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;
}
}
@@ -0,0 +1,54 @@
package jadx.gui.ui.action;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JClass;
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.utils.NLS;
public final class ViewClassInheritanceGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewClassInheritanceGraphAction.class);
private static final long serialVersionUID = -331826691076655264L;
public ViewClassInheritanceGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CLASS_INHERITANCE_GRAPH, codeArea);
}
@Override
public void runAction(JNode node) {
try {
JClass classNode;
if (node instanceof JMethod) {
classNode = node.getJParent();
} else if (node instanceof JField) {
classNode = node.getJParent();
} else if (node instanceof JClass) {
classNode = (JClass) node;
} 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);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
}
@@ -0,0 +1,54 @@
package jadx.gui.ui.action;
import javax.swing.JOptionPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.treemodel.JClass;
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.utils.NLS;
public final class ViewClassMethodGraphAction extends JNodeAction {
private static final Logger LOG = LoggerFactory.getLogger(ViewClassMethodGraphAction.class);
private static final long serialVersionUID = -331826691076655264L;
public ViewClassMethodGraphAction(CodeArea codeArea) {
super(ActionModel.VIEW_CLASS_METHOD_GRAPH, codeArea);
}
@Override
public void runAction(JNode node) {
try {
JClass classNode;
if (node instanceof JMethod) {
classNode = node.getJParent();
} else if (node instanceof JField) {
classNode = node.getJParent();
} else if (node instanceof JClass) {
classNode = (JClass) node;
} 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);
JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"),
JOptionPane.ERROR_MESSAGE);
}
}
@Override
public boolean isActionEnabled(JNode node) {
return node instanceof JMethod || node instanceof JClass || node instanceof JField;
}
}
@@ -0,0 +1,58 @@
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 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);
}
@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);
} 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, false).getFullFile(mth);
return file.exists() && !file.isDirectory();
}
}
@@ -0,0 +1,58 @@
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();
}
}
@@ -0,0 +1,58 @@
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();
}
}
@@ -234,14 +234,31 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_C && UiUtils.isCtrlDown(e)) {
if (StringUtils.isEmpty(getSelectedText())) {
UiUtils.copyToClipboard(getWordUnderCaret());
}
UiUtils.copyToClipboard(getSelectedTokenOrWord());
}
}
});
}
/**
* If the user has selected an individual word, for example by clicking and dragging
* the mouse, then get that. Otherwise get the token underneath the cursor.
* This is useful when the token is a string or comment and we want to control or copy
* the word rather than the whole thing.
*
* @return The word or the token text
*/
public @Nullable String getSelectedTokenOrWord() {
final String rc = getSelectedText();
if (rc == null) {
return getWordUnderCaret();
}
if (StringUtils.isEmpty(rc)) {
return getWordUnderCaret();
}
return rc;
}
private void addSaveActions(JEditableNode node) {
addKeyListener(new KeyAdapter() {
@Override
@@ -4,20 +4,25 @@ import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.JCheckBox;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.DecompilationMode;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.codearea.mode.JCodeMode;
import jadx.gui.ui.codearea.sync.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.fallback.FallbackSyncer;
import jadx.gui.ui.panel.IViewStateSupport;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
@@ -25,7 +30,7 @@ import jadx.gui.utils.NLS;
import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT;
/**
* Displays one class with two different view:
* Displays one class with two different views:
*
* <ul>
* <li>Java source code of the selected class (default)</li>
@@ -39,6 +44,7 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
private final transient CodePanel javaCodePanel;
private final transient CodePanel smaliCodePanel;
private final transient JTabbedPane areaTabbedPane;
private final AtomicBoolean syncInProgress = new AtomicBoolean(false);
private boolean splitView = false;
@@ -46,12 +52,12 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
super(panel, jCls);
javaCodePanel = new CodePanel(new CodeArea(this, jCls));
smaliCodePanel = new CodePanel(new SmaliArea(this, jCls));
areaTabbedPane = buildTabbedPane(jCls, false);
smaliCodePanel = new CodePanel(new SmaliArea(this, jCls, false));
areaTabbedPane = buildTabbedPane(jCls);
addCustomControls(areaTabbedPane);
initView();
javaCodePanel.load();
initView();
}
private void initView() {
@@ -59,28 +65,108 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
if (splitView) {
JTabbedPane splitPaneView = buildTabbedPane(((JClass) node), true);
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, areaTabbedPane, splitPaneView);
add(splitPane);
splitPane.setDividerLocation(0.5);
splitPaneView.setSelectedIndex(1);
setupSplitPane();
} else {
javaCodePanel.load();
smaliCodePanel.load();
attachSyncListeners(javaCodePanel, smaliCodePanel);
areaTabbedPane.setSelectedIndex(0); // default to Java
add(areaTabbedPane);
}
invalidate();
revalidate();
repaint();
}
private JTabbedPane buildTabbedPane(JClass jCls, boolean split) {
private void attachSyncListeners(CodePanel javaPanel, CodePanel smaliPanel) {
javaPanel.getCodeArea().addCaretListener(e -> {
if (syncInProgress.get()) {
return;
}
syncInProgress.set(true);
syncToMethod(javaPanel, smaliPanel);
syncInProgress.set(false);
});
smaliPanel.getCodeArea().addCaretListener(e -> {
if (syncInProgress.get()) {
return;
}
syncInProgress.set(true);
syncToMethod(smaliPanel, javaPanel);
syncInProgress.set(false);
});
}
private void setupSplitPane() {
JTabbedPane leftTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
JTabbedPane rightTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
CodePanel[] leftPanels = {
new CodePanel(new CodeArea(this, (JClass) node)), // Java
new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali
new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback
};
CodePanel[] rightPanels = {
new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali
new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik
new CodePanel(new CodeArea(this, (JClass) node)), // Java
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple
new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback
};
leftTabbedPane.add(leftPanels[0], NLS.str("tabs.code"));
leftTabbedPane.add(leftPanels[1], NLS.str("tabs.smali"));
leftTabbedPane.add(leftPanels[2], NLS.str("tabs.smali_bytecode"));
leftTabbedPane.add(leftPanels[3], "Simple");
leftTabbedPane.add(leftPanels[4], "Fallback");
rightTabbedPane.add(rightPanels[0], NLS.str("tabs.smali"));
rightTabbedPane.add(rightPanels[1], NLS.str("tabs.smali_bytecode"));
rightTabbedPane.add(rightPanels[2], NLS.str("tabs.code"));
rightTabbedPane.add(rightPanels[3], "Simple");
rightTabbedPane.add(rightPanels[4], "Fallback");
for (CodePanel p : leftPanels) {
p.load();
}
for (CodePanel p : rightPanels) {
p.load();
}
leftTabbedPane.addChangeListener(e -> ((CodePanel) leftTabbedPane.getSelectedComponent()).load());
rightTabbedPane.addChangeListener(e -> ((CodePanel) rightTabbedPane.getSelectedComponent()).load());
// Attach caret sync between all combinations
for (CodePanel leftPanel : leftPanels) {
for (CodePanel rightPanel : rightPanels) {
attachSyncListeners(leftPanel, rightPanel);
}
}
// Create and configure split pane
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabbedPane, rightTabbedPane);
splitPane.setResizeWeight(0.5);
leftTabbedPane.setMinimumSize(new Dimension(200, 200));
rightTabbedPane.setMinimumSize(new Dimension(200, 200));
add(splitPane);
// Set divider location after layout
SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5));
rightTabbedPane.setSelectedIndex(0);
addCustomControls(leftTabbedPane);
}
private JTabbedPane buildTabbedPane(JClass jCls) {
JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0));
areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
if (split) {
areaTabbedPane.add(new CodePanel(new CodeArea(this, jCls)), NLS.str("tabs.code"));
areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls)), NLS.str("tabs.smali"));
} else {
areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code"));
areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali"));
}
areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code"));
areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali"));
areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls, true)), NLS.str("tabs.smali_bytecode"));
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.SIMPLE))), "Simple");
areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.FALLBACK))), "Fallback");
areaTabbedPane.addChangeListener(e -> {
@@ -108,11 +194,6 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
tabbedPane.putClientProperty(TABBED_PANE_TRAILING_COMPONENT, trailing);
}
private void execInBackground(Runnable runnable) {
BackgroundExecutor bgExec = this.tabbedPane.getMainWindow().getBackgroundExecutor();
bgExec.execute("Loading", runnable);
}
@Override
public void loadSettings() {
javaCodePanel.loadSettings();
@@ -195,4 +276,27 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
}
super.dispose();
}
private void syncToMethod(CodePanel fromPanel, CodePanel toPanel) {
if (!fromPanel.isShowing() || !toPanel.isShowing()) {
return;
}
try {
AbstractCodeArea from = fromPanel.getCodeArea();
AbstractCodeArea to = toPanel.getCodeArea();
toPanel.load();
if (from instanceof CodePanelSyncerAbstractFactory && to instanceof CodePanelSyncee) {
CodePanelSyncer syncer = ((CodePanelSyncerAbstractFactory) from).createCodePanelSyncer();
if (((CodePanelSyncee) to).sync(syncer)) {
return;
}
}
if (!FallbackSyncer.sync(fromPanel, toPanel)) {
LOG.warn("Code pane area sync not possible");
}
} catch (Exception ex) {
LOG.warn("Failed to sync method/class across views: {}", ex.getLocalizedMessage());
}
}
}
@@ -4,7 +4,10 @@ import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
@@ -20,6 +23,7 @@ import jadx.api.ICodeInfo;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeMetadata;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.IBackgroundTask;
import jadx.gui.jobs.LoadTask;
@@ -30,13 +34,31 @@ 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.*;
import jadx.gui.ui.action.CommentSearchAction;
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.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.JavaSyncer;
import jadx.gui.ui.panel.ContentPanel;
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;
@@ -44,7 +66,7 @@ import jadx.gui.utils.shortcut.ShortcutsController;
* The {@link AbstractCodeArea} implementation used for displaying Java code and text based
* resources (e.g. AndroidManifest.xml)
*/
public final class CodeArea extends AbstractCodeArea {
public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee {
private static final Logger LOG = LoggerFactory.getLogger(CodeArea.class);
private static final long serialVersionUID = 6312736869579635796L;
@@ -172,6 +194,18 @@ public final class CodeArea extends AbstractCodeArea {
popup.addSeparator();
popup.add(new FridaAction(this));
popup.add(new XposedAction(this));
popup.addSeparator();
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.addSeparator();
popup.add(new ConvertNumberAction(this));
getMainWindow().getWrapper().getGuiPluginsContext().appendPopupMenus(this, popup);
// move caret on mouse right button click
@@ -362,13 +396,22 @@ public final class CodeArea extends AbstractCodeArea {
}
public void refreshClass() {
refreshClass(false);
}
public void refreshClass(boolean alreadyReloaded) {
if (node instanceof JClass) {
JClass cls = node.getRootClass();
try {
CaretPositionFix caretFix = new CaretPositionFix(this);
caretFix.save();
cachedCodeInfo = cls.reload(getMainWindow().getCacheObject());
if (alreadyReloaded) {
cachedCodeInfo = cls.getCodeInfo();
} else {
// bad. blocks the UI thread for a potentially expensive decomp
cachedCodeInfo = cls.reload(getMainWindow().getCacheObject());
}
ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel;
codeContentPanel.getTabbedPane().refresh(cls);
@@ -379,6 +422,20 @@ public final class CodeArea extends AbstractCodeArea {
}
}
/**
* Refresh the class in the background, updating the UI once the potential decomp is complete.
* Should be called from the UI thread.
*/
public void backgroundRefreshClass() {
UiUtils.uiThreadGuard();
this.getMainWindow().getBackgroundExecutor().execute("Refreshing...", () -> {
this.getNode().getRootClass().reload(this.getMainWindow().getCacheObject());
UiUtils.uiRunAndWait(() -> {
this.refreshClass(true);
});
});
}
public MainWindow getMainWindow() {
return contentPanel.getMainWindow();
}
@@ -398,4 +455,65 @@ public final class CodeArea extends AbstractCodeArea {
super.dispose();
cachedCodeInfo = null;
}
@Override
public CodePanelSyncer createCodePanelSyncer() {
return new JavaSyncer(this);
}
@Override
public boolean sync(CodePanelSyncer codePanelSyncer) {
return codePanelSyncer.syncTo(this);
}
@Nullable
public ICodeMetadata getCodeMetadata() {
ICodeInfo codeInfo = getCodeInfo();
if (!codeInfo.hasMetadata()) {
LOG.warn("No code info metadata for {}", codeInfo.toString());
return null;
}
return codeInfo.getCodeMetadata();
}
/**
* Returns a mapping of 'decompilation output line number' to 'dex debug line number'
* These are 1-indexed line numbers not the line indices of the CodeArea
*
* @return the line mapping
*/
public Map<Integer, Integer> getLineMappings() {
ICodeInfo codeInfo = getCodeInfo();
if (!codeInfo.hasMetadata()) {
LOG.debug("No code info metadata for {}", codeInfo.toString());
return Map.of();
}
Map<Integer, Integer> lineMapping = codeInfo.getCodeMetadata().getLineMapping();
if (lineMapping.isEmpty()) {
LOG.debug("Line mappings are empty for {}", codeInfo.toString());
return Map.of();
}
return lineMapping;
}
/**
* Returns the same as {@link #getLineMappings()} but only if each value (dex debug line number)
* appears only once.
* If a value appears more than once then it suggests that methods might share dex debug line
* numbers.
* If this is the case then the line mapping cannot be used for code sync correlation.
*
* @return the line mapping
*/
public Map<Integer, Integer> getFunctionUniqueLineMappings() {
final var lineMappings = getLineMappings();
final boolean isAnyRepeated =
lineMappings.values().stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).values().stream()
.filter(v -> v > 1).findAny().isPresent();
if (isAnyRepeated) {
LOG.debug("Dex debug line mappings are not unique");
return Map.of();
}
return lineMappings;
}
}
@@ -41,7 +41,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class);
private final boolean enabled;
protected final boolean enabled;
private @Nullable ICodeComment actionComment;
private boolean updateComment;
@@ -50,6 +50,11 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
this.enabled = codeArea.getNode() instanceof JClass;
}
public CommentAction(ActionModel actionModel, CodeArea codeArea) {
super(actionModel, codeArea);
this.enabled = codeArea.getNode() instanceof JClass;
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
if (enabled && updateCommentAction(UiUtils.getOffsetAtMousePosition(codeArea))) {
@@ -98,7 +103,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
CommentDialog.show(codeArea, actionComment, updateComment);
}
private @Nullable ICodeComment searchForExistComment(ICodeComment blankComment) {
protected @Nullable ICodeComment searchForExistComment(ICodeComment blankComment) {
try {
JadxProject project = codeArea.getProject();
JadxCodeData codeData = project.getCodeData();
@@ -123,7 +128,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
* @return blank code comment object (comment string empty)
*/
@Nullable
private ICodeComment getCommentRef(int pos) {
protected ICodeComment getCommentRef(int pos) {
if (pos == -1) {
return null;
}
@@ -181,7 +186,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis
/**
* Check if all tokens are 'comment' in line at 'pos'
*/
private boolean isCommentLine(int pos) {
protected boolean isCommentLine(int pos) {
try {
int line = codeArea.getLineOfOffset(pos);
Token lineTokens = codeArea.getTokenListForLine(line);
@@ -0,0 +1,227 @@
package jadx.gui.ui.codearea;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
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.utils.NLS;
public class ConvertNumberAction extends CommentAction {
private static final Logger LOG = LoggerFactory.getLogger(ConvertNumberAction.class);
private static final String DEFAULT_TEXT = "";
private final String tooltipText;
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);
setEnabled(true);
}
}
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
// reset menu to disabled on cancel
setEnabled(false);
setNameAndDesc(DEFAULT_TEXT);
}
@Override
public void actionPerformed(ActionEvent e) {
if (!super.enabled) {
return;
}
String newText = e.getActionCommand();
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));
}
/**
* 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
* but includes "-" for negative numbers
*/
public @Nullable String getWordByPosition(int offset) {
Token token = codeArea.getWordTokenAtOffset(offset);
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);
}
return str;
}
/**
* 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;
}
int i32 = 0;
long i64 = 0;
int radix = 10;
boolean parsedLong = false;
// handle hex
if (word.startsWith("0x")) {
word = word.substring(2);
radix = 16;
}
// handle long int syntax like "12345L"
if (word.endsWith("L")) {
word = word.substring(0, word.length() - 1);
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);
parsedLong = true;
} catch (NumberFormatException ignore) {
return null;
}
}
// if we parsed decimal, output hex and vice versa
if (radix == 10) {
if (parsedLong) {
conversions.add(String.format("0x%x", i64));
} else {
conversions.add(String.format("0x%x", i32));
}
} else if (radix == 16) {
conversions.add(String.format("%d", 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;
}
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
if (i32 >= ' ' && i32 <= '~') {
conversions.add(String.format("'%c'", (int) i32));
}
return conversions; // no match
}
}
@@ -1,5 +1,6 @@
package jadx.gui.ui.codearea;
import javax.swing.JMenu;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuListener;
@@ -37,6 +38,21 @@ 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);
@@ -43,11 +43,15 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.TextNode;
import jadx.gui.ui.codearea.sync.CodePanelSyncee;
import jadx.gui.ui.codearea.sync.CodePanelSyncer;
import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory;
import jadx.gui.ui.codearea.sync.SmaliSyncer;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public final class SmaliArea extends AbstractCodeArea {
public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee {
private static final Logger LOG = LoggerFactory.getLogger(SmaliArea.class);
private static final long serialVersionUID = 1334485631870306494L;
@@ -59,12 +63,16 @@ public final class SmaliArea extends AbstractCodeArea {
private final JNode textNode;
private final JCheckBoxMenuItem cbUseSmaliV2;
private final boolean allowToggleV2 = false; // add to constructor args to change back
private final boolean initialDisplayV2;
private boolean curVersion = false;
private SmaliModel model;
SmaliArea(ContentPanel contentPanel, JClass node) {
SmaliArea(ContentPanel contentPanel, JClass node, boolean initialDisplayV2) {
super(contentPanel, node);
this.textNode = new TextNode(node.getName());
this.initialDisplayV2 = initialDisplayV2;
setCodeFoldingEnabled(true);
@@ -85,7 +93,9 @@ public final class SmaliArea extends AbstractCodeArea {
settings.sync();
}
});
getPopupMenu().add(cbUseSmaliV2);
if (allowToggleV2) {
getPopupMenu().add(cbUseSmaliV2);
}
switchModel();
}
@@ -117,6 +127,10 @@ public final class SmaliArea extends AbstractCodeArea {
return textNode;
}
public boolean isShowingDalvikBytecode() {
return model instanceof DebugModel;
}
public JClass getJClass() {
return (JClass) node;
}
@@ -454,4 +468,14 @@ public final class SmaliArea extends AbstractCodeArea {
}
};
}
@Override
public CodePanelSyncer createCodePanelSyncer() {
return new SmaliSyncer(this);
}
@Override
public boolean sync(CodePanelSyncer codePanelSyncer) {
return codePanelSyncer.syncTo(this);
}
}
@@ -0,0 +1,42 @@
package jadx.gui.ui.codearea.sync;
import java.util.Map;
import jadx.api.metadata.ICodeAnnotation;
/**
* Marks the start and end of annotation within a CodeMetadataStorage
*/
public class CodeMetadataRange {
// Use Map.Entry here because Java has no built in tuple/pair utility
private final Map.Entry<Integer, ICodeAnnotation> start;
private final Map.Entry<Integer, ICodeAnnotation> end;
CodeMetadataRange(
Map.Entry<Integer, ICodeAnnotation> start,
Map.Entry<Integer, ICodeAnnotation> end) {
this.start = start;
this.end = end;
}
Map.Entry<Integer, ICodeAnnotation> getStart() {
return start;
}
Map.Entry<Integer, ICodeAnnotation> getEnd() {
return end;
}
@Override
public String toString() {
return "CodeMetadataRange{start="
+ start.getKey()
+ "->"
+ start.getValue()
+ ",end="
+ end.getKey()
+ "->"
+ end.getValue()
+ "}";
}
}
@@ -0,0 +1,8 @@
package jadx.gui.ui.codearea.sync;
/**
* Accepts a code panel syncer for syncing code areas
*/
public interface CodePanelSyncee {
boolean sync(CodePanelSyncer syncer);
}
@@ -0,0 +1,4 @@
package jadx.gui.ui.codearea.sync;
public interface CodePanelSyncer extends IToJavaSyncStrategy, IToSmaliSyncStrategy {
}
@@ -0,0 +1,5 @@
package jadx.gui.ui.codearea.sync;
public interface CodePanelSyncerAbstractFactory {
CodePanelSyncer createCodePanelSyncer();
}
@@ -0,0 +1,47 @@
package jadx.gui.ui.codearea.sync;
import java.awt.Color;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import javax.swing.text.Highlighter.HighlightPainter;
import jadx.gui.ui.codearea.AbstractCodeArea;
/**
* Highlighting and scrolling utility into a CodeArea for a given color
*/
public class CodeSyncHighlighter {
private final Color color;
public CodeSyncHighlighter(Color color) {
this.color = color;
}
public void highlightAndScrollToLine(AbstractCodeArea area, int lineIndex) throws BadLocationException {
highlightLine(area, lineIndex);
area.scrollToPos(area.getLineStartOffset(lineIndex));
}
public void highlightLine(AbstractCodeArea area, int lineIndex) throws BadLocationException {
int startOffset = area.getLineStartOffset(lineIndex);
int endOffset = area.getLineEndOffset(lineIndex);
highlightRange(area, startOffset, endOffset);
}
// Highlight range in code area with a temporary yellow highlight
public void highlightRange(AbstractCodeArea area, int startOffset, int endOffset) throws BadLocationException {
Highlighter hl = area.getHighlighter();
HighlightPainter painter =
new DefaultHighlighter.DefaultHighlightPainter(this.color);
Object tag = hl.addHighlight(startOffset, endOffset, painter);
new Timer(1000, e -> hl.removeHighlight(tag)).start();
}
public static CodeSyncHighlighter defaultHighlighter() {
return new CodeSyncHighlighter(UIManager.getColor("TabbedPane.hoverColor"));
}
}
@@ -0,0 +1,115 @@
package jadx.gui.ui.codearea.sync;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Use debug line info from dex to correlate from java to java/smali
*/
public class DebugLineJavaSyncer implements IToSmaliSyncStrategy, IToJavaSyncStrategy {
private static final Logger LOG = LoggerFactory.getLogger(DebugLineJavaSyncer.class);
private final CodeArea from;
public DebugLineJavaSyncer(CodeArea area) {
this.from = area;
}
@Override
public boolean syncTo(CodeArea to) {
// This might be any combination between java/simple/fallback
// We cannot just rely on the current line.
// Instead try to correlate with line mappings.
try {
int lineIndex = from.getCaretLineNumber();
Map<Integer, Integer> toLineMapping = to.getFunctionUniqueLineMappings();
// lineIndex is 0-indexed whereas the line mappings are based off a 1-index.
Integer sourceLine = getClosestSourceLine(lineIndex + 1);
if (sourceLine == null) {
return false;
}
// find the equivalent linenumber in the 'to' by a reverse lookup from the source line
for (Map.Entry<Integer, Integer> entry : toLineMapping.entrySet()) {
int toLine = entry.getKey();
int candidateSourceLine = entry.getValue();
if (sourceLine == candidateSourceLine) {
// we have the mapped line we target the lineIndex which is a 0-index
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, toLine - 1);
LOG.info("{} - successful sync of code to code", LOG.getName());
return true;
}
}
} catch (Exception e) {
LOG.error("{} - Failed to sync from CodeArea to CodeArea: {}", LOG.getName(), e.getLocalizedMessage());
}
return false;
}
@Override
public boolean syncTo(SmaliArea to) {
try {
int lineIndex = from.getCaretLineNumber();
// lineIndex is 0-indexed but the line mappings are based of 1-indexed line numbers.
int lineNum = lineIndex + 1;
Integer sourceLine = getClosestSourceLine(lineNum);
if (sourceLine == null) {
to.removeAllLineHighlights();
LOG.debug("decompiled line {} not mapped to source line", lineNum);
return false;
}
// find the smali line where ".line <sourceLine>" is
LOG.debug("Finding \".line {}\" in smali", sourceLine);
int smaliLine = findSmaliLineIndex(to, sourceLine);
if (smaliLine < 0) {
LOG.warn("{} - Source line {} not annotated in Smali", LOG.getName(), sourceLine);
return false;
}
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLine);
LOG.info("{} - successful sync of code to smali", LOG.getName());
return true;
} catch (Exception ex) {
LOG.error("{} - Failed to sync CodeArea to SmaliArea: {}", LOG.getName(), ex.getLocalizedMessage());
}
return false;
}
private @Nullable Integer getClosestSourceLine(int lineNum) {
// get the line mappings of the Java/Simple/Fallback code
Map<Integer, Integer> lineMapping = from.getFunctionUniqueLineMappings();
if (lineMapping == null || lineMapping.isEmpty()) {
return null;
}
// get the source line from the decomp line
Integer sourceLine = null;
// Some of the intermediate lines are not mapped so keep going back until we find one
// e.g. multiple instruction lines in the 'Simple' view belong to a single source line
while (lineNum >= 0 && (sourceLine = lineMapping.get(lineNum)) == null) {
--lineNum;
}
return sourceLine;
}
/**
* find the ".line \d+" line in the smali
*/
private static int findSmaliLineIndex(SmaliArea smaliArea, int sourceLine) {
String line = ".line " + Integer.toString(sourceLine);
String[] smaliLines = smaliArea.getText().split("\\R");
for (int i = 0; i < smaliLines.length; ++i) {
String l = smaliLines[i];
if (l.trim().equals(line)) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,140 @@
package jadx.gui.ui.codearea.sync;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Use Debug lines in smali from dex debug info to correlate with code
*/
public class DebugLineSmaliSyncer implements IToJavaSyncStrategy {
private static final Logger LOG = LoggerFactory.getLogger(DebugLineSmaliSyncer.class);
private final SmaliArea from;
public DebugLineSmaliSyncer(SmaliArea area) {
this.from = area;
}
@Override
public boolean syncTo(CodeArea to) {
try {
// Get the from lines and currentline index
int lineIndex = from.getCaretLineNumber();
String[] fromLines = from.getText().split("\\R");
if (lineIndex >= fromLines.length) {
return false;
}
// find an Anchor to guide what to look for and highlight in the CodeArea
Anchor anchor = findNearestAnchor(lineIndex, fromLines);
if (anchor == null) {
LOG.error("{} - No Smali Anchor found", LOG.getName());
return false;
}
if (anchor.getType() == Anchor.Type.SOURCE_LINE) {
LOG.debug(anchor.toString());
Map<Integer, Integer> toDecompToSourceMapping = to.getFunctionUniqueLineMappings();
for (Map.Entry<Integer, Integer> entry : toDecompToSourceMapping.entrySet()) {
int decompLine = entry.getKey();
int sourceLine = entry.getValue();
if (anchor.getCodeMappedLineNumber() == sourceLine) {
int decompLineIndex = decompLine - 1;
LOG.debug("Highlighting {} on {}", decompLine, to);
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, decompLineIndex);
LOG.info("{} - successful sync of smali to code", LOG.getName());
return true;
}
}
}
to.removeAllLineHighlights();
} catch (Exception ex) {
LOG.error("{} - Failed to sync from Smali to Code", LOG.getName(), ex);
}
return false;
}
@Nullable
private Anchor findNearestAnchor(int smaliLineNumber, String[] lines) {
for (int i = smaliLineNumber; i >= 0; i--) {
String trimmedLine = lines[i].trim();
if (trimmedLine.startsWith(".line")) {
return new Anchor(Anchor.Type.SOURCE_LINE, trimmedLine, i);
}
if (trimmedLine.startsWith(".method")) {
return new Anchor(Anchor.Type.METHOD_START, trimmedLine, i);
}
if (trimmedLine.startsWith(".end")) {
return new Anchor(Anchor.Type.METHOD_END, trimmedLine, i);
}
if (trimmedLine.startsWith(".field")) {
return new Anchor(Anchor.Type.FIELD, trimmedLine, i);
}
if (trimmedLine.startsWith(".class")) {
return new Anchor(Anchor.Type.CLASS, trimmedLine, smaliLineNumber);
}
}
return null;
}
/**
* Line in the smali that can be used to find a section to highlight in the code area
*/
private static class Anchor {
public enum Type {
SOURCE_LINE,
METHOD_START,
METHOD_END,
FIELD,
CLASS
}
private final Type type;
private final String line;
private final int smaliLineNumber;
private int codeMappedLineNumber = -1;
public Anchor(Type type, String line, int smaliLineNumber) {
this.type = type;
this.line = line;
this.smaliLineNumber = smaliLineNumber;
this.map();
}
public Type getType() {
return type;
}
public int getCodeMappedLineNumber() {
return codeMappedLineNumber;
}
private void map() {
switch (type) {
case SOURCE_LINE:
Pattern p = Pattern.compile("(\\.line\\s)(\\d+)");
Matcher m = p.matcher(line);
if (m.find()) {
codeMappedLineNumber = Integer.parseInt(m.group(2));
}
break;
default:
codeMappedLineNumber = -1;
break;
}
}
@Override
public String toString() {
return String.format("Anchor %s, %d, %d", type.name(), smaliLineNumber, codeMappedLineNumber);
}
}
}
@@ -0,0 +1,7 @@
package jadx.gui.ui.codearea.sync;
import jadx.gui.ui.codearea.CodeArea;
public interface IToJavaSyncStrategy {
boolean syncTo(CodeArea area);
}
@@ -0,0 +1,7 @@
package jadx.gui.ui.codearea.sync;
import jadx.gui.ui.codearea.SmaliArea;
public interface IToSmaliSyncStrategy {
boolean syncTo(SmaliArea area);
}
@@ -0,0 +1,323 @@
package jadx.gui.ui.codearea.sync;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.MethodNode;
import jadx.gui.device.debugger.DbgUtils;
import jadx.gui.device.debugger.smali.SmaliMethodNode;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Use insn code offsets to sync code panel area to code/smali
* This only works for Smali when SmaliArea is showing the dalvik bytecode.
*/
public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncStrategy {
private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetJavaSyncer.class);
private final CodeArea from;
public InsnOffsetJavaSyncer(CodeArea area) {
this.from = area;
}
@Override
public boolean syncTo(SmaliArea to) {
if (!to.isShowingDalvikBytecode()) {
return false;
}
// 1. Find the Method start and end boundaries enclosing the caret position in the code metadata
// 2. Find the closest InsnCodeOffset range within the method boundary corresponding to the caret
// position
// 3. Get all of the smali lines which fall within the InsnCodeOffset range.
// 4. Highlight those found in 3. and scroll to the first one.
int caretPos = from.getCaretPosition();
CodeMetadataRange mthRange = findEnclosingMethodRange(caretPos);
if (mthRange == null) {
return false;
}
Integer mthDefPos = mthRange.getStart().getKey();
Integer mthEndPos = mthRange.getEnd().getKey();
LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos);
LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos);
LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos);
CodeMetadataRange insnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos);
if (insnOffsetRange == null) {
return false;
}
String mthID = getMthRawFullID(mthDefPos);
SmaliMethodNode smaliMthNode = DbgUtils.getSmaliMethodNode(to.getJClass(), mthID);
if (smaliMthNode == null) {
LOG.error("{} - mth ID {} not mapped to a SmaliMethodNode", LOG.getName(), mthID);
return false;
}
List<Integer> smaliLines = getMappedSmaliLines(smaliMthNode, insnOffsetRange);
if (smaliLines.size() < 2) {
return false;
}
try {
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLines.get(0));
for (int i = 1; i < smaliLines.size(); ++i) {
CodeSyncHighlighter.defaultHighlighter().highlightLine(to, smaliLines.get(i));
}
LOG.info("{} - successful sync of code to smali", LOG.getName());
return true;
} catch (Exception ex) {
LOG.error("{} - Failed to sync code to smali with instruction offsets ", LOG.getName(), ex);
}
return false;
}
@Override
public boolean syncTo(CodeArea to) {
int caretPos = from.getCaretPosition();
CodeMetadataRange fromMthRange = findEnclosingMethodRange(caretPos);
if (fromMthRange == null) {
return false;
}
Integer mthDefPos = fromMthRange.getStart().getKey();
Integer mthEndPos = fromMthRange.getEnd().getKey();
LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos);
LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos);
LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos);
CodeMetadataRange fromInsnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos);
if (fromInsnOffsetRange == null) {
return false;
}
String mthID = getMthRawFullID(mthDefPos);
// now search for this range within the target area
CodeMetadataRange toMthRange = findMethodRange(mthID, to);
if (toMthRange == null) {
return false;
}
// search for the first insn offset
int firstInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getStart().getValue()).getOffset();
Integer highlightPosStart = to.getCodeMetadata().searchDown(toMthRange.getStart().getKey(), (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
int pos = ((InsnCodeOffset) ann).getOffset();
if (pos != firstInsnOffset) {
return null;
}
return offset;
});
if (highlightPosStart == null) {
return false;
}
// search for the second insn offset
int secondInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getEnd().getValue()).getOffset();
Integer highlightPosEnd = to.getCodeMetadata().searchDown(highlightPosStart, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
int pos = ((InsnCodeOffset) ann).getOffset();
if (pos != secondInsnOffset) {
return null;
}
return offset;
});
if (highlightPosEnd == null) {
return false;
}
to.scrollToPos(highlightPosStart);
try {
CodeSyncHighlighter.defaultHighlighter().highlightRange(to, highlightPosStart, highlightPosEnd);
LOG.info("{} - successful sync of code to code", LOG.getName());
return true;
} catch (Exception ex) {
LOG.error("{} - Unable to highlight code area from insn offset mappings {} -> {}", LOG.getName(), highlightPosStart,
highlightPosEnd);
}
return false;
}
@Nullable
private static CodeMetadataRange findMethodRange(String mthFullRawID, CodeArea area) {
Map.Entry<Integer, ICodeAnnotation> toMthDecl = area.getCodeMetadata().searchDown(0, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) {
return null;
}
NodeDeclareRef decl = (NodeDeclareRef) ann;
ICodeNodeRef node = decl.getNode();
if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) {
return null;
}
MethodNode mth = (MethodNode) node;
if (!mth.getMethodInfo().getRawFullId().equals(mthFullRawID)) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (toMthDecl == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> toMthEnd = area.getCodeMetadata().searchDown(toMthDecl.getKey(), (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.END) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (toMthEnd == null) {
return null;
}
return new CodeMetadataRange(toMthDecl, toMthEnd);
}
@Nullable
private CodeMetadataRange findEnclosingMethodRange(Integer startPos) {
Map.Entry<Integer, ICodeAnnotation> mthDef = from.getCodeMetadata().searchUp(startPos, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) {
return null;
}
NodeDeclareRef decl = (NodeDeclareRef) ann;
ICodeNodeRef node = decl.getNode();
if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (mthDef == null) {
return null;
}
Map.Entry<Integer, ICodeAnnotation> mthEnd = from.getCodeMetadata().searchDown(startPos, (offset, ann) -> {
if (ann.getAnnType() != ICodeAnnotation.AnnType.END) {
return null;
}
return new SimpleEntry<>(offset, ann);
});
if (mthEnd == null) {
return null;
}
return new CodeMetadataRange(mthDef, mthEnd);
}
/**
* Gets a CodeMetadataRange for the from CodeArea where start and end
* are InsnCodeOffsets whose offsets are monotonically increasing.
*
* @param - startPos the starting position to start searching from
* @param - mthDefPos the method node decl position enclosing the range
* @param - mthEndPos the method end position enclosing the range
*/
@Nullable
private CodeMetadataRange findOffsetRange(Integer startPos, Integer mthDefPos, Integer mthEndPos) {
Map.Entry<Integer, ICodeAnnotation> first = findInsnOffsetBeforePos(startPos, mthDefPos);
Map.Entry<Integer, ICodeAnnotation> second = findInsnOffsetAfterPos(startPos, mthEndPos);
if (first == null || second == null) {
LOG.warn("{} - Unable to find InsnCodeOffsets between {} -> {}", LOG.getName(), mthDefPos, mthEndPos);
return null;
}
int startOffset = ((InsnCodeOffset) first.getValue()).getOffset();
int endOffset = ((InsnCodeOffset) second.getValue()).getOffset();
if (startOffset > endOffset) {
LOG.warn("{} - insn startOffset={} is greater than insn endOffset={} - cannot construct range", LOG.getName(), startOffset,
endOffset);
return null;
}
return new CodeMetadataRange(first, second);
}
@Nullable
private Map.Entry<Integer, ICodeAnnotation> findInsnOffsetBeforePos(Integer startPos, Integer limit) {
return from.getCodeMetadata().searchUp(startPos, (offset, ann) -> {
if (offset <= limit) {
return null;
}
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
return new SimpleEntry<Integer, ICodeAnnotation>(offset, ann);
});
}
@Nullable
private Map.Entry<Integer, ICodeAnnotation> findInsnOffsetAfterPos(Integer startPos, Integer limit) {
return from.getCodeMetadata().searchDown(startPos, (offset, ann) -> {
if (offset >= limit) {
return null;
}
if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
return null;
}
return new SimpleEntry<Integer, ICodeAnnotation>(offset, ann);
});
}
/**
* Assumes that there is a NodeDeclareRef{MethodNode{}} annotation at mthDefPos in the `from`
* CodeInfoMetadata
*/
private String getMthRawFullID(Integer mthDefPos) {
ICodeAnnotation ann = from.getCodeMetadata().getAt(mthDefPos);
NodeDeclareRef ref = (NodeDeclareRef) ann;
MethodNode mth = (MethodNode) ref.getNode();
return mth.getMethodInfo().getRawFullId();
}
/**
* Gets the mapped smali line indices for the code offsets of interest
*
* @param smaliMethodNode - method of interest
* @param insnCodeOffsetRange - code offset range from the caret pos
* @return
*/
private static List<Integer> getMappedSmaliLines(
SmaliMethodNode smaliMethodNode,
CodeMetadataRange insnCodeOffsetRange) {
List<Integer> lines = new ArrayList<>();
int startInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getStart().getValue()).getOffset();
int endInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getEnd().getValue()).getOffset();
// Line mappings are Line index -> Code offset
Map<Integer, Integer> smaliLineMapping = smaliMethodNode.getLineMapping();
LOG.debug("startInsnPos={}, endInsnPos={}", startInsnCodeOffset, endInsnCodeOffset);
for (Map.Entry<Integer, Integer> lineToCodeOffset : smaliLineMapping.entrySet()) {
LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue());
// Asume code offsets from smali debug utils are the same as those in the code metadata
if (lineToCodeOffset.getValue() == startInsnCodeOffset || lineToCodeOffset.getValue() == endInsnCodeOffset) {
lines.add(lineToCodeOffset.getKey());
}
}
Collections.sort(lines); // only two elements
return lines;
}
}
@@ -0,0 +1,135 @@
package jadx.gui.ui.codearea.sync;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.core.dex.nodes.MethodNode;
import jadx.gui.device.debugger.DbgUtils;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/*
* Use insn code offsets to sync smali to code panel area
* This only works for Smali when the SmaliArea is showing the dalvik bytecode
*/
public class InsnOffsetSmaliSyncer implements IToJavaSyncStrategy {
private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetSmaliSyncer.class);
private final SmaliArea from;
public InsnOffsetSmaliSyncer(SmaliArea area) {
this.from = area;
}
@Override
public boolean syncTo(CodeArea to) {
if (!from.isShowingDalvikBytecode()) {
// This strategy can only be used when the debug model has been used to generate the smali.
// This populates the code offsets by line as opposed to just text.
return false;
}
// 1. Get the code offset from the Smali caret line number
// 2. Find the appropriate NodeDeclareRef for the method enclosed in the CodeArea annotations
// 3. Find all code offset range intervals in the map which contain the code offset
// 4. Get the CodeArea positions of these intervals and hightlight them in the code area
// 5. Scroll to the first one.
JClass jclass = from.getJClass();
Map.Entry<String, Integer> lineInfo = DbgUtils.getCodeOffsetInfoByLine(jclass, from.getCaretLineNumber());
if (lineInfo == null) {
return false;
}
Integer lineInfoPos = lineInfo.getValue();
LOG.debug("lineInfo key {}, lineInfo value {}, caretLineNumber {}", lineInfo.getKey(), lineInfo.getValue(),
from.getCaretLineNumber());
ICodeMetadata toMetadata = to.getCodeMetadata();
NavigableMap<Integer, ICodeAnnotation> codeAreaAnnotationMap =
(NavigableMap<Integer, ICodeAnnotation>) toMetadata.getAsMap();
Iterator<NavigableMap.Entry<Integer, ICodeAnnotation>> methodDecl =
findMethodDeclAnnotation(codeAreaAnnotationMap, lineInfo.getKey());
if (methodDecl == null) {
LOG.warn("{} - No NodeDeclareRef exists for {}", LOG.getName(), lineInfo.getKey());
return false;
}
// Looking through the annotations in order from the Method declaration to its end
// compare every adjacent pair of instruction offsets where the second is greater than the first.
// Highlight if the smali offset falls between the second and the first.
Iterator<NavigableMap.Entry<Integer, ICodeAnnotation>> it = methodDecl;
NavigableMap.Entry<Integer, ICodeAnnotation> prev = null;
List<CodeMetadataRange> offsetBoundariesToHighlight = new ArrayList<>();
while (it.hasNext()) {
NavigableMap.Entry<Integer, ICodeAnnotation> entry = it.next();
if (entry.getValue().getAnnType() == ICodeAnnotation.AnnType.END) {
break;
}
if (entry.getValue().getAnnType() != ICodeAnnotation.AnnType.OFFSET) {
continue;
}
if (prev != null) {
InsnCodeOffset currentInsnOffset = (InsnCodeOffset) entry.getValue();
InsnCodeOffset prevInsnOffset = (InsnCodeOffset) prev.getValue();
if (prevInsnOffset.getOffset() <= lineInfoPos && lineInfoPos <= currentInsnOffset.getOffset()) {
offsetBoundariesToHighlight.add(new CodeMetadataRange(prev, entry));
}
}
prev = entry;
}
if (offsetBoundariesToHighlight.isEmpty()) {
return false;
}
to.scrollToPos(offsetBoundariesToHighlight.get(0).getStart().getKey());
try {
for (CodeMetadataRange cmr : offsetBoundariesToHighlight) {
LOG.debug("Highlighting {}", cmr);
CodeSyncHighlighter.defaultHighlighter().highlightRange(to, cmr.getStart().getKey(), cmr.getEnd().getKey());
}
LOG.info("{} - successful sync of smali to code", LOG.getName());
return true;
} catch (Exception ex) {
LOG.error("{} - Unable to highlight smali -> code insn offset range: {}", LOG.getName(), ex.getLocalizedMessage());
}
return false;
}
/**
* Find the NodeDeclareRef annotation of the method identified by smaliLineMthFullID
*
* @param map the annotation map from the CodeArea
* @param smaliLineMthFullID the raw full method ID to look for
* @return iterator to the entry in the annotation map
*/
@Nullable
private static Iterator<NavigableMap.Entry<Integer, ICodeAnnotation>> findMethodDeclAnnotation(
NavigableMap<Integer, ICodeAnnotation> map,
String smaliLineMthFullID) {
// Ensure we use NavigableMap here to get ordering guarantee from iterator call
Iterator<NavigableMap.Entry<Integer, ICodeAnnotation>> it = map.descendingMap().entrySet().iterator();
while (it.hasNext()) {
NavigableMap.Entry<Integer, ICodeAnnotation> entry = it.next();
if (entry.getValue() instanceof NodeDeclareRef) {
NodeDeclareRef nodeDeclareRef = (NodeDeclareRef) entry.getValue();
if (nodeDeclareRef.getNode() instanceof MethodNode) {
MethodNode mth = (MethodNode) nodeDeclareRef.getNode();
if (mth.getMethodInfo().getRawFullId().equals(smaliLineMthFullID)) {
return it;
}
}
}
}
return null;
}
}
@@ -0,0 +1,32 @@
package jadx.gui.ui.codearea.sync;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Syncs a Java code panel area (Java/Simple/Fallback) to another area
*/
public class JavaSyncer implements CodePanelSyncer {
private static final Logger LOG = LoggerFactory.getLogger(JavaSyncer.class);
private final DebugLineJavaSyncer debugLineSyncer;
private final InsnOffsetJavaSyncer insnOffsetSyncer;
public JavaSyncer(CodeArea area) {
this.debugLineSyncer = new DebugLineJavaSyncer(area);
this.insnOffsetSyncer = new InsnOffsetJavaSyncer(area);
}
@Override
public boolean syncTo(CodeArea to) {
return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to);
}
@Override
public boolean syncTo(SmaliArea to) {
return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to);
}
}
@@ -0,0 +1,39 @@
package jadx.gui.ui.codearea.sync;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.SmaliArea;
/**
* Syncs a Smali code panel area to another area
*/
public class SmaliSyncer implements CodePanelSyncer {
private static final Logger LOG = LoggerFactory.getLogger(SmaliSyncer.class);
private final SmaliArea from;
private final InsnOffsetSmaliSyncer insnOffsetSyncer;
private final DebugLineSmaliSyncer debugLineSyncer;
public SmaliSyncer(SmaliArea area) {
this.from = area;
this.insnOffsetSyncer = new InsnOffsetSmaliSyncer(area);
this.debugLineSyncer = new DebugLineSmaliSyncer(area);
}
@Override
public boolean syncTo(CodeArea to) {
// first try debug lines then insn offsets
return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to);
}
@Override
public boolean syncTo(SmaliArea to) {
if (from.isShowingDalvikBytecode() == to.isShowingDalvikBytecode()) {
// smali -> smali just highlight the current line but only if content is the same
to.scrollToPos(from.getLineStartOffsetOfCurrentLine());
}
return true; // Prevent fallback syncing
}
}
@@ -0,0 +1,95 @@
package jadx.gui.ui.codearea.sync.fallback;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.Nullable;
import jadx.gui.ui.codearea.AbstractCodeArea;
abstract class AbstractCodeAreaLine {
private final AbstractCodeArea area;
private final int lineIndex;
private final String line;
protected AbstractCodeAreaLine(AbstractCodeArea area, int lineIndex) throws BadLocationException {
this.area = area;
this.lineIndex = lineIndex;
this.line = this.area.getText().split("\\R")[lineIndex];
}
public AbstractCodeArea getArea() {
return area;
}
public int getLineIndex() {
return lineIndex;
}
public String getStr() {
return line;
}
public String getTrimmedStr() {
return line.trim();
}
public abstract AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException;
public abstract boolean isClassDeclaration();
public abstract boolean isMethodOrConstructorDeclaration();
public abstract boolean isFieldDeclaration();
@Nullable
public abstract String extractDeclaredMethodName();
@Nullable
public abstract String extractDeclaredClassName();
protected abstract MethodDeclaration createMethodDeclaration() throws FallbackSyncException;
/**
* This could be itself or:
* - the enclosing method delcaration if line is in a method
* - the enclosing class declaration if line is a field declaration
*/
public IDeclaration getEnclosingScopeDeclaration() throws BadLocationException, FallbackSyncException {
IDeclaration decl = this.getDeclaration();
if (decl != null) {
return decl;
}
for (int i = lineIndex - 1; i >= 0; i--) {
AbstractCodeAreaLine line = getLineAt(i);
boolean enclosingDecl = line.isScopeDeclarationLine();
if (enclosingDecl) {
return line.getDeclaration();
}
}
throw new FallbackSyncException("No enclosing declaration found for " + this);
}
public boolean isScopeDeclarationLine() {
return isClassDeclaration() || isMethodOrConstructorDeclaration();
}
public boolean isDeclarationLine() {
return isScopeDeclarationLine() || isFieldDeclaration();
}
@Nullable
public IDeclaration getDeclaration() throws FallbackSyncException {
if (isClassDeclaration()) {
return new ClassDeclaration(this);
}
if (isMethodOrConstructorDeclaration()) {
return createMethodDeclaration();
}
return null;
}
@Override
public String toString() {
return line;
}
}
@@ -0,0 +1,78 @@
package jadx.gui.ui.codearea.sync.fallback;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.BadLocationException;
import jadx.gui.ui.codearea.AbstractCodeArea;
public abstract class AbstractCodeAreaToken {
protected final AbstractCodeArea area;
private final int atPos;
protected int startPos;
protected int length;
protected AbstractCodeAreaToken(AbstractCodeArea area, int at) throws BadLocationException, FallbackSyncException {
this.area = area;
this.atPos = at;
this.extractTokenAt();
}
public int getAtPos() {
return atPos;
}
public String getStr() throws BadLocationException {
return area.getText(this.startPos, this.length);
}
public boolean isMethodConstructorDeclarationOrCall() throws BadLocationException {
return area.getText(this.startPos + this.length, 1).equals("(");
}
// Class field reference within a method
public abstract boolean isFieldReference() throws BadLocationException;
// Class field token in class field declaration
public abstract boolean isClassField() throws BadLocationException;
public abstract AbstractCodeAreaLine getLine() throws BadLocationException;
// Helper to extract token under caret (at pos)
private void extractTokenAt() throws FallbackSyncException, BadLocationException {
String text = area.getText();
if (text == null || text.isEmpty()) {
throw new FallbackSyncException("text area is null or empty");
}
// Find word boundaries around caretPos
int start = atPos;
int end = atPos;
while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - 1))) {
start--;
}
while (end < text.length() && Character.isJavaIdentifierPart(text.charAt(end))) {
end++;
}
if (start == end) {
// No identifier found, try string literal at caret line
int line = area.getLineOfOffset(atPos);
String lineText = area.getText(area.getLineStartOffset(line), area.getLineEndOffset(line) - area.getLineStartOffset(line));
Pattern p = Pattern.compile("\"([^\"]*)\"");
Matcher m = p.matcher(lineText);
while (m.find()) {
int litStart = area.getLineStartOffset(line) + m.start(1);
int litEnd = area.getLineStartOffset(line) + m.end(1);
if (atPos >= litStart && atPos <= litEnd) {
this.startPos = m.start(1);
this.length = m.end(1) - m.start(1);
return;
}
}
throw new FallbackSyncException("Unable to extract token at position " + atPos);
}
this.startPos = start;
this.length = end - start;
}
}
@@ -0,0 +1,41 @@
package jadx.gui.ui.codearea.sync.fallback;
import java.util.Objects;
public class ClassDeclaration implements IDeclaration {
private final AbstractCodeAreaLine line;
private final String name;
public ClassDeclaration(AbstractCodeAreaLine line) throws FallbackSyncException {
this.name = line.extractDeclaredClassName();
if (this.name == null) {
throw new FallbackSyncException("line does not declare a class: " + toString());
}
this.line = line;
}
@Override
public String getIdentifyingName() {
return name;
}
@Override
public AbstractCodeAreaLine getLine() {
return line;
}
@Override
public boolean equals(Object o) {
if (o instanceof ClassDeclaration) {
ClassDeclaration cd = (ClassDeclaration) o;
return this.getIdentifyingName().equals(cd.getIdentifyingName());
}
return false;
}
// Not necessary but removes checkstyle warning
@Override
public int hashCode() {
return Objects.hash(line, name);
}
}
@@ -0,0 +1,7 @@
package jadx.gui.ui.codearea.sync.fallback;
public class FallbackSyncException extends Exception {
public FallbackSyncException(String msg) {
super(msg);
}
}
@@ -0,0 +1,256 @@
package jadx.gui.ui.codearea.sync.fallback;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.text.BadLocationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.ui.codearea.CodeArea;
import jadx.gui.ui.codearea.CodePanel;
import jadx.gui.ui.codearea.SmaliArea;
import jadx.gui.ui.codearea.sync.CodeSyncHighlighter;
/**
* Regex/String based sync strategy of toPanel when clicking in fromPanel
* Summary of syncing strategy:
* 1) Look for an identifying class member token under the caret position.
* 2) If found look for the enclosing method or class declaration.
* 3) If the line is a declaration line, find the equivalent line in the other code panel.
* 4) Otherwise find the nth occurence of the token in the enclosing method/class in the other code
* panel.
* The following are not yet supported:
* - generic classes/methods
* - anonymous classes
* - lambda functions
* - constructors
*/
public class FallbackSyncer {
private static final Logger LOG = LoggerFactory.getLogger(FallbackSyncer.class);
public static boolean sync(CodePanel fromPanel, CodePanel toPanel) throws BadLocationException, Exception {
LOG.debug("FALLBACK SYNC START");
try {
AbstractCodeArea from = fromPanel.getCodeArea();
AbstractCodeArea to = toPanel.getCodeArea();
int caretPos = from.getCaretPosition();
int lineIndex = from.getLineOfOffset(caretPos);
String[] fromLines = from.getText().split("\\R");
if (lineIndex >= fromLines.length) {
return false;
}
String caretLine = fromLines[lineIndex];
LOG.debug("Caret line [{}]: {}", caretPos, caretLine);
// Extract token under caret (string literal or identifier)
AbstractCodeAreaToken areaToken = FallbackSyncer.getToken(from, caretPos);
String token = areaToken.getStr();
LOG.debug("Token at caret: '{}'", token);
if (token == null || token.isEmpty()) {
return false;
}
if (!allowSync(areaToken)) {
LOG.debug("Fallback matching only applicable for variable, classname, field or method tokens");
return false;
}
return syncToIdentifyingNthOccurence(areaToken, to);
} finally {
LOG.debug("FALLBACK SYNC END");
}
}
// This function just serves as a way to create the correct Token type
// FallbackSyncer should be refactored to use CodePanelSyncer
private static AbstractCodeAreaToken getToken(AbstractCodeArea from, int caretPos) throws BadLocationException, FallbackSyncException {
if (from instanceof SmaliArea) {
return new SmaliAreaToken((SmaliArea) from, caretPos);
}
if (from instanceof CodeArea) {
return new JavaCodeAreaToken((CodeArea) from, caretPos);
}
throw new FallbackSyncException("Unknown AbstractCodeArea type for " + from);
}
/**
* Looks for the nth occurence of the token in the enclosing class/method scope in the `to` area.
* If found, sync to it in the `to` area.
*/
private static boolean syncToIdentifyingNthOccurence(AbstractCodeAreaToken sourceToken, AbstractCodeArea to)
throws BadLocationException, FallbackSyncException {
AbstractCodeAreaLine tokenLine = sourceToken.getLine();
// Locate the method/class declaration line for context
IDeclaration fromDeclaration = tokenLine.getEnclosingScopeDeclaration();
if (fromDeclaration == null) {
LOG.warn("Unable to find declaration line above {}", tokenLine);
return false;
}
AbstractCodeAreaLine fromDeclaringLine = fromDeclaration.getLine();
AbstractCodeArea from = fromDeclaringLine.getArea();
String declarationLineStr = fromDeclaringLine.getStr();
LOG.debug("Found declaration line: {}", declarationLineStr);
String nameToFind = fromDeclaration.getIdentifyingName();
if (nameToFind == null || nameToFind.isEmpty()) {
return false;
}
// Determine whether we're matching a class or method
boolean isClass = fromDeclaringLine.isClassDeclaration();
String regex = isClass
? generateClassRegex(nameToFind)
: generateMethodRegex(nameToFind);
// Find the declaration in target text
Matcher matcher = Pattern.compile(regex).matcher(to.getText());
LOG.debug("Searching for {} in targetText, isClass {}", nameToFind, isClass);
AbstractCodeAreaLine targetDeclLine = findTargetDeclaringLine(to, matcher, fromDeclaration);
if (targetDeclLine == null) {
LOG.debug("Cannot find target declaration line");
return false;
}
int targetDeclarationLineIndex = targetDeclLine.getLineIndex();
LOG.debug("Target declaration line {}", targetDeclLine.getStr());
if (tokenLine.isScopeDeclarationLine()) {
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, targetDeclarationLineIndex);
LOG.info("{} - Highlighted target declaration line", LOG.getName(), targetDeclLine.getStr());
return true;
}
// Extract the method/class body from target
String methodBody = extractMethodBody(to, matcher.start());
// Find nth occurrence of token in source method
// Extract method body from source (to count occurrences)
Matcher fromMatcher = Pattern.compile(regex).matcher(from.getText());
if (!fromMatcher.find()) {
LOG.debug("No method/class match found in source for regex: {}", regex);
return false;
}
String sourceMethodBody = extractMethodBody(from, fromMatcher.start());
// Count which occurrence of token the caret corresponds to in the source method body
String tokenStr = sourceToken.getStr();
int caretPos = sourceToken.getAtPos();
int caretOffsetInMethod = caretPos - fromMatcher.start();
int nthOccurrence = 0;
Pattern tokenPattern = Pattern.compile("\"" + Pattern.quote(tokenStr) + "\"|\\b" + Pattern.quote(tokenStr) + "\\b");
Matcher tokenMatcher = tokenPattern.matcher(sourceMethodBody);
while (tokenMatcher.find()) {
if (tokenMatcher.start() > caretOffsetInMethod) {
break;
}
nthOccurrence++;
}
LOG.debug("Caret is at occurrence number: {}", nthOccurrence);
// Now find nth occurrence of token in target method body
tokenMatcher = tokenPattern.matcher(methodBody);
int occurrenceCount = 0;
while (tokenMatcher.find()) {
occurrenceCount++;
if (occurrenceCount == nthOccurrence) {
// Find absolute offset of this line in targetText
int tokenPosInMethod = tokenMatcher.start();
int absoluteOffset = matcher.start() + tokenPosInMethod;
// Find line start and end offset in target
int tokenLineIndex = to.getLineOfOffset(absoluteOffset);
CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, tokenLineIndex);
LOG.info("{} - Highlighted token '{}' at nth occurrence: {}", LOG.getName(), tokenStr, nthOccurrence);
return true;
}
}
LOG.debug("No matching token or instruction found in method: {}", nameToFind);
return false;
}
private static AbstractCodeAreaLine findTargetDeclaringLine(
AbstractCodeArea to, // target area
Matcher matcher, // matcher to search for method/ctor name
IDeclaration sourceDecl // source decl to match against
) throws BadLocationException, FallbackSyncException {
// Find the declaration in target text
while (matcher.find()) {
LOG.debug("Match found at offset: {}", matcher.start());
int targetDeclarationLineIndex = to.getLineOfOffset(matcher.start());
AbstractCodeAreaLine toDeclCandidate = getLine(to, targetDeclarationLineIndex);
if (!toDeclCandidate.isScopeDeclarationLine()) {
continue;
}
IDeclaration targetDecl = toDeclCandidate.getDeclaration();
if (sourceDecl.equals(targetDecl)) {
return toDeclCandidate;
}
}
return null;
}
// Similar with the function above if refactored to use the CodePanelSyncer Abstraction we can
// remove this.
private static AbstractCodeAreaLine getLine(AbstractCodeArea area, int lineIndex) throws BadLocationException, FallbackSyncException {
if (area instanceof SmaliArea) {
return new SmaliAreaLine((SmaliArea) area, lineIndex);
}
if (area instanceof CodeArea) {
return new JavaCodeAreaLine((CodeArea) area, lineIndex);
}
throw new FallbackSyncException("Unknown AbstractCodeArea type for " + area);
}
private static boolean allowSync(AbstractCodeAreaToken areaToken) throws BadLocationException {
boolean isOnDeclarationLine = areaToken.getLine().isDeclarationLine();
return isOnDeclarationLine
|| areaToken.isClassField()
|| areaToken.isFieldReference()
|| areaToken.isMethodConstructorDeclarationOrCall();
}
private static String generateClassRegex(String name) {
return "\\b(class|interface|enum)\\s+" + Pattern.quote(name) + "\\b" // java
+ "|"
+ "\\.class.*L.*" + Pattern.quote(name) + ";" // smali text
+ "|"
+ "Class:\\sL.*" + Pattern.quote(name) + ";"; // smali + dalvik
}
private static String generateMethodRegex(String name) {
return "\\b" + Pattern.quote(name) + "\\s*\\(" // java like
+ "|"
+ "\\.method.*" + Pattern.quote(name) + "\\s*\\("; // smali
}
private static String extractMethodBody(AbstractCodeArea area, int startIndex) {
String text = area.getText();
if (area instanceof SmaliArea) {
int end = text.indexOf(".end method", startIndex);
return end != -1 ? text.substring(startIndex, end + ".end method".length()) : text.substring(startIndex);
} else {
int brace = 0;
boolean inMethod = false;
for (int i = startIndex; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '{') {
brace++;
inMethod = true;
} else if (c == '}') {
brace--;
if (brace == 0 && inMethod) {
return text.substring(startIndex, i + 1);
}
}
}
return text.substring(startIndex);
}
}
}
@@ -0,0 +1,7 @@
package jadx.gui.ui.codearea.sync.fallback;
interface IDeclaration {
String getIdentifyingName();
AbstractCodeAreaLine getLine();
}
@@ -0,0 +1,104 @@
package jadx.gui.ui.codearea.sync.fallback;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.ui.codearea.CodeArea;
public class JavaCodeAreaLine extends AbstractCodeAreaLine {
private static final Logger LOG = LoggerFactory.getLogger(JavaCodeAreaLine.class);
public JavaCodeAreaLine(CodeArea area, int lineIndex) throws BadLocationException {
super(area, lineIndex);
}
@Override
public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException {
return new JavaCodeAreaLine((CodeArea) getArea(), lineIndex);
}
@Override
public boolean isClassDeclaration() {
return getTrimmedStr().matches(".*\\b(class|interface|enum)\\b.*\\{");
}
@Override
public boolean isMethodOrConstructorDeclaration() {
String l = getTrimmedStr();
// Skip control-flow constructs (to avoid matching 'if', 'for', etc.)
// WARNING - we are relying on the code gen format output of jadx here and that it is trimmed.
// it also assumes that jadx will never output two statements on the same line separated by ';'
if (l.startsWith("if ")
|| l.startsWith("for ")
|| l.startsWith("while ")
|| l.startsWith("switch ")
|| l.startsWith("case ")
|| l.startsWith("break ")
|| l.startsWith("default ")
|| l.startsWith("} else if ")
|| l.startsWith("} else ")
|| l.startsWith("try ")
|| l.startsWith("} catch ")
|| l.startsWith("} finally ")
|| l.startsWith("throw ")
|| l.startsWith("do ")
|| l.startsWith("synchronized ")) {
return false;
}
boolean hasParens = l.contains("(") && l.contains(")");
boolean isDefined = l.endsWith("{");
boolean isAbstract = l.contains("abstract") && l.endsWith(";");
return hasParens && (isDefined || isAbstract);
}
@Override
public boolean isFieldDeclaration() {
try {
IDeclaration enclosingDeclaration = getEnclosingScopeDeclaration();
if (!(enclosingDeclaration instanceof ClassDeclaration)) {
return false;
}
String line = getTrimmedStr();
// This may also include fields which are anonymous classes or lambdas
return line.endsWith(";") || line.contains(" = ");
} catch (Exception ex) {
LOG.error("{} - Unable to determine if line is a field declaration", LOG.getName(), ex);
}
return false;
}
@Override
public final @Nullable String extractDeclaredClassName() {
if (!isClassDeclaration()) {
return null;
}
String[] tokens = getTrimmedStr().split("\\s+");
for (int i = 0; i < tokens.length; i++) {
if (tokens[i].equals("class") || tokens[i].equals("interface") || tokens[i].equals("enum")) {
if (i + 1 < tokens.length) {
return tokens[i + 1];
}
}
}
return null;
}
@Override
public @Nullable String extractDeclaredMethodName() {
if (!isMethodOrConstructorDeclaration()) {
return null;
}
int paren = getTrimmedStr().indexOf('(');
String before = getTrimmedStr().substring(0, paren).trim();
String[] parts = before.split("\\s+");
return parts[parts.length - 1]; // last token
}
@Override
protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException {
return MethodDeclaration.create(this);
}
}
@@ -0,0 +1,35 @@
package jadx.gui.ui.codearea.sync.fallback;
import javax.swing.text.BadLocationException;
import jadx.gui.ui.codearea.CodeArea;
public class JavaCodeAreaToken extends AbstractCodeAreaToken {
public JavaCodeAreaToken(CodeArea area, int at) throws BadLocationException, FallbackSyncException {
super(area, at);
}
@Override
public boolean isClassField() throws BadLocationException {
AbstractCodeAreaLine line = getLine();
if (!line.isFieldDeclaration()) {
return false;
}
// assignment immediately follows the token
if (line.getStr().contains("=")) {
return area.getText(this.startPos + this.length, 2).equals(" =");
}
// ends with ';'
return area.getText(this.startPos + this.length, 1).equals(";");
}
@Override
public boolean isFieldReference() throws BadLocationException {
return area.getText(this.startPos - 5, 5).equals("this.");
}
@Override
public AbstractCodeAreaLine getLine() throws BadLocationException {
return new JavaCodeAreaLine((CodeArea) area, area.getLineOfOffset(getAtPos()));
}
}
@@ -0,0 +1,205 @@
package jadx.gui.ui.codearea.sync.fallback;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import jadx.core.utils.Utils;
class MethodDeclaration implements IDeclaration {
private final AbstractCodeAreaLine line;
private final Type returnType;
private final List<Type> argTypes;
private final String name;
boolean isStatic;
public static MethodDeclaration create(JavaCodeAreaLine line) throws FallbackSyncException {
String methodName = line.extractDeclaredMethodName();
if (methodName == null) {
throw new FallbackSyncException("no method name found in java declaration");
}
// Get the return string
String trimmed = line.getTrimmedStr();
int methodNameStartPos = trimmed.indexOf(methodName);
// -2 to jump to last char of return type
// +1 to get to first char of return type
int returnTypeStartPos = trimmed.lastIndexOf(' ', methodNameStartPos - 2) + 1;
returnTypeStartPos = returnTypeStartPos > -1 ? returnTypeStartPos : 0;
String returnStr = trimmed.substring(returnTypeStartPos, methodNameStartPos - 1);
// Get the arg types
String argString = trimmed.substring(trimmed.indexOf('(') + 1, trimmed.indexOf(')'));
String[] argStringParts = argString.split(", ");
List<String> argTypeStrings = new ArrayList<>();
for (int i = 0; i < argStringParts.length; i++) {
String part = argStringParts[i];
if (part.isEmpty()) {
break;
}
argTypeStrings.add(part.substring(0, part.indexOf(" ")));
}
boolean isStatic = trimmed.contains("static ");
List<Type> argTypes = argTypeStrings.stream().map(s -> Type.fromJavaName(s)).collect(Collectors.toList());
return new MethodDeclaration(line, Type.fromJavaName(returnStr), argTypes, isStatic, methodName);
}
public static MethodDeclaration create(SmaliAreaLine line) throws FallbackSyncException {
String methodName = line.extractDeclaredMethodName();
if (methodName == null) {
throw new FallbackSyncException("no method name found in smali declaration");
}
// Get the return string
String trimmed = line.getTrimmedStr();
String returnStr = trimmed.substring(trimmed.indexOf(')') + 1);
returnStr = returnStr.endsWith(";") ? returnStr.substring(0, returnStr.length() - 1) : returnStr;
boolean isStatic = trimmed.contains("static ");
return new MethodDeclaration(line, Type.fromSmaliName(returnStr), parseSmaliArgs(trimmed), isStatic, methodName);
}
private MethodDeclaration(AbstractCodeAreaLine line, Type returnType, List<Type> argTypes, boolean isStatic, String name) {
this.line = line;
this.returnType = returnType;
this.argTypes = argTypes;
this.isStatic = isStatic;
this.name = name;
}
@Override
public String getIdentifyingName() {
return name;
}
@Override
public AbstractCodeAreaLine getLine() {
return line;
}
private static List<Type> parseSmaliArgs(String lineStr) {
List<String> argTypeStrings = new ArrayList<>();
String argString = lineStr.substring(lineStr.indexOf('(') + 1, lineStr.indexOf(')'));
for (int i = 0; i < argString.length();) {
char c = argString.charAt(i);
if (c == 'L') {
int j = i;
for (; j < argString.length(); ++j) {
if (argString.charAt(j) == ';') {
argTypeStrings.add(argString.substring(i, j + 1));
break;
}
}
i = j + 1;
} else if (c == '[') {
argTypeStrings.add(argString.substring(i, i + 2));
i += 2;
} else if (c != ' ') {
argTypeStrings.add(argString.substring(i, i + 1));
++i;
} else {
++i;
}
}
return argTypeStrings.stream().map(s -> Type.fromSmaliName(s)).collect(Collectors.toList());
}
@Override
public boolean equals(Object o) {
if (o instanceof MethodDeclaration) {
MethodDeclaration decl = (MethodDeclaration) o;
if (!decl.name.equals(this.name)) {
return false;
}
if (decl.isStatic != this.isStatic) {
return false;
}
if (!decl.returnType.equals(this.returnType)) {
return false;
}
if (decl.argTypes.size() != this.argTypes.size()) {
return false;
}
for (int i = 0; i < decl.argTypes.size(); ++i) {
if (!decl.argTypes.get(i).equals(this.argTypes.get(i))) {
return false;
}
}
return true;
}
return false;
}
// Not necessary but removes checkstyle warning
@Override
public int hashCode() {
return Objects.hash(name, isStatic, returnType, argTypes);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("NAME=").append(name).append("+++")
.append("RETURN=").append(returnType).append("+++")
.append("ARGS=");
for (final var a : argTypes) {
sb.append(a).append(",");
}
return sb.toString();
}
private static class Type {
private String smaliName;
private String javaName;
public static Type fromJavaName(String name) {
return new Type(Utils.javaNameToSmaliName(name), name);
}
public static Type fromSmaliName(String name) {
return new Type(name, Utils.smaliNameToJavaName(name));
}
private Type(String smaliName, String javaName) {
this.smaliName = smaliName;
this.javaName = javaName;
}
private boolean isNonPrimitive() {
return smaliName.startsWith("L");
}
@Override
public boolean equals(Object o) {
if (o instanceof Type) {
Type t = (Type) o;
if (t.isNonPrimitive() || this.isNonPrimitive()) {
// One of them might be missing the package prefix
return t.javaName.endsWith(this.javaName)
|| this.javaName.endsWith(t.javaName);
}
return t.javaName.equals(this.javaName)
|| t.smaliName.equals(this.smaliName);
// Slightly less strict - should think about this more
// && t.smaliName.equals(this.smaliName);
}
return false;
}
// Not necessary but removes checkstyle warning
@Override
public int hashCode() {
return Objects.hash(this, javaName, smaliName);
}
@Override
public String toString() {
return "@" + smaliName + "-OR-" + javaName + "@";
}
}
}
@@ -0,0 +1,75 @@
package jadx.gui.ui.codearea.sync.fallback;
import javax.swing.text.BadLocationException;
import org.jetbrains.annotations.Nullable;
import jadx.gui.ui.codearea.SmaliArea;
public class SmaliAreaLine extends AbstractCodeAreaLine {
public SmaliAreaLine(SmaliArea area, int lineIndex) throws BadLocationException {
super(area, lineIndex);
}
@Override
public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException {
return new SmaliAreaLine((SmaliArea) getArea(), lineIndex);
}
@Override
public boolean isClassDeclaration() {
return getTrimmedStr().startsWith("Class: ") || getTrimmedStr().startsWith(".class ");
}
@Override
public boolean isMethodOrConstructorDeclaration() {
return getTrimmedStr().startsWith(".method");
}
@Override
public boolean isFieldDeclaration() {
return getTrimmedStr().startsWith(".field");
}
@Override
public final @Nullable String extractDeclaredClassName() {
if (!isClassDeclaration()) {
return null;
}
String[] parts = getTrimmedStr().split("\\s+");
for (String part : parts) {
if (part.startsWith("L") && part.endsWith(";")) {
String fileClassName;
if (part.contains("/")) {
fileClassName = part.substring(part.lastIndexOf('/') + 1, part.length() - 1);
} else {
fileClassName = part.substring(1, part.length() - 1); // remove leading 'L' and trailing ';'
}
if (fileClassName.contains("$")) { // inner class
return fileClassName.substring(fileClassName.lastIndexOf('$') + 1);
}
return fileClassName;
}
}
return null;
}
@Override
public final @Nullable String extractDeclaredMethodName() {
if (!isMethodOrConstructorDeclaration()) {
return null;
}
int parenIndex = getTrimmedStr().indexOf('(');
if (parenIndex > 0) {
String beforeParen = getTrimmedStr().substring(0, parenIndex).trim();
String[] tokens = beforeParen.split("\\s+");
return tokens[tokens.length - 1];
}
return null;
}
@Override
protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException {
return MethodDeclaration.create(this);
}
}
@@ -0,0 +1,45 @@
package jadx.gui.ui.codearea.sync.fallback;
import javax.swing.text.BadLocationException;
import jadx.gui.ui.codearea.SmaliArea;
public class SmaliAreaToken extends AbstractCodeAreaToken {
public SmaliAreaToken(SmaliArea area, int at) throws BadLocationException, FallbackSyncException {
super(area, at);
}
@Override
public boolean isFieldReference() throws BadLocationException {
return area.getText(this.startPos - 2, 2).equals("->");
}
@Override
public boolean isClassField() throws BadLocationException {
AbstractCodeAreaLine line = this.getLine();
boolean startsWithField = line.isFieldDeclaration();
if (startsWithField) {
String tokenStr = getStr();
String trimmedLine = line.getTrimmedStr();
int lineTokenStartPos = trimmedLine.indexOf(tokenStr);
int lineTokenAfterPos = lineTokenStartPos + this.length;
for (int i = lineTokenAfterPos; i < trimmedLine.length(); ++i) {
char c = trimmedLine.charAt(i);
switch (c) {
case ' ':
break;
case ':':
return true;
default:
return false;
}
}
}
return false;
}
@Override
public AbstractCodeAreaLine getLine() throws BadLocationException {
return new SmaliAreaLine((SmaliArea) area, area.getLineOfOffset(getAtPos()));
}
}
@@ -0,0 +1,342 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.util.Formatter;
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;
import javax.swing.BoxLayout;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
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;
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 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 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) {
super(mainWindow,
String.format("%s: %s", NLS.str("graph_viewer.call_graph.title"), DotGraphUtils.methodFormatName(javaMethod, false)));
this.javaMethod = javaMethod;
}
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
// Long names checkbox
JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names"));
showLongNames.setSelected(false);
showLongNames.addItemListener(e -> {
longNames = showLongNames.isSelected();
reload();
});
// Calee spinner
SpinnerNumberModel calleeDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1);
JSpinner calleeDepthSpinner = new JSpinner(calleeDepthSpinnerModel);
calleeDepthSpinner.addChangeListener(e -> {
calleeDepthLimit = (int) calleeDepthSpinner.getValue();
reload();
});
// Callee label
JLabel calleeLbl = new JLabel(NLS.str("graph_viewer.callee_depth"));
calleeLbl.setLabelFor(calleeDepthSpinner);
calleeLbl.setHorizontalAlignment(SwingConstants.LEFT);
// Assemble callee panel
JPanel calleePanel = new JPanel();
calleePanel.setOpaque(false);
calleePanel.setLayout(new BoxLayout(calleePanel, BoxLayout.LINE_AXIS));
calleePanel.add(calleeLbl);
calleePanel.add(Box.createRigidArea(new Dimension(3, 0)));
calleePanel.add(calleeDepthSpinner);
// Caller spinner
SpinnerNumberModel callerDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1);
JSpinner callerDepthSpinner = new JSpinner(callerDepthSpinnerModel);
callerDepthSpinner.addChangeListener(e -> {
callerDepthLimit = (int) callerDepthSpinner.getValue();
reload();
});
// Caller label
JLabel callerLbl = new JLabel(NLS.str("graph_viewer.caller_depth"));
callerLbl.setLabelFor(callerDepthSpinner);
callerLbl.setHorizontalAlignment(SwingConstants.LEFT);
// Assemble caller panel
JPanel callerPanel = new JPanel();
callerPanel.setOpaque(false);
callerPanel.setLayout(new BoxLayout(callerPanel, BoxLayout.LINE_AXIS));
callerPanel.add(callerLbl);
callerPanel.add(Box.createRigidArea(new Dimension(3, 0)));
callerPanel.add(callerDepthSpinner);
// Assemble menubar panel
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
menuBarPanel.add(showLongNames, BorderLayout.PAGE_START);
menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0)));
menuBarPanel.add(calleePanel);
menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0)));
menuBarPanel.add(callerPanel);
// Add menubar panel to menuBar
menuBar.add(menuBarPanel);
return menuBar;
}
public static void open(MainWindow window, JMethod method) {
JavaMethod javaMethod = method.getJavaMethod();
CallGraphDialog graphDialog = new CallGraphDialog(window, javaMethod);
graphDialog.addMenuBar();
graphDialog.setVisible(true);
graphDialog.reload();
}
public void reload() {
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");
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 highlightColor =
String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(),
themeHighlight.getBlue());
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);
f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor);
f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor);
nextNodeID = 0;
methodToNodeID = new HashMap<>();
unresolvedMethodToNodeID = new HashMap<>();
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();
}
}
private void addCallers(int depth, Formatter f, JavaMethod javaMethod) {
if (depth >= callerDepthLimit) {
return;
}
List<JavaNode> uses = javaMethod.getUseIn();
// add "calls" relationships
for (JavaNode node : uses) {
if (!(node instanceof JavaMethod)) {
continue;
}
JavaMethod caller = (JavaMethod) node;
int nodeID = addNode(f, caller);
addEdge(f, nodeID, methodToNodeID.get(javaMethod));
addCallers(depth + 1, f, caller);
}
}
private void addCallees(int depth, Formatter f, JavaMethod javaMethod) {
if (depth >= calleeDepthLimit) {
return;
}
List<JavaNode> used = javaMethod.getUsed();
// add "calls" relationships
for (JavaNode node : used) {
if (!(node instanceof JavaMethod)) {
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);
}
private void addUnresolvedCallees(int depth, Formatter f, JavaMethod javaMethod) {
if (depth >= calleeDepthLimit) {
return;
}
List<IMethodRef> used = javaMethod.getUnresolvedUsed();
// add "calls" relationships
for (IMethodRef callee : used) {
String name = callee.getName();
if (name == null) {
continue;
}
int nodeID = addNode(f, callee);
addEdge(f, methodToNodeID.get(javaMethod), nodeID);
}
}
private int addNode(Formatter f, JavaMethod method) {
return addNode(f, method, "");
}
// 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)) {
nodeID = methodToNodeID.get(method);
} else {
nodeID = nextNodeID;
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;
}
private int addNode(Formatter f, IMethodRef method) {
return addNode(f, method, "");
}
// 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)) {
nodeID = unresolvedMethodToNodeID.get(method);
} else {
nodeID = nextNodeID;
nextNodeID++;
unresolvedMethodToNodeID.put(method, nodeID);
}
String name = DotGraphUtils.unresolvedMethodFormatName(method, longNames);
Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor");
String outOfFocus =
String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(),
themeOutOfFocus.getBlue());
f.format("Node_%d [ label=\"{%s}\" style=dashed %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra);
return nodeID;
}
// Add an edge between sourceID and destID to the graph in f
private void addEdge(Formatter f, int sourceID, int destID) {
Edge edge = new Edge(sourceID, destID);
if (!edges.contains(edge)) {
f.format("Node_%d -> Node_%d\n", sourceID, destID);
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,282 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JCheckBox;
import javax.swing.JMenuBar;
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;
import jadx.core.dex.attributes.nodes.MethodOverrideAttr;
import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.IMethodDetails;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
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 boolean longNames = false;
private boolean overrides = false;
private Map<Object, Integer> objectToNodeID = new HashMap<>();
private int nextNodeID = 0;
public ClassInheritanceGraphDialog(MainWindow mainWindow, ClassNode cls) {
super(mainWindow,
String.format("%s: %s", NLS.str("graph_viewer.inheritance_graph.title"), DotGraphUtils.classFormatName(cls, false)));
this.cls = cls;
}
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
// Long names checkbox
JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names"));
showLongNames.setSelected(false);
showLongNames.addItemListener(e -> {
longNames = showLongNames.isSelected();
reload();
});
// Overrides checkbox
JCheckBox showOverrides = new JCheckBox(NLS.str("graph_viewer.overrides"));
showOverrides.setSelected(false);
showOverrides.addItemListener(e -> {
overrides = showOverrides.isSelected();
reload();
});
// Assemble menubar panel
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
menuBarPanel.add(showLongNames, BorderLayout.PAGE_START);
menuBarPanel.add(showOverrides, BorderLayout.PAGE_START);
// Add menubar panel to menuBar
menuBar.add(menuBarPanel);
return menuBar;
}
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();
}
public void reload() {
SwingUtilities.invokeLater(() -> {
String graph = generateGraph(cls);
getPanel().setGraph(graph);
});
}
private String generateGraph(ClassNode rootClass) {
StringBuilder sb = new StringBuilder();
ClassNode cls = rootClass;
objectToNodeID = new HashMap<>();
Color themeBackground = UIManager.getColor("Panel.background");
Color themeForeground = UIManager.getColor("Label.foreground");
Color themeHighlight = UIManager.getColor("Component.focusedBorderColor");
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 highlightColor =
String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(),
themeHighlight.getBlue());
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);
f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor);
f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor);
// add nodes
processClass(f, cls, highlightColor);
// close graph
f.format("}");
return f.toString();
}
}
private int processClass(Formatter f, ClassNode cls) {
return processClass(f, cls, "");
}
private int processClass(Formatter f, ClassNode cls, String extra) {
if (objectToNodeID.containsKey(cls)) {
// Don't process a class that has been processed before
return objectToNodeID.get(cls);
}
int classID = addNode(f, cls, extra);
// add interface relationships
List<ArgType> ifaces = cls.getInterfaces();
for (int i = 0; i < ifaces.size(); i++) {
ArgType iface = ifaces.get(i);
int ifaceID;
ClassNode ifaceNode = cls.root().resolveClass(iface);
if (ifaceNode != null) {
ifaceID = processClass(f, ifaceNode);
objectToNodeID.put(iface, ifaceID);
} else {
ifaceID = addNode(f, iface);
}
// Classes implement interfaces, interfaces extend interfaces
String edgeLabel = cls.getAccessFlags().isInterface() ? "extends" : "implements";
f.format("Node_%d -> Node_%d [label=\"%s\" style=\"dashed\" ]\n", classID, ifaceID, edgeLabel);
}
// add superclass relationship
ArgType superClass = cls.getSuperClass();
if (superClass != ArgType.OBJECT) {
int superClsID;
cls = cls.root().resolveClass(superClass);
if (cls != null) {
superClsID = processClass(f, cls);
objectToNodeID.put(superClass, superClsID);
} else {
superClsID = addNode(f, superClass);
}
f.format("Node_%d -> Node_%d [label=\"extends\" ]\n", classID, superClsID);
}
return classID;
}
// Add a node for a class
private int addNode(Formatter f, ClassNode cls) {
return addNode(f, cls, "");
}
private int addNode(Formatter f, ClassNode cls, String extra) {
int nodeID;
if (objectToNodeID.containsKey(cls)) {
nodeID = objectToNodeID.get(cls);
} else {
nodeID = nextNodeID;
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<>();
for (MethodNode method : cls.getMethods()) {
MethodOverrideAttr ovrdAttr = method.get(AType.METHOD_OVERRIDE);
if (ovrdAttr != null) {
if (!ovrdAttr.getOverrideList().isEmpty()) {
String methodName = DotGraphUtils.methodFormatName(method, longNames);
Formatter details = new Formatter();
details.format(" overrides ");
for (IMethodDetails baseMthDetails : ovrdAttr.getOverrideList()) {
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;
}
// Add a node for an unresolved argtype
private int addNode(Formatter f, ArgType argType) {
return addNode(f, argType, "");
}
private int addNode(Formatter f, ArgType argType, String extra) {
int nodeID;
if (objectToNodeID.containsKey(argType)) {
nodeID = objectToNodeID.get(argType);
} else {
nodeID = nextNodeID;
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;
}
}
@@ -0,0 +1,216 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.util.Collections;
import java.util.Formatter;
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;
import javax.swing.JCheckBox;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.DotGraphUtils;
import jadx.gui.treemodel.JClass;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
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 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) {
super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.method_graph.title"), DotGraphUtils.classFormatName(cls, false)));
this.cls = cls;
}
public JMenuBar addMenuBar() {
JMenuBar menuBar = super.addMenuBar();
// Long names checkbox
JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names"));
showLongNames.setSelected(false);
showLongNames.addItemListener(e -> {
longNames = showLongNames.isSelected();
reload();
});
// Assemble menubar panel
JPanel menuBarPanel = new JPanel();
menuBarPanel.setOpaque(false);
menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
menuBarPanel.add(showLongNames, BorderLayout.PAGE_START);
// Add menubar panel to menuBar
menuBar.add(menuBarPanel);
return menuBar;
}
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);
});
}
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)) {
// graph header
f.format("digraph G {\n");
f.format("%s\n", bgColor);
f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor);
f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor);
nextNodeID = 0;
methodToNodeID = new HashMap<>();
edges = new HashSet<>();
List<MethodNode> methods = classNode.getMethods();
javaMethods = methods.stream().map(method -> method.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 void addCallers(int depth, Formatter f, JavaMethod javaMethod) {
if (depth >= callerDepthLimit) {
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);
}
}
// Add a node representing method to the graph in f. Returns the ID of the new node
private int addNode(Formatter f, JavaMethod method) {
int nodeID;
if (methodToNodeID.containsKey(method)) {
nodeID = methodToNodeID.get(method);
} else {
nodeID = nextNodeID;
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;
}
// Add an edge between sourceID and destID to the graph in f
private void addEdge(Formatter f, int sourceID, int destID) {
Edge edge = new Edge(sourceID, destID);
if (!edges.contains(edge)) {
f.format("Node_%d -> Node_%d\n", sourceID, destID);
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);
}
}
}
@@ -61,8 +61,8 @@ public class CommentDialog extends CommonDialog {
LOG.error("Comment action failed", e);
}
try {
// refresh code
codeArea.refreshClass();
// refresh code in a background thread to avoid blocking the ui
codeArea.backgroundRefreshClass();
} catch (Exception e) {
LOG.error("Failed to reload code", e);
}
@@ -0,0 +1,52 @@
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;
}
}
}
@@ -0,0 +1,417 @@
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;
}
}
@@ -78,6 +78,10 @@ public class TabsController {
blueprint = newBlueprint;
}
setTabHiddenInternal(blueprint, hidden);
if (!blueprint.isCreated()) {
LOG.warn("No content panel for node: {}", node);
closeTabForce(blueprint);
}
return blueprint;
}
@@ -82,10 +82,20 @@ public class JNodeCache {
public void removeWholeClass(JavaClass javaCls) {
remove(javaCls);
javaCls.getMethods().forEach(this::remove);
javaCls.getFields().forEach(this::remove);
javaCls.getInnerClasses().forEach(this::remove);
javaCls.getInlinedClasses().forEach(this::remove);
/*
* These javaCls.get...() calls require the class to be loaded, or will force it to load, generating
* a potentially large decompilation task if needed, before throwing away that work when the class
* is unloaded. To avoid this, which is very slow, we only bother to remove things from the cache if
* the class is already loaded. If it's not then there either isn't going to be anything relevant in
* the node cache or decompilation would regenerate the cache anyway.
*/
// if (true) {
if (!javaCls.loadingWouldRequireDecompilation()) {
javaCls.getMethods().forEach(this::remove);
javaCls.getFields().forEach(this::remove);
javaCls.getInnerClasses().forEach(this::remove);
javaCls.getInlinedClasses().forEach(this::remove);
}
}
public void reset() {
@@ -21,17 +21,24 @@ import jadx.gui.settings.JadxSettings;
public class LafManager {
private static final Logger LOG = LoggerFactory.getLogger(LafManager.class);
public static final String SYSTEM_THEME_NAME = "default";
public static final String INITIAL_THEME_NAME = FlatLightLaf.NAME;
private static final Map<String, String> THEMES_MAP = initThemesMap();
public static void init(JadxSettings settings) {
if (setupLaf(getThemeClass(settings))) {
String preferredThemeClass = getThemeClass(settings);
// reset if settings refers to missing theme
if (preferredThemeClass == null) {
settings.setLafTheme(INITIAL_THEME_NAME);
preferredThemeClass = getThemeClass(settings);
}
if (setupLaf(preferredThemeClass)) {
return;
}
setupLaf(SYSTEM_THEME_NAME);
settings.setLafTheme(SYSTEM_THEME_NAME);
setupLaf(INITIAL_THEME_NAME);
settings.setLafTheme(INITIAL_THEME_NAME);
settings.sync();
}
@@ -48,9 +55,7 @@ public class LafManager {
}
private static boolean setupLaf(String themeClass) {
if (SYSTEM_THEME_NAME.equals(themeClass)) {
return applyLaf(UIManager.getSystemLookAndFeelClassName());
}
if (themeClass != null && !themeClass.isEmpty()) {
return applyLaf(themeClass);
}
@@ -59,7 +64,6 @@ public class LafManager {
private static Map<String, String> initThemesMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put(SYSTEM_THEME_NAME, SYSTEM_THEME_NAME);
// default flatlaf themes
map.put(FlatLightLaf.NAME, FlatLightLaf.class.getName());
@@ -562,4 +562,11 @@ public class UiUtils {
public static boolean nearlyEqual(float a, float b) {
return Math.abs(a - b) < 1E-6f;
}
// Formats a string to be in a .DOT node
public static String toDotNodeName(String fullName) {
String newName = fullName.replace("<", "\\<");
newName = newName.replace(">", "\\>");
return newName;
}
}
@@ -112,6 +112,7 @@ tabs.closeAllRight=Alles rechts schließen
#tabs.closeAllLeft=Close All Left
tabs.code=Code
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=Zurück
nav.forward=Vorwärts
@@ -136,7 +137,7 @@ message.desktop_entry_creation_success=Desktop-Eintrag erfolgreich erstellt!
message.success_title=Erfolg
#message.unable_preview_font=Unable preview font
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB (%.2f GB Höhepunkt)
common_dialog.ok=OK
common_dialog.cancel=Abbrechen
@@ -370,6 +371,20 @@ popup.find_usage=Verwendung suchen
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.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.add_comment=Kommentar
popup.update_comment=Kommentar aktualisieren
popup.search_comment=Kommentar suchen
@@ -522,3 +537,20 @@ action_category.plugin_script=Plugin-Skript
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=Close All Right
tabs.closeAllLeft=Close All Left
tabs.code=Code
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=Back
nav.forward=Forward
@@ -136,7 +137,7 @@ message.desktop_entry_creation_success=Desktop entry created successfully!
message.success_title=Success
message.unable_preview_font=Unable preview font
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak)
common_dialog.ok=OK
common_dialog.cancel=Cancel
@@ -370,6 +371,20 @@ 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.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.add_comment=Comment
popup.update_comment=Update comment
popup.search_comment=Search comments
@@ -522,3 +537,20 @@ hex_viewer.change_encoding=Change Encoding
hex_viewer.goto_address=Go To Address
hex_viewer.enter_address=Enter address range:
hex_viewer.find=Find
graph_viewer.long_names=Show full names
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
graph_viewer.default_title=Graph Viewer
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
@@ -112,6 +112,7 @@ tabs.closeAllRight=Cierra todo a la derecha
#tabs.closeAllLeft=Close All Left
#tabs.code=Code
#tabs.smali=Smali
#tabs.smali_bytecode=Smali+Bytecode
nav.back=Atrás
nav.forward=Adelante
@@ -136,7 +137,7 @@ nav.forward=Adelante
#message.success_title=Success
#message.unable_preview_font=Unable preview font
#heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
#heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak)
#common_dialog.ok=OK
#common_dialog.cancel=Cancel
@@ -370,6 +371,20 @@ 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.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.add_comment=Comment
#popup.update_comment=Update comment
#popup.search_comment=Search comments
@@ -522,3 +537,20 @@ certificate.serialPubKeyY=Y
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=Tutup Semua yang Kanan
#tabs.closeAllLeft=Close All Left
tabs.code=Kode
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=Kembali
nav.forward=Maju
@@ -136,7 +137,7 @@ message.indexingClassesSkipped=<html>JADX kekurangan memori. Oleh karena itu %d
#message.success_title=Success
#message.unable_preview_font=Unable preview font
heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB
heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB (%.2f GB tertinggi)
common_dialog.ok=OK
common_dialog.cancel=Batal
@@ -370,6 +371,20 @@ 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.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.add_comment=Komentar
#popup.update_comment=Update comment
popup.search_comment=Cari komentar
@@ -522,3 +537,20 @@ action_category.plugin_script=Plugin Script
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오
#tabs.closeAllLeft=Close All Left
tabs.code=코드
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=뒤로
nav.forward=앞으로
@@ -136,7 +137,7 @@ message.indexingClassesSkipped=<html>Jadx의 메모리가 부족합니다. 따
#message.success_title=Success
#message.unable_preview_font=Unable preview font
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB (%.2f GB 첨단)
common_dialog.ok=확인
common_dialog.cancel=취소
@@ -370,6 +371,20 @@ popup.find_usage=사용 찾기
popup.go_to_declaration=선언문으로 이동
popup.exclude=제외
popup.exclude_packages=패키지 제외
#popup.convert_number=Add conversion 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.add_comment=주석
#popup.update_comment=Update comment
popup.search_comment=주석 검색
@@ -522,3 +537,20 @@ adb_dialog.starting_debugger=디버거 시작 중 ...
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=Feche tudo à direita
#tabs.closeAllLeft=Close All Left
tabs.code=Código
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=Voltar
nav.forward=Avançar
@@ -136,7 +137,7 @@ message.indexingClassesSkipped=<html>Jadx está rodando com pouca memória. Por
#message.success_title=Success
#message.unable_preview_font=Unable preview font
heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB
heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB (%.2f GB pico)
common_dialog.ok=Ok
common_dialog.cancel=Cancelar
@@ -370,6 +371,20 @@ 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.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.add_comment=Comentar
#popup.update_comment=Update comment
popup.search_comment=Buscar comentários
@@ -522,3 +537,20 @@ adb_dialog.starting_debugger=Iniciando depurador...
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=Закройте все справа
#tabs.closeAllLeft=Close All Left
tabs.code=Код
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=Назад
nav.forward=Вперед
@@ -136,7 +137,7 @@ message.indexingClassesSkipped=<html>JaDX запущен с малым коли
#message.success_title=Success
#message.unable_preview_font=Unable preview font
heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ
heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ (%.2f GB пикпик)
common_dialog.ok=Ok
common_dialog.cancel=Отмена
@@ -370,6 +371,20 @@ popup.find_usage=Найти использования
popup.go_to_declaration=Перейти к объявлению
popup.exclude=Исключить
popup.exclude_packages=Исключить пакеты
#popup.convert_number=Add conversion 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.add_comment=Комментарий
#popup.update_comment=Update comment
popup.search_comment=Поиск комментариев
@@ -522,3 +537,20 @@ action_category.plugin_script=Скрипты и плагины
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -112,6 +112,7 @@ tabs.closeAllRight=关闭右边的所有
tabs.closeAllLeft=关闭左边的所有
tabs.code=代码
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=后退
nav.forward=前进
@@ -136,7 +137,7 @@ message.desktop_entry_creation_success=创建桌面入口创建成功!
message.success_title=成功
message.unable_preview_font=无法预览字体
heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB
heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB (%.2f GB 顶点)
common_dialog.ok=确定
common_dialog.cancel=取消
@@ -370,6 +371,20 @@ popup.find_usage=查找用例
popup.go_to_declaration=跳到声明
popup.exclude=排除此包
popup.exclude_packages=排除包
#popup.convert_number=Add conversion 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.add_comment=添加注释
popup.update_comment=更新注释
popup.search_comment=搜索注释
@@ -522,3 +537,20 @@ hex_viewer.change_encoding=更改编码
hex_viewer.goto_address=跳转到地址
hex_viewer.enter_address=输入地址范围:
hex_viewer.find=查找
graph_viewer.long_names=Show full names
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
graph_viewer.default_title=Graph Viewer
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
@@ -112,6 +112,7 @@ tabs.closeAllRight=關閉右邊的所有
#tabs.closeAllLeft=Close All Left
tabs.code=程式碼
tabs.smali=Smali
tabs.smali_bytecode=Smali+Bytecode
nav.back=返回
nav.forward=向前
@@ -136,7 +137,7 @@ message.desktop_entry_creation_success=成功建立桌面項目!
message.success_title=成功
#message.unable_preview_font=Unable preview font
heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB
heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB (%.2f GB 潼)
common_dialog.ok=Ok
common_dialog.cancel=取消
@@ -370,6 +371,20 @@ popup.find_usage=尋找使用情況
popup.go_to_declaration=前往宣告
popup.exclude=排除
popup.exclude_packages=排除套件
#popup.convert_number=Add conversion 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.add_comment=註解
popup.update_comment=更新註解
popup.search_comment=搜尋註解
@@ -522,3 +537,20 @@ action_category.plugin_script=外掛程式腳本
#hex_viewer.goto_address=Go To Address
#hex_viewer.enter_address=Enter address range:
#hex_viewer.find=Find
#graph_viewer.long_names=Show full names
#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
#graph_viewer.default_title=Graph Viewer
#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
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg
width="16"
height="16"
viewBox="0 0 0.48 0.48"
fill="none"
version="1.1"
id="svg1"
sodipodi:docname="A.svg"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="12.723714"
inkscape:cx="54.897494"
inkscape:cy="25.424967"
inkscape:window-width="2494"
inkscape:window-height="1371"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path2" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path3" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path4" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path5" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path6" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path7" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path8" />
<path
style="fill:#6e6e6e;fill-opacity:0;stroke-width:0.0206363"
d="M 4.33362,21.975593 C 4.1655225,21.947921 3.9365124,21.827275 3.8078433,21.698605 3.6734073,21.564169 3.556131,21.33668 3.5237211,21.147474 c -0.07447,-0.434755 0.1910799,-0.88897 0.6269823,-1.072433 0.1019397,-0.0429 0.1513365,-0.04744 0.6248844,-0.05738 L 5.2897827,20.006879 8.1949128,11.29163 C 10.308432,4.9511757 11.117454,2.5492491 11.16394,2.4768077 11.34743,2.1908626 11.664765,2.0159332 12,2.0159332 c 0.335235,0 0.65257,0.1749294 0.83606,0.4608745 0.04649,0.072441 0.855508,2.474368 2.969027,8.8148223 l 2.90513,8.715249 0.514195,0.01079 c 0.473548,0.0099 0.522945,0.01447 0.624885,0.05738 0.435902,0.183463 0.701452,0.637678 0.626982,1.072433 -0.03241,0.189206 -0.149686,0.416695 -0.284122,0.551131 -0.13928,0.13928 -0.359816,0.249969 -0.557086,0.279607 -0.09575,0.01439 -0.76695,0.01962 -1.981084,0.01544 l -1.836274,-0.0063 -0.122152,-0.04621 c -0.48418,-0.183157 -0.761159,-0.650321 -0.663439,-1.118985 0.06832,-0.327667 0.246952,-0.559912 0.543876,-0.70711 l 0.176535,-0.08752 0.429838,-0.007 c 0.307893,-0.005 0.429839,-0.01374 0.429839,-0.03086 0,-0.01314 -0.373746,-1.145264 -0.830547,-2.515823 L 14.951117,14.981943 H 12 9.0488832 L 8.2183368,17.47387 c -0.4568007,1.370559 -0.8305467,2.50268 -0.8305467,2.515823 0,0.01712 0.1219458,0.02587 0.4298385,0.03086 l 0.4298385,0.007 0.176535,0.08752 c 0.6126051,0.303695 0.7537068,1.070151 0.2870877,1.559446 -0.128667,0.13492 -0.2243985,0.197698 -0.4066338,0.266662 l -0.1221687,0.04623 -1.867584,0.0035 c -1.0365198,0.0019 -1.9180905,-0.0049 -1.9810833,-0.01523 z m 9.94669,-8.990281 c 0,-0.02192 -2.266371,-6.8183203 -2.277336,-6.8292853 -0.0084,-0.00842 -2.2832836,6.7957663 -2.2832836,6.8293563 0,0.0085 1.0261396,0.01548 2.2803096,0.01548 1.25417,0 2.28031,-0.007 2.28031,-0.01555 z"
id="path9" />
<path
style="fill:#6e6e6e;fill-opacity:1;stroke-width:0.000412726"
d="m 0.08669201,0.43932517 c -0.0034,-5.53e-4 -0.0079,-0.003 -0.01051,-0.0055 -0.0027,-0.0027 -0.005,-0.0072 -0.0057,-0.01102 -0.0015,-0.0087 0.0038,-0.01778 0.01254,-0.02145 0.002,-8.58e-4 0.003,-9.49e-4 0.0125,-0.0011 l 0.01028,-2.16e-4 0.0581,-0.174305 c 0.04227,-0.126809 0.05845,-0.174848 0.05938,-0.176296 0.0037,-0.0057 0.01002,-0.0092 0.01672,-0.0092 0.0067,0 0.01305,0.0035 0.01672,0.0092 9.3e-4,0.0014 0.01711,0.04949 0.05938,0.176296 l 0.0581,0.174305 0.01028,2.16e-4 c 0.0095,1.98e-4 0.01046,2.89e-4 0.0125,0.0011 0.0087,0.0037 0.01403,0.01275 0.01254,0.02145 -6.49e-4,0.0038 -0.003,0.0083 -0.0057,0.01102 -0.0028,0.0028 -0.0072,0.005 -0.01114,0.0056 -0.0019,2.88e-4 -0.01534,3.92e-4 -0.03962,3.09e-4 l -0.03673,-1.26e-4 -0.0024,-9.25e-4 c -0.0097,-0.0037 -0.01522,-0.01301 -0.01327,-0.02238 0.0014,-0.0066 0.0049,-0.0112 0.01088,-0.01414 l 0.0035,-0.0018 0.0086,-1.4e-4 c 0.0062,-1e-4 0.0086,-2.75e-4 0.0086,-6.17e-4 0,-2.63e-4 -0.0075,-0.0229 -0.01661,-0.05032 l -0.01661,-0.04984 h -0.059002 -0.059022 l -0.01661,0.04984 c -0.0091,0.02741 -0.01661,0.05005 -0.01661,0.05032 0,3.43e-4 0.0024,5.18e-4 0.0086,6.17e-4 l 0.0086,1.4e-4 0.0035,0.0018 c 0.01225,0.0061 0.01507,0.0214 0.0057,0.03119 -0.0026,0.0027 -0.0045,0.004 -0.0081,0.0053 l -0.0024,9.25e-4 -0.03735,7e-5 c -0.02073,3.8e-5 -0.03836,-9.8e-5 -0.03962,-3.05e-4 z m 0.198934,-0.179805 c 0,-4.39e-4 -0.04533,-0.136367 -0.04555,-0.136586 -1.68e-4,-1.68e-4 -0.04567,0.135915 -0.04567,0.136587 0,1.7e-4 0.02052,3.1e-4 0.04561,3.1e-4 0.02508,0 0.04561,-1.4e-4 0.04561,-3.11e-4 z"
id="path10" />
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

@@ -0,0 +1,212 @@
package jadx.gui.ui.codearea;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ConvertNumberActionTest {
@Test
public void nonNumeric() {
assertThat(ConvertNumberAction.getConversionsFromWord("non-numeric")).isNullOrEmpty();
assertThat(ConvertNumberAction.getConversionsFromWord("0xnon-numeric")).isNullOrEmpty();
assertThat(ConvertNumberAction.getConversionsFromWord("non-numericL")).isNullOrEmpty();
assertThat(ConvertNumberAction.getConversionsFromWord("-non-numeric")).isNullOrEmpty();
assertThat(ConvertNumberAction.getConversionsFromWord("ABCD")).isNullOrEmpty();
}
@Test
public void simpleDecimalToHex() {
List<String> expected = new ArrayList<String>();
expected.add("0x7b");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("123");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@Test
public void negativeDecimalToHex() {
List<String> expected = new ArrayList<String>();
expected.add("0xffffff85");
expected.add("0b11111111111111111111111110000101");
List<String> result = ConvertNumberAction.getConversionsFromWord("-123");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@Test
public void negativeLongDecimalToHex() {
List<String> expected = new ArrayList<String>();
expected.add("0xFFFFFFE8B7891800".toLowerCase());
expected.add("0b1111111111111111111111111110100010110111100010010001100000000000");
List<String> result = ConvertNumberAction.getConversionsFromWord("-100000000000");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@Test
public void simpleHexToDecimal() {
List<String> expected = new ArrayList<String>();
expected.add("123");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("0x7b");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@Test
public void zeroToHex() {
List<String> expected = new ArrayList<String>();
expected.add("0x0");
expected.add("0b00000000");
List<String> result = ConvertNumberAction.getConversionsFromWord(Integer.toString(0));
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@Test
public void minIntToHex() {
List<String> expected = new ArrayList<String>();
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();
}
@Test
public void maxIntToHex() {
List<String> expected = new ArrayList<String>();
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();
}
@Test
public void minLongToHex() {
List<String> expected = new ArrayList<String>();
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();
}
@Test
public void maxLongToHex() {
List<String> expected = new ArrayList<String>();
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();
}
@Test
public void simpleLongSuffix() {
List<String> expected = new ArrayList<String>();
expected.add("0x7b");
expected.add("0b01111011");
expected.add("'{'");
List<String> result = ConvertNumberAction.getConversionsFromWord("123L");
assertThat(result).isNotEmpty();
expected.removeAll(result);
assertThat(expected).isEmpty();
}
@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");
assertThat(ConvertNumberAction.getConversionsFromWord("0x7fff")).containsOnlyOnce("0b0111111111111111");
assertThat(ConvertNumberAction.getConversionsFromWord("0xffff")).containsOnlyOnce("0b1111111111111111");
assertThat(ConvertNumberAction.getConversionsFromWord("0x10000")).containsOnlyOnce("0b000000010000000000000000");
assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffff")).containsOnlyOnce("0b11111111111111111111111111111111");
assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffffffff"))
.containsOnlyOnce("0b111111111111111111111111111111111111111111111111");
assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffff"))
.containsOnlyOnce("0b011111111111111111111111111111111111111111111111");
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);
}
}
}