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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user