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.
@@ -58,6 +58,9 @@ dependencies {
58
58
  api 'com.android.tools.smali:smali:3.0.3'
59
59
  api 'com.android.tools.smali:smali-baksmali:3.0.3'
60
60
 
61
+ // Guava - dexlib2 依赖
62
+ api 'com.google.guava:guava:32.1.3-android'
63
+
61
64
  // Gson - JSON序列化
62
65
  api 'com.google.code.gson:gson:2.10.1'
63
66
 
@@ -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
- info.put("stringCount", dexFile.getStringCount());
186
- info.put("typeCount", dexFile.getTypeCount());
187
- info.put("methodCount", dexFile.getMethodCount());
188
- info.put("fieldCount", dexFile.getFieldCount());
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
- for (int i = 0; i < session.originalDexFile.getStringCount(); i++) {
705
- String str = session.originalDexFile.getStringSection().get(i);
706
- boolean match;
707
-
708
- if (regex) {
709
- match = pattern.matcher(str).find();
710
- } else if (caseSensitive) {
711
- match = str.contains(query);
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
- for (int i = 0; i < session.originalDexFile.getStringCount(); i++) {
897
- JSObject item = new JSObject();
898
- item.put("index", i);
899
- item.put("value", session.originalDexFile.getStringSection().get(i));
900
- strings.put(item);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Capacitor-plugin-for-editing-DEX-files-in-APK",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",