From 8030c2f84ed05aa88dc1aa87d68e50984cae2e30 Mon Sep 17 00:00:00 2001 From: skylot <118523+skylot@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:23:08 +0100 Subject: [PATCH] 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 --- .../src/main/java/jadx/api/ResourceType.java | 47 +++++--- .../main/java/jadx/api/ResourcesLoader.java | 7 +- .../api/resources/ResourceContentType.java | 8 ++ .../jadx/gui/jobs/BackgroundExecutor.java | 8 ++ .../java/jadx/gui/search/SearchSettings.java | 16 ++- .../gui/search/providers/ResourceFilter.java | 102 ++++++++++++++++++ .../providers/ResourceSearchProvider.java | 84 ++++++++------- .../jadx/gui/settings/data/ProjectData.java | 3 +- .../main/java/jadx/gui/treemodel/JNode.java | 5 + .../java/jadx/gui/treemodel/JResource.java | 40 +++++-- .../src/main/java/jadx/gui/ui/MainWindow.java | 16 +-- .../ui/codearea/AbstractCodeContentPanel.java | 12 ++- .../gui/ui/codearea/BinaryContentPanel.java | 69 ++++++------ .../java/jadx/gui/ui/dialog/SearchDialog.java | 56 +++++++++- .../gui/ui/hexviewer/HexPreviewPanel.java | 6 ++ .../java/jadx/gui/ui/panel/ContentPanel.java | 8 +- .../main/java/jadx/gui/ui/tab/TabbedPane.java | 34 +++--- .../resources/i18n/Messages_de_DE.properties | 2 + .../resources/i18n/Messages_en_US.properties | 2 + .../resources/i18n/Messages_es_ES.properties | 2 + .../resources/i18n/Messages_id_ID.properties | 2 + .../resources/i18n/Messages_ko_KR.properties | 2 + .../resources/i18n/Messages_pt_BR.properties | 2 + .../resources/i18n/Messages_ru_RU.properties | 2 + .../resources/i18n/Messages_zh_CN.properties | 2 + .../resources/i18n/Messages_zh_TW.properties | 2 + 26 files changed, 401 insertions(+), 138 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/api/resources/ResourceContentType.java create mode 100644 jadx-gui/src/main/java/jadx/gui/search/providers/ResourceFilter.java diff --git a/jadx-core/src/main/java/jadx/api/ResourceType.java b/jadx-core/src/main/java/jadx/api/ResourceType.java index 6a69f3aa4..a5a20ec0d 100644 --- a/jadx-core/src/main/java/jadx/api/ResourceType.java +++ b/jadx-core/src/main/java/jadx/api/ResourceType.java @@ -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; } diff --git a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java index ea4ec3e8c..a5e9a8f75 100644 --- a/jadx-core/src/main/java/jadx/api/ResourcesLoader.java +++ b/jadx-core/src/main/java/jadx/api/ResourcesLoader.java @@ -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() { diff --git a/jadx-core/src/main/java/jadx/api/resources/ResourceContentType.java b/jadx-core/src/main/java/jadx/api/resources/ResourceContentType.java new file mode 100644 index 000000000..17ee586cc --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/resources/ResourceContentType.java @@ -0,0 +1,8 @@ +package jadx.api.resources; + +public enum ResourceContentType { + CONTENT_TEXT, + CONTENT_BINARY, + CONTENT_NONE, + CONTENT_UNKNOWN, +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java index d9630d416..36da45943 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java @@ -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(); 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 0bbbbbae0..a94c563ca 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceFilter.java b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceFilter.java new file mode 100644 index 000000000..c7d93adda --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceFilter.java @@ -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 contentTypes = EnumSet.noneOf(ResourceContentType.class); + Set 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 list = new ArrayList<>(); + Set 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 contentTypes) { + ResourceFilter filter = parse(filterStr); + return format(new ResourceFilter(contentTypes, filter.getExtSet())); + } + + private final boolean anyFile; + private final Set contentTypes; + private final Set extSet; + + private ResourceFilter(Set contentTypes, Set 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 getContentTypes() { + return contentTypes; + } + + public Set getExtSet() { + return extSet; + } + + @Override + public String toString() { + return format(this); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java index 30460e6ad..3cf6c885d 100644 --- a/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java +++ b/jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java @@ -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 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 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 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; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java index adfd8dee9..1c0e70b69 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/data/ProjectData.java @@ -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 searchHistory = new ArrayList<>(); - private String searchResourcesFilter = "*"; + private String searchResourcesFilter = ResourceFilter.DEFAULT_STR; private int searchResourcesSizeLimit = 0; // in MB protected Map pluginOptions = new HashMap<>(); diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index aaddb2d11..8d12a0f9c 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index 46e40cc15..a071f6803 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -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)); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index ff43f08bc..0b6932a2f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -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); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java index 06fb3f733..450ee78cc 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeContentPanel.java @@ -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); + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java index 68e53f34a..aed9d3ad9 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/BinaryContentPanel.java @@ -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 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(); 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 01438bec8..8bd472e52 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 @@ -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 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; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java index f3de7f108..e685b8d9d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/hexviewer/HexPreviewPanel.java @@ -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); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java index 20f1e1ba4..0d1673ba1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/ContentPanel.java @@ -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. diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java index 84fffabc7..25a19d62a 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabbedPane.java @@ -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; } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 228ac009f..d089d3499 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index d753ae9d7..691cb21fb 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index b7b9eb3b3..7373ef44e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 5740c1d09..dc5bb02f9 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 90fd3358c..ebfd0ce0c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -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=모두 복사 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 15e2ca1b9..882958016 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 3e96bd9fb..4f67d7525 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -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=скопировать все diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index fdbe3b46d..7c78bf030 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -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=复制全部 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 8717290f7..c41e44aae 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -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=複製全部