capacitor-dex-editor 0.0.35 → 0.0.37

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
+ }
@@ -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,34 +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) {
2684
+ if (manifestEntry == null) {
2685
+ zipFile.close();
2677
2686
  throw new Exception("AndroidManifest.xml not found in APK");
2678
2687
  }
2679
2688
 
2680
- // 获取 XML 字符串进行替换
2681
- String xmlContent = manifest.serializeToXml();
2682
- String originalContent = xmlContent;
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
- // 执行替换
2685
2705
  for (int i = 0; i < replacements.length(); i++) {
2686
2706
  org.json.JSONObject replacement = replacements.getJSONObject(i);
2687
2707
  String oldValue = replacement.getString("oldValue");
2688
2708
  String newValue = replacement.getString("newValue");
2689
2709
 
2690
- int count = 0;
2691
- String before = xmlContent;
2692
- xmlContent = xmlContent.replace(oldValue, newValue);
2693
-
2694
- // 计算替换次数
2695
- int idx = 0;
2696
- while ((idx = before.indexOf(oldValue, idx)) != -1) {
2697
- count++;
2698
- idx += oldValue.length();
2699
- }
2710
+ int count = editor.replaceString(oldValue, newValue);
2700
2711
 
2701
2712
  JSObject detail = new JSObject();
2702
2713
  detail.put("oldValue", oldValue);
@@ -2708,7 +2719,6 @@ public class DexManager {
2708
2719
  }
2709
2720
 
2710
2721
  if (replacedCount == 0) {
2711
- apkModule.close();
2712
2722
  result.put("success", true);
2713
2723
  result.put("replacedCount", 0);
2714
2724
  result.put("details", details);
@@ -2716,17 +2726,55 @@ public class DexManager {
2716
2726
  return result;
2717
2727
  }
2718
2728
 
2719
- // 解析修改后的 XML 并更新 Manifest
2720
- manifest.parseXmlString(xmlContent);
2721
- manifest.refresh();
2729
+ // 获取修改后的数据
2730
+ byte[] modifiedData = editor.build();
2722
2731
 
2723
- // 保存 APK
2732
+ // 替换 APK 中的 AndroidManifest.xml
2733
+ java.io.File apkFile = new java.io.File(apkPath);
2724
2734
  java.io.File tempApk = new java.io.File(apkPath + ".tmp");
2725
- apkModule.writeApk(tempApk);
2726
- 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();
2727
2776
 
2728
2777
  // 替换原文件
2729
- java.io.File apkFile = new java.io.File(apkPath);
2730
2778
  if (!apkFile.delete()) {
2731
2779
  Log.e(TAG, "Failed to delete original APK");
2732
2780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
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",