Skip to content

Commit 8030c2f

Browse files
authored
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
1 parent 47224dc commit 8030c2f

26 files changed

+401
-138
lines changed

jadx-core/src/main/java/jadx/api/ResourceType.java

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,44 @@
44
import java.util.Locale;
55
import java.util.Map;
66

7+
import jadx.api.resources.ResourceContentType;
78
import jadx.core.utils.exceptions.JadxRuntimeException;
89

10+
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
11+
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
12+
import static jadx.api.resources.ResourceContentType.CONTENT_UNKNOWN;
13+
914
public enum ResourceType {
10-
CODE(".dex", ".jar", ".class"),
11-
XML(".xml"),
12-
ARSC(".arsc"),
13-
APK(".apk", ".apkm", ".apks"),
14-
FONT(".ttf", ".ttc", ".otf"),
15-
IMG(".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
16-
ARCHIVE(".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz", ".tgz", ".bz2"),
17-
VIDEOS(".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
18-
SOUNDS(".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
19-
JSON(".json"),
20-
TEXT(".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
21-
HTML(".html"),
22-
LIB(".so"),
23-
MANIFEST,
24-
UNKNOWN;
15+
CODE(CONTENT_BINARY, ".dex", ".jar", ".class"),
16+
XML(CONTENT_TEXT, ".xml"),
17+
ARSC(CONTENT_TEXT, ".arsc"),
18+
APK(CONTENT_BINARY, ".apk", ".apkm", ".apks"),
19+
FONT(CONTENT_BINARY, ".ttf", ".ttc", ".otf"),
20+
IMG(CONTENT_BINARY, ".png", ".gif", ".jpg", ".webp", ".bmp", ".tiff"),
21+
ARCHIVE(CONTENT_BINARY, ".zip", ".rar", ".7zip", ".7z", ".arj", ".tar", ".gzip", ".bzip", ".bzip2", ".cab", ".cpio", ".ar", ".gz",
22+
".tgz", ".bz2"),
23+
VIDEOS(CONTENT_BINARY, ".mp4", ".mkv", ".webm", ".avi", ".flv", ".3gp"),
24+
SOUNDS(CONTENT_BINARY, ".aac", ".ogg", ".opus", ".mp3", ".wav", ".wma", ".mid", ".midi"),
25+
JSON(CONTENT_TEXT, ".json"),
26+
TEXT(CONTENT_TEXT, ".txt", ".ini", ".conf", ".yaml", ".properties", ".js"),
27+
HTML(CONTENT_TEXT, ".html"),
28+
LIB(CONTENT_BINARY, ".so"),
29+
MANIFEST(CONTENT_TEXT),
30+
UNKNOWN_BIN(CONTENT_BINARY, ".bin"),
31+
UNKNOWN(CONTENT_UNKNOWN);
2532

33+
private final ResourceContentType contentType;
2634
private final String[] exts;
2735

28-
ResourceType(String... exts) {
36+
ResourceType(ResourceContentType contentType, String... exts) {
37+
this.contentType = contentType;
2938
this.exts = exts;
3039
}
3140

41+
public ResourceContentType getContentType() {
42+
return contentType;
43+
}
44+
3245
public String[] getExts() {
3346
return exts;
3447
}

jadx-core/src/main/java/jadx/api/ResourcesLoader.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.FileInputStream;
77
import java.io.IOException;
88
import java.io.InputStream;
9+
import java.nio.charset.Charset;
910
import java.nio.charset.StandardCharsets;
1011
import java.util.ArrayList;
1112
import java.util.List;
@@ -229,9 +230,13 @@ public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, Str
229230
}
230231

231232
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
233+
return loadToCodeWriter(is, StandardCharsets.UTF_8);
234+
}
235+
236+
public static ICodeInfo loadToCodeWriter(InputStream is, Charset charset) throws IOException {
232237
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
233238
copyStream(is, baos);
234-
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
239+
return new SimpleCodeInfo(baos.toString(charset));
235240
}
236241

237242
private synchronized BinaryXMLParser loadBinaryXmlParser() {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package jadx.api.resources;
2+
3+
public enum ResourceContentType {
4+
CONTENT_TEXT,
5+
CONTENT_BINARY,
6+
CONTENT_NONE,
7+
CONTENT_UNKNOWN,
8+
}

jadx-gui/src/main/java/jadx/gui/jobs/BackgroundExecutor.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ public Future<TaskStatus> execute(String title, Runnable backgroundRunnable) {
104104
return execute(new SimpleTask(title, Collections.singletonList(backgroundRunnable)));
105105
}
106106

107+
public void startLoading(Runnable backgroundRunnable, Runnable onFinishUiRunnable) {
108+
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable, onFinishUiRunnable));
109+
}
110+
111+
public void startLoading(Runnable backgroundRunnable) {
112+
execute(new SimpleTask(NLS.str("progress.load"), backgroundRunnable));
113+
}
114+
107115
private synchronized void reset() {
108116
taskQueueExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(1, Utils.simpleThreadFactory("bg"));
109117
taskRunning.clear();

jadx-gui/src/main/java/jadx/gui/search/SearchSettings.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import jadx.api.JavaClass;
99
import jadx.api.JavaPackage;
1010
import jadx.core.dex.nodes.PackageNode;
11+
import jadx.core.utils.exceptions.InvalidDataException;
12+
import jadx.gui.search.providers.ResourceFilter;
1113
import jadx.gui.treemodel.JClass;
1214
import jadx.gui.treemodel.JResource;
1315
import jadx.gui.ui.MainWindow;
@@ -26,6 +28,7 @@ public class SearchSettings {
2628
private Pattern regexPattern;
2729
private ISearchMethod searchMethod;
2830
private JavaPackage searchPackage;
31+
private ResourceFilter resourceFilter;
2932

3033
public SearchSettings(String searchString) {
3134
this.searchString = searchString;
@@ -49,6 +52,11 @@ public SearchSettings(String searchString) {
4952
searchPackage = pkg.getJavaNode();
5053
}
5154
searchMethod = ISearchMethod.build(this);
55+
try {
56+
resourceFilter = ResourceFilter.parse(resFilterStr);
57+
} catch (InvalidDataException e) {
58+
return "Invalid resource file filter: " + e.getMessage();
59+
}
5260
return null;
5361
}
5462

@@ -112,14 +120,14 @@ public ISearchMethod getSearchMethod() {
112120
return searchMethod;
113121
}
114122

115-
public String getResFilterStr() {
116-
return resFilterStr;
117-
}
118-
119123
public void setResFilterStr(String resFilterStr) {
120124
this.resFilterStr = resFilterStr;
121125
}
122126

127+
public ResourceFilter getResourceFilter() {
128+
return resourceFilter;
129+
}
130+
123131
public int getResSizeLimit() {
124132
return resSizeLimit;
125133
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package jadx.gui.search.providers;
2+
3+
import java.util.ArrayList;
4+
import java.util.EnumSet;
5+
import java.util.LinkedHashSet;
6+
import java.util.List;
7+
import java.util.Set;
8+
9+
import jadx.api.resources.ResourceContentType;
10+
import jadx.core.utils.Utils;
11+
import jadx.core.utils.exceptions.InvalidDataException;
12+
13+
import static jadx.api.resources.ResourceContentType.CONTENT_BINARY;
14+
import static jadx.api.resources.ResourceContentType.CONTENT_TEXT;
15+
16+
public class ResourceFilter {
17+
18+
private static final ResourceFilter ANY = new ResourceFilter(Set.of(), Set.of());
19+
20+
private static final String VAR_TEXT = "$TEXT";
21+
private static final String VAR_BIN = "$BIN";
22+
23+
public static final String DEFAULT_STR = VAR_TEXT;
24+
25+
public static ResourceFilter parse(String filterStr) {
26+
String str = filterStr.trim();
27+
if (str.isEmpty() || str.equals("*")) {
28+
return ANY;
29+
}
30+
Set<ResourceContentType> contentTypes = EnumSet.noneOf(ResourceContentType.class);
31+
Set<String> extSet = new LinkedHashSet<>();
32+
String[] parts = filterStr.split("[|, ]");
33+
for (String part : parts) {
34+
if (part.isEmpty()) {
35+
continue;
36+
}
37+
if (part.startsWith("$")) {
38+
switch (part) {
39+
case VAR_TEXT:
40+
contentTypes.add(CONTENT_TEXT);
41+
break;
42+
case VAR_BIN:
43+
contentTypes.add(CONTENT_BINARY);
44+
break;
45+
default:
46+
throw new InvalidDataException("Unknown var name: " + part);
47+
}
48+
} else {
49+
extSet.add(part);
50+
}
51+
}
52+
return new ResourceFilter(contentTypes, extSet);
53+
}
54+
55+
public static String format(ResourceFilter filter) {
56+
if (filter.isAnyFile()) {
57+
return "*";
58+
}
59+
List<String> list = new ArrayList<>();
60+
Set<ResourceContentType> types = filter.getContentTypes();
61+
if (types.contains(CONTENT_TEXT)) {
62+
list.add(VAR_TEXT);
63+
}
64+
if (types.contains(CONTENT_BINARY)) {
65+
list.add(VAR_BIN);
66+
}
67+
list.addAll(filter.getExtSet());
68+
return Utils.listToString(list, "|");
69+
}
70+
71+
public static String withContentType(String filterStr, Set<ResourceContentType> contentTypes) {
72+
ResourceFilter filter = parse(filterStr);
73+
return format(new ResourceFilter(contentTypes, filter.getExtSet()));
74+
}
75+
76+
private final boolean anyFile;
77+
private final Set<ResourceContentType> contentTypes;
78+
private final Set<String> extSet;
79+
80+
private ResourceFilter(Set<ResourceContentType> contentTypes, Set<String> extSet) {
81+
this.anyFile = contentTypes.isEmpty() && extSet.isEmpty();
82+
this.contentTypes = contentTypes.isEmpty() ? Set.of() : contentTypes;
83+
this.extSet = extSet.isEmpty() ? Set.of() : extSet;
84+
}
85+
86+
public boolean isAnyFile() {
87+
return anyFile;
88+
}
89+
90+
public Set<ResourceContentType> getContentTypes() {
91+
return contentTypes;
92+
}
93+
94+
public Set<String> getExtSet() {
95+
return extSet;
96+
}
97+
98+
@Override
99+
public String toString() {
100+
return format(this);
101+
}
102+
}

jadx-gui/src/main/java/jadx/gui/search/providers/ResourceSearchProvider.java

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import java.util.Collections;
55
import java.util.Deque;
66
import java.util.Enumeration;
7-
import java.util.HashSet;
8-
import java.util.Set;
97

108
import javax.swing.tree.TreeNode;
119

@@ -16,6 +14,7 @@
1614
import jadx.api.ResourceFile;
1715
import jadx.api.ResourceType;
1816
import jadx.api.plugins.utils.CommonFileUtils;
17+
import jadx.api.resources.ResourceContentType;
1918
import jadx.api.utils.CodeUtils;
2019
import jadx.gui.jobs.Cancelable;
2120
import jadx.gui.search.ISearchProvider;
@@ -32,10 +31,9 @@ public class ResourceSearchProvider implements ISearchProvider {
3231
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
3332

3433
private final SearchSettings searchSettings;
35-
private final Set<String> extSet;
3634
private final SearchDialog searchDialog;
35+
private final ResourceFilter resourceFilter;
3736
private final int sizeLimit;
38-
private boolean anyExt;
3937

4038
/**
4139
* Resources queue for process. Using UI nodes to reuse loading cache
@@ -48,7 +46,7 @@ public class ResourceSearchProvider implements ISearchProvider {
4846

4947
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings, SearchDialog searchDialog) {
5048
this.searchSettings = searchSettings;
51-
this.extSet = buildAllowedFilesExtensions(searchSettings.getResFilterStr());
49+
this.resourceFilter = searchSettings.getResourceFilter();
5250
this.sizeLimit = searchSettings.getResSizeLimit() * 1024 * 1024;
5351
this.searchDialog = searchDialog;
5452
JResource activeResource = searchSettings.getActiveResource();
@@ -95,12 +93,20 @@ private JNode search(JResource resNode) {
9593
if (newPos == -1) {
9694
return null;
9795
}
98-
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
99-
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
100-
int end = lineEnd == -1 ? content.length() : lineEnd;
101-
String line = content.substring(lineStart, end);
102-
this.pos = end;
103-
return new JResSearchNode(resNode, line.trim(), newPos);
96+
if (resNode.getContentType() == ResourceContentType.CONTENT_TEXT) {
97+
int lineStart = 1 + CodeUtils.getNewLinePosBefore(content, newPos);
98+
int lineEnd = CodeUtils.getNewLinePosAfter(content, newPos);
99+
int end = lineEnd == -1 ? content.length() : lineEnd;
100+
String line = content.substring(lineStart, end);
101+
this.pos = end;
102+
return new JResSearchNode(resNode, line.trim(), newPos);
103+
} else {
104+
int start = Math.max(0, newPos - 30);
105+
int end = Math.min(newPos + 50, content.length());
106+
String line = content.substring(start, end);
107+
this.pos = newPos + searchString.length() + 1;
108+
return new JResSearchNode(resNode, line, newPos);
109+
}
104110
}
105111

106112
private @Nullable JResource getNextResFile(Cancelable cancelable) {
@@ -167,41 +173,41 @@ private static Deque<JResource> initResQueue(MainWindow mw) {
167173
return deque;
168174
}
169175

170-
private Set<String> buildAllowedFilesExtensions(String srhResourceFileExt) {
171-
String str = srhResourceFileExt.trim();
172-
if (str.isEmpty() || str.equals("*")) {
173-
anyExt = true;
174-
return Collections.emptySet();
175-
}
176-
Set<String> set = new HashSet<>();
177-
for (String extStr : str.split("[|.]")) {
178-
String ext = extStr.trim();
179-
if (!ext.isEmpty()) {
180-
anyExt = ext.equals("*");
181-
if (anyExt) {
182-
break;
183-
}
184-
set.add(ext);
185-
}
176+
private boolean shouldProcess(JResource resNode) {
177+
if (resNode.getResFile().getType() == ResourceType.ARSC) {
178+
// don't check the size of generated resource table, it will also skip all subfiles
179+
return resourceFilter.isAnyFile()
180+
|| resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_TEXT)
181+
|| resourceFilter.getExtSet().contains("xml");
186182
}
187-
return set;
183+
if (!isAllowedFileType(resNode)) {
184+
return false;
185+
}
186+
return isAllowedFileSize(resNode);
188187
}
189188

190-
private boolean shouldProcess(JResource resNode) {
189+
private boolean isAllowedFileType(JResource resNode) {
191190
ResourceFile resFile = resNode.getResFile();
192-
if (resFile.getType() == ResourceType.ARSC) {
193-
// don't check size of generated resource table, it will also skip all sub files
194-
return anyExt || extSet.contains("xml");
191+
if (resourceFilter.isAnyFile()) {
192+
return true;
195193
}
196-
if (!anyExt) {
197-
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
198-
if (fileExt == null) {
199-
return false;
200-
}
201-
if (!extSet.contains(fileExt)) {
202-
return false;
203-
}
194+
ResourceContentType resContentType = resNode.getContentType();
195+
if (resourceFilter.getContentTypes().contains(resContentType)) {
196+
return true;
197+
}
198+
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
199+
if (fileExt != null && resourceFilter.getExtSet().contains(fileExt)) {
200+
return true;
201+
}
202+
if (resContentType == ResourceContentType.CONTENT_UNKNOWN
203+
&& resourceFilter.getContentTypes().contains(ResourceContentType.CONTENT_BINARY)) {
204+
// treat unknown file type as binary
205+
return true;
204206
}
207+
return false;
208+
}
209+
210+
private boolean isAllowedFileSize(JResource resNode) {
205211
if (sizeLimit <= 0) {
206212
return true;
207213
}

0 commit comments

Comments
 (0)