feat(gui): new search options to search in text or binary resources (PR #2526)

* search in text or binary resources

* load matching tab for scroll to pos in binary panel, treat unknown files as binary in search
This commit is contained in:
skylot
2025-06-07 19:23:08 +01:00
committed by GitHub
parent 47224dc599
commit 8030c2f84e
26 changed files with 401 additions and 138 deletions
@@ -4,31 +4,44 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.exceptions.JadxRuntimeException;
public enum ResourceType {
CODE(".dex", ".jar", ".class"),
XML(".xml"),
ARSC(".arsc"),
APK(".apk", ".apkm", ".apks"),
FONT(".ttf", ".ttc", ".otf"),
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
JSON(".json"),
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
HTML(".html"),
LIB(".so"),
MANIFEST,
UNKNOWN;
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN;
public enum ResourceType {
CODE(CONTENT_BINARY, ".dex", ".jar", ".class"),
XML(CONTENT_TEXT, ".xml"),
ARSC(CONTENT_TEXT, ".arsc"),
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz",
".tgz", ".bz2"),
VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
JSON(CONTENT_TEXT, ".json"),
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
HTML(CONTENT_TEXT, ".html"),
LIB(CONTENT_BINARY, ".so"),
MANIFEST(CONTENT_TEXT),
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
UNKNOWN(CONTENT_UNKNOWN);
private final ResourceContentType contentType;
private final String[] exts;
ResourceType(String... exts) {
ResourceType(ResourceContentType contentType, String... exts) {
this.contentType = contentType;
this.exts = exts;
}
public ResourceContentType getContentType() {
return contentType;
}
public String[] getExts() {
return exts;
}
@@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@@ -229,9 +230,13 @@ public final class ResourcesLoader implements IResourcesLoader {
}
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
return loadToCodeWriter(is, StandardCharsets.UTF_8);
}
public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
copyStream(is, baos);
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
return new SimpleCodeInfo(baos.toString(charset));
}
private synchronized BinaryXMLParser loadBinaryXmlParser() {
@@ -0,0 +1,8 @@
package jadx.api.resources;
public enum ResourceContentType {
CONTENT_TEXT,
CONTENT_BINARY,
CONTENT_NONE,
CONTENT_UNKNOWN,
}
@@ -104,6 +104,14 @@ public class BackgroundExecutor {
return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable)));
}
public void startLoading(Runnable backgroundRunnable, Runnable onFinishUiRunnable) {
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable, onFinishUiRunnable));
}
public void startLoading(Runnable backgroundRunnable) {
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable));
}
private synchronized void reset() {
taskQueueExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, Utils.simpleThreadFactory("bg"));
taskRunning.clear();
@@ -8,6 +8,8 @@ import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaPackage;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.utils.exceptions.InvalidDataException;
import jadx.gui.search.providers.ResourceFilter;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.MainWindow;
@@ -26,6 +28,7 @@ public class SearchSettings {
private Pattern regexPattern;
private ISearchMethod searchMethod;
private JavaPackage searchPackage;
private ResourceFilter resourceFilter;
public SearchSettings(String searchString) {
this.searchString = searchString;
@@ -49,6 +52,11 @@ public class SearchSettings {
searchPackage = pkg.getJavaNode();
}
searchMethod = ISearchMethod.build(this);
try {
resourceFilter = ResourceFilter.parse(resFilterStr);
} catch (InvalidDataException e) {
return "Invalid resource file filter: " + e.getMessage();
}
return null;
}
@@ -112,14 +120,14 @@ public class SearchSettings {
return searchMethod;
}
public String getResFilterStr() {
return resFilterStr;
}
public void setResFilterStr(String resFilterStr) {
this.resFilterStr = resFilterStr;
}
public ResourceFilter getResourceFilter() {
return resourceFilter;
}
public int getResSizeLimit() {
return resSizeLimit;
}
@@ -0,0 +1,102 @@
package jadx.gui.search.providers;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.InvalidDataException;
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
public class ResourceFilter {
private static final ResourceFilter ANY = new ResourceFilter(Set.of(), Set.of());
private static final String VAR_TEXT = "$TEXT";
private static final String VAR_BIN = "$BIN";
public static final String DEFAULT_STR = VAR_TEXT;
public static ResourceFilter parse(String filterStr) {
String str = filterStr.trim();
if (str.isEmpty() || str.equals("*")) {
return ANY;
}
Set<ResourceContentType> contentTypes = EnumSet.noneOf(ResourceContentType.class);
Set<String> extSet = new LinkedHashSet<>();
String[] parts = filterStr.split("[|, ]");
for (String part : parts) {
if (part.isEmpty()) {
continue;
}
if (part.startsWith("$")) {
switch (part) {
case VAR_TEXT:
contentTypes.add(CONTENT_TEXT);
break;
case VAR_BIN:
contentTypes.add(CONTENT_BINARY);
break;
default:
throw new InvalidDataException("Unknown var name: " + part);
}
} else {
extSet.add(part);
}
}
return new ResourceFilter(contentTypes, extSet);
}
public static String format(ResourceFilter filter) {
if (filter.isAnyFile()) {
return "*";
}
List<String> list = new ArrayList<>();
Set<ResourceContentType> types = filter.getContentTypes();
if (types.contains(CONTENT_TEXT)) {
list.add(VAR_TEXT);
}
if (types.contains(CONTENT_BINARY)) {
list.add(VAR_BIN);
}
list.addAll(filter.getExtSet());
return Utils.listToString(list, "|");
}
public static String withContentType(String filterStr, Set<ResourceContentType> contentTypes) {
ResourceFilter filter = parse(filterStr);
return format(new ResourceFilter(contentTypes, filter.getExtSet()));
}
private final boolean anyFile;
private final Set<ResourceContentType> contentTypes;
private final Set<String> extSet;
private ResourceFilter(Set<ResourceContentType> contentTypes, Set<String> extSet) {
this.anyFile = contentTypes.isEmpty() && extSet.isEmpty();
this.contentTypes = contentTypes.isEmpty() ? Set.of() : contentTypes;
this.extSet = extSet.isEmpty() ? Set.of() : extSet;
}
public boolean isAnyFile() {
return anyFile;
}
public Set<ResourceContentType> getContentTypes() {
return contentTypes;
}
public Set<String> getExtSet() {
return extSet;
}
@Override
public String toString() {
return format(this);
}
}
@@ -4,8 +4,6 @@ import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import javax.swing.tree.TreeNode;
@@ -16,6 +14,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.api.resources.ResourceContentType;
import jadx.api.utils.CodeUtils;
import jadx.gui.jobs.Cancelable;
import jadx.gui.search.ISearchProvider;
@@ -32,10 +31,9 @@ public class ResourceSearchProvider implements ISearchProvider {
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
private final SearchSettings searchSettings;
private final Set<String> extSet;
private final SearchDialog searchDialog;
private final ResourceFilter resourceFilter;
private final int sizeLimit;
private boolean anyExt;
/**
* Resources queue for process. Using UI nodes to reuse loading cache
@@ -48,7 +46,7 @@ public class ResourceSearchProvider implements ISearchProvider {
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
this.searchSettings = searchSettings;
this.extSet = buildAllowedFilesExtensions(searchSettings.getResFilterStr());
this.resourceFilter = searchSettings.getResourceFilter();
this.sizeLimit = searchSettings.getResSizeLimit() * 1024 * 1024;
this.searchDialog = searchDialog;
JResource activeResource = searchSettings.getActiveResource();
@@ -95,12 +93,20 @@ public class ResourceSearchProvider implements ISearchProvider {
if (newPos == -1) {
return null;
}
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
int end = lineEnd == -1 ? content.length() : lineEnd;
String line = content.substring(lineStart, end);
this.pos = end;
return new JResSearchNode(resNode, line.trim(), newPos);
if (resNode.getContentType() == ResourceContentType.CONTENT_TEXT) {
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
int end = lineEnd == -1 ? content.length() : lineEnd;
String line = content.substring(lineStart, end);
this.pos = end;
return new JResSearchNode(resNode, line.trim(), newPos);
} else {
int start = Math.max(0, newPos - 30);
int end = Math.min(newPos + 50, content.length());
String line = content.substring(start, end);
this.pos = newPos + searchString.length() + 1;
return new JResSearchNode(resNode, line, newPos);
}
}
private @Nullable JResource getNextResFile(Cancelable cancelable) {
@@ -167,41 +173,41 @@ public class ResourceSearchProvider implements ISearchProvider {
return deque;
}
private Set<String> buildAllowedFilesExtensions(String srhResourceFileExt) {
String str = srhResourceFileExt.trim();
if (str.isEmpty() || str.equals("*")) {
anyExt = true;
return Collections.emptySet();
private boolean shouldProcess(JResource resNode) {
if (resNode.getResFile().getType() == ResourceType.ARSC) {
// don't check the size of generated resource table, it will also skip all subfiles
return resourceFilter.isAnyFile()
|| resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT)
|| resourceFilter.getExtSet().contains("xml");
}
Set<String> set = new HashSet<>();
for (String extStr : str.split("[|.]")) {
String ext = extStr.trim();
if (!ext.isEmpty()) {
anyExt = ext.equals("*");
if (anyExt) {
break;
}
set.add(ext);
}
if (!isAllowedFileType(resNode)) {
return false;
}
return set;
return isAllowedFileSize(resNode);
}
private boolean shouldProcess(JResource resNode) {
private boolean isAllowedFileType(JResource resNode) {
ResourceFile resFile = resNode.getResFile();
if (resFile.getType() == ResourceType.ARSC) {
// don't check size of generated resource table, it will also skip all sub files
return anyExt || extSet.contains("xml");
if (resourceFilter.isAnyFile()) {
return true;
}
if (!anyExt) {
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
if (fileExt == null) {
return false;
}
if (!extSet.contains(fileExt)) {
return false;
}
ResourceContentType resContentType = resNode.getContentType();
if (resourceFilter.getContentTypes().contains(resContentType)) {
return true;
}
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
if (fileExt != null && resourceFilter.getExtSet().contains(fileExt)) {
return true;
}
if (resContentType == ResourceContentType.CONTENT_UNKNOWN
&& resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)) {
// treat unknown file type as binary
return true;
}
return false;
}
private boolean isAllowedFileSize(JResource resNode) {
if (sizeLimit <= 0) {
return true;
}
@@ -11,6 +11,7 @@ import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import jadx.api.data.impl.JadxCodeData;
import jadx.gui.search.providers.ResourceFilter;
public class ProjectData {
private int projectVersion = 2;
@@ -22,7 +23,7 @@ public class ProjectData {
private @Nullable String cacheDir; // don't use relative path adapter
private boolean enableLiveReload = false;
private List<String> searchHistory = new ArrayList<>();
private String searchResourcesFilter = "*";
private String searchResourcesFilter = ResourceFilter.DEFAULT_STR;
private int searchResourcesSizeLimit = 0; // in MB
protected Map<String, String> pluginOptions = new HashMap<>();
@@ -17,6 +17,7 @@ import jadx.api.ICodeInfo;
import jadx.api.JavaNode;
import jadx.api.gui.tree.ITreeNode;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.ListUtils;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ContentPanel;
@@ -56,6 +57,10 @@ public abstract class JNode extends DefaultMutableTreeNode implements ITreeNode,
return ICodeInfo.EMPTY;
}
public ResourceContentType getContentType() {
return ResourceContentType.CONTENT_TEXT;
}
public boolean isEditable() {
return false;
}
@@ -1,5 +1,7 @@
package jadx.gui.treemodel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -17,6 +19,7 @@ import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.ResourcesLoader;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.resources.ResourceContentType;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
@@ -157,16 +160,17 @@ public class JResource extends JLoadableNode {
switch (resFile.getType()) {
case IMG:
return new ImagePanel(tabbedPane, this);
case LIB:
case CODE:
return new BinaryContentPanel(tabbedPane, this, false);
case FONT:
return new FontPanel(tabbedPane, this);
}
if (getSyntaxByExtension(resFile.getDeobfName()) == null) {
return new BinaryContentPanel(tabbedPane, this);
if (getContentType() == ResourceContentType.CONTENT_BINARY) {
return new BinaryContentPanel(tabbedPane, this, false);
}
return new CodeContentPanel(tabbedPane, this);
if (getSyntaxByExtension(resFile.getDeobfName()) != null) {
return new CodeContentPanel(tabbedPane, this);
}
// unknown file type, show both text and binary
return new BinaryContentPanel(tabbedPane, this, true);
}
@Override
@@ -185,13 +189,18 @@ public class JResource extends JLoadableNode {
return codeInfo;
}
@Override
public ResourceContentType getContentType() {
if (type == JResType.FILE) {
return resFile.getType().getContentType();
}
return ResourceContentType.CONTENT_NONE;
}
private ICodeInfo loadContent() {
if (resFile == null || type != JResType.FILE) {
return ICodeInfo.EMPTY;
}
if (!isSupportedForView(resFile.getType())) {
return ICodeInfo.EMPTY;
}
ResContainer rc = resFile.loadContent();
if (rc == null) {
return ICodeInfo.EMPTY;
@@ -216,11 +225,20 @@ public class JResource extends JLoadableNode {
case RES_LINK:
try {
return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> {
ResourceFile resourceFile = rc.getResLink();
return ResourcesLoader.decodeStream(resourceFile, (size, is) -> {
// TODO: check size before loading
if (size > 10 * 1024 * 1024L) {
return new SimpleCodeInfo("File too large for view");
}
return ResourcesLoader.loadToCodeWriter(is);
Charset charset;
if (resourceFile.getType().getContentType() == ResourceContentType.CONTENT_TEXT) {
charset = StandardCharsets.UTF_8;
} else {
// force one byte charset for binary data to have the same offsets as in a byte array
charset = StandardCharsets.US_ASCII;
}
return ResourcesLoader.loadToCodeWriter(is, charset);
});
} catch (Exception e) {
return new SimpleCodeInfo("Failed to load resource file:\n" + Utils.getStackTrace(e));
@@ -990,13 +990,15 @@ public class MainWindow extends JFrame {
ContentPanel panel = tabbedPane.getSelectedContentPanel();
if (panel instanceof AbstractCodeContentPanel) {
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) panel).getCodeArea();
String preferText = codeArea.getSelectedText();
if (StringUtils.isEmpty(preferText)) {
preferText = codeArea.getWordUnderCaret();
}
if (!StringUtils.isEmpty(preferText)) {
SearchDialog.searchText(MainWindow.this, preferText);
return;
if (codeArea != null) {
String preferText = codeArea.getSelectedText();
if (StringUtils.isEmpty(preferText)) {
preferText = codeArea.getWordUnderCaret();
}
if (!StringUtils.isEmpty(preferText)) {
SearchDialog.searchText(MainWindow.this, preferText);
return;
}
}
}
SearchDialog.search(MainWindow.this, SearchDialog.SearchPreset.TEXT);
@@ -2,6 +2,8 @@ package jadx.gui.ui.codearea;
import java.awt.Component;
import org.jetbrains.annotations.Nullable;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.tab.TabbedPane;
@@ -16,7 +18,15 @@ public abstract class AbstractCodeContentPanel extends ContentPanel {
super(panel, jnode);
}
public abstract AbstractCodeArea getCodeArea();
public abstract @Nullable AbstractCodeArea getCodeArea();
public abstract Component getChildrenComponent();
public void scrollToPos(int pos) {
AbstractCodeArea codeArea = getCodeArea();
if (codeArea != null) {
codeArea.requestFocus();
codeArea.scrollToPos(pos);
}
}
}
@@ -3,7 +3,6 @@ package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Component;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
@@ -14,14 +13,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ResourcesLoader;
import jadx.core.utils.exceptions.JadxException;
import jadx.api.resources.ResourceContentType;
import jadx.gui.jobs.BackgroundExecutor;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.ui.hexviewer.HexPreviewPanel;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class BinaryContentPanel extends AbstractCodeContentPanel {
private static final Logger LOG = LoggerFactory.getLogger(BinaryContentPanel.class);
@@ -29,10 +29,6 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
private final transient HexPreviewPanel hexPreviewPanel;
private final transient JTabbedPane areaTabbedPane;
public BinaryContentPanel(TabbedPane panel, JNode jnode) {
this(panel, jnode, true);
}
public BinaryContentPanel(TabbedPane panel, JNode jnode, boolean supportsText) {
super(panel, jnode);
setLayout(new BorderLayout());
@@ -51,33 +47,26 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
SwingUtilities.invokeLater(this::loadSelectedPanel);
}
private void loadToHexView(JNode binaryNode) {
private void loadHexView() {
if (hexPreviewPanel.isDataLoaded()) {
return;
}
AtomicReference<byte[]> bytesRef = new AtomicReference<>();
getMainWindow().getBackgroundExecutor().execute(NLS.str("progress.load"),
() -> {
byte[] bytes = null;
if (binaryNode instanceof JResource) {
JResource jResource = (JResource) binaryNode;
try {
bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
} catch (JadxException e) {
LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage());
}
}
if (bytes == null) {
bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8);
}
bytesRef.set(bytes);
},
taskStatus -> {
byte[] bytes = bytesRef.get();
if (bytes != null) {
hexPreviewPanel.setData(bytes);
}
});
UiUtils.notUiThreadGuard();
byte[] bytes = getNodeBytes();
UiUtils.uiRunAndWait(() -> hexPreviewPanel.setData(bytes));
}
private byte[] getNodeBytes() {
JNode binaryNode = getNode();
if (binaryNode instanceof JResource) {
JResource jResource = (JResource) binaryNode;
try {
return ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
} catch (Exception e) {
LOG.error("Failed to directly load resource binary data {}: {}", jResource.getName(), e.getMessage());
}
}
return binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.US_ASCII);
}
private JTabbedPane buildTabbedPane() {
@@ -96,12 +85,13 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
}
private void loadSelectedPanel() {
BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor();
Component codePanel = getSelectedPanel();
if (codePanel instanceof CodeArea) {
CodeArea codeArea = (CodeArea) codePanel;
codeArea.load();
bgExec.startLoading(codeArea::load);
} else {
loadToHexView(getNode());
bgExec.startLoading(this::loadHexView);
}
}
@@ -114,6 +104,19 @@ public class BinaryContentPanel extends AbstractCodeContentPanel {
}
}
@Override
public void scrollToPos(int pos) {
BackgroundExecutor bgExec = getMainWindow().getBackgroundExecutor();
if (getNode().getContentType() == ResourceContentType.CONTENT_TEXT) {
areaTabbedPane.setSelectedComponent(textCodePanel);
AbstractCodeArea codeArea = textCodePanel.getCodeArea();
bgExec.startLoading(codeArea::load, () -> codeArea.scrollToPos(pos));
} else {
areaTabbedPane.setSelectedComponent(hexPreviewPanel);
bgExec.startLoading(this::loadHexView, () -> hexPreviewPanel.scrollToOffset(pos));
}
}
@Override
public Component getChildrenComponent() {
return getSelectedPanel();
@@ -6,6 +6,7 @@ import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -51,6 +52,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
import jadx.api.JavaClass;
import jadx.api.JavaPackage;
import jadx.api.resources.ResourceContentType;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.utils.ListUtils;
import jadx.core.utils.StringUtils;
@@ -65,6 +67,7 @@ import jadx.gui.search.providers.CommentSearchProvider;
import jadx.gui.search.providers.FieldSearchProvider;
import jadx.gui.search.providers.MergedSearchProvider;
import jadx.gui.search.providers.MethodSearchProvider;
import jadx.gui.search.providers.ResourceFilter;
import jadx.gui.search.providers.ResourceSearchProvider;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
@@ -80,6 +83,7 @@ import jadx.gui.utils.UiUtils;
import jadx.gui.utils.cache.ValueCache;
import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.rx.RxUtils;
import jadx.gui.utils.ui.DocumentUpdateListener;
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB;
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS;
@@ -318,15 +322,56 @@ public class SearchDialog extends CommonSearchDialog {
searchPackagePanel
.setPreferredSize(new Dimension(Math.max(packageField.getPreferredSize().width, minPanelSize.width), minPanelSize.height));
resExtField = new JTextField();
resExtField = new JTextField(30);
TextStandardActions.attach(resExtField);
resExtField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
resExtField.setToolTipText(NLS.str("preferences.res_file_ext"));
resExtField.setText(mainWindow.getProject().getSearchResourcesFilter());
String resFilterStr = mainWindow.getProject().getSearchResourcesFilter();
resExtField.setText(resFilterStr);
JPanel resExtFilePanel = new JPanel(new BorderLayout());
ResourceFilter resFilter = ResourceFilter.parse(resFilterStr);
JCheckBox textResBox = new JCheckBox(NLS.str("search_dialog.res_text"));
textResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT));
JCheckBox binResBox = new JCheckBox(NLS.str("search_dialog.res_binary"));
binResBox.setSelected(resFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY));
ItemListener resContentTypeListener = ev -> {
try {
Set<ResourceContentType> contentTypes = EnumSet.noneOf(ResourceContentType.class);
if (textResBox.isSelected()) {
contentTypes.add(ResourceContentType.CONTENT_TEXT);
}
if (binResBox.isSelected()) {
contentTypes.add(ResourceContentType.CONTENT_BINARY);
}
String newStr = ResourceFilter.withContentType(resExtField.getText(), contentTypes);
if (!newStr.equals(resExtField.getText())) {
resExtField.setText(newStr);
}
} catch (Exception e) {
// ignore
}
};
textResBox.addItemListener(resContentTypeListener);
binResBox.addItemListener(resContentTypeListener);
resExtField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> UiUtils.uiRun(() -> {
try {
ResourceFilter filter = ResourceFilter.parse(resExtField.getText());
textResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT));
binResBox.setSelected(filter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY));
} catch (Exception e) {
// ignore
}
})));
JPanel resExtFilePanel = new JPanel();
resExtFilePanel.setLayout(new BoxLayout(resExtFilePanel, BoxLayout.LINE_AXIS));
resExtFilePanel.setBorder(BorderFactory.createTitledBorder(NLS.str("preferences.res_file_ext")));
resExtFilePanel.add(resExtField, BorderLayout.CENTER);
resExtFilePanel.add(resExtField);
resExtFilePanel.add(textResBox);
resExtFilePanel.add(binResBox);
resExtFilePanel.setPreferredSize(calcMinSizeForTitledBorder(resExtFilePanel));
resSizeLimit = new JSpinner(new SpinnerNumberModel(mainWindow.getProject().getSearchResourcesSizeLimit(), 0, Integer.MAX_VALUE, 1));
@@ -562,10 +607,11 @@ public class SearchDialog extends CommonSearchDialog {
SearchTask newSearchTask = new SearchTask(mainWindow, this::addSearchResult, this::searchFinished);
if (!buildSearch(newSearchTask, text, searchSettings)) {
UiUtils.highlightAsErrorField(searchField, true);
return null;
}
// save search settings
mainWindow.getProject().setSearchResourcesFilter(searchSettings.getResFilterStr());
mainWindow.getProject().setSearchResourcesFilter(resExtField.getText().trim());
mainWindow.getProject().setSearchResourcesSizeLimit(searchSettings.getResSizeLimit());
return newSearchTask;
}
@@ -135,6 +135,12 @@ public class HexPreviewPanel extends JPanel {
}
}
public void scrollToOffset(int pos) {
hexCodeArea.setSelection(pos, pos + 1);
hexCodeArea.setActiveCaretPosition(pos);
hexCodeArea.centerOnPosition(hexCodeArea.getActiveCaretPosition());
}
public void enableUpdate() {
CodeAreaCaretListener caretMovedListener = (CodeAreaCaretPosition caretPosition) -> updateValues();
hexCodeArea.addCaretMovedListener(caretMovedListener);
@@ -3,6 +3,8 @@ package jadx.gui.ui.panel;
import javax.swing.JPanel;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JClass;
@@ -12,7 +14,7 @@ import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.ui.tab.TabsController;
public abstract class ContentPanel extends JPanel {
private static final Logger LOG = LoggerFactory.getLogger(ContentPanel.class);
private static final long serialVersionUID = 3237031760631677822L;
protected TabbedPane tabbedPane;
@@ -41,6 +43,10 @@ public abstract class ContentPanel extends JPanel {
return node;
}
public void scrollToPos(int pos) {
LOG.warn("ContentPanel.scrollToPos method not implemented, class: {}", getClass().getSimpleName());
}
/**
* Allows to show a tool tip on the tab e.g. for displaying a long path of the
* selected entry inside the APK file.
@@ -225,26 +225,21 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
}
private @Nullable ContentPanel showCode(JumpPosition jumpPos) {
ContentPanel contentPanel = getContentPanel(jumpPos.getNode());
if (contentPanel != null) {
selectTab(contentPanel);
scrollToPos(contentPanel, jumpPos.getPos());
JNode jumpNode = jumpPos.getNode();
ContentPanel contentPanel = getContentPanel(jumpNode);
if (contentPanel == null) {
return null;
}
selectTab(contentPanel);
int pos = jumpPos.getPos();
if (pos < 0) {
LOG.warn("Invalid jump: {}", jumpPos, new JadxRuntimeException());
pos = 0;
}
contentPanel.scrollToPos(pos);
return contentPanel;
}
private void scrollToPos(ContentPanel contentPanel, int pos) {
if (pos == 0) {
LOG.warn("Ignore zero jump!", new JadxRuntimeException());
return;
}
if (contentPanel instanceof AbstractCodeContentPanel) {
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) contentPanel).getCodeArea();
codeArea.requestFocus();
codeArea.scrollToPos(pos);
}
}
public void selectTab(ContentPanel contentPanel) {
controller.selectTab(contentPanel.getNode());
}
@@ -259,7 +254,7 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
} else {
selectTab(panel);
}
ClassCodeContentPanel codePane = ((ClassCodeContentPanel) panel);
ClassCodeContentPanel codePane = (ClassCodeContentPanel) panel;
codePane.showSmaliPane();
SmaliArea smaliArea = (SmaliArea) codePane.getSmaliCodeArea();
if (debugMode) {
@@ -272,7 +267,10 @@ public class TabbedPane extends JTabbedPane implements ITabStatesListener {
public @Nullable JumpPosition getCurrentPosition() {
ContentPanel selectedCodePanel = getSelectedContentPanel();
if (selectedCodePanel instanceof AbstractCodeContentPanel) {
return ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea().getCurrentPosition();
AbstractCodeArea codeArea = ((AbstractCodeContentPanel) selectedCodePanel).getCodeArea();
if (codeArea != null) {
return codeArea.getCurrentPosition();
}
}
return null;
}
@@ -179,6 +179,8 @@ search_dialog.resource=Ressource
search_dialog.keep_open=Offen halten
search_dialog.tip_searching=Suchen…
search_dialog.limit_package=Begrenzung auf Paket:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
search_dialog.package_not_found=Kein passendes Paket gefunden
search_dialog.copy=alles kopieren
@@ -179,6 +179,8 @@ search_dialog.resource=Resource
search_dialog.keep_open=Keep open
search_dialog.tip_searching=Searching
search_dialog.limit_package=Limit to package:
search_dialog.res_text=Text
search_dialog.res_binary=Binary
search_dialog.package_not_found=No matching package found
search_dialog.copy=Copy All
@@ -179,6 +179,8 @@ search_dialog.regex=Regex
#search_dialog.keep_open=Keep open
#search_dialog.tip_searching=Searching
#search_dialog.limit_package=Limit to package:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
#search_dialog.package_not_found=No matching package found
search_dialog.copy=copiar todo
@@ -179,6 +179,8 @@ search_dialog.resource=Sumber daya
search_dialog.keep_open=Tetap terbuka
search_dialog.tip_searching=Mencari
#search_dialog.limit_package=Limit to package:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
#search_dialog.package_not_found=No matching package found
search_dialog.copy=salin semua
@@ -179,6 +179,8 @@ search_dialog.resource=리소스
search_dialog.keep_open=열어 두기
search_dialog.tip_searching=검색 중...
#search_dialog.limit_package=Limit to package:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
#search_dialog.package_not_found=No matching package found
search_dialog.copy=모두 복사
@@ -179,6 +179,8 @@ search_dialog.resource=Recursos
search_dialog.keep_open=Manter aberto
search_dialog.tip_searching=Buscando
#search_dialog.limit_package=Limit to package:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
#search_dialog.package_not_found=No matching package found
search_dialog.copy=skopiuj wszystko
@@ -179,6 +179,8 @@ search_dialog.resource=Ресурсы
search_dialog.keep_open=Оставлять поиск открытым
search_dialog.tip_searching=Поиск...
#search_dialog.limit_package=Limit to package:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
#search_dialog.package_not_found=No matching package found
search_dialog.copy=скопировать все
@@ -179,6 +179,8 @@ search_dialog.resource=资源
search_dialog.keep_open=保持窗口
search_dialog.tip_searching=搜索中…
search_dialog.limit_package=限制package
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
search_dialog.package_not_found=没有找到匹配的package
search_dialog.copy=复制全部
@@ -179,6 +179,8 @@ search_dialog.resource=資源
search_dialog.keep_open=保持開啟
search_dialog.tip_searching=正在搜尋
search_dialog.limit_package=限制至套件:
#search_dialog.res_text=Text
#search_dialog.res_binary=Binary
search_dialog.package_not_found=找不到符合的套件
search_dialog.copy=複製全部