capacitor-dex-editor 0.0.27 → 0.0.29
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 +0 -3
- package/android/src/main/java/com/aetherlink/dexeditor/AxmlParser.java +289 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +90 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexManager.java +4 -8
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -67,9 +67,6 @@ 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
|
-
|
|
73
70
|
testImplementation "junit:junit:$junitVersion"
|
|
74
71
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
75
72
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
package com.aetherlink.dexeditor;
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayInputStream;
|
|
4
|
+
import java.nio.ByteBuffer;
|
|
5
|
+
import java.nio.ByteOrder;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 简单的 AXML (Android Binary XML) 解析器
|
|
9
|
+
* 用于解码 APK 中的二进制 XML 文件
|
|
10
|
+
*/
|
|
11
|
+
public class AxmlParser {
|
|
12
|
+
|
|
13
|
+
// AXML 文件头魔数
|
|
14
|
+
private static final int AXML_MAGIC = 0x00080003;
|
|
15
|
+
|
|
16
|
+
// Chunk 类型
|
|
17
|
+
private static final int CHUNK_STRING_POOL = 0x001C0001;
|
|
18
|
+
private static final int CHUNK_RESOURCE_IDS = 0x00080180;
|
|
19
|
+
private static final int CHUNK_START_NAMESPACE = 0x00100100;
|
|
20
|
+
private static final int CHUNK_END_NAMESPACE = 0x00100101;
|
|
21
|
+
private static final int CHUNK_START_TAG = 0x00100102;
|
|
22
|
+
private static final int CHUNK_END_TAG = 0x00100103;
|
|
23
|
+
private static final int CHUNK_TEXT = 0x00100104;
|
|
24
|
+
|
|
25
|
+
private ByteBuffer buffer;
|
|
26
|
+
private String[] stringPool;
|
|
27
|
+
private StringBuilder xml;
|
|
28
|
+
private int indent = 0;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 解码 AXML 数据为可读 XML 字符串
|
|
32
|
+
*/
|
|
33
|
+
public static String decode(byte[] data) throws Exception {
|
|
34
|
+
AxmlParser parser = new AxmlParser();
|
|
35
|
+
return parser.parse(data);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private String parse(byte[] data) throws Exception {
|
|
39
|
+
buffer = ByteBuffer.wrap(data);
|
|
40
|
+
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
41
|
+
xml = new StringBuilder();
|
|
42
|
+
|
|
43
|
+
// 读取文件头
|
|
44
|
+
int magic = buffer.getInt();
|
|
45
|
+
if (magic != AXML_MAGIC) {
|
|
46
|
+
throw new Exception("Invalid AXML magic: " + Integer.toHexString(magic));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
int fileSize = buffer.getInt();
|
|
50
|
+
|
|
51
|
+
// 解析所有 chunks
|
|
52
|
+
while (buffer.position() < data.length) {
|
|
53
|
+
int chunkType = buffer.getInt();
|
|
54
|
+
int chunkSize = buffer.getInt();
|
|
55
|
+
int startPos = buffer.position() - 8;
|
|
56
|
+
|
|
57
|
+
switch (chunkType) {
|
|
58
|
+
case CHUNK_STRING_POOL:
|
|
59
|
+
parseStringPool(chunkSize);
|
|
60
|
+
break;
|
|
61
|
+
case CHUNK_RESOURCE_IDS:
|
|
62
|
+
// 跳过资源 ID
|
|
63
|
+
buffer.position(startPos + chunkSize);
|
|
64
|
+
break;
|
|
65
|
+
case CHUNK_START_NAMESPACE:
|
|
66
|
+
parseStartNamespace();
|
|
67
|
+
break;
|
|
68
|
+
case CHUNK_END_NAMESPACE:
|
|
69
|
+
parseEndNamespace();
|
|
70
|
+
break;
|
|
71
|
+
case CHUNK_START_TAG:
|
|
72
|
+
parseStartTag();
|
|
73
|
+
break;
|
|
74
|
+
case CHUNK_END_TAG:
|
|
75
|
+
parseEndTag();
|
|
76
|
+
break;
|
|
77
|
+
case CHUNK_TEXT:
|
|
78
|
+
parseText();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
// 跳过未知 chunk
|
|
82
|
+
buffer.position(startPos + chunkSize);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return xml.toString();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private void parseStringPool(int chunkSize) {
|
|
91
|
+
int stringCount = buffer.getInt();
|
|
92
|
+
int styleCount = buffer.getInt();
|
|
93
|
+
int flags = buffer.getInt();
|
|
94
|
+
int stringsOffset = buffer.getInt();
|
|
95
|
+
int stylesOffset = buffer.getInt();
|
|
96
|
+
|
|
97
|
+
// 读取字符串偏移表
|
|
98
|
+
int[] stringOffsets = new int[stringCount];
|
|
99
|
+
for (int i = 0; i < stringCount; i++) {
|
|
100
|
+
stringOffsets[i] = buffer.getInt();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 跳过样式偏移表
|
|
104
|
+
for (int i = 0; i < styleCount; i++) {
|
|
105
|
+
buffer.getInt();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 读取字符串
|
|
109
|
+
int stringsStart = buffer.position();
|
|
110
|
+
stringPool = new String[stringCount];
|
|
111
|
+
|
|
112
|
+
boolean isUtf8 = (flags & 0x100) != 0;
|
|
113
|
+
|
|
114
|
+
for (int i = 0; i < stringCount; i++) {
|
|
115
|
+
buffer.position(stringsStart + stringOffsets[i]);
|
|
116
|
+
stringPool[i] = readString(isUtf8);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private String readString(boolean isUtf8) {
|
|
121
|
+
if (isUtf8) {
|
|
122
|
+
// UTF-8 编码
|
|
123
|
+
int charLen = buffer.get() & 0xFF;
|
|
124
|
+
if ((charLen & 0x80) != 0) {
|
|
125
|
+
charLen = ((charLen & 0x7F) << 8) | (buffer.get() & 0xFF);
|
|
126
|
+
}
|
|
127
|
+
int byteLen = buffer.get() & 0xFF;
|
|
128
|
+
if ((byteLen & 0x80) != 0) {
|
|
129
|
+
byteLen = ((byteLen & 0x7F) << 8) | (buffer.get() & 0xFF);
|
|
130
|
+
}
|
|
131
|
+
byte[] bytes = new byte[byteLen];
|
|
132
|
+
buffer.get(bytes);
|
|
133
|
+
return new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
|
|
134
|
+
} else {
|
|
135
|
+
// UTF-16 编码
|
|
136
|
+
int charLen = buffer.getShort() & 0xFFFF;
|
|
137
|
+
if ((charLen & 0x8000) != 0) {
|
|
138
|
+
charLen = ((charLen & 0x7FFF) << 16) | (buffer.getShort() & 0xFFFF);
|
|
139
|
+
}
|
|
140
|
+
char[] chars = new char[charLen];
|
|
141
|
+
for (int i = 0; i < charLen; i++) {
|
|
142
|
+
chars[i] = buffer.getChar();
|
|
143
|
+
}
|
|
144
|
+
return new String(chars);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private void parseStartNamespace() {
|
|
149
|
+
int lineNumber = buffer.getInt();
|
|
150
|
+
int comment = buffer.getInt();
|
|
151
|
+
int prefix = buffer.getInt();
|
|
152
|
+
int uri = buffer.getInt();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void parseEndNamespace() {
|
|
156
|
+
int lineNumber = buffer.getInt();
|
|
157
|
+
int comment = buffer.getInt();
|
|
158
|
+
int prefix = buffer.getInt();
|
|
159
|
+
int uri = buffer.getInt();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private void parseStartTag() {
|
|
163
|
+
int lineNumber = buffer.getInt();
|
|
164
|
+
int comment = buffer.getInt();
|
|
165
|
+
int namespaceUri = buffer.getInt();
|
|
166
|
+
int name = buffer.getInt();
|
|
167
|
+
int flags = buffer.getShort() & 0xFFFF;
|
|
168
|
+
int attributeCount = buffer.getShort() & 0xFFFF;
|
|
169
|
+
int classAttribute = buffer.getShort() & 0xFFFF;
|
|
170
|
+
int idAttribute = buffer.getShort() & 0xFFFF;
|
|
171
|
+
int styleAttribute = buffer.getShort() & 0xFFFF;
|
|
172
|
+
|
|
173
|
+
// 添加缩进
|
|
174
|
+
for (int i = 0; i < indent; i++) {
|
|
175
|
+
xml.append(" ");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
xml.append("<");
|
|
179
|
+
xml.append(getString(name));
|
|
180
|
+
|
|
181
|
+
// 解析属性
|
|
182
|
+
for (int i = 0; i < attributeCount; i++) {
|
|
183
|
+
int attrNs = buffer.getInt();
|
|
184
|
+
int attrName = buffer.getInt();
|
|
185
|
+
int attrRawValue = buffer.getInt();
|
|
186
|
+
int attrType = buffer.getShort() & 0xFFFF;
|
|
187
|
+
buffer.getShort(); // size
|
|
188
|
+
int attrData = buffer.getInt();
|
|
189
|
+
|
|
190
|
+
xml.append("\n");
|
|
191
|
+
for (int j = 0; j <= indent; j++) {
|
|
192
|
+
xml.append(" ");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 命名空间前缀
|
|
196
|
+
if (attrNs >= 0 && attrNs < stringPool.length) {
|
|
197
|
+
String ns = stringPool[attrNs];
|
|
198
|
+
if (ns.contains("android")) {
|
|
199
|
+
xml.append("android:");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
xml.append(getString(attrName));
|
|
204
|
+
xml.append("=\"");
|
|
205
|
+
xml.append(getAttributeValue(attrRawValue, attrType, attrData));
|
|
206
|
+
xml.append("\"");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
xml.append(">\n");
|
|
210
|
+
indent++;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private void parseEndTag() {
|
|
214
|
+
int lineNumber = buffer.getInt();
|
|
215
|
+
int comment = buffer.getInt();
|
|
216
|
+
int namespaceUri = buffer.getInt();
|
|
217
|
+
int name = buffer.getInt();
|
|
218
|
+
|
|
219
|
+
indent--;
|
|
220
|
+
for (int i = 0; i < indent; i++) {
|
|
221
|
+
xml.append(" ");
|
|
222
|
+
}
|
|
223
|
+
xml.append("</");
|
|
224
|
+
xml.append(getString(name));
|
|
225
|
+
xml.append(">\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private void parseText() {
|
|
229
|
+
int lineNumber = buffer.getInt();
|
|
230
|
+
int comment = buffer.getInt();
|
|
231
|
+
int text = buffer.getInt();
|
|
232
|
+
buffer.getInt(); // unknown
|
|
233
|
+
buffer.getInt(); // unknown
|
|
234
|
+
|
|
235
|
+
for (int i = 0; i < indent; i++) {
|
|
236
|
+
xml.append(" ");
|
|
237
|
+
}
|
|
238
|
+
xml.append(getString(text));
|
|
239
|
+
xml.append("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private String getString(int index) {
|
|
243
|
+
if (index >= 0 && index < stringPool.length) {
|
|
244
|
+
return escapeXml(stringPool[index]);
|
|
245
|
+
}
|
|
246
|
+
return "";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private String getAttributeValue(int rawValue, int type, int data) {
|
|
250
|
+
// 如果有原始字符串值
|
|
251
|
+
if (rawValue >= 0 && rawValue < stringPool.length) {
|
|
252
|
+
return escapeXml(stringPool[rawValue]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 根据类型解析数据
|
|
256
|
+
switch (type >> 8) {
|
|
257
|
+
case 0x01: // 引用
|
|
258
|
+
return "@" + Integer.toHexString(data);
|
|
259
|
+
case 0x02: // 属性引用
|
|
260
|
+
return "?" + Integer.toHexString(data);
|
|
261
|
+
case 0x03: // 字符串
|
|
262
|
+
if (data >= 0 && data < stringPool.length) {
|
|
263
|
+
return escapeXml(stringPool[data]);
|
|
264
|
+
}
|
|
265
|
+
return "";
|
|
266
|
+
case 0x10: // 整数
|
|
267
|
+
return String.valueOf(data);
|
|
268
|
+
case 0x11: // 十六进制
|
|
269
|
+
return "0x" + Integer.toHexString(data);
|
|
270
|
+
case 0x12: // 布尔
|
|
271
|
+
return data != 0 ? "true" : "false";
|
|
272
|
+
case 0x1C: // 颜色 ARGB8
|
|
273
|
+
return String.format("#%08X", data);
|
|
274
|
+
case 0x1D: // 颜色 RGB8
|
|
275
|
+
return String.format("#%06X", data & 0xFFFFFF);
|
|
276
|
+
default:
|
|
277
|
+
return "0x" + Integer.toHexString(data);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private String escapeXml(String s) {
|
|
282
|
+
if (s == null) return "";
|
|
283
|
+
return s.replace("&", "&")
|
|
284
|
+
.replace("<", "<")
|
|
285
|
+
.replace(">", ">")
|
|
286
|
+
.replace("\"", """)
|
|
287
|
+
.replace("'", "'");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -441,6 +441,96 @@ public class DexEditorPluginPlugin extends Plugin {
|
|
|
441
441
|
));
|
|
442
442
|
break;
|
|
443
443
|
|
|
444
|
+
// ==================== MCP 工作流操作 ====================
|
|
445
|
+
case "listDexFiles":
|
|
446
|
+
result.put("data", dexManager.listDexFilesInApk(
|
|
447
|
+
params.getString("apkPath")
|
|
448
|
+
));
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case "openDex":
|
|
452
|
+
result.put("data", dexManager.openMultipleDex(
|
|
453
|
+
params.getString("apkPath"),
|
|
454
|
+
params.getJSONArray("dexFiles")
|
|
455
|
+
));
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case "listClasses":
|
|
459
|
+
result.put("data", dexManager.getClassesFromMultiSession(
|
|
460
|
+
params.getString("sessionId"),
|
|
461
|
+
params.optString("packageFilter", ""),
|
|
462
|
+
params.optInt("offset", 0),
|
|
463
|
+
params.optInt("limit", 100)
|
|
464
|
+
));
|
|
465
|
+
break;
|
|
466
|
+
|
|
467
|
+
case "searchInDexSession":
|
|
468
|
+
result.put("data", dexManager.searchInMultiSession(
|
|
469
|
+
params.getString("sessionId"),
|
|
470
|
+
params.getString("query"),
|
|
471
|
+
params.getString("searchType"),
|
|
472
|
+
params.optBoolean("caseSensitive", false),
|
|
473
|
+
params.optInt("maxResults", 50)
|
|
474
|
+
));
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
case "getClassSmaliFromSession":
|
|
478
|
+
result.put("data", dexManager.getClassSmaliFromSession(
|
|
479
|
+
params.getString("sessionId"),
|
|
480
|
+
params.getString("className")
|
|
481
|
+
));
|
|
482
|
+
break;
|
|
483
|
+
|
|
484
|
+
case "modifyClass":
|
|
485
|
+
dexManager.modifyClassInSession(
|
|
486
|
+
params.getString("sessionId"),
|
|
487
|
+
params.getString("className"),
|
|
488
|
+
params.getString("smaliContent")
|
|
489
|
+
);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case "saveDexToApk":
|
|
493
|
+
result.put("data", dexManager.saveMultiDexSessionToApk(
|
|
494
|
+
params.getString("sessionId")
|
|
495
|
+
));
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case "closeMultiDexSession":
|
|
499
|
+
dexManager.closeMultiDexSession(params.getString("sessionId"));
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
case "listSessions":
|
|
503
|
+
result.put("data", dexManager.listAllSessions());
|
|
504
|
+
break;
|
|
505
|
+
|
|
506
|
+
// ==================== XML/资源操作 ====================
|
|
507
|
+
case "getManifest":
|
|
508
|
+
result.put("data", dexManager.getManifestFromApk(
|
|
509
|
+
params.getString("apkPath")
|
|
510
|
+
));
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case "modifyManifest":
|
|
514
|
+
result.put("data", dexManager.modifyManifestInApk(
|
|
515
|
+
params.getString("apkPath"),
|
|
516
|
+
params.getString("newManifest")
|
|
517
|
+
));
|
|
518
|
+
break;
|
|
519
|
+
|
|
520
|
+
case "listResources":
|
|
521
|
+
result.put("data", dexManager.listResourcesInApk(
|
|
522
|
+
params.getString("apkPath"),
|
|
523
|
+
params.optString("filter", "")
|
|
524
|
+
));
|
|
525
|
+
break;
|
|
526
|
+
|
|
527
|
+
case "getResource":
|
|
528
|
+
result.put("data", dexManager.getResourceFromApk(
|
|
529
|
+
params.getString("apkPath"),
|
|
530
|
+
params.getString("resourcePath")
|
|
531
|
+
));
|
|
532
|
+
break;
|
|
533
|
+
|
|
444
534
|
default:
|
|
445
535
|
result.put("success", false);
|
|
446
536
|
result.put("error", "Unknown action: " + action);
|
|
@@ -2446,16 +2446,13 @@ public class DexManager {
|
|
|
2446
2446
|
}
|
|
2447
2447
|
|
|
2448
2448
|
/**
|
|
2449
|
-
* 解码二进制 AXML 为可读 XML
|
|
2449
|
+
* 解码二进制 AXML 为可读 XML(使用内置解析器)
|
|
2450
2450
|
*/
|
|
2451
2451
|
private String decodeAxml(byte[] axmlData) {
|
|
2452
2452
|
try {
|
|
2453
|
-
|
|
2454
|
-
com.phlox.axml.AXMLDocument doc = new com.phlox.axml.AXMLDocument(axmlData);
|
|
2455
|
-
return doc.toXmlString();
|
|
2453
|
+
return AxmlParser.decode(axmlData);
|
|
2456
2454
|
} catch (Exception e) {
|
|
2457
2455
|
Log.e(TAG, "AXML decode error: " + e.getMessage());
|
|
2458
|
-
// 降级:返回十六进制
|
|
2459
2456
|
return "# 无法解码 AXML: " + e.getMessage();
|
|
2460
2457
|
}
|
|
2461
2458
|
}
|
|
@@ -2538,11 +2535,10 @@ public class DexManager {
|
|
|
2538
2535
|
|
|
2539
2536
|
/**
|
|
2540
2537
|
* 将 XML 编码为二进制 AXML
|
|
2538
|
+
* 注意:AXML 编码比较复杂,暂时不支持修改功能
|
|
2541
2539
|
*/
|
|
2542
2540
|
private byte[] encodeAxml(String xmlContent) throws Exception {
|
|
2543
|
-
|
|
2544
|
-
com.phlox.axml.AXMLDocument doc = com.phlox.axml.AXMLDocument.parseXml(xmlContent);
|
|
2545
|
-
return doc.toByteArray();
|
|
2541
|
+
throw new UnsupportedOperationException("AXML 编码功能暂不支持,请使用 APKTool 进行 Manifest 修改");
|
|
2546
2542
|
}
|
|
2547
2543
|
|
|
2548
2544
|
/**
|