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.
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
*/
|