Skip to content

Commit

Permalink
fix(gui): search constant fields usage in all classes (#1801)
Browse files Browse the repository at this point in the history
  • Loading branch information
skylot committed Mar 16, 2023
1 parent 78c976a commit 5d6b827
Show file tree
Hide file tree
Showing 16 changed files with 140 additions and 72 deletions.
20 changes: 14 additions & 6 deletions jadx-core/src/main/java/jadx/core/dex/info/ConstStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,22 @@ public void processConstFields(ClassNode cls, List<FieldNode> staticFields) {
return;
}
for (FieldNode f : staticFields) {
AccessInfo accFlags = f.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = f.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null && constVal.getValue() != null) {
addConstField(cls, f, constVal.getValue(), accFlags.isPublic());
}
Object value = getFieldConstValue(f);
if (value != null) {
addConstField(cls, f, value, f.getAccessFlags().isPublic());
}
}
}

public static @Nullable Object getFieldConstValue(FieldNode fld) {
AccessInfo accFlags = fld.getAccessFlags();
if (accFlags.isStatic() && accFlags.isFinal()) {
EncodedValue constVal = fld.get(JadxAttrType.CONSTANT_VALUE);
if (constVal != null) {
return constVal.getValue();
}
}
return null;
}

public void removeForClass(ClassNode cls) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,13 @@ private static List<List<JavaClass>> buildFallback(List<JavaClass> classes) {
}

private void dumpBatchesStats(List<JavaClass> classes, List<List<JavaClass>> result, List<DepInfo> deps) {
int clsInBatches = result.stream().mapToInt(List::size).sum();
double avg = result.stream().mapToInt(List::size).average().orElse(-1);
int maxSingleDeps = classes.stream().mapToInt(JavaClass::getTotalDepsCount).max().orElse(-1);
int maxSubDeps = deps.stream().mapToInt(DepInfo::getDepsCount).max().orElse(-1);
LOG.info("Batches stats:"
+ "\n input classes: " + classes.size()
+ ",\n classes in batches: " + clsInBatches
+ ",\n batches: " + result.size()
+ ",\n average batch size: " + String.format("%.2f", avg)
+ ",\n max single deps count: " + maxSingleDeps
Expand Down
9 changes: 0 additions & 9 deletions jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel;
Expand Down Expand Up @@ -60,14 +59,6 @@ public synchronized Future<TaskStatus> execute(IBackgroundTask task) {
return taskWorker;
}

public TaskStatus executeAndWait(IBackgroundTask task) {
try {
return execute(task).get();
} catch (Exception e) {
throw new JadxRuntimeException("Task execution error", e);
}
}

public synchronized void cancelAll() {
try {
taskRunning.values().forEach(Cancelable::cancel);
Expand Down
49 changes: 46 additions & 3 deletions jadx-gui/src/main/java/jadx/gui/jobs/DecompileTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.JOptionPane;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jadx.api.ICodeCache;
import jadx.api.JavaClass;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;

Expand All @@ -23,14 +26,16 @@ public static int calcDecompileTimeLimit(int classCount) {
return classCount * CLS_LIMIT + 5000;
}

private final MainWindow mainWindow;
private final JadxWrapper wrapper;
private final AtomicInteger complete = new AtomicInteger(0);
private int expectedCompleteCount;

private ProcessResult result;

public DecompileTask(JadxWrapper wrapper) {
this.wrapper = wrapper;
public DecompileTask(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.wrapper = mainWindow.getWrapper();
}

@Override
Expand All @@ -40,6 +45,10 @@ public String getTitle() {

@Override
public List<Runnable> scheduleJobs() {
if (mainWindow.getCacheObject().isFullDecompilationFinished()) {
return Collections.emptyList();
}

List<JavaClass> classes = wrapper.getIncludedClasses();
expectedCompleteCount = classes.size();
complete.set(0);
Expand Down Expand Up @@ -87,7 +96,41 @@ public void onDone(ITaskInfo taskInfo) {
+ ", time limit:{ total: " + timeLimit + "ms, per cls: " + CLS_LIMIT + "ms }"
+ ", status: " + taskInfo.getStatus());
}
this.result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);
result = new ProcessResult(skippedCls, taskInfo.getStatus(), timeLimit);

wrapper.unloadClasses();
processDecompilationResults();
System.gc();

mainWindow.getCacheObject().setFullDecompilationFinished(skippedCls == 0);
}

private void processDecompilationResults() {
int skippedCls = result.getSkipped();
if (skippedCls == 0) {
return;
}
TaskStatus status = result.getStatus();
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", result.getTimeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(mainWindow, message);
break;
}
case CANCEL_BY_MEMORY: {
mainWindow.showHeapUsageBar();
JOptionPane.showMessageDialog(mainWindow, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
}
}

@Override
Expand Down
53 changes: 11 additions & 42 deletions jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.jobs.DecompileTask;
import jadx.gui.jobs.ExportTask;
import jadx.gui.jobs.ProcessResult;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.plugins.mappings.MappingExporter;
import jadx.gui.plugins.quark.QuarkDialog;
Expand Down Expand Up @@ -173,6 +172,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_QUARK = UiUtils.openSvgIcon("ui/quark");
private static final ImageIcon ICON_PREF = UiUtils.openSvgIcon("ui/settings");
private static final ImageIcon ICON_DEOBF = UiUtils.openSvgIcon("ui/helmChartLock");
private static final ImageIcon ICON_DECOMPILE_ALL = UiUtils.openSvgIcon("ui/runAll");
private static final ImageIcon ICON_LOG = UiUtils.openSvgIcon("ui/logVerbose");
private static final ImageIcon ICON_INFO = UiUtils.openSvgIcon("ui/showInfos");
private static final ImageIcon ICON_DEBUGGER = UiUtils.openSvgIcon("ui/startDebugger");
Expand Down Expand Up @@ -608,54 +608,17 @@ synchronized void runInitialBackgroundJobs() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
waitDecompileTask();
requestFullDecompilation();
}
}, 1000);
}
}

private static final Object DECOMPILER_TASK_SYNC = new Object();

public void waitDecompileTask() {
synchronized (DECOMPILER_TASK_SYNC) {
try {
DecompileTask decompileTask = new DecompileTask(wrapper);
backgroundExecutor.executeAndWait(decompileTask);
backgroundExecutor.execute(decompileTask.getTitle(), wrapper::unloadClasses).get();
processDecompilationResults(decompileTask.getResult());
System.gc();
} catch (Exception e) {
LOG.error("Decompile task execution failed", e);
}
}
}

private void processDecompilationResults(ProcessResult decompile) {
int skippedCls = decompile.getSkipped();
if (skippedCls == 0) {
public void requestFullDecompilation() {
if (cacheObject.isFullDecompilationFinished()) {
return;
}
TaskStatus status = decompile.getStatus();
LOG.warn("Decompile and indexing of some classes skipped: {}, status: {}", skippedCls, status);
switch (status) {
case CANCEL_BY_USER: {
String reason = NLS.str("message.userCancelTask");
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_TIMEOUT: {
String reason = NLS.str("message.taskTimeout", decompile.getTimeLimit());
String message = NLS.str("message.indexIncomplete", reason, skippedCls);
JOptionPane.showMessageDialog(this, message);
break;
}
case CANCEL_BY_MEMORY: {
showHeapUsageBar();
JOptionPane.showMessageDialog(this, NLS.str("message.indexingClassesSkipped", skippedCls));
break;
}
}
backgroundExecutor.execute(new DecompileTask(this));
}

public void cancelBackgroundJobs() {
Expand Down Expand Up @@ -1041,6 +1004,10 @@ public void actionPerformed(ActionEvent e) {
commentSearchAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_SEMICOLON,
UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK));

ActionHandler decompileAllAction = new ActionHandler(ev -> requestFullDecompilation());
decompileAllAction.setNameAndDesc(NLS.str("menu.decompile_all"));
decompileAllAction.setIcon(ICON_DECOMPILE_ALL);

Action deobfAction = new AbstractAction(NLS.str("menu.deobfuscation"), ICON_DEOBF) {
@Override
public void actionPerformed(ActionEvent e) {
Expand Down Expand Up @@ -1152,6 +1119,7 @@ public void actionPerformed(ActionEvent e) {

JMenu tools = new JMenu(NLS.str("menu.tools"));
tools.setMnemonic(KeyEvent.VK_T);
tools.add(decompileAllAction);
tools.add(deobfMenuItem);
tools.add(quarkAction);
tools.add(openDeviceAction);
Expand Down Expand Up @@ -1231,6 +1199,7 @@ public void actionPerformed(ActionEvent e) {
exportAction.setEnabled(loaded);
saveProjectAsAction.setEnabled(loaded);
reload.setEnabled(loaded);
decompileAllAction.setEnabled(loaded);
deobfAction.setEnabled(loaded);
quarkAction.setEnabled(loaded);
return false;
Expand Down
53 changes: 41 additions & 12 deletions jadx-gui/src/main/java/jadx/gui/ui/dialog/UsageDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.utils.CodeUtils;
import jadx.core.dex.info.ConstStorage;
import jadx.core.dex.nodes.FieldNode;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JField;
import jadx.gui.treemodel.JMethod;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
Expand Down Expand Up @@ -51,6 +54,7 @@ public UsageDialog(MainWindow mainWindow, JNode node) {
@Override
protected void openInit() {
progressStartCommon();
prepareUsageData();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"),
this::collectUsageData,
(status) -> {
Expand All @@ -63,26 +67,39 @@ protected void openInit() {
});
}

private void prepareUsageData() {
if (mainWindow.getSettings().isReplaceConsts() && node instanceof JField) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// run full decompilation to prepare for full code scan
mainWindow.requestFullDecompilation();
}
}
}

private void collectUsageData() {
usageList = new ArrayList<>();
Map<JavaNode, List<JavaNode>> usageQuery = buildUsageQuery();
usageQuery.forEach((searchNode, useNodes) -> useNodes.stream()
.map(JavaNode::getTopParentClass)
.distinct()
.forEach(u -> processUsage(searchNode, u)));
buildUsageQuery().forEach(
(searchNode, useNodes) -> useNodes.stream()
.map(JavaNode::getTopParentClass)
.distinct()
.forEach(u -> processUsage(searchNode, u)));
}

/**
* Return mapping of 'node to search' to 'use places'
*/
private Map<JavaNode, List<JavaNode>> buildUsageQuery() {
Map<JavaNode, List<JavaNode>> map = new HashMap<>();
private Map<JavaNode, List<? extends JavaNode>> buildUsageQuery() {
Map<JavaNode, List<? extends JavaNode>> map = new HashMap<>();
if (node instanceof JMethod) {
JavaMethod javaMethod = ((JMethod) node).getJavaMethod();
for (JavaMethod mth : getMethodWithOverrides(javaMethod)) {
map.put(mth, mth.getUseIn());
}
} else if (node instanceof JClass) {
return map;
}
if (node instanceof JClass) {
JavaClass javaCls = ((JClass) node).getCls();
map.put(javaCls, javaCls.getUseIn());
// add constructors usage into class usage
Expand All @@ -91,10 +108,19 @@ private Map<JavaNode, List<JavaNode>> buildUsageQuery() {
map.put(javaMth, javaMth.getUseIn());
}
}
} else {
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
return map;
}
if (node instanceof JField && mainWindow.getSettings().isReplaceConsts()) {
FieldNode fld = ((JField) node).getJavaField().getFieldNode();
boolean constField = ConstStorage.getFieldConstValue(fld) != null;
if (constField && !fld.getAccessFlags().isPrivate()) {
// search all classes to collect usage of replaced constants
map.put(fld.getJavaNode(), mainWindow.getWrapper().getIncludedClasses());
return map;
}
}
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
return map;
}

Expand All @@ -108,9 +134,12 @@ private List<JavaMethod> getMethodWithOverrides(JavaMethod javaMethod) {

private void processUsage(JavaNode searchNode, JavaClass topUseClass) {
ICodeInfo codeInfo = topUseClass.getCodeInfo();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
if (usePositions.isEmpty()) {
return;
}
String code = codeInfo.getCodeStr();
JadxWrapper wrapper = mainWindow.getWrapper();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
for (int pos : usePositions) {
String line = CodeUtils.getLineForPos(code, pos);
if (line.startsWith("import ")) {
Expand Down
11 changes: 11 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class CacheObject {

private List<List<JavaClass>> decompileBatches;

private volatile boolean fullDecompilationFinished;

public CacheObject() {
reset();
}
Expand All @@ -27,6 +29,7 @@ public void reset() {
jNodeCache = new JNodeCache();
lastSearchOptions = new HashMap<>();
decompileBatches = null;
fullDecompilationFinished = false;
}

@Nullable
Expand All @@ -53,4 +56,12 @@ public Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> getLastSe
public void setDecompileBatches(List<List<JavaClass>> decompileBatches) {
this.decompileBatches = decompileBatches;
}

public boolean isFullDecompilationFinished() {
return fullDecompilationFinished;
}

public void setFullDecompilationFinished(boolean fullDecompilationFinished) {
this.fullDecompilationFinished = fullDecompilationFinished;
}
}
Loading

0 comments on commit 5d6b827

Please sign in to comment.