capacitor-dex-editor 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/android/build.gradle
CHANGED
|
@@ -113,7 +113,7 @@ public class ApkManager {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* 获取 APK 信息
|
|
116
|
+
* 获取 APK 信息 - 快速版本,直接解析二进制 AndroidManifest.xml
|
|
117
117
|
*/
|
|
118
118
|
public JSObject getApkInfo(String apkPath) throws Exception {
|
|
119
119
|
File apkFile = new File(apkPath);
|
|
@@ -126,32 +126,13 @@ public class ApkManager {
|
|
|
126
126
|
info.put("size", apkFile.length());
|
|
127
127
|
info.put("lastModified", apkFile.lastModified());
|
|
128
128
|
|
|
129
|
-
//
|
|
130
|
-
if (context != null) {
|
|
131
|
-
PackageManager pm = context.getPackageManager();
|
|
132
|
-
PackageInfo packageInfo = pm.getPackageArchiveInfo(apkPath,
|
|
133
|
-
PackageManager.GET_META_DATA | PackageManager.GET_SIGNATURES);
|
|
134
|
-
|
|
135
|
-
if (packageInfo != null) {
|
|
136
|
-
info.put("packageName", packageInfo.packageName);
|
|
137
|
-
info.put("versionName", packageInfo.versionName);
|
|
138
|
-
info.put("versionCode", packageInfo.versionCode);
|
|
139
|
-
|
|
140
|
-
if (packageInfo.applicationInfo != null) {
|
|
141
|
-
packageInfo.applicationInfo.sourceDir = apkPath;
|
|
142
|
-
packageInfo.applicationInfo.publicSourceDir = apkPath;
|
|
143
|
-
CharSequence label = pm.getApplicationLabel(packageInfo.applicationInfo);
|
|
144
|
-
info.put("appName", label.toString());
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 列出 APK 内容
|
|
129
|
+
// 直接从 ZIP 读取并解析 AndroidManifest.xml(比 PackageManager 快很多)
|
|
150
130
|
try (ZipFile zipFile = new ZipFile(apkFile)) {
|
|
151
131
|
int dexCount = 0;
|
|
152
132
|
int resCount = 0;
|
|
153
133
|
int libCount = 0;
|
|
154
134
|
|
|
135
|
+
// 统计文件数量
|
|
155
136
|
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
|
156
137
|
while (entries.hasMoreElements()) {
|
|
157
138
|
ZipEntry entry = entries.nextElement();
|
|
@@ -165,10 +146,167 @@ public class ApkManager {
|
|
|
165
146
|
info.put("dexCount", dexCount);
|
|
166
147
|
info.put("resourceCount", resCount);
|
|
167
148
|
info.put("nativeLibCount", libCount);
|
|
149
|
+
|
|
150
|
+
// 解析 AndroidManifest.xml
|
|
151
|
+
ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
152
|
+
if (manifestEntry != null) {
|
|
153
|
+
try (InputStream is = zipFile.getInputStream(manifestEntry)) {
|
|
154
|
+
byte[] data = readAllBytes(is);
|
|
155
|
+
parseManifestBinary(data, info);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
168
158
|
}
|
|
169
159
|
|
|
170
160
|
return info;
|
|
171
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 解析二进制格式的 AndroidManifest.xml
|
|
165
|
+
*/
|
|
166
|
+
private void parseManifestBinary(byte[] data, JSObject info) {
|
|
167
|
+
try {
|
|
168
|
+
// 二进制 XML 格式解析
|
|
169
|
+
if (data.length < 8) return;
|
|
170
|
+
|
|
171
|
+
// 跳过 XML 头部,查找字符串池
|
|
172
|
+
int offset = 8; // 跳过 magic number 和 file size
|
|
173
|
+
|
|
174
|
+
// 读取字符串池
|
|
175
|
+
if (offset + 8 > data.length) return;
|
|
176
|
+
int stringPoolOffset = offset;
|
|
177
|
+
int stringPoolType = readShort(data, offset);
|
|
178
|
+
if (stringPoolType != 0x0001) return; // String Pool chunk type
|
|
179
|
+
|
|
180
|
+
int stringPoolSize = readInt(data, offset + 4);
|
|
181
|
+
int stringCount = readInt(data, offset + 8);
|
|
182
|
+
int styleCount = readInt(data, offset + 12);
|
|
183
|
+
int stringsStart = readInt(data, offset + 20);
|
|
184
|
+
|
|
185
|
+
// 读取字符串偏移表
|
|
186
|
+
int[] stringOffsets = new int[stringCount];
|
|
187
|
+
int offsetTableStart = offset + 28;
|
|
188
|
+
for (int i = 0; i < stringCount && offsetTableStart + i * 4 + 4 <= data.length; i++) {
|
|
189
|
+
stringOffsets[i] = readInt(data, offsetTableStart + i * 4);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 字符串数据起始位置
|
|
193
|
+
int stringDataStart = stringPoolOffset + stringsStart;
|
|
194
|
+
|
|
195
|
+
// 提取字符串池
|
|
196
|
+
String[] strings = new String[stringCount];
|
|
197
|
+
for (int i = 0; i < stringCount; i++) {
|
|
198
|
+
int strOffset = stringDataStart + stringOffsets[i];
|
|
199
|
+
if (strOffset + 2 > data.length) continue;
|
|
200
|
+
|
|
201
|
+
// 检查是否 UTF-8 或 UTF-16
|
|
202
|
+
int len = data[strOffset] & 0xFF;
|
|
203
|
+
if (len == ((data[strOffset + 1] & 0xFF))) {
|
|
204
|
+
// UTF-8
|
|
205
|
+
strOffset += 2;
|
|
206
|
+
if (strOffset + len <= data.length) {
|
|
207
|
+
strings[i] = new String(data, strOffset, len, "UTF-8");
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
// UTF-16
|
|
211
|
+
int charLen = readShort(data, strOffset);
|
|
212
|
+
strOffset += 2;
|
|
213
|
+
if (strOffset + charLen * 2 <= data.length) {
|
|
214
|
+
strings[i] = new String(data, strOffset, charLen * 2, "UTF-16LE");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 在字符串池中查找关键信息
|
|
220
|
+
String packageName = null;
|
|
221
|
+
String versionName = null;
|
|
222
|
+
String versionCode = null;
|
|
223
|
+
String appLabel = null;
|
|
224
|
+
|
|
225
|
+
for (int i = 0; i < strings.length; i++) {
|
|
226
|
+
String s = strings[i];
|
|
227
|
+
if (s == null) continue;
|
|
228
|
+
|
|
229
|
+
// 查找 package 属性的值(通常在 manifest 标签后)
|
|
230
|
+
if (s.matches("^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$")) {
|
|
231
|
+
if (packageName == null) packageName = s;
|
|
232
|
+
}
|
|
233
|
+
// 版本名通常包含数字和点
|
|
234
|
+
if (s.matches("^\\d+\\.\\d+.*$") || s.matches("^v?\\d+\\.\\d+.*$")) {
|
|
235
|
+
if (versionName == null) versionName = s;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 更精确的解析:遍历 XML 元素
|
|
240
|
+
offset = stringPoolOffset + stringPoolSize;
|
|
241
|
+
while (offset + 8 < data.length) {
|
|
242
|
+
int chunkType = readShort(data, offset);
|
|
243
|
+
int chunkSize = readInt(data, offset + 4);
|
|
244
|
+
|
|
245
|
+
if (chunkType == 0x0102) { // START_TAG
|
|
246
|
+
int attrStart = offset + 28;
|
|
247
|
+
int attrCount = readShort(data, offset + 24);
|
|
248
|
+
|
|
249
|
+
for (int i = 0; i < attrCount && attrStart + 20 <= data.length; i++) {
|
|
250
|
+
int nameIdx = readInt(data, attrStart + 4);
|
|
251
|
+
int valueIdx = readInt(data, attrStart + 8);
|
|
252
|
+
int valueType = data[attrStart + 15] & 0xFF;
|
|
253
|
+
int valueData = readInt(data, attrStart + 16);
|
|
254
|
+
|
|
255
|
+
if (nameIdx >= 0 && nameIdx < strings.length) {
|
|
256
|
+
String attrName = strings[nameIdx];
|
|
257
|
+
if ("package".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
258
|
+
packageName = strings[valueIdx];
|
|
259
|
+
} else if ("versionName".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
260
|
+
versionName = strings[valueIdx];
|
|
261
|
+
} else if ("versionCode".equals(attrName)) {
|
|
262
|
+
if (valueType == 0x10) { // TYPE_INT_DEC
|
|
263
|
+
versionCode = String.valueOf(valueData);
|
|
264
|
+
}
|
|
265
|
+
} else if ("label".equals(attrName) && valueIdx >= 0 && valueIdx < strings.length) {
|
|
266
|
+
String label = strings[valueIdx];
|
|
267
|
+
if (label != null && !label.startsWith("@")) {
|
|
268
|
+
appLabel = label;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
attrStart += 20;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (chunkSize <= 0) break;
|
|
277
|
+
offset += chunkSize;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (packageName != null) info.put("packageName", packageName);
|
|
281
|
+
if (versionName != null) info.put("versionName", versionName);
|
|
282
|
+
if (versionCode != null) info.put("versionCode", Integer.parseInt(versionCode));
|
|
283
|
+
if (appLabel != null) info.put("appName", appLabel);
|
|
284
|
+
|
|
285
|
+
} catch (Exception e) {
|
|
286
|
+
Log.w(TAG, "Failed to parse manifest binary: " + e.getMessage());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private int readShort(byte[] data, int offset) {
|
|
291
|
+
return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private int readInt(byte[] data, int offset) {
|
|
295
|
+
return (data[offset] & 0xFF) |
|
|
296
|
+
((data[offset + 1] & 0xFF) << 8) |
|
|
297
|
+
((data[offset + 2] & 0xFF) << 16) |
|
|
298
|
+
((data[offset + 3] & 0xFF) << 24);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private byte[] readAllBytes(InputStream is) throws IOException {
|
|
302
|
+
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
|
303
|
+
byte[] data = new byte[BUFFER_SIZE];
|
|
304
|
+
int len;
|
|
305
|
+
while ((len = is.read(data)) != -1) {
|
|
306
|
+
buffer.write(data, 0, len);
|
|
307
|
+
}
|
|
308
|
+
return buffer.toByteArray();
|
|
309
|
+
}
|
|
172
310
|
|
|
173
311
|
/**
|
|
174
312
|
* 列出 APK 内容
|
|
@@ -182,10 +182,16 @@ public class DexManager {
|
|
|
182
182
|
info.put("sessionId", sessionId);
|
|
183
183
|
info.put("filePath", session.filePath);
|
|
184
184
|
info.put("classCount", dexFile.getClasses().size());
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
|
|
186
|
+
// 统计方法和字段数量
|
|
187
|
+
int methodCount = 0;
|
|
188
|
+
int fieldCount = 0;
|
|
189
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
190
|
+
for (Method ignored : classDef.getMethods()) methodCount++;
|
|
191
|
+
for (Field ignored : classDef.getFields()) fieldCount++;
|
|
192
|
+
}
|
|
193
|
+
info.put("methodCount", methodCount);
|
|
194
|
+
info.put("fieldCount", fieldCount);
|
|
189
195
|
info.put("dexVersion", dexFile.getOpcodes().api);
|
|
190
196
|
info.put("modified", session.modified);
|
|
191
197
|
return info;
|
|
@@ -701,23 +707,14 @@ public class DexManager {
|
|
|
701
707
|
pattern = Pattern.compile(query, flags);
|
|
702
708
|
}
|
|
703
709
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
} else {
|
|
713
|
-
match = str.toLowerCase().contains(query.toLowerCase());
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (match) {
|
|
717
|
-
JSObject item = new JSObject();
|
|
718
|
-
item.put("index", i);
|
|
719
|
-
item.put("value", str);
|
|
720
|
-
results.put(item);
|
|
710
|
+
// 遍历所有类中的字符串引用进行搜索
|
|
711
|
+
Set<String> searchedStrings = new HashSet<>();
|
|
712
|
+
for (ClassDef classDef : session.originalDexFile.getClasses()) {
|
|
713
|
+
// 类名
|
|
714
|
+
checkAndAddString(classDef.getType(), query, regex, caseSensitive, pattern, searchedStrings, results);
|
|
715
|
+
// 父类
|
|
716
|
+
if (classDef.getSuperclass() != null) {
|
|
717
|
+
checkAndAddString(classDef.getSuperclass(), query, regex, caseSensitive, pattern, searchedStrings, results);
|
|
721
718
|
}
|
|
722
719
|
}
|
|
723
720
|
|
|
@@ -893,11 +890,17 @@ public class DexManager {
|
|
|
893
890
|
DexSession session = getSession(sessionId);
|
|
894
891
|
JSArray strings = new JSArray();
|
|
895
892
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
893
|
+
// 收集所有类中的字符串
|
|
894
|
+
Set<String> collectedStrings = new HashSet<>();
|
|
895
|
+
int index = 0;
|
|
896
|
+
for (ClassDef classDef : session.originalDexFile.getClasses()) {
|
|
897
|
+
if (!collectedStrings.contains(classDef.getType())) {
|
|
898
|
+
collectedStrings.add(classDef.getType());
|
|
899
|
+
JSObject item = new JSObject();
|
|
900
|
+
item.put("index", index++);
|
|
901
|
+
item.put("value", classDef.getType());
|
|
902
|
+
strings.put(item);
|
|
903
|
+
}
|
|
901
904
|
}
|
|
902
905
|
|
|
903
906
|
return strings;
|
|
@@ -940,6 +943,29 @@ public class DexManager {
|
|
|
940
943
|
return session;
|
|
941
944
|
}
|
|
942
945
|
|
|
946
|
+
private void checkAndAddString(String str, String query, boolean regex,
|
|
947
|
+
boolean caseSensitive, Pattern pattern,
|
|
948
|
+
Set<String> searchedStrings, JSArray results) {
|
|
949
|
+
if (str == null || searchedStrings.contains(str)) return;
|
|
950
|
+
searchedStrings.add(str);
|
|
951
|
+
|
|
952
|
+
boolean match;
|
|
953
|
+
if (regex) {
|
|
954
|
+
match = pattern.matcher(str).find();
|
|
955
|
+
} else if (caseSensitive) {
|
|
956
|
+
match = str.contains(query);
|
|
957
|
+
} else {
|
|
958
|
+
match = str.toLowerCase().contains(query.toLowerCase());
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (match) {
|
|
962
|
+
JSObject item = new JSObject();
|
|
963
|
+
item.put("index", searchedStrings.size() - 1);
|
|
964
|
+
item.put("value", str);
|
|
965
|
+
results.put(item);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
943
969
|
private ClassDef findClass(DexSession session, String className) {
|
|
944
970
|
// 先检查修改后的类
|
|
945
971
|
for (ClassDef classDef : session.modifiedClasses) {
|