capacitor-dex-editor 0.0.31 → 0.0.32

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.
@@ -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);
@@ -2635,6 +2635,307 @@ public class DexManager {
2635
2635
  return result;
2636
2636
  }
2637
2637
 
2638
+ /**
2639
+ * 精准替换 AndroidManifest.xml 中的字符串(直接修改二进制 AXML)
2640
+ */
2641
+ public JSObject replaceInManifest(String apkPath, org.json.JSONArray replacements) throws Exception {
2642
+ JSObject result = new JSObject();
2643
+ JSArray details = new JSArray();
2644
+ int replacedCount = 0;
2645
+
2646
+ try {
2647
+ // 读取 APK 中的 AndroidManifest.xml
2648
+ java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(apkPath);
2649
+ java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
2650
+
2651
+ if (manifestEntry == null) {
2652
+ throw new Exception("AndroidManifest.xml not found in APK");
2653
+ }
2654
+
2655
+ // 读取 AXML 数据
2656
+ java.io.InputStream is = zipFile.getInputStream(manifestEntry);
2657
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
2658
+ byte[] buffer = new byte[8192];
2659
+ int len;
2660
+ while ((len = is.read(buffer)) != -1) {
2661
+ baos.write(buffer, 0, len);
2662
+ }
2663
+ is.close();
2664
+ zipFile.close();
2665
+
2666
+ byte[] axmlData = baos.toByteArray();
2667
+
2668
+ // 执行替换
2669
+ for (int i = 0; i < replacements.length(); i++) {
2670
+ org.json.JSONObject replacement = replacements.getJSONObject(i);
2671
+ String oldValue = replacement.getString("oldValue");
2672
+ String newValue = replacement.getString("newValue");
2673
+
2674
+ // 在 AXML 中替换字符串
2675
+ ReplaceResult replaceResult = replaceStringInAxml(axmlData, oldValue, newValue);
2676
+ axmlData = replaceResult.data;
2677
+
2678
+ JSObject detail = new JSObject();
2679
+ detail.put("oldValue", oldValue);
2680
+ detail.put("newValue", newValue);
2681
+ detail.put("count", replaceResult.count);
2682
+ details.put(detail);
2683
+
2684
+ replacedCount += replaceResult.count;
2685
+ }
2686
+
2687
+ if (replacedCount == 0) {
2688
+ result.put("success", true);
2689
+ result.put("replacedCount", 0);
2690
+ result.put("details", details);
2691
+ result.put("message", "未找到匹配的字符串");
2692
+ return result;
2693
+ }
2694
+
2695
+ // 替换 APK 中的 AndroidManifest.xml
2696
+ java.io.File apkFile = new java.io.File(apkPath);
2697
+ java.io.File tempApk = new java.io.File(apkPath + ".tmp");
2698
+
2699
+ java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
2700
+ new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
2701
+ java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
2702
+ new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
2703
+
2704
+ java.util.zip.ZipEntry entry;
2705
+ while ((entry = zis.getNextEntry()) != null) {
2706
+ if (entry.getName().equals("AndroidManifest.xml")) {
2707
+ // 写入修改后的 Manifest
2708
+ java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
2709
+ newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
2710
+ zos.putNextEntry(newEntry);
2711
+ zos.write(axmlData);
2712
+ zos.closeEntry();
2713
+ } else {
2714
+ // 复制其他文件
2715
+ java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
2716
+ newEntry.setTime(entry.getTime());
2717
+ if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
2718
+ newEntry.setMethod(java.util.zip.ZipEntry.STORED);
2719
+ newEntry.setSize(entry.getSize());
2720
+ newEntry.setCrc(entry.getCrc());
2721
+ } else {
2722
+ newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
2723
+ }
2724
+ zos.putNextEntry(newEntry);
2725
+ if (!entry.isDirectory()) {
2726
+ byte[] buf = new byte[8192];
2727
+ int n;
2728
+ while ((n = zis.read(buf)) != -1) {
2729
+ zos.write(buf, 0, n);
2730
+ }
2731
+ }
2732
+ zos.closeEntry();
2733
+ }
2734
+ zis.closeEntry();
2735
+ }
2736
+
2737
+ zis.close();
2738
+ zos.close();
2739
+
2740
+ // 替换原文件
2741
+ if (!apkFile.delete()) {
2742
+ Log.e(TAG, "Failed to delete original APK");
2743
+ }
2744
+ if (!tempApk.renameTo(apkFile)) {
2745
+ copyFile(tempApk, apkFile);
2746
+ tempApk.delete();
2747
+ }
2748
+
2749
+ result.put("success", true);
2750
+ result.put("replacedCount", replacedCount);
2751
+ result.put("details", details);
2752
+
2753
+ } catch (Exception e) {
2754
+ Log.e(TAG, "Replace in manifest error: " + e.getMessage(), e);
2755
+ result.put("success", false);
2756
+ result.put("error", e.getMessage());
2757
+ }
2758
+
2759
+ return result;
2760
+ }
2761
+
2762
+ /**
2763
+ * 替换结果
2764
+ */
2765
+ private static class ReplaceResult {
2766
+ byte[] data;
2767
+ int count;
2768
+
2769
+ ReplaceResult(byte[] data, int count) {
2770
+ this.data = data;
2771
+ this.count = count;
2772
+ }
2773
+ }
2774
+
2775
+ /**
2776
+ * 在 AXML 二进制数据中替换字符串
2777
+ * 直接修改字符串池中的字符串
2778
+ */
2779
+ private ReplaceResult replaceStringInAxml(byte[] data, String oldValue, String newValue) {
2780
+ int count = 0;
2781
+
2782
+ try {
2783
+ // 解析 AXML 结构找到字符串池
2784
+ if (data.length < 8) return new ReplaceResult(data, 0);
2785
+
2786
+ // 检查魔数
2787
+ int magic = (data[0] & 0xFF) | ((data[1] & 0xFF) << 8) |
2788
+ ((data[2] & 0xFF) << 16) | ((data[3] & 0xFF) << 24);
2789
+ if (magic != 0x00080003) return new ReplaceResult(data, 0);
2790
+
2791
+ // 找到字符串池 chunk (类型 0x0001)
2792
+ int pos = 8;
2793
+ while (pos < data.length - 8) {
2794
+ int chunkType = (data[pos] & 0xFF) | ((data[pos + 1] & 0xFF) << 8);
2795
+ int headerSize = (data[pos + 2] & 0xFF) | ((data[pos + 3] & 0xFF) << 8);
2796
+ int chunkSize = (data[pos + 4] & 0xFF) | ((data[pos + 5] & 0xFF) << 8) |
2797
+ ((data[pos + 6] & 0xFF) << 16) | ((data[pos + 7] & 0xFF) << 24);
2798
+
2799
+ if (chunkType == 0x0001) {
2800
+ // 字符串池 chunk
2801
+ int stringCount = (data[pos + 8] & 0xFF) | ((data[pos + 9] & 0xFF) << 8) |
2802
+ ((data[pos + 10] & 0xFF) << 16) | ((data[pos + 11] & 0xFF) << 24);
2803
+ int styleCount = (data[pos + 12] & 0xFF) | ((data[pos + 13] & 0xFF) << 8) |
2804
+ ((data[pos + 14] & 0xFF) << 16) | ((data[pos + 15] & 0xFF) << 24);
2805
+ int flags = (data[pos + 16] & 0xFF) | ((data[pos + 17] & 0xFF) << 8) |
2806
+ ((data[pos + 18] & 0xFF) << 16) | ((data[pos + 19] & 0xFF) << 24);
2807
+ int stringsOffset = (data[pos + 20] & 0xFF) | ((data[pos + 21] & 0xFF) << 8) |
2808
+ ((data[pos + 22] & 0xFF) << 16) | ((data[pos + 23] & 0xFF) << 24);
2809
+
2810
+ boolean isUtf8 = (flags & 0x100) != 0;
2811
+
2812
+ // 读取字符串偏移表
2813
+ int offsetTableStart = pos + 28;
2814
+ int stringsStart = pos + stringsOffset;
2815
+
2816
+ // 遍历所有字符串
2817
+ for (int i = 0; i < stringCount; i++) {
2818
+ int offsetPos = offsetTableStart + i * 4;
2819
+ int stringOffset = (data[offsetPos] & 0xFF) | ((data[offsetPos + 1] & 0xFF) << 8) |
2820
+ ((data[offsetPos + 2] & 0xFF) << 16) | ((data[offsetPos + 3] & 0xFF) << 24);
2821
+
2822
+ int stringPos = stringsStart + stringOffset;
2823
+ if (stringPos >= data.length) continue;
2824
+
2825
+ // 读取当前字符串
2826
+ String currentString = readStringFromAxml(data, stringPos, isUtf8);
2827
+
2828
+ // 检查是否匹配
2829
+ if (currentString.equals(oldValue)) {
2830
+ // 执行替换(仅当新字符串长度 <= 旧字符串长度时可以直接替换)
2831
+ if (isUtf8) {
2832
+ byte[] newBytes = newValue.getBytes(java.nio.charset.StandardCharsets.UTF_8);
2833
+ byte[] oldBytes = oldValue.getBytes(java.nio.charset.StandardCharsets.UTF_8);
2834
+
2835
+ if (newBytes.length <= oldBytes.length) {
2836
+ // 可以直接替换
2837
+ int dataStart = getStringDataStart(data, stringPos, isUtf8);
2838
+
2839
+ // 更新长度
2840
+ if (newBytes.length < 128) {
2841
+ data[stringPos] = (byte) newValue.length();
2842
+ data[stringPos + 1] = (byte) newBytes.length;
2843
+ }
2844
+
2845
+ // 写入新数据
2846
+ System.arraycopy(newBytes, 0, data, dataStart, newBytes.length);
2847
+
2848
+ // 用 0 填充剩余空间
2849
+ for (int j = newBytes.length; j < oldBytes.length; j++) {
2850
+ data[dataStart + j] = 0;
2851
+ }
2852
+
2853
+ count++;
2854
+ } else {
2855
+ Log.w(TAG, "New string is longer than old string, cannot replace: " + oldValue);
2856
+ }
2857
+ }
2858
+ }
2859
+ }
2860
+ break;
2861
+ }
2862
+
2863
+ pos += chunkSize;
2864
+ }
2865
+ } catch (Exception e) {
2866
+ Log.e(TAG, "Replace string error: " + e.getMessage(), e);
2867
+ }
2868
+
2869
+ return new ReplaceResult(data, count);
2870
+ }
2871
+
2872
+ /**
2873
+ * 从 AXML 数据中读取字符串
2874
+ */
2875
+ private String readStringFromAxml(byte[] data, int pos, boolean isUtf8) {
2876
+ try {
2877
+ if (isUtf8) {
2878
+ int charLen = data[pos] & 0xFF;
2879
+ int byteLen;
2880
+ int dataStart;
2881
+
2882
+ if ((charLen & 0x80) != 0) {
2883
+ charLen = ((charLen & 0x7F) << 8) | (data[pos + 1] & 0xFF);
2884
+ byteLen = data[pos + 2] & 0xFF;
2885
+ if ((byteLen & 0x80) != 0) {
2886
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 3] & 0xFF);
2887
+ dataStart = pos + 4;
2888
+ } else {
2889
+ dataStart = pos + 3;
2890
+ }
2891
+ } else {
2892
+ byteLen = data[pos + 1] & 0xFF;
2893
+ if ((byteLen & 0x80) != 0) {
2894
+ byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 2] & 0xFF);
2895
+ dataStart = pos + 3;
2896
+ } else {
2897
+ dataStart = pos + 2;
2898
+ }
2899
+ }
2900
+
2901
+ if (dataStart + byteLen > data.length) {
2902
+ byteLen = data.length - dataStart;
2903
+ }
2904
+ if (byteLen <= 0) return "";
2905
+
2906
+ return new String(data, dataStart, byteLen, java.nio.charset.StandardCharsets.UTF_8);
2907
+ }
2908
+ } catch (Exception e) {
2909
+ return "";
2910
+ }
2911
+ return "";
2912
+ }
2913
+
2914
+ /**
2915
+ * 获取字符串数据开始位置
2916
+ */
2917
+ private int getStringDataStart(byte[] data, int pos, boolean isUtf8) {
2918
+ if (isUtf8) {
2919
+ int charLen = data[pos] & 0xFF;
2920
+ if ((charLen & 0x80) != 0) {
2921
+ int byteLen = data[pos + 2] & 0xFF;
2922
+ if ((byteLen & 0x80) != 0) {
2923
+ return pos + 4;
2924
+ } else {
2925
+ return pos + 3;
2926
+ }
2927
+ } else {
2928
+ int byteLen = data[pos + 1] & 0xFF;
2929
+ if ((byteLen & 0x80) != 0) {
2930
+ return pos + 3;
2931
+ } else {
2932
+ return pos + 2;
2933
+ }
2934
+ }
2935
+ }
2936
+ return pos + 2;
2937
+ }
2938
+
2638
2939
  /**
2639
2940
  * 清理临时目录
2640
2941
  */
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.32",
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",