capacitor-dex-editor 0.0.36 → 0.0.38

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.
@@ -68,8 +68,7 @@ dependencies {
68
68
  // APK Signer - V1/V2/V3/V4 签名支持 (Android 7.0+)
69
69
  api 'com.android.tools.build:apksig:8.7.2'
70
70
 
71
- // ARSCLib - 二进制 XML 解析和修改(无限制)
72
- api 'io.github.reandroid:ARSCLib:1.3.8'
71
+ // 移除 ARSCLib,使用内置 AXML 解析器
73
72
 
74
73
  testImplementation "junit:junit:$junitVersion"
75
74
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
@@ -0,0 +1,331 @@
1
+ package com.aetherlink.dexeditor;
2
+
3
+ import java.io.ByteArrayOutputStream;
4
+ import java.nio.ByteBuffer;
5
+ import java.nio.ByteOrder;
6
+ import java.nio.charset.StandardCharsets;
7
+ import java.util.ArrayList;
8
+ import java.util.List;
9
+
10
+ /**
11
+ * AXML 编辑器 - 支持任意长度的字符串替换
12
+ * 通过重建字符串池实现
13
+ */
14
+ public class AxmlEditor {
15
+
16
+ private static final int AXML_MAGIC = 0x00080003;
17
+ private static final int STRING_POOL_TYPE = 0x0001;
18
+
19
+ private byte[] data;
20
+ private int stringPoolStart;
21
+ private int stringPoolSize;
22
+ private int stringCount;
23
+ private int styleCount;
24
+ private int flags;
25
+ private int stringsOffset;
26
+ private int stylesOffset;
27
+ private List<String> strings;
28
+ private byte[] beforeStringPool;
29
+ private byte[] afterStringPool;
30
+ private boolean isUtf8;
31
+
32
+ public AxmlEditor(byte[] axmlData) {
33
+ this.data = axmlData;
34
+ this.strings = new ArrayList<>();
35
+ parse();
36
+ }
37
+
38
+ private void parse() {
39
+ if (data.length < 8) return;
40
+
41
+ ByteBuffer buffer = ByteBuffer.wrap(data);
42
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
43
+
44
+ // 检查魔数
45
+ int magic = buffer.getInt();
46
+ if (magic != AXML_MAGIC) return;
47
+
48
+ int fileSize = buffer.getInt();
49
+
50
+ // 查找字符串池
51
+ while (buffer.position() < data.length - 8) {
52
+ int chunkStart = buffer.position();
53
+ int chunkType = buffer.getShort() & 0xFFFF;
54
+ int headerSize = buffer.getShort() & 0xFFFF;
55
+ int chunkSize = buffer.getInt();
56
+
57
+ if (chunkType == STRING_POOL_TYPE) {
58
+ stringPoolStart = chunkStart;
59
+ stringPoolSize = chunkSize;
60
+
61
+ // 保存字符串池之前的数据
62
+ beforeStringPool = new byte[chunkStart];
63
+ System.arraycopy(data, 0, beforeStringPool, 0, chunkStart);
64
+
65
+ // 保存字符串池之后的数据
66
+ int afterStart = chunkStart + chunkSize;
67
+ if (afterStart < data.length) {
68
+ afterStringPool = new byte[data.length - afterStart];
69
+ System.arraycopy(data, afterStart, afterStringPool, 0, afterStringPool.length);
70
+ } else {
71
+ afterStringPool = new byte[0];
72
+ }
73
+
74
+ // 解析字符串池
75
+ stringCount = buffer.getInt();
76
+ styleCount = buffer.getInt();
77
+ flags = buffer.getInt();
78
+ stringsOffset = buffer.getInt();
79
+ stylesOffset = buffer.getInt();
80
+
81
+ isUtf8 = (flags & 0x100) != 0;
82
+
83
+ // 读取字符串偏移表
84
+ int[] offsets = new int[stringCount];
85
+ for (int i = 0; i < stringCount; i++) {
86
+ offsets[i] = buffer.getInt();
87
+ }
88
+
89
+ // 读取字符串
90
+ int stringsStart = chunkStart + stringsOffset;
91
+ for (int i = 0; i < stringCount; i++) {
92
+ int stringStart = stringsStart + offsets[i];
93
+ if (stringStart < data.length) {
94
+ strings.add(readStringAt(stringStart));
95
+ } else {
96
+ strings.add("");
97
+ }
98
+ }
99
+
100
+ break;
101
+ }
102
+
103
+ buffer.position(chunkStart + chunkSize);
104
+ }
105
+ }
106
+
107
+ private String readStringAt(int pos) {
108
+ try {
109
+ if (isUtf8) {
110
+ int charLen = data[pos] & 0xFF;
111
+ int byteLen;
112
+ int dataStart;
113
+
114
+ if ((charLen & 0x80) != 0) {
115
+ charLen = ((charLen & 0x7F) << 8) | (data[pos + 1] & 0xFF);
116
+ byteLen = data[pos + 2] & 0xFF;
117
+ if ((byteLen & 0x80) != 0) {
118
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 3] & 0xFF);
119
+ dataStart = pos + 4;
120
+ } else {
121
+ dataStart = pos + 3;
122
+ }
123
+ } else {
124
+ byteLen = data[pos + 1] & 0xFF;
125
+ if ((byteLen & 0x80) != 0) {
126
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 2] & 0xFF);
127
+ dataStart = pos + 3;
128
+ } else {
129
+ dataStart = pos + 2;
130
+ }
131
+ }
132
+
133
+ if (dataStart + byteLen > data.length) {
134
+ byteLen = data.length - dataStart;
135
+ }
136
+ if (byteLen <= 0) return "";
137
+
138
+ return new String(data, dataStart, byteLen, StandardCharsets.UTF_8);
139
+ } else {
140
+ int charLen = (data[pos] & 0xFF) | ((data[pos + 1] & 0xFF) << 8);
141
+ if ((charLen & 0x8000) != 0) {
142
+ int high = (data[pos + 2] & 0xFF) | ((data[pos + 3] & 0xFF) << 8);
143
+ charLen = ((charLen & 0x7FFF) << 16) | high;
144
+ pos += 4;
145
+ } else {
146
+ pos += 2;
147
+ }
148
+
149
+ if (pos + charLen * 2 > data.length) {
150
+ charLen = (data.length - pos) / 2;
151
+ }
152
+ if (charLen <= 0) return "";
153
+
154
+ char[] chars = new char[charLen];
155
+ for (int i = 0; i < charLen; i++) {
156
+ chars[i] = (char) ((data[pos + i * 2] & 0xFF) | ((data[pos + i * 2 + 1] & 0xFF) << 8));
157
+ }
158
+ return new String(chars);
159
+ }
160
+ } catch (Exception e) {
161
+ return "";
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 替换字符串
167
+ * @return 替换的次数
168
+ */
169
+ public int replaceString(String oldValue, String newValue) {
170
+ int count = 0;
171
+ for (int i = 0; i < strings.size(); i++) {
172
+ String str = strings.get(i);
173
+ if (str != null && str.contains(oldValue)) {
174
+ strings.set(i, str.replace(oldValue, newValue));
175
+ count++;
176
+ }
177
+ }
178
+ return count;
179
+ }
180
+
181
+ /**
182
+ * 构建修改后的 AXML 数据
183
+ */
184
+ public byte[] build() {
185
+ if (strings.isEmpty() || beforeStringPool == null) {
186
+ return data;
187
+ }
188
+
189
+ try {
190
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
191
+
192
+ // 写入字符串池之前的数据
193
+ baos.write(beforeStringPool);
194
+
195
+ // 构建新的字符串池
196
+ byte[] newStringPool = buildStringPool();
197
+ baos.write(newStringPool);
198
+
199
+ // 写入字符串池之后的数据
200
+ baos.write(afterStringPool);
201
+
202
+ // 更新文件大小
203
+ byte[] result = baos.toByteArray();
204
+ int newFileSize = result.length;
205
+
206
+ // 更新文件头中的大小
207
+ result[4] = (byte) (newFileSize & 0xFF);
208
+ result[5] = (byte) ((newFileSize >> 8) & 0xFF);
209
+ result[6] = (byte) ((newFileSize >> 16) & 0xFF);
210
+ result[7] = (byte) ((newFileSize >> 24) & 0xFF);
211
+
212
+ return result;
213
+ } catch (Exception e) {
214
+ return data;
215
+ }
216
+ }
217
+
218
+ private byte[] buildStringPool() throws Exception {
219
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
220
+
221
+ // 先构建字符串数据
222
+ ByteArrayOutputStream stringData = new ByteArrayOutputStream();
223
+ int[] offsets = new int[strings.size()];
224
+
225
+ for (int i = 0; i < strings.size(); i++) {
226
+ offsets[i] = stringData.size();
227
+ String str = strings.get(i);
228
+ if (str == null) str = "";
229
+
230
+ if (isUtf8) {
231
+ byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
232
+ int charLen = str.length();
233
+ int byteLen = bytes.length;
234
+
235
+ // 写入字符长度
236
+ if (charLen > 127) {
237
+ stringData.write((charLen >> 8) | 0x80);
238
+ stringData.write(charLen & 0xFF);
239
+ } else {
240
+ stringData.write(charLen);
241
+ }
242
+
243
+ // 写入字节长度
244
+ if (byteLen > 127) {
245
+ stringData.write((byteLen >> 8) | 0x80);
246
+ stringData.write(byteLen & 0xFF);
247
+ } else {
248
+ stringData.write(byteLen);
249
+ }
250
+
251
+ // 写入字符串数据
252
+ stringData.write(bytes);
253
+ stringData.write(0); // null terminator
254
+ } else {
255
+ // UTF-16
256
+ int charLen = str.length();
257
+ if (charLen > 0x7FFF) {
258
+ stringData.write((charLen & 0xFF) | 0x80);
259
+ stringData.write((charLen >> 8) | 0x80);
260
+ stringData.write((charLen >> 16) & 0xFF);
261
+ stringData.write((charLen >> 24) & 0xFF);
262
+ } else {
263
+ stringData.write(charLen & 0xFF);
264
+ stringData.write((charLen >> 8) & 0xFF);
265
+ }
266
+
267
+ for (int j = 0; j < str.length(); j++) {
268
+ char c = str.charAt(j);
269
+ stringData.write(c & 0xFF);
270
+ stringData.write((c >> 8) & 0xFF);
271
+ }
272
+ // null terminator
273
+ stringData.write(0);
274
+ stringData.write(0);
275
+ }
276
+ }
277
+
278
+ // 计算头部大小
279
+ int headerSize = 28; // 固定头部
280
+ int offsetTableSize = strings.size() * 4;
281
+ int styleOffsetTableSize = styleCount * 4;
282
+ int stringsDataSize = stringData.size();
283
+
284
+ // 对齐到 4 字节
285
+ int padding = (4 - (stringsDataSize % 4)) % 4;
286
+ stringsDataSize += padding;
287
+
288
+ int newStringsOffset = headerSize + offsetTableSize + styleOffsetTableSize;
289
+ int totalSize = newStringsOffset + stringsDataSize;
290
+
291
+ // 写入 chunk 头
292
+ ByteBuffer header = ByteBuffer.allocate(headerSize);
293
+ header.order(ByteOrder.LITTLE_ENDIAN);
294
+ header.putShort((short) STRING_POOL_TYPE); // type
295
+ header.putShort((short) headerSize); // header size
296
+ header.putInt(totalSize); // chunk size
297
+ header.putInt(strings.size()); // string count
298
+ header.putInt(styleCount); // style count
299
+ header.putInt(flags); // flags
300
+ header.putInt(newStringsOffset); // strings offset
301
+ header.putInt(0); // styles offset (暂不支持)
302
+
303
+ baos.write(header.array());
304
+
305
+ // 写入偏移表
306
+ for (int offset : offsets) {
307
+ baos.write(offset & 0xFF);
308
+ baos.write((offset >> 8) & 0xFF);
309
+ baos.write((offset >> 16) & 0xFF);
310
+ baos.write((offset >> 24) & 0xFF);
311
+ }
312
+
313
+ // 写入样式偏移表(空)
314
+ for (int i = 0; i < styleCount; i++) {
315
+ baos.write(0);
316
+ baos.write(0);
317
+ baos.write(0);
318
+ baos.write(0);
319
+ }
320
+
321
+ // 写入字符串数据
322
+ baos.write(stringData.toByteArray());
323
+
324
+ // 写入对齐填充
325
+ for (int i = 0; i < padding; i++) {
326
+ baos.write(0);
327
+ }
328
+
329
+ return baos.toByteArray();
330
+ }
331
+ }
@@ -538,6 +538,25 @@ public class DexEditorPluginPlugin extends Plugin {
538
538
  ));
539
539
  break;
540
540
 
541
+ case "listApkFiles":
542
+ result.put("data", dexManager.listApkFiles(
543
+ params.getString("apkPath"),
544
+ params.optString("filter", ""),
545
+ params.optInt("limit", 100),
546
+ params.optInt("offset", 0)
547
+ ));
548
+ break;
549
+
550
+ case "readApkFile":
551
+ result.put("data", dexManager.readApkFile(
552
+ params.getString("apkPath"),
553
+ params.getString("filePath"),
554
+ params.optBoolean("asBase64", false),
555
+ params.optInt("maxBytes", 0),
556
+ params.optInt("offset", 0)
557
+ ));
558
+ break;
559
+
541
560
  default:
542
561
  result.put("success", false);
543
562
  result.put("error", "Unknown action: " + action);
@@ -2407,31 +2407,39 @@ public class DexManager {
2407
2407
  // ==================== XML/资源操作方法 ====================
2408
2408
 
2409
2409
  /**
2410
- * 获取 APK 的 AndroidManifest.xml(使用 ARSCLib 解码为可读 XML)
2410
+ * 获取 APK 的 AndroidManifest.xml(解码为可读 XML)
2411
2411
  */
2412
2412
  public JSObject getManifestFromApk(String apkPath) throws Exception {
2413
2413
  JSObject result = new JSObject();
2414
2414
 
2415
+ java.util.zip.ZipFile zipFile = null;
2415
2416
  try {
2416
- // 使用 ARSCLib 读取 Manifest
2417
- com.reandroid.apk.ApkModule apkModule = com.reandroid.apk.ApkModule.loadApkFile(new java.io.File(apkPath));
2418
- com.reandroid.arsc.chunk.xml.AndroidManifestBlock manifest = apkModule.getAndroidManifest();
2417
+ zipFile = new java.util.zip.ZipFile(apkPath);
2418
+ java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
2419
2419
 
2420
- if (manifest == null) {
2421
- apkModule.close();
2420
+ if (manifestEntry == null) {
2422
2421
  throw new Exception("AndroidManifest.xml not found in APK");
2423
2422
  }
2424
2423
 
2425
- // 序列化为可读 XML
2426
- String xmlContent = manifest.serializeToXml();
2427
- apkModule.close();
2424
+ java.io.InputStream is = zipFile.getInputStream(manifestEntry);
2425
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
2426
+ byte[] buffer = new byte[8192];
2427
+ int len;
2428
+ while ((len = is.read(buffer)) != -1) {
2429
+ baos.write(buffer, 0, len);
2430
+ }
2431
+ is.close();
2432
+
2433
+ // 解析二进制 AXML
2434
+ byte[] axmlData = baos.toByteArray();
2435
+ String xmlContent = decodeAxml(axmlData);
2428
2436
 
2429
2437
  result.put("manifest", xmlContent);
2430
2438
 
2431
- } catch (Exception e) {
2432
- Log.e(TAG, "Get manifest error: " + e.getMessage(), e);
2433
- // 回退到旧实现
2434
- result.put("manifest", getManifestFallback(apkPath));
2439
+ } finally {
2440
+ if (zipFile != null) {
2441
+ try { zipFile.close(); } catch (Exception ignored) {}
2442
+ }
2435
2443
  }
2436
2444
 
2437
2445
  return result;
@@ -2661,7 +2669,7 @@ public class DexManager {
2661
2669
  }
2662
2670
 
2663
2671
  /**
2664
- * 精准替换 AndroidManifest.xml 中的字符串(使用 ARSCLib)
2672
+ * 精准替换 AndroidManifest.xml 中的字符串(二进制替换,支持任意长度)
2665
2673
  */
2666
2674
  public JSObject replaceInManifest(String apkPath, org.json.JSONArray replacements) throws Exception {
2667
2675
  JSObject result = new JSObject();
@@ -2669,36 +2677,37 @@ public class DexManager {
2669
2677
  int replacedCount = 0;
2670
2678
 
2671
2679
  try {
2672
- // 使用 ARSCLib 读取和修改 Manifest
2673
- com.reandroid.apk.ApkModule apkModule = com.reandroid.apk.ApkModule.loadApkFile(new java.io.File(apkPath));
2674
- com.reandroid.arsc.chunk.xml.AndroidManifestBlock manifest = apkModule.getAndroidManifest();
2680
+ // 读取 APK 中的 AndroidManifest.xml
2681
+ java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(apkPath);
2682
+ java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
2675
2683
 
2676
- if (manifest == null) {
2677
- apkModule.close();
2684
+ if (manifestEntry == null) {
2685
+ zipFile.close();
2678
2686
  throw new Exception("AndroidManifest.xml not found in APK");
2679
2687
  }
2680
2688
 
2681
- // 遍历所有字符串并替换
2682
- com.reandroid.arsc.pool.ResXmlStringPool stringPool = manifest.getStringPool();
2689
+ // 读取 AXML 数据
2690
+ java.io.InputStream is = zipFile.getInputStream(manifestEntry);
2691
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
2692
+ byte[] buffer = new byte[8192];
2693
+ int len;
2694
+ while ((len = is.read(buffer)) != -1) {
2695
+ baos.write(buffer, 0, len);
2696
+ }
2697
+ is.close();
2698
+ zipFile.close();
2699
+
2700
+ byte[] axmlData = baos.toByteArray();
2701
+
2702
+ // 使用 AxmlEditor 执行替换(支持任意长度)
2703
+ AxmlEditor editor = new AxmlEditor(axmlData);
2683
2704
 
2684
2705
  for (int i = 0; i < replacements.length(); i++) {
2685
2706
  org.json.JSONObject replacement = replacements.getJSONObject(i);
2686
2707
  String oldValue = replacement.getString("oldValue");
2687
2708
  String newValue = replacement.getString("newValue");
2688
2709
 
2689
- int count = 0;
2690
-
2691
- // 遍历字符串池中的所有字符串
2692
- for (com.reandroid.arsc.item.StringItem stringItem : stringPool.listItems()) {
2693
- if (stringItem != null) {
2694
- String str = stringItem.get();
2695
- if (str != null && str.contains(oldValue)) {
2696
- String newStr = str.replace(oldValue, newValue);
2697
- stringItem.set(newStr);
2698
- count++;
2699
- }
2700
- }
2701
- }
2710
+ int count = editor.replaceString(oldValue, newValue);
2702
2711
 
2703
2712
  JSObject detail = new JSObject();
2704
2713
  detail.put("oldValue", oldValue);
@@ -2710,7 +2719,6 @@ public class DexManager {
2710
2719
  }
2711
2720
 
2712
2721
  if (replacedCount == 0) {
2713
- apkModule.close();
2714
2722
  result.put("success", true);
2715
2723
  result.put("replacedCount", 0);
2716
2724
  result.put("details", details);
@@ -2718,16 +2726,55 @@ public class DexManager {
2718
2726
  return result;
2719
2727
  }
2720
2728
 
2721
- // 刷新并保存
2722
- manifest.refresh();
2729
+ // 获取修改后的数据
2730
+ byte[] modifiedData = editor.build();
2723
2731
 
2724
- // 保存 APK
2732
+ // 替换 APK 中的 AndroidManifest.xml
2733
+ java.io.File apkFile = new java.io.File(apkPath);
2725
2734
  java.io.File tempApk = new java.io.File(apkPath + ".tmp");
2726
- apkModule.writeApk(tempApk);
2727
- apkModule.close();
2735
+
2736
+ java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
2737
+ new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
2738
+ java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
2739
+ new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
2740
+
2741
+ java.util.zip.ZipEntry entry;
2742
+ while ((entry = zis.getNextEntry()) != null) {
2743
+ if (entry.getName().equals("AndroidManifest.xml")) {
2744
+ // 写入修改后的 Manifest
2745
+ java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
2746
+ newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
2747
+ zos.putNextEntry(newEntry);
2748
+ zos.write(modifiedData);
2749
+ zos.closeEntry();
2750
+ } else {
2751
+ // 复制其他文件
2752
+ java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
2753
+ newEntry.setTime(entry.getTime());
2754
+ if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
2755
+ newEntry.setMethod(java.util.zip.ZipEntry.STORED);
2756
+ newEntry.setSize(entry.getSize());
2757
+ newEntry.setCrc(entry.getCrc());
2758
+ } else {
2759
+ newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
2760
+ }
2761
+ zos.putNextEntry(newEntry);
2762
+ if (!entry.isDirectory()) {
2763
+ byte[] buf = new byte[8192];
2764
+ int n;
2765
+ while ((n = zis.read(buf)) != -1) {
2766
+ zos.write(buf, 0, n);
2767
+ }
2768
+ }
2769
+ zos.closeEntry();
2770
+ }
2771
+ zis.closeEntry();
2772
+ }
2773
+
2774
+ zis.close();
2775
+ zos.close();
2728
2776
 
2729
2777
  // 替换原文件
2730
- java.io.File apkFile = new java.io.File(apkPath);
2731
2778
  if (!apkFile.delete()) {
2732
2779
  Log.e(TAG, "Failed to delete original APK");
2733
2780
  }
@@ -2926,6 +2973,171 @@ public class DexManager {
2926
2973
  return pos + 2;
2927
2974
  }
2928
2975
 
2976
+ /**
2977
+ * 列出 APK 中的所有文件
2978
+ */
2979
+ public JSObject listApkFiles(String apkPath, String filter, int limit, int offset) throws Exception {
2980
+ JSObject result = new JSObject();
2981
+ JSArray files = new JSArray();
2982
+
2983
+ java.util.zip.ZipFile zipFile = null;
2984
+ try {
2985
+ zipFile = new java.util.zip.ZipFile(apkPath);
2986
+ java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
2987
+
2988
+ java.util.List<JSObject> allFiles = new java.util.ArrayList<>();
2989
+
2990
+ while (entries.hasMoreElements()) {
2991
+ java.util.zip.ZipEntry entry = entries.nextElement();
2992
+ String name = entry.getName();
2993
+
2994
+ // 应用过滤
2995
+ if (!filter.isEmpty() && !name.contains(filter)) {
2996
+ continue;
2997
+ }
2998
+
2999
+ JSObject fileInfo = new JSObject();
3000
+ fileInfo.put("path", name);
3001
+ fileInfo.put("size", entry.getSize());
3002
+ fileInfo.put("compressedSize", entry.getCompressedSize());
3003
+ fileInfo.put("isDirectory", entry.isDirectory());
3004
+
3005
+ // 判断文件类型
3006
+ String type = "unknown";
3007
+ if (name.endsWith(".dex")) type = "dex";
3008
+ else if (name.endsWith(".so")) type = "native";
3009
+ else if (name.endsWith(".xml")) type = "xml";
3010
+ else if (name.startsWith("res/")) type = "resource";
3011
+ else if (name.startsWith("assets/")) type = "asset";
3012
+ else if (name.startsWith("lib/")) type = "native";
3013
+ else if (name.startsWith("META-INF/")) type = "meta";
3014
+ else if (name.equals("AndroidManifest.xml")) type = "manifest";
3015
+ else if (name.equals("resources.arsc")) type = "arsc";
3016
+ fileInfo.put("type", type);
3017
+
3018
+ allFiles.add(fileInfo);
3019
+ }
3020
+
3021
+ int total = allFiles.size();
3022
+
3023
+ // 分页
3024
+ int start = Math.min(offset, total);
3025
+ int end = Math.min(offset + limit, total);
3026
+
3027
+ for (int i = start; i < end; i++) {
3028
+ files.put(allFiles.get(i));
3029
+ }
3030
+
3031
+ result.put("files", files);
3032
+ result.put("total", total);
3033
+ result.put("offset", offset);
3034
+ result.put("limit", limit);
3035
+ result.put("returned", files.length());
3036
+ result.put("hasMore", end < total);
3037
+
3038
+ } finally {
3039
+ if (zipFile != null) {
3040
+ try { zipFile.close(); } catch (Exception ignored) {}
3041
+ }
3042
+ }
3043
+
3044
+ return result;
3045
+ }
3046
+
3047
+ /**
3048
+ * 读取 APK 中的任意文件
3049
+ */
3050
+ public JSObject readApkFile(String apkPath, String filePath, boolean asBase64, int maxBytes, int offset) throws Exception {
3051
+ JSObject result = new JSObject();
3052
+
3053
+ java.util.zip.ZipFile zipFile = null;
3054
+ try {
3055
+ zipFile = new java.util.zip.ZipFile(apkPath);
3056
+ java.util.zip.ZipEntry entry = zipFile.getEntry(filePath);
3057
+
3058
+ if (entry == null) {
3059
+ result.put("error", "File not found: " + filePath);
3060
+ return result;
3061
+ }
3062
+
3063
+ result.put("path", filePath);
3064
+ result.put("size", entry.getSize());
3065
+ result.put("compressedSize", entry.getCompressedSize());
3066
+
3067
+ java.io.InputStream is = zipFile.getInputStream(entry);
3068
+
3069
+ // 跳过偏移量
3070
+ if (offset > 0) {
3071
+ is.skip(offset);
3072
+ }
3073
+
3074
+ // 读取数据
3075
+ int readSize = maxBytes > 0 ? maxBytes : (int) entry.getSize();
3076
+ if (readSize > 1024 * 1024) { // 限制最大 1MB
3077
+ readSize = 1024 * 1024;
3078
+ }
3079
+
3080
+ byte[] buffer = new byte[readSize];
3081
+ int totalRead = 0;
3082
+ int read;
3083
+ while (totalRead < readSize && (read = is.read(buffer, totalRead, readSize - totalRead)) != -1) {
3084
+ totalRead += read;
3085
+ }
3086
+ is.close();
3087
+
3088
+ byte[] data = new byte[totalRead];
3089
+ System.arraycopy(buffer, 0, data, 0, totalRead);
3090
+
3091
+ if (asBase64) {
3092
+ // Base64 编码返回
3093
+ result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
3094
+ result.put("encoding", "base64");
3095
+ } else {
3096
+ // 尝试作为文本返回
3097
+ String content = new String(data, java.nio.charset.StandardCharsets.UTF_8);
3098
+
3099
+ // 检查是否是二进制文件
3100
+ boolean isBinary = false;
3101
+ for (int i = 0; i < Math.min(100, data.length); i++) {
3102
+ if (data[i] == 0) {
3103
+ isBinary = true;
3104
+ break;
3105
+ }
3106
+ }
3107
+
3108
+ if (isBinary && !filePath.endsWith(".xml")) {
3109
+ // 二进制文件自动使用 Base64
3110
+ result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
3111
+ result.put("encoding", "base64");
3112
+ result.put("note", "Binary file, auto-encoded as base64");
3113
+ } else {
3114
+ // 如果是 XML 文件,尝试解码 AXML
3115
+ if (filePath.endsWith(".xml") && data.length > 4) {
3116
+ int magic = (data[0] & 0xFF) | ((data[1] & 0xFF) << 8) |
3117
+ ((data[2] & 0xFF) << 16) | ((data[3] & 0xFF) << 24);
3118
+ if (magic == 0x00080003) {
3119
+ // 是 AXML 格式,解码
3120
+ content = AxmlParser.decode(data);
3121
+ }
3122
+ }
3123
+ result.put("content", content);
3124
+ result.put("encoding", "text");
3125
+ }
3126
+ }
3127
+
3128
+ result.put("offset", offset);
3129
+ result.put("bytesRead", totalRead);
3130
+ result.put("hasMore", offset + totalRead < entry.getSize());
3131
+
3132
+ } finally {
3133
+ if (zipFile != null) {
3134
+ try { zipFile.close(); } catch (Exception ignored) {}
3135
+ }
3136
+ }
3137
+
3138
+ return result;
3139
+ }
3140
+
2929
3141
  /**
2930
3142
  * 清理临时目录
2931
3143
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
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",