capacitor-dex-editor 0.0.31 → 0.0.33

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.
@@ -67,6 +67,9 @@ dependencies {
67
67
  // APK Signer - V1/V2/V3/V4 签名支持 (Android 7.0+)
68
68
  api 'com.android.tools.build:apksig:8.7.2'
69
69
 
70
+ // ARSCLib - 二进制 XML 解析和修改(无限制)
71
+ api 'io.github.nicholsontech:arsc:2.6.3'
72
+
70
73
  testImplementation "junit:junit:$junitVersion"
71
74
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
72
75
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -531,6 +531,13 @@ public class DexEditorPluginPlugin extends Plugin {
531
531
  ));
532
532
  break;
533
533
 
534
+ case "replaceInManifest":
535
+ result.put("data", dexManager.replaceInManifest(
536
+ params.getString("apkPath"),
537
+ params.getJSONArray("replacements")
538
+ ));
539
+ break;
540
+
534
541
  default:
535
542
  result.put("success", false);
536
543
  result.put("error", "Unknown action: " + action);
@@ -2407,18 +2407,47 @@ public class DexManager {
2407
2407
  // ==================== XML/资源操作方法 ====================
2408
2408
 
2409
2409
  /**
2410
- * 获取 APK 的 AndroidManifest.xml(解码为可读 XML)
2410
+ * 获取 APK 的 AndroidManifest.xml(使用 ARSCLib 解码为可读 XML)
2411
2411
  */
2412
2412
  public JSObject getManifestFromApk(String apkPath) throws Exception {
2413
2413
  JSObject result = new JSObject();
2414
2414
 
2415
+ 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();
2419
+
2420
+ if (manifest == null) {
2421
+ apkModule.close();
2422
+ throw new Exception("AndroidManifest.xml not found in APK");
2423
+ }
2424
+
2425
+ // 序列化为可读 XML
2426
+ String xmlContent = manifest.serializeToXml();
2427
+ apkModule.close();
2428
+
2429
+ result.put("manifest", xmlContent);
2430
+
2431
+ } catch (Exception e) {
2432
+ Log.e(TAG, "Get manifest error: " + e.getMessage(), e);
2433
+ // 回退到旧实现
2434
+ result.put("manifest", getManifestFallback(apkPath));
2435
+ }
2436
+
2437
+ return result;
2438
+ }
2439
+
2440
+ /**
2441
+ * 获取 Manifest 的回退实现(使用简单 AXML 解析器)
2442
+ */
2443
+ private String getManifestFallback(String apkPath) {
2415
2444
  java.util.zip.ZipFile zipFile = null;
2416
2445
  try {
2417
2446
  zipFile = new java.util.zip.ZipFile(apkPath);
2418
2447
  java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
2419
2448
 
2420
2449
  if (manifestEntry == null) {
2421
- throw new Exception("AndroidManifest.xml not found in APK");
2450
+ return "# AndroidManifest.xml not found";
2422
2451
  }
2423
2452
 
2424
2453
  java.io.InputStream is = zipFile.getInputStream(manifestEntry);
@@ -2430,19 +2459,15 @@ public class DexManager {
2430
2459
  }
2431
2460
  is.close();
2432
2461
 
2433
- // 解析二进制 AXML
2434
- byte[] axmlData = baos.toByteArray();
2435
- String xmlContent = decodeAxml(axmlData);
2436
-
2437
- result.put("manifest", xmlContent);
2462
+ return decodeAxml(baos.toByteArray());
2438
2463
 
2464
+ } catch (Exception e) {
2465
+ return "# Error reading manifest: " + e.getMessage();
2439
2466
  } finally {
2440
2467
  if (zipFile != null) {
2441
2468
  try { zipFile.close(); } catch (Exception ignored) {}
2442
2469
  }
2443
2470
  }
2444
-
2445
- return result;
2446
2471
  }
2447
2472
 
2448
2473
  /**
@@ -2635,6 +2660,271 @@ public class DexManager {
2635
2660
  return result;
2636
2661
  }
2637
2662
 
2663
+ /**
2664
+ * 精准替换 AndroidManifest.xml 中的字符串(使用 ARSCLib,无长度限制)
2665
+ */
2666
+ public JSObject replaceInManifest(String apkPath, org.json.JSONArray replacements) throws Exception {
2667
+ JSObject result = new JSObject();
2668
+ JSArray details = new JSArray();
2669
+ int replacedCount = 0;
2670
+
2671
+ 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();
2675
+
2676
+ if (manifest == null) {
2677
+ throw new Exception("AndroidManifest.xml not found in APK");
2678
+ }
2679
+
2680
+ // 获取 XML 字符串进行替换
2681
+ String xmlContent = manifest.serializeToXml();
2682
+ String originalContent = xmlContent;
2683
+
2684
+ // 执行替换
2685
+ for (int i = 0; i < replacements.length(); i++) {
2686
+ org.json.JSONObject replacement = replacements.getJSONObject(i);
2687
+ String oldValue = replacement.getString("oldValue");
2688
+ String newValue = replacement.getString("newValue");
2689
+
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
+ }
2700
+
2701
+ JSObject detail = new JSObject();
2702
+ detail.put("oldValue", oldValue);
2703
+ detail.put("newValue", newValue);
2704
+ detail.put("count", count);
2705
+ details.put(detail);
2706
+
2707
+ replacedCount += count;
2708
+ }
2709
+
2710
+ if (replacedCount == 0) {
2711
+ apkModule.close();
2712
+ result.put("success", true);
2713
+ result.put("replacedCount", 0);
2714
+ result.put("details", details);
2715
+ result.put("message", "未找到匹配的字符串");
2716
+ return result;
2717
+ }
2718
+
2719
+ // 解析修改后的 XML 并更新 Manifest
2720
+ manifest.parseXmlString(xmlContent);
2721
+ manifest.refresh();
2722
+
2723
+ // 保存 APK
2724
+ java.io.File tempApk = new java.io.File(apkPath + ".tmp");
2725
+ apkModule.writeApk(tempApk);
2726
+ apkModule.close();
2727
+
2728
+ // 替换原文件
2729
+ java.io.File apkFile = new java.io.File(apkPath);
2730
+ if (!apkFile.delete()) {
2731
+ Log.e(TAG, "Failed to delete original APK");
2732
+ }
2733
+ if (!tempApk.renameTo(apkFile)) {
2734
+ copyFile(tempApk, apkFile);
2735
+ tempApk.delete();
2736
+ }
2737
+
2738
+ result.put("success", true);
2739
+ result.put("replacedCount", replacedCount);
2740
+ result.put("details", details);
2741
+
2742
+ } catch (Exception e) {
2743
+ Log.e(TAG, "Replace in manifest error: " + e.getMessage(), e);
2744
+ result.put("success", false);
2745
+ result.put("error", e.getMessage());
2746
+ }
2747
+
2748
+ return result;
2749
+ }
2750
+
2751
+ /**
2752
+ * 替换结果
2753
+ */
2754
+ private static class ReplaceResult {
2755
+ byte[] data;
2756
+ int count;
2757
+
2758
+ ReplaceResult(byte[] data, int count) {
2759
+ this.data = data;
2760
+ this.count = count;
2761
+ }
2762
+ }
2763
+
2764
+ /**
2765
+ * 在 AXML 二进制数据中替换字符串
2766
+ * 直接修改字符串池中的字符串
2767
+ */
2768
+ private ReplaceResult replaceStringInAxml(byte[] data, String oldValue, String newValue) {
2769
+ int count = 0;
2770
+
2771
+ try {
2772
+ // 解析 AXML 结构找到字符串池
2773
+ if (data.length < 8) return new ReplaceResult(data, 0);
2774
+
2775
+ // 检查魔数
2776
+ int magic = (data[0] & 0xFF) | ((data[1] & 0xFF) << 8) |
2777
+ ((data[2] & 0xFF) << 16) | ((data[3] & 0xFF) << 24);
2778
+ if (magic != 0x00080003) return new ReplaceResult(data, 0);
2779
+
2780
+ // 找到字符串池 chunk (类型 0x0001)
2781
+ int pos = 8;
2782
+ while (pos < data.length - 8) {
2783
+ int chunkType = (data[pos] & 0xFF) | ((data[pos + 1] & 0xFF) << 8);
2784
+ int headerSize = (data[pos + 2] & 0xFF) | ((data[pos + 3] & 0xFF) << 8);
2785
+ int chunkSize = (data[pos + 4] & 0xFF) | ((data[pos + 5] & 0xFF) << 8) |
2786
+ ((data[pos + 6] & 0xFF) << 16) | ((data[pos + 7] & 0xFF) << 24);
2787
+
2788
+ if (chunkType == 0x0001) {
2789
+ // 字符串池 chunk
2790
+ int stringCount = (data[pos + 8] & 0xFF) | ((data[pos + 9] & 0xFF) << 8) |
2791
+ ((data[pos + 10] & 0xFF) << 16) | ((data[pos + 11] & 0xFF) << 24);
2792
+ int styleCount = (data[pos + 12] & 0xFF) | ((data[pos + 13] & 0xFF) << 8) |
2793
+ ((data[pos + 14] & 0xFF) << 16) | ((data[pos + 15] & 0xFF) << 24);
2794
+ int flags = (data[pos + 16] & 0xFF) | ((data[pos + 17] & 0xFF) << 8) |
2795
+ ((data[pos + 18] & 0xFF) << 16) | ((data[pos + 19] & 0xFF) << 24);
2796
+ int stringsOffset = (data[pos + 20] & 0xFF) | ((data[pos + 21] & 0xFF) << 8) |
2797
+ ((data[pos + 22] & 0xFF) << 16) | ((data[pos + 23] & 0xFF) << 24);
2798
+
2799
+ boolean isUtf8 = (flags & 0x100) != 0;
2800
+
2801
+ // 读取字符串偏移表
2802
+ int offsetTableStart = pos + 28;
2803
+ int stringsStart = pos + stringsOffset;
2804
+
2805
+ // 遍历所有字符串
2806
+ for (int i = 0; i < stringCount; i++) {
2807
+ int offsetPos = offsetTableStart + i * 4;
2808
+ int stringOffset = (data[offsetPos] & 0xFF) | ((data[offsetPos + 1] & 0xFF) << 8) |
2809
+ ((data[offsetPos + 2] & 0xFF) << 16) | ((data[offsetPos + 3] & 0xFF) << 24);
2810
+
2811
+ int stringPos = stringsStart + stringOffset;
2812
+ if (stringPos >= data.length) continue;
2813
+
2814
+ // 读取当前字符串
2815
+ String currentString = readStringFromAxml(data, stringPos, isUtf8);
2816
+
2817
+ // 检查是否匹配
2818
+ if (currentString.equals(oldValue)) {
2819
+ // 执行替换(仅当新字符串长度 <= 旧字符串长度时可以直接替换)
2820
+ if (isUtf8) {
2821
+ byte[] newBytes = newValue.getBytes(java.nio.charset.StandardCharsets.UTF_8);
2822
+ byte[] oldBytes = oldValue.getBytes(java.nio.charset.StandardCharsets.UTF_8);
2823
+
2824
+ if (newBytes.length <= oldBytes.length) {
2825
+ // 可以直接替换
2826
+ int dataStart = getStringDataStart(data, stringPos, isUtf8);
2827
+
2828
+ // 更新长度
2829
+ if (newBytes.length < 128) {
2830
+ data[stringPos] = (byte) newValue.length();
2831
+ data[stringPos + 1] = (byte) newBytes.length;
2832
+ }
2833
+
2834
+ // 写入新数据
2835
+ System.arraycopy(newBytes, 0, data, dataStart, newBytes.length);
2836
+
2837
+ // 用 0 填充剩余空间
2838
+ for (int j = newBytes.length; j < oldBytes.length; j++) {
2839
+ data[dataStart + j] = 0;
2840
+ }
2841
+
2842
+ count++;
2843
+ } else {
2844
+ Log.w(TAG, "New string is longer than old string, cannot replace: " + oldValue);
2845
+ }
2846
+ }
2847
+ }
2848
+ }
2849
+ break;
2850
+ }
2851
+
2852
+ pos += chunkSize;
2853
+ }
2854
+ } catch (Exception e) {
2855
+ Log.e(TAG, "Replace string error: " + e.getMessage(), e);
2856
+ }
2857
+
2858
+ return new ReplaceResult(data, count);
2859
+ }
2860
+
2861
+ /**
2862
+ * 从 AXML 数据中读取字符串
2863
+ */
2864
+ private String readStringFromAxml(byte[] data, int pos, boolean isUtf8) {
2865
+ try {
2866
+ if (isUtf8) {
2867
+ int charLen = data[pos] & 0xFF;
2868
+ int byteLen;
2869
+ int dataStart;
2870
+
2871
+ if ((charLen & 0x80) != 0) {
2872
+ charLen = ((charLen & 0x7F) << 8) | (data[pos + 1] & 0xFF);
2873
+ byteLen = data[pos + 2] & 0xFF;
2874
+ if ((byteLen & 0x80) != 0) {
2875
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 3] & 0xFF);
2876
+ dataStart = pos + 4;
2877
+ } else {
2878
+ dataStart = pos + 3;
2879
+ }
2880
+ } else {
2881
+ byteLen = data[pos + 1] & 0xFF;
2882
+ if ((byteLen & 0x80) != 0) {
2883
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 2] & 0xFF);
2884
+ dataStart = pos + 3;
2885
+ } else {
2886
+ dataStart = pos + 2;
2887
+ }
2888
+ }
2889
+
2890
+ if (dataStart + byteLen > data.length) {
2891
+ byteLen = data.length - dataStart;
2892
+ }
2893
+ if (byteLen <= 0) return "";
2894
+
2895
+ return new String(data, dataStart, byteLen, java.nio.charset.StandardCharsets.UTF_8);
2896
+ }
2897
+ } catch (Exception e) {
2898
+ return "";
2899
+ }
2900
+ return "";
2901
+ }
2902
+
2903
+ /**
2904
+ * 获取字符串数据开始位置
2905
+ */
2906
+ private int getStringDataStart(byte[] data, int pos, boolean isUtf8) {
2907
+ if (isUtf8) {
2908
+ int charLen = data[pos] & 0xFF;
2909
+ if ((charLen & 0x80) != 0) {
2910
+ int byteLen = data[pos + 2] & 0xFF;
2911
+ if ((byteLen & 0x80) != 0) {
2912
+ return pos + 4;
2913
+ } else {
2914
+ return pos + 3;
2915
+ }
2916
+ } else {
2917
+ int byteLen = data[pos + 1] & 0xFF;
2918
+ if ((byteLen & 0x80) != 0) {
2919
+ return pos + 3;
2920
+ } else {
2921
+ return pos + 2;
2922
+ }
2923
+ }
2924
+ }
2925
+ return pos + 2;
2926
+ }
2927
+
2638
2928
  /**
2639
2929
  * 清理临时目录
2640
2930
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
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",