fix(gui): properly handle excluded classes in code search (#2432)

This commit is contained in:
Skylot
2025-04-18 22:30:00 +01:00
parent 03d4cb134f
commit ea6492e5ba
8 changed files with 146 additions and 104 deletions
@@ -28,7 +28,8 @@ public class DecompilerScheduler implements IDecompileScheduler {
long start = System.currentTimeMillis();
List<List<JavaClass>> result = internalBatches(classes);
if (LOG.isDebugEnabled()) {
LOG.debug("Build decompilation batches in {}ms", System.currentTimeMillis() - start);
LOG.debug("Build decompilation batches in {}ms for {} classes",
System.currentTimeMillis() - start, classes.size());
}
if (DEBUG_BATCHES) {
check(result, classes);
@@ -77,7 +78,7 @@ public class DecompilerScheduler implements IDecompileScheduler {
result.add(batch);
}
}
if (mergedBatch.size() > 0) {
if (!mergedBatch.isEmpty()) {
result.add(mergedBatch);
}
if (DEBUG_BATCHES) {
@@ -4,31 +4,36 @@ import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaPackage;
import jadx.core.dex.nodes.PackageNode;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
public class SearchSettings {
private final String searchString;
private final boolean useRegex;
private final boolean ignoreCase;
private final JavaPackage searchPackage;
private final String searchPkgStr;
private JClass activeCls;
private JResource activeResource;
private Pattern regexPattern;
private ISearchMethod searchMethod;
private JavaPackage searchPackage;
public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex, JavaPackage searchPackage) {
public SearchSettings(String searchString, boolean ignoreCase, boolean useRegex, String searchPkgStr) {
this.searchString = searchString;
this.useRegex = useRegex;
this.ignoreCase = ignoreCase;
this.searchPackage = searchPackage;
this.searchPkgStr = searchPkgStr;
}
@Nullable
public String prepare() {
public String prepare(MainWindow mainWindow) {
if (useRegex) {
try {
int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
@@ -37,6 +42,14 @@ public class SearchSettings {
return "Invalid Regex: " + e.getMessage();
}
}
if (!searchPkgStr.isBlank()) {
JadxDecompiler decompiler = mainWindow.getWrapper().getDecompiler();
PackageNode pkg = decompiler.getRoot().resolvePackage(searchPkgStr);
if (pkg == null) {
return NLS.str("search_dialog.package_not_found");
}
searchPackage = pkg.getJavaNode();
}
searchMethod = ISearchMethod.build(this);
return null;
}
@@ -57,6 +70,10 @@ public class SearchSettings {
return this.searchPackage;
}
public boolean isInSearchPkg(JavaClass cls) {
return cls.getJavaPackage().isDescendantOf(searchPackage);
}
public String getSearchString() {
return this.searchString;
}
@@ -1,6 +1,7 @@
package jadx.gui.search.providers;
import java.util.List;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@@ -27,34 +28,44 @@ public final class CodeSearchProvider extends BaseSearchProvider {
private final ICodeCache codeCache;
private final JadxWrapper wrapper;
private final @Nullable Set<JavaClass> includedClasses;
private @Nullable String code;
private int clsNum = 0;
private int pos = 0;
public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, List<JavaClass> classes) {
public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings,
List<JavaClass> classes, @Nullable Set<JavaClass> includedClasses) {
super(mw, searchSettings, classes);
this.codeCache = mw.getWrapper().getArgs().getCodeCache();
this.wrapper = mw.getWrapper();
this.includedClasses = includedClasses;
}
@Override
public @Nullable JNode next(Cancelable cancelable) {
Set<JavaClass> inclCls = includedClasses;
while (true) {
if (cancelable.isCanceled() || clsNum >= classes.size()) {
return null;
}
JavaClass cls = classes.get(clsNum);
String clsCode = code;
if (clsCode == null && !cls.isInner() && !cls.isNoCode()) {
clsCode = getClassCode(cls, codeCache);
}
if (clsCode != null) {
JNode newResult = searchNext(cls, clsCode);
if (newResult != null) {
code = clsCode;
return newResult;
if (inclCls == null || inclCls.contains(cls)) {
String clsCode = code;
if (clsCode == null && !cls.isInner() && !cls.isNoCode()) {
clsCode = getClassCode(cls, codeCache);
}
if (clsCode != null) {
JNode newResult = searchNext(cls, clsCode);
if (newResult != null) {
code = clsCode;
return newResult;
}
}
} else {
// force decompilation for not included classes
cls.decompile();
}
clsNum++;
pos = 0;
@@ -62,8 +73,7 @@ public final class CodeSearchProvider extends BaseSearchProvider {
}
}
@Nullable
private JNode searchNext(JavaClass javaClass, String clsCode) {
private @Nullable JNode searchNext(JavaClass javaClass, String clsCode) {
int newPos = searchMth.find(clsCode, searchStr, pos);
if (newPos == -1) {
return null;
@@ -99,9 +109,10 @@ public final class CodeSearchProvider extends BaseSearchProvider {
if (code != null) {
return code;
}
// start decompilation
return javaClass.getCode();
} catch (Exception e) {
LOG.warn("Failed to get class code: " + javaClass, e);
LOG.warn("Failed to get class code: {}", javaClass, e);
return "";
}
}
@@ -1,7 +1,9 @@
package jadx.gui.search.providers;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.swing.Icon;
@@ -39,14 +41,16 @@ public class CommentSearchProvider implements ISearchProvider {
private final CacheObject cacheObject;
private final JadxProject project;
private final SearchSettings searchSettings;
private final Set<JavaClass> searchClsSet;
private int progress = 0;
public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings) {
public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings, List<JavaClass> searchClasses) {
this.wrapper = mw.getWrapper();
this.cacheObject = mw.getCacheObject();
this.project = mw.getProject();
this.searchSettings = searchSettings;
this.searchClsSet = new HashSet<>(searchClasses);
}
@Override
@@ -70,17 +74,12 @@ public class CommentSearchProvider implements ISearchProvider {
boolean all = searchSettings.getSearchString().isEmpty();
if (all || searchSettings.isMatch(comment.getComment())) {
JNode refNode = getRefNode(comment);
if (refNode != null) {
if (searchSettings.getSearchPackage() != null
&& !refNode.getRootClass().getCls().getJavaPackage().isDescendantOf(searchSettings.getSearchPackage())) {
return null;
}
JClass activeCls = searchSettings.getActiveCls();
if (activeCls == null || Objects.equals(activeCls, refNode.getRootClass())) {
return getCommentNode(comment, refNode);
}
} else {
if (refNode == null) {
LOG.warn("Failed to get ref node for comment: {}", comment);
return null;
}
if (searchClsSet.contains(refNode.getRootClass().getCls())) {
return getCommentNode(comment, refNode);
}
}
return null;
@@ -22,6 +22,10 @@ public class MergedSearchProvider implements ISearchProvider {
list.add(provider);
}
public boolean isEmpty() {
return list.isEmpty();
}
public void prepare() {
current = list.isEmpty() ? -1 : 0;
total = list.stream().mapToInt(ISearchProvider::total).sum();
@@ -7,12 +7,14 @@ import java.awt.FlowLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
import javax.swing.Box;
@@ -62,6 +64,7 @@ import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.cache.ValueCache;
import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.rx.RxUtils;
@@ -160,6 +163,10 @@ public class SearchDialog extends CommonSearchDialog {
*/
private final Executor searchBackgroundExecutor = Executors.newSingleThreadExecutor();
// save values between searches
private final ValueCache<String, List<JavaClass>> includedClsCache = new ValueCache<>();
private final ValueCache<List<JavaClass>, List<List<JavaClass>>> batchesCache = new ValueCache<>();
private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set<SearchOptions> additionalOptions) {
super(mainWindow, NLS.str("menu.text_search"));
this.searchPreset = preset;
@@ -452,37 +459,16 @@ public class SearchDialog extends CommonSearchDialog {
if (text == null || options.isEmpty()) {
return null;
}
// allow empty text for comments search
// allow empty text for search in comments
if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) {
return null;
}
LOG.debug("Building search for '{}', options: {}", text, options);
boolean ignoreCase = options.contains(IGNORE_CASE);
boolean useRegex = options.contains(USE_REGEX);
// Find the JavaPackage for the searched package string
String packageText = packageField.getText();
JavaPackage searchPackage = null;
if (!packageText.isBlank()) {
searchPackage = mainWindow
.getWrapper()
.getPackages()
.stream()
.filter(p -> p.getFullName().equals(packageText))
.findFirst()
.orElse(null);
if (searchPackage == null) {
resultsInfoLabel.setText(NLS.str("search_dialog.package_not_found"));
packageField.setBackground(SEARCH_FIELD_ERROR_COLOR);
return null;
}
}
if (Objects.equals(packageField.getBackground(), SEARCH_FIELD_ERROR_COLOR)) {
packageField.setBackground(searchFieldDefaultBgColor);
}
SearchSettings searchSettings = new SearchSettings(text, ignoreCase, useRegex, searchPackage);
String error = searchSettings.prepare();
String searchPackageText = packageField.getText();
SearchSettings searchSettings = new SearchSettings(text, ignoreCase, useRegex, searchPackageText);
String error = searchSettings.prepare(mainWindow);
if (error == null) {
if (Objects.equals(searchField.getBackground(), SEARCH_FIELD_ERROR_COLOR)) {
searchField.setBackground(searchFieldDefaultBgColor);
@@ -500,7 +486,7 @@ public class SearchDialog extends CommonSearchDialog {
}
private boolean buildSearch(SearchTask newSearchTask, String text, SearchSettings searchSettings) {
List<JavaClass> allClasses;
List<JavaClass> searchClasses;
if (options.contains(ACTIVE_TAB)) {
JumpPosition currentPos = mainWindow.getTabbedPane().getCurrentPosition();
if (currentPos == null) {
@@ -511,57 +497,67 @@ public class SearchDialog extends CommonSearchDialog {
if (currentNode instanceof JClass) {
JClass activeCls = currentNode.getRootClass();
searchSettings.setActiveCls(activeCls);
allClasses = Collections.singletonList(activeCls.getCls());
searchClasses = Collections.singletonList(activeCls.getCls());
} else if (currentNode instanceof JResource) {
searchSettings.setActiveResource((JResource) currentNode);
allClasses = Collections.emptyList();
searchClasses = Collections.emptyList();
} else {
resultsInfoLabel.setText("Can't search in current tab");
return false;
}
} else {
allClasses = mainWindow.getWrapper().getIncludedClassesWithInners();
searchClasses = includedClsCache.get(mainWindow.getSettings().getExcludedPackages(),
exc -> mainWindow.getWrapper().getIncludedClassesWithInners());
}
JavaPackage searchPkg = searchSettings.getSearchPackage();
if (searchPkg != null) {
searchClasses = searchClasses.stream()
.filter(searchSettings::isInSearchPkg)
.collect(Collectors.toList());
}
// allow empty text for comments search
if (text.isEmpty() && options.contains(SearchOptions.COMMENT)) {
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
// allow empty text for comment search
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings, searchClasses));
return true;
}
// using ordered execution for fast tasks
MergedSearchProvider merged = new MergedSearchProvider();
if (options.contains(CLASS)) {
merged.add(new ClassSearchProvider(mainWindow, searchSettings, allClasses));
}
if (options.contains(METHOD)) {
merged.add(new MethodSearchProvider(mainWindow, searchSettings, allClasses));
}
if (options.contains(FIELD)) {
merged.add(new FieldSearchProvider(mainWindow, searchSettings, allClasses));
}
if (options.contains(CODE)) {
int clsCount = allClasses.size();
if (clsCount == 1) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses));
} else if (clsCount > 1) {
List<List<JavaClass>> batches = mainWindow.getCacheObject().getDecompileBatches();
if (batches == null) {
List<JavaClass> topClasses = ListUtils.filter(allClasses, c -> !c.isInner());
batches = mainWindow.getWrapper().buildDecompileBatches(topClasses);
mainWindow.getCacheObject().setDecompileBatches(batches);
}
for (List<JavaClass> batch : batches) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch));
if (!searchClasses.isEmpty()) {
// using ordered execution for fast tasks
MergedSearchProvider merged = new MergedSearchProvider();
if (options.contains(CLASS)) {
merged.add(new ClassSearchProvider(mainWindow, searchSettings, searchClasses));
}
if (options.contains(METHOD)) {
merged.add(new MethodSearchProvider(mainWindow, searchSettings, searchClasses));
}
if (options.contains(FIELD)) {
merged.add(new FieldSearchProvider(mainWindow, searchSettings, searchClasses));
}
if (!merged.isEmpty()) {
merged.prepare();
newSearchTask.addProviderJob(merged);
}
if (options.contains(CODE)) {
int clsCount = searchClasses.size();
if (clsCount == 1) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, searchClasses, null));
} else if (clsCount > 1) {
List<JavaClass> topClasses = ListUtils.filter(searchClasses, c -> !c.isInner());
List<List<JavaClass>> batches = batchesCache.get(topClasses,
clsList -> mainWindow.getWrapper().buildDecompileBatches(clsList));
Set<JavaClass> includedClasses = new HashSet<>(topClasses);
for (List<JavaClass> batch : batches) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch, includedClasses));
}
}
}
if (options.contains(COMMENT)) {
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings, searchClasses));
}
}
if (options.contains(RESOURCE)) {
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings, this));
}
if (options.contains(COMMENT)) {
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
}
merged.prepare();
newSearchTask.addProviderJob(merged);
return true;
}
@@ -1,13 +1,11 @@
package jadx.gui.utils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.api.JavaClass;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.utils.pkgs.PackageHelper;
@@ -21,8 +19,6 @@ public class CacheObject {
private Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> lastSearchOptions;
private String lastSearchPackage;
private List<List<JavaClass>> decompileBatches;
private volatile boolean fullDecompilationFinished;
public CacheObject(JadxWrapper wrapper) {
@@ -37,7 +33,6 @@ public class CacheObject {
jNodeCache.reset();
lastSearchOptions = new HashMap<>();
lastSearchPackage = null;
decompileBatches = null;
fullDecompilationFinished = false;
}
@@ -67,14 +62,6 @@ public class CacheObject {
return lastSearchOptions;
}
public @Nullable List<List<JavaClass>> getDecompileBatches() {
return decompileBatches;
}
public void setDecompileBatches(List<List<JavaClass>> decompileBatches) {
this.decompileBatches = decompileBatches;
}
public PackageHelper getPackageHelper() {
return packageHelper;
}
@@ -0,0 +1,27 @@
package jadx.gui.utils.cache;
import java.util.function.Function;
/**
* Simple store for values depending on 'key' object.
*
* @param <K> key object type
* @param <V> stored object type
*/
public class ValueCache<K, V> {
private K key;
private V value;
/**
* Return a stored object if key not changed, load a new object overwise.
*/
public synchronized V get(K requestKey, Function<K, V> loadFunc) {
if (key != null && key.equals(requestKey)) {
return value;
}
V newValue = loadFunc.apply(requestKey);
key = requestKey;
value = newValue;
return newValue;
}
}