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
|
*/
|