capacitor-dex-editor 0.0.25 → 0.0.27
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
|
+
// AXML 解析 - AndroidManifest.xml 二进制解析
|
|
71
|
+
api 'com.phlox.axml:axml:1.0.2'
|
|
72
|
+
|
|
70
73
|
testImplementation "junit:junit:$junitVersion"
|
|
71
74
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
72
75
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -61,6 +61,9 @@ public class DexManager {
|
|
|
61
61
|
|
|
62
62
|
// 存储活跃的 DEX 会话
|
|
63
63
|
private final Map<String, DexSession> sessions = new HashMap<>();
|
|
64
|
+
|
|
65
|
+
// 存储多 DEX 会话(MCP 工作流)
|
|
66
|
+
private final Map<String, MultiDexSession> multiDexSessions = new HashMap<>();
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* DEX 会话 - 存储加载的 DEX 文件及其修改状态
|
|
@@ -82,6 +85,28 @@ public class DexManager {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
/**
|
|
89
|
+
* 多 DEX 会话 - 用于 MCP 工作流,支持同时编辑多个 DEX 文件
|
|
90
|
+
*/
|
|
91
|
+
private static class MultiDexSession {
|
|
92
|
+
String sessionId;
|
|
93
|
+
String apkPath;
|
|
94
|
+
Map<String, DexBackedDexFile> dexFiles;
|
|
95
|
+
Map<String, ClassDef> modifiedClasses;
|
|
96
|
+
boolean modified = false;
|
|
97
|
+
|
|
98
|
+
MultiDexSession(String sessionId, String apkPath) {
|
|
99
|
+
this.sessionId = sessionId;
|
|
100
|
+
this.apkPath = apkPath;
|
|
101
|
+
this.dexFiles = new HashMap<>();
|
|
102
|
+
this.modifiedClasses = new HashMap<>();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
void addDex(String dexName, DexBackedDexFile dexFile) {
|
|
106
|
+
this.dexFiles.put(dexName, dexFile);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
85
110
|
// ==================== DEX 文件操作 ====================
|
|
86
111
|
|
|
87
112
|
/**
|
|
@@ -1500,6 +1525,531 @@ public class DexManager {
|
|
|
1500
1525
|
return result;
|
|
1501
1526
|
}
|
|
1502
1527
|
|
|
1528
|
+
// ==================== MCP 工作流支持方法 ====================
|
|
1529
|
+
|
|
1530
|
+
/**
|
|
1531
|
+
* 列出 APK 中的所有 DEX 文件
|
|
1532
|
+
*/
|
|
1533
|
+
public JSObject listDexFilesInApk(String apkPath) throws Exception {
|
|
1534
|
+
JSObject result = new JSObject();
|
|
1535
|
+
JSArray dexFiles = new JSArray();
|
|
1536
|
+
|
|
1537
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1538
|
+
try {
|
|
1539
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1540
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
1541
|
+
|
|
1542
|
+
while (entries.hasMoreElements()) {
|
|
1543
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
1544
|
+
String name = entry.getName();
|
|
1545
|
+
if (name.endsWith(".dex") && !name.contains("/")) {
|
|
1546
|
+
JSObject dexInfo = new JSObject();
|
|
1547
|
+
dexInfo.put("name", name);
|
|
1548
|
+
dexInfo.put("size", entry.getSize());
|
|
1549
|
+
dexFiles.put(dexInfo);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
result.put("apkPath", apkPath);
|
|
1554
|
+
result.put("dexFiles", dexFiles);
|
|
1555
|
+
result.put("count", dexFiles.length());
|
|
1556
|
+
|
|
1557
|
+
} finally {
|
|
1558
|
+
if (zipFile != null) {
|
|
1559
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
return result;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* 打开多个 DEX 文件创建会话(MCP 工作流)
|
|
1568
|
+
*/
|
|
1569
|
+
public JSObject openMultipleDex(String apkPath, JSONArray dexFiles) throws Exception {
|
|
1570
|
+
JSObject result = new JSObject();
|
|
1571
|
+
String sessionId = UUID.randomUUID().toString();
|
|
1572
|
+
|
|
1573
|
+
// 创建复合会话
|
|
1574
|
+
MultiDexSession multiSession = new MultiDexSession(sessionId, apkPath);
|
|
1575
|
+
|
|
1576
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1577
|
+
int totalClasses = 0;
|
|
1578
|
+
|
|
1579
|
+
try {
|
|
1580
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1581
|
+
|
|
1582
|
+
for (int i = 0; i < dexFiles.length(); i++) {
|
|
1583
|
+
String dexName = dexFiles.getString(i);
|
|
1584
|
+
java.util.zip.ZipEntry dexEntry = zipFile.getEntry(dexName);
|
|
1585
|
+
|
|
1586
|
+
if (dexEntry == null) {
|
|
1587
|
+
Log.w(TAG, "DEX not found: " + dexName);
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// 读取 DEX 到内存
|
|
1592
|
+
java.io.InputStream is = zipFile.getInputStream(dexEntry);
|
|
1593
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
1594
|
+
byte[] buffer = new byte[8192];
|
|
1595
|
+
int len;
|
|
1596
|
+
while ((len = is.read(buffer)) != -1) {
|
|
1597
|
+
baos.write(buffer, 0, len);
|
|
1598
|
+
}
|
|
1599
|
+
is.close();
|
|
1600
|
+
|
|
1601
|
+
// 解析 DEX
|
|
1602
|
+
DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), baos.toByteArray());
|
|
1603
|
+
multiSession.addDex(dexName, dexFile);
|
|
1604
|
+
totalClasses += dexFile.getClasses().size();
|
|
1605
|
+
|
|
1606
|
+
Log.d(TAG, "Loaded DEX: " + dexName + " with " + dexFile.getClasses().size() + " classes");
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
} finally {
|
|
1610
|
+
if (zipFile != null) {
|
|
1611
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
multiDexSessions.put(sessionId, multiSession);
|
|
1616
|
+
|
|
1617
|
+
result.put("sessionId", sessionId);
|
|
1618
|
+
result.put("apkPath", apkPath);
|
|
1619
|
+
result.put("dexCount", multiSession.dexFiles.size());
|
|
1620
|
+
result.put("classCount", totalClasses);
|
|
1621
|
+
|
|
1622
|
+
return result;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* 列出所有打开的会话
|
|
1627
|
+
*/
|
|
1628
|
+
public JSArray listAllSessions() {
|
|
1629
|
+
JSArray result = new JSArray();
|
|
1630
|
+
|
|
1631
|
+
// 单 DEX 会话
|
|
1632
|
+
for (Map.Entry<String, DexSession> entry : sessions.entrySet()) {
|
|
1633
|
+
JSObject session = new JSObject();
|
|
1634
|
+
session.put("sessionId", entry.getKey());
|
|
1635
|
+
session.put("type", "single");
|
|
1636
|
+
session.put("filePath", entry.getValue().filePath);
|
|
1637
|
+
session.put("modified", entry.getValue().modified);
|
|
1638
|
+
result.put(session);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// 多 DEX 会话
|
|
1642
|
+
for (Map.Entry<String, MultiDexSession> entry : multiDexSessions.entrySet()) {
|
|
1643
|
+
JSObject session = new JSObject();
|
|
1644
|
+
session.put("sessionId", entry.getKey());
|
|
1645
|
+
session.put("type", "multi");
|
|
1646
|
+
session.put("apkPath", entry.getValue().apkPath);
|
|
1647
|
+
session.put("dexCount", entry.getValue().dexFiles.size());
|
|
1648
|
+
session.put("modified", entry.getValue().modified);
|
|
1649
|
+
result.put(session);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* 关闭多 DEX 会话
|
|
1657
|
+
*/
|
|
1658
|
+
public void closeMultiDexSession(String sessionId) {
|
|
1659
|
+
multiDexSessions.remove(sessionId);
|
|
1660
|
+
Log.d(TAG, "Closed multi-dex session: " + sessionId);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* 获取多 DEX 会话中的类列表(支持分页和过滤)
|
|
1665
|
+
*/
|
|
1666
|
+
public JSObject getClassesFromMultiSession(String sessionId, String packageFilter, int offset, int limit) throws Exception {
|
|
1667
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1668
|
+
if (session == null) {
|
|
1669
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
JSObject result = new JSObject();
|
|
1673
|
+
JSArray classes = new JSArray();
|
|
1674
|
+
List<String> allClasses = new ArrayList<>();
|
|
1675
|
+
|
|
1676
|
+
// 收集所有类
|
|
1677
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1678
|
+
String dexName = entry.getKey();
|
|
1679
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1680
|
+
|
|
1681
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1682
|
+
String className = convertTypeToClassName(classDef.getType());
|
|
1683
|
+
|
|
1684
|
+
// 包名过滤
|
|
1685
|
+
if (packageFilter != null && !packageFilter.isEmpty()) {
|
|
1686
|
+
if (!className.startsWith(packageFilter)) {
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
allClasses.add(className + "|" + dexName);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 排序
|
|
1696
|
+
java.util.Collections.sort(allClasses);
|
|
1697
|
+
|
|
1698
|
+
// 分页
|
|
1699
|
+
int total = allClasses.size();
|
|
1700
|
+
int end = Math.min(offset + limit, total);
|
|
1701
|
+
|
|
1702
|
+
for (int i = offset; i < end; i++) {
|
|
1703
|
+
String[] parts = allClasses.get(i).split("\\|");
|
|
1704
|
+
JSObject classInfo = new JSObject();
|
|
1705
|
+
classInfo.put("className", parts[0]);
|
|
1706
|
+
classInfo.put("dexFile", parts[1]);
|
|
1707
|
+
classes.put(classInfo);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
result.put("total", total);
|
|
1711
|
+
result.put("offset", offset);
|
|
1712
|
+
result.put("limit", limit);
|
|
1713
|
+
result.put("classes", classes);
|
|
1714
|
+
result.put("hasMore", end < total);
|
|
1715
|
+
|
|
1716
|
+
return result;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* 在多 DEX 会话中搜索
|
|
1721
|
+
*/
|
|
1722
|
+
public JSObject searchInMultiSession(String sessionId, String query, String searchType,
|
|
1723
|
+
boolean caseSensitive, int maxResults) throws Exception {
|
|
1724
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1725
|
+
if (session == null) {
|
|
1726
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
JSObject result = new JSObject();
|
|
1730
|
+
JSArray results = new JSArray();
|
|
1731
|
+
String queryMatch = caseSensitive ? query : query.toLowerCase();
|
|
1732
|
+
|
|
1733
|
+
outerLoop:
|
|
1734
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1735
|
+
String dexName = entry.getKey();
|
|
1736
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1737
|
+
|
|
1738
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1739
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1740
|
+
|
|
1741
|
+
String className = convertTypeToClassName(classDef.getType());
|
|
1742
|
+
String classNameMatch = caseSensitive ? className : className.toLowerCase();
|
|
1743
|
+
|
|
1744
|
+
switch (searchType) {
|
|
1745
|
+
case "class":
|
|
1746
|
+
if (classNameMatch.contains(queryMatch)) {
|
|
1747
|
+
JSObject item = new JSObject();
|
|
1748
|
+
item.put("type", "class");
|
|
1749
|
+
item.put("className", className);
|
|
1750
|
+
item.put("dexFile", dexName);
|
|
1751
|
+
results.put(item);
|
|
1752
|
+
}
|
|
1753
|
+
break;
|
|
1754
|
+
|
|
1755
|
+
case "package":
|
|
1756
|
+
if (classNameMatch.startsWith(queryMatch)) {
|
|
1757
|
+
JSObject item = new JSObject();
|
|
1758
|
+
item.put("type", "package");
|
|
1759
|
+
item.put("className", className);
|
|
1760
|
+
item.put("dexFile", dexName);
|
|
1761
|
+
results.put(item);
|
|
1762
|
+
}
|
|
1763
|
+
break;
|
|
1764
|
+
|
|
1765
|
+
case "method":
|
|
1766
|
+
for (Method method : classDef.getMethods()) {
|
|
1767
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1768
|
+
String methodName = method.getName();
|
|
1769
|
+
String methodMatch = caseSensitive ? methodName : methodName.toLowerCase();
|
|
1770
|
+
if (methodMatch.contains(queryMatch)) {
|
|
1771
|
+
JSObject item = new JSObject();
|
|
1772
|
+
item.put("type", "method");
|
|
1773
|
+
item.put("className", className);
|
|
1774
|
+
item.put("methodName", methodName);
|
|
1775
|
+
item.put("dexFile", dexName);
|
|
1776
|
+
results.put(item);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
break;
|
|
1780
|
+
|
|
1781
|
+
case "field":
|
|
1782
|
+
for (Field field : classDef.getFields()) {
|
|
1783
|
+
if (results.length() >= maxResults) break outerLoop;
|
|
1784
|
+
String fieldName = field.getName();
|
|
1785
|
+
String fieldMatch = caseSensitive ? fieldName : fieldName.toLowerCase();
|
|
1786
|
+
if (fieldMatch.contains(queryMatch)) {
|
|
1787
|
+
JSObject item = new JSObject();
|
|
1788
|
+
item.put("type", "field");
|
|
1789
|
+
item.put("className", className);
|
|
1790
|
+
item.put("fieldName", fieldName);
|
|
1791
|
+
item.put("dexFile", dexName);
|
|
1792
|
+
results.put(item);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
break;
|
|
1796
|
+
|
|
1797
|
+
case "string":
|
|
1798
|
+
case "code":
|
|
1799
|
+
// 需要反编译 smali 搜索
|
|
1800
|
+
String smali = getSmaliForClass(dexFile, classDef);
|
|
1801
|
+
String smaliMatch = caseSensitive ? smali : smali.toLowerCase();
|
|
1802
|
+
if (smaliMatch.contains(queryMatch)) {
|
|
1803
|
+
JSObject item = new JSObject();
|
|
1804
|
+
item.put("type", searchType);
|
|
1805
|
+
item.put("className", className);
|
|
1806
|
+
item.put("dexFile", dexName);
|
|
1807
|
+
// 找到匹配的行
|
|
1808
|
+
String[] lines = smali.split("\n");
|
|
1809
|
+
for (int i = 0; i < lines.length; i++) {
|
|
1810
|
+
String lineMatch = caseSensitive ? lines[i] : lines[i].toLowerCase();
|
|
1811
|
+
if (lineMatch.contains(queryMatch)) {
|
|
1812
|
+
item.put("line", i + 1);
|
|
1813
|
+
item.put("content", lines[i].trim());
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
results.put(item);
|
|
1818
|
+
}
|
|
1819
|
+
break;
|
|
1820
|
+
|
|
1821
|
+
case "int":
|
|
1822
|
+
// 搜索整数常量
|
|
1823
|
+
String smaliForInt = getSmaliForClass(dexFile, classDef);
|
|
1824
|
+
if (smaliForInt.contains("0x" + query) || smaliForInt.contains(" " + query + "\n") ||
|
|
1825
|
+
smaliForInt.contains(" " + query + " ")) {
|
|
1826
|
+
JSObject item = new JSObject();
|
|
1827
|
+
item.put("type", "int");
|
|
1828
|
+
item.put("className", className);
|
|
1829
|
+
item.put("dexFile", dexName);
|
|
1830
|
+
results.put(item);
|
|
1831
|
+
}
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
result.put("query", query);
|
|
1838
|
+
result.put("searchType", searchType);
|
|
1839
|
+
result.put("total", results.length());
|
|
1840
|
+
result.put("results", results);
|
|
1841
|
+
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* 获取类的 Smali 代码(内部方法)
|
|
1847
|
+
*/
|
|
1848
|
+
private String getSmaliForClass(DexBackedDexFile dexFile, ClassDef classDef) {
|
|
1849
|
+
try {
|
|
1850
|
+
BaksmaliOptions options = new BaksmaliOptions();
|
|
1851
|
+
ClassDefinition classDefinition = new ClassDefinition(options, classDef);
|
|
1852
|
+
java.io.StringWriter stringWriter = new java.io.StringWriter();
|
|
1853
|
+
BaksmaliWriter writer = new BaksmaliWriter(stringWriter, null);
|
|
1854
|
+
classDefinition.writeTo(writer);
|
|
1855
|
+
writer.close();
|
|
1856
|
+
return stringWriter.toString();
|
|
1857
|
+
} catch (Exception e) {
|
|
1858
|
+
return "";
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* 从多 DEX 会话获取类的 Smali 代码
|
|
1864
|
+
*/
|
|
1865
|
+
public JSObject getClassSmaliFromSession(String sessionId, String className) throws Exception {
|
|
1866
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1867
|
+
if (session == null) {
|
|
1868
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
String targetType = convertClassNameToType(className);
|
|
1872
|
+
|
|
1873
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1874
|
+
String dexName = entry.getKey();
|
|
1875
|
+
DexBackedDexFile dexFile = entry.getValue();
|
|
1876
|
+
|
|
1877
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1878
|
+
if (classDef.getType().equals(targetType)) {
|
|
1879
|
+
JSObject result = new JSObject();
|
|
1880
|
+
result.put("className", className);
|
|
1881
|
+
result.put("dexFile", dexName);
|
|
1882
|
+
result.put("smaliContent", getSmaliForClass(dexFile, classDef));
|
|
1883
|
+
return result;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
throw new IllegalArgumentException("Class not found: " + className);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* 修改类并保存到多 DEX 会话
|
|
1893
|
+
*/
|
|
1894
|
+
public void modifyClassInSession(String sessionId, String className, String smaliContent) throws Exception {
|
|
1895
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1896
|
+
if (session == null) {
|
|
1897
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
String targetType = convertClassNameToType(className);
|
|
1901
|
+
|
|
1902
|
+
// 找到类所在的 DEX
|
|
1903
|
+
String targetDex = null;
|
|
1904
|
+
for (Map.Entry<String, DexBackedDexFile> entry : session.dexFiles.entrySet()) {
|
|
1905
|
+
for (ClassDef classDef : entry.getValue().getClasses()) {
|
|
1906
|
+
if (classDef.getType().equals(targetType)) {
|
|
1907
|
+
targetDex = entry.getKey();
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
if (targetDex != null) break;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (targetDex == null) {
|
|
1915
|
+
throw new IllegalArgumentException("Class not found: " + className);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// 编译新的 Smali
|
|
1919
|
+
ClassDef newClassDef = compileSmaliToClass(smaliContent, Opcodes.getDefault());
|
|
1920
|
+
|
|
1921
|
+
// 记录修改
|
|
1922
|
+
session.modifiedClasses.put(targetDex + "|" + className, newClassDef);
|
|
1923
|
+
session.modified = true;
|
|
1924
|
+
|
|
1925
|
+
Log.d(TAG, "Modified class in session: " + className);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* 保存多 DEX 会话的修改到 APK
|
|
1930
|
+
*/
|
|
1931
|
+
public JSObject saveMultiDexSessionToApk(String sessionId) throws Exception {
|
|
1932
|
+
MultiDexSession session = multiDexSessions.get(sessionId);
|
|
1933
|
+
if (session == null) {
|
|
1934
|
+
throw new IllegalArgumentException("Session not found: " + sessionId);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (!session.modified || session.modifiedClasses.isEmpty()) {
|
|
1938
|
+
JSObject result = new JSObject();
|
|
1939
|
+
result.put("success", true);
|
|
1940
|
+
result.put("message", "没有需要保存的修改");
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// 按 DEX 文件分组修改
|
|
1945
|
+
Map<String, List<ClassDef>> modifiedByDex = new HashMap<>();
|
|
1946
|
+
for (Map.Entry<String, ClassDef> entry : session.modifiedClasses.entrySet()) {
|
|
1947
|
+
String[] parts = entry.getKey().split("\\|");
|
|
1948
|
+
String dexName = parts[0];
|
|
1949
|
+
modifiedByDex.computeIfAbsent(dexName, k -> new ArrayList<>()).add(entry.getValue());
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// 为每个修改的 DEX 创建新版本
|
|
1953
|
+
Map<String, byte[]> newDexData = new HashMap<>();
|
|
1954
|
+
|
|
1955
|
+
for (Map.Entry<String, List<ClassDef>> entry : modifiedByDex.entrySet()) {
|
|
1956
|
+
String dexName = entry.getKey();
|
|
1957
|
+
List<ClassDef> modifiedClasses = entry.getValue();
|
|
1958
|
+
DexBackedDexFile originalDex = session.dexFiles.get(dexName);
|
|
1959
|
+
|
|
1960
|
+
// 合并类
|
|
1961
|
+
Set<String> modifiedTypes = new HashSet<>();
|
|
1962
|
+
for (ClassDef c : modifiedClasses) {
|
|
1963
|
+
modifiedTypes.add(c.getType());
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
List<ClassDef> allClasses = new ArrayList<>(modifiedClasses);
|
|
1967
|
+
for (ClassDef c : originalDex.getClasses()) {
|
|
1968
|
+
if (!modifiedTypes.contains(c.getType())) {
|
|
1969
|
+
allClasses.add(c);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// 创建新 DEX
|
|
1974
|
+
java.io.File tempDex = java.io.File.createTempFile("dex_", ".dex");
|
|
1975
|
+
DexPool dexPool = new DexPool(Opcodes.getDefault());
|
|
1976
|
+
for (ClassDef c : allClasses) {
|
|
1977
|
+
dexPool.internClass(c);
|
|
1978
|
+
}
|
|
1979
|
+
dexPool.writeTo(new FileDataStore(tempDex));
|
|
1980
|
+
|
|
1981
|
+
newDexData.put(dexName, readFileBytes(tempDex));
|
|
1982
|
+
tempDex.delete();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// 替换 APK 中的 DEX
|
|
1986
|
+
java.io.File apkFile = new java.io.File(session.apkPath);
|
|
1987
|
+
java.io.File tempApk = new java.io.File(session.apkPath + ".tmp");
|
|
1988
|
+
|
|
1989
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
1990
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
1991
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
1992
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
1993
|
+
|
|
1994
|
+
java.util.zip.ZipEntry entry;
|
|
1995
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
1996
|
+
if (newDexData.containsKey(entry.getName())) {
|
|
1997
|
+
// 替换 DEX
|
|
1998
|
+
byte[] dexBytes = newDexData.get(entry.getName());
|
|
1999
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2000
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2001
|
+
zos.putNextEntry(newEntry);
|
|
2002
|
+
zos.write(dexBytes);
|
|
2003
|
+
zos.closeEntry();
|
|
2004
|
+
} else {
|
|
2005
|
+
// 复制原条目
|
|
2006
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2007
|
+
newEntry.setTime(entry.getTime());
|
|
2008
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2009
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2010
|
+
newEntry.setSize(entry.getSize());
|
|
2011
|
+
newEntry.setCrc(entry.getCrc());
|
|
2012
|
+
} else {
|
|
2013
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2014
|
+
}
|
|
2015
|
+
zos.putNextEntry(newEntry);
|
|
2016
|
+
if (!entry.isDirectory()) {
|
|
2017
|
+
byte[] buf = new byte[8192];
|
|
2018
|
+
int n;
|
|
2019
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2020
|
+
zos.write(buf, 0, n);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
zos.closeEntry();
|
|
2024
|
+
}
|
|
2025
|
+
zis.closeEntry();
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
zis.close();
|
|
2029
|
+
zos.close();
|
|
2030
|
+
|
|
2031
|
+
// 替换原文件
|
|
2032
|
+
if (!apkFile.delete()) {
|
|
2033
|
+
Log.e(TAG, "Failed to delete original APK");
|
|
2034
|
+
}
|
|
2035
|
+
if (!tempApk.renameTo(apkFile)) {
|
|
2036
|
+
copyFile(tempApk, apkFile);
|
|
2037
|
+
tempApk.delete();
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// 清除修改状态
|
|
2041
|
+
session.modifiedClasses.clear();
|
|
2042
|
+
session.modified = false;
|
|
2043
|
+
|
|
2044
|
+
JSObject result = new JSObject();
|
|
2045
|
+
result.put("success", true);
|
|
2046
|
+
result.put("message", "DEX 已保存到 APK");
|
|
2047
|
+
result.put("apkPath", session.apkPath);
|
|
2048
|
+
result.put("needSign", true);
|
|
2049
|
+
|
|
2050
|
+
return result;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1503
2053
|
/**
|
|
1504
2054
|
* 将 DEX 类型格式转换为 Java 类名格式
|
|
1505
2055
|
* 例如: Lcom/example/Class; -> com.example.Class
|
|
@@ -1854,6 +2404,241 @@ public class DexManager {
|
|
|
1854
2404
|
fos.close();
|
|
1855
2405
|
}
|
|
1856
2406
|
|
|
2407
|
+
// ==================== XML/资源操作方法 ====================
|
|
2408
|
+
|
|
2409
|
+
/**
|
|
2410
|
+
* 获取 APK 的 AndroidManifest.xml(解码为可读 XML)
|
|
2411
|
+
*/
|
|
2412
|
+
public JSObject getManifestFromApk(String apkPath) throws Exception {
|
|
2413
|
+
JSObject result = new JSObject();
|
|
2414
|
+
|
|
2415
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2416
|
+
try {
|
|
2417
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2418
|
+
java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
2419
|
+
|
|
2420
|
+
if (manifestEntry == null) {
|
|
2421
|
+
throw new Exception("AndroidManifest.xml not found in APK");
|
|
2422
|
+
}
|
|
2423
|
+
|
|
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);
|
|
2436
|
+
|
|
2437
|
+
result.put("manifest", xmlContent);
|
|
2438
|
+
|
|
2439
|
+
} finally {
|
|
2440
|
+
if (zipFile != null) {
|
|
2441
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
return result;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
/**
|
|
2449
|
+
* 解码二进制 AXML 为可读 XML
|
|
2450
|
+
*/
|
|
2451
|
+
private String decodeAxml(byte[] axmlData) {
|
|
2452
|
+
try {
|
|
2453
|
+
// 使用 phlox AXML 库解析
|
|
2454
|
+
com.phlox.axml.AXMLDocument doc = new com.phlox.axml.AXMLDocument(axmlData);
|
|
2455
|
+
return doc.toXmlString();
|
|
2456
|
+
} catch (Exception e) {
|
|
2457
|
+
Log.e(TAG, "AXML decode error: " + e.getMessage());
|
|
2458
|
+
// 降级:返回十六进制
|
|
2459
|
+
return "# 无法解码 AXML: " + e.getMessage();
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* 修改 AndroidManifest.xml
|
|
2465
|
+
*/
|
|
2466
|
+
public JSObject modifyManifestInApk(String apkPath, String newManifestXml) throws Exception {
|
|
2467
|
+
JSObject result = new JSObject();
|
|
2468
|
+
|
|
2469
|
+
try {
|
|
2470
|
+
// 将 XML 编码为二进制 AXML
|
|
2471
|
+
byte[] newAxmlData = encodeAxml(newManifestXml);
|
|
2472
|
+
|
|
2473
|
+
// 替换 APK 中的 AndroidManifest.xml
|
|
2474
|
+
java.io.File apkFile = new java.io.File(apkPath);
|
|
2475
|
+
java.io.File tempApk = new java.io.File(apkPath + ".tmp");
|
|
2476
|
+
|
|
2477
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
2478
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
2479
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
2480
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
2481
|
+
|
|
2482
|
+
java.util.zip.ZipEntry entry;
|
|
2483
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
2484
|
+
if (entry.getName().equals("AndroidManifest.xml")) {
|
|
2485
|
+
// 替换 Manifest
|
|
2486
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
|
|
2487
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2488
|
+
zos.putNextEntry(newEntry);
|
|
2489
|
+
zos.write(newAxmlData);
|
|
2490
|
+
zos.closeEntry();
|
|
2491
|
+
} else {
|
|
2492
|
+
// 复制其他文件
|
|
2493
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2494
|
+
newEntry.setTime(entry.getTime());
|
|
2495
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2496
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2497
|
+
newEntry.setSize(entry.getSize());
|
|
2498
|
+
newEntry.setCrc(entry.getCrc());
|
|
2499
|
+
} else {
|
|
2500
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2501
|
+
}
|
|
2502
|
+
zos.putNextEntry(newEntry);
|
|
2503
|
+
if (!entry.isDirectory()) {
|
|
2504
|
+
byte[] buf = new byte[8192];
|
|
2505
|
+
int n;
|
|
2506
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2507
|
+
zos.write(buf, 0, n);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
zos.closeEntry();
|
|
2511
|
+
}
|
|
2512
|
+
zis.closeEntry();
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
zis.close();
|
|
2516
|
+
zos.close();
|
|
2517
|
+
|
|
2518
|
+
// 替换原文件
|
|
2519
|
+
if (!apkFile.delete()) {
|
|
2520
|
+
Log.e(TAG, "Failed to delete original APK");
|
|
2521
|
+
}
|
|
2522
|
+
if (!tempApk.renameTo(apkFile)) {
|
|
2523
|
+
copyFile(tempApk, apkFile);
|
|
2524
|
+
tempApk.delete();
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
result.put("success", true);
|
|
2528
|
+
result.put("message", "AndroidManifest.xml 已修改");
|
|
2529
|
+
|
|
2530
|
+
} catch (Exception e) {
|
|
2531
|
+
Log.e(TAG, "Modify manifest error: " + e.getMessage(), e);
|
|
2532
|
+
result.put("success", false);
|
|
2533
|
+
result.put("error", e.getMessage());
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
return result;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
/**
|
|
2540
|
+
* 将 XML 编码为二进制 AXML
|
|
2541
|
+
*/
|
|
2542
|
+
private byte[] encodeAxml(String xmlContent) throws Exception {
|
|
2543
|
+
// 使用 phlox AXML 库编码
|
|
2544
|
+
com.phlox.axml.AXMLDocument doc = com.phlox.axml.AXMLDocument.parseXml(xmlContent);
|
|
2545
|
+
return doc.toByteArray();
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/**
|
|
2549
|
+
* 列出 APK 中的资源文件
|
|
2550
|
+
*/
|
|
2551
|
+
public JSObject listResourcesInApk(String apkPath, String filter) throws Exception {
|
|
2552
|
+
JSObject result = new JSObject();
|
|
2553
|
+
JSArray resources = new JSArray();
|
|
2554
|
+
|
|
2555
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2556
|
+
try {
|
|
2557
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2558
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
2559
|
+
|
|
2560
|
+
while (entries.hasMoreElements()) {
|
|
2561
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
2562
|
+
String name = entry.getName();
|
|
2563
|
+
|
|
2564
|
+
// 只列出 res 目录下的文件
|
|
2565
|
+
if (name.startsWith("res/")) {
|
|
2566
|
+
// 过滤
|
|
2567
|
+
if (filter != null && !filter.isEmpty()) {
|
|
2568
|
+
if (!name.contains(filter)) {
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
JSObject resource = new JSObject();
|
|
2574
|
+
resource.put("path", name);
|
|
2575
|
+
resource.put("size", entry.getSize());
|
|
2576
|
+
resource.put("isXml", name.endsWith(".xml"));
|
|
2577
|
+
resources.put(resource);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
result.put("total", resources.length());
|
|
2582
|
+
result.put("resources", resources);
|
|
2583
|
+
|
|
2584
|
+
} finally {
|
|
2585
|
+
if (zipFile != null) {
|
|
2586
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
return result;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* 获取 APK 中的资源文件内容
|
|
2595
|
+
*/
|
|
2596
|
+
public JSObject getResourceFromApk(String apkPath, String resourcePath) throws Exception {
|
|
2597
|
+
JSObject result = new JSObject();
|
|
2598
|
+
|
|
2599
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2600
|
+
try {
|
|
2601
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2602
|
+
java.util.zip.ZipEntry entry = zipFile.getEntry(resourcePath);
|
|
2603
|
+
|
|
2604
|
+
if (entry == null) {
|
|
2605
|
+
throw new Exception("Resource not found: " + resourcePath);
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
java.io.InputStream is = zipFile.getInputStream(entry);
|
|
2609
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
2610
|
+
byte[] buffer = new byte[8192];
|
|
2611
|
+
int len;
|
|
2612
|
+
while ((len = is.read(buffer)) != -1) {
|
|
2613
|
+
baos.write(buffer, 0, len);
|
|
2614
|
+
}
|
|
2615
|
+
is.close();
|
|
2616
|
+
|
|
2617
|
+
byte[] data = baos.toByteArray();
|
|
2618
|
+
|
|
2619
|
+
// 如果是 XML 文件,尝试解码 AXML
|
|
2620
|
+
if (resourcePath.endsWith(".xml")) {
|
|
2621
|
+
String xmlContent = decodeAxml(data);
|
|
2622
|
+
result.put("content", xmlContent);
|
|
2623
|
+
result.put("type", "xml");
|
|
2624
|
+
} else {
|
|
2625
|
+
// 其他文件返回 base64
|
|
2626
|
+
result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
|
|
2627
|
+
result.put("type", "binary");
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
result.put("path", resourcePath);
|
|
2631
|
+
result.put("size", data.length);
|
|
2632
|
+
|
|
2633
|
+
} finally {
|
|
2634
|
+
if (zipFile != null) {
|
|
2635
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
return result;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
1857
2642
|
/**
|
|
1858
2643
|
* 清理临时目录
|
|
1859
2644
|
*/
|