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.
@@ -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("&", "&amp;")
284
+ .replace("<", "&lt;")
285
+ .replace(">", "&gt;")
286
+ .replace("\"", "&quot;")
287
+ .replace("'", "&apos;");
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
- // 使用 phlox AXML 库解析
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
- // 使用 phlox AXML 库编码
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-dex-editor",
3
- "version": "0.0.27",
3
+ "version": "0.0.29",
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",