From ea6492e5ba3ac50122ee76be4cd3f488ebe9e83a Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Fri, 18 Apr 2025 22:30:00 +0100 Subject: [PATCH] fix(gui): properly handle excluded classes in code search (#2432) --- .../jadx/core/utils/DecompilerScheduler.java | 5 +- .../java/jadx/gui/search/SearchSettings.java | 27 +++- .../search/providers/CodeSearchProvider.java | 37 ++++-- .../providers/CommentSearchProvider.java | 21 ++-- .../providers/MergedSearchProvider.java | 4 + .../java/jadx/gui/ui/dialog/SearchDialog.java | 116 +++++++++--------- .../main/java/jadx/gui/utils/CacheObject.java | 13 -- .../java/jadx/gui/utils/cache/ValueCache.java | 27 ++++ 8 files changed, 146 insertions(+), 104 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/cache/ValueCache.java diff --git a/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java b/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java index 522888ccb..7ee9cfa24 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java +++ b/jadx-core/src/main/java/jadx/core/utils/DecompilerScheduler.java @@ -28,7 +28,8 @@ public class DecompilerScheduler implements IDecompileScheduler { long start = System.currentTimeMillis(); List> 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) { diff --git a/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java b/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java index a5574ec78..6b2179e39 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java index 96e15805d..3cfb9c131 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/CodeSearchProvider.java @@ -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 includedClasses; private @Nullable String code; private int clsNum = 0; private int pos = 0; - public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, List classes) { + public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, + List classes, @Nullable Set 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 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 ""; } } diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java index 363c42ecf..8e766f080 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/CommentSearchProvider.java @@ -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 searchClsSet; private int progress = 0; - public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings) { + public CommentSearchProvider(MainWindow mw, SearchSettings searchSettings, List 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; diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java index 13e020430..9715f9a0a 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/MergedSearchProvider.java @@ -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(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java index b3f04498a..97cefecb3 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/SearchDialog.java @@ -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> includedClsCache = new ValueCache<>(); + private final ValueCache, List>> batchesCache = new ValueCache<>(); + private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set 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 allClasses; + List 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> batches = mainWindow.getCacheObject().getDecompileBatches(); - if (batches == null) { - List topClasses = ListUtils.filter(allClasses, c -> !c.isInner()); - batches = mainWindow.getWrapper().buildDecompileBatches(topClasses); - mainWindow.getCacheObject().setDecompileBatches(batches); - } - for (List 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 topClasses = ListUtils.filter(searchClasses, c -> !c.isInner()); + List> batches = batchesCache.get(topClasses, + clsList -> mainWindow.getWrapper().buildDecompileBatches(clsList)); + Set includedClasses = new HashSet<>(topClasses); + for (List 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; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index adbd154fb..ff3e45daa 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -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> lastSearchOptions; private String lastSearchPackage; - private List> 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> getDecompileBatches() { - return decompileBatches; - } - - public void setDecompileBatches(List> decompileBatches) { - this.decompileBatches = decompileBatches; - } - public PackageHelper getPackageHelper() { return packageHelper; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/cache/ValueCache.java b/jadx-gui/src/main/java/jadx/gui/utils/cache/ValueCache.java new file mode 100644 index 000000000..78a175e34 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/cache/ValueCache.java @@ -0,0 +1,27 @@ +package jadx.gui.utils.cache; + +import java.util.function.Function; + +/** + * Simple store for values depending on 'key' object. + * + * @param key object type + * @param stored object type + */ +public class ValueCache { + 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 loadFunc) { + if (key != null && key.equals(requestKey)) { + return value; + } + V newValue = loadFunc.apply(requestKey); + key = requestKey; + value = newValue; + return newValue; + } +}