capacitor-dex-editor 0.0.36 → 0.0.38
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 +1 -2
- package/android/src/main/java/com/aetherlink/dexeditor/AxmlEditor.java +331 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexEditorPluginPlugin.java +19 -0
- package/android/src/main/java/com/aetherlink/dexeditor/DexManager.java +253 -41
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -68,8 +68,7 @@ dependencies {
|
|
|
68
68
|
// APK Signer - V1/V2/V3/V4 签名支持 (Android 7.0+)
|
|
69
69
|
api 'com.android.tools.build:apksig:8.7.2'
|
|
70
70
|
|
|
71
|
-
// ARSCLib
|
|
72
|
-
api 'io.github.reandroid:ARSCLib:1.3.8'
|
|
71
|
+
// 移除 ARSCLib,使用内置 AXML 解析器
|
|
73
72
|
|
|
74
73
|
testImplementation "junit:junit:$junitVersion"
|
|
75
74
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
package com.aetherlink.dexeditor;
|
|
2
|
+
|
|
3
|
+
import java.io.ByteArrayOutputStream;
|
|
4
|
+
import java.nio.ByteBuffer;
|
|
5
|
+
import java.nio.ByteOrder;
|
|
6
|
+
import java.nio.charset.StandardCharsets;
|
|
7
|
+
import java.util.ArrayList;
|
|
8
|
+
import java.util.List;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AXML 编辑器 - 支持任意长度的字符串替换
|
|
12
|
+
* 通过重建字符串池实现
|
|
13
|
+
*/
|
|
14
|
+
public class AxmlEditor {
|
|
15
|
+
|
|
16
|
+
private static final int AXML_MAGIC = 0x00080003;
|
|
17
|
+
private static final int STRING_POOL_TYPE = 0x0001;
|
|
18
|
+
|
|
19
|
+
private byte[] data;
|
|
20
|
+
private int stringPoolStart;
|
|
21
|
+
private int stringPoolSize;
|
|
22
|
+
private int stringCount;
|
|
23
|
+
private int styleCount;
|
|
24
|
+
private int flags;
|
|
25
|
+
private int stringsOffset;
|
|
26
|
+
private int stylesOffset;
|
|
27
|
+
private List<String> strings;
|
|
28
|
+
private byte[] beforeStringPool;
|
|
29
|
+
private byte[] afterStringPool;
|
|
30
|
+
private boolean isUtf8;
|
|
31
|
+
|
|
32
|
+
public AxmlEditor(byte[] axmlData) {
|
|
33
|
+
this.data = axmlData;
|
|
34
|
+
this.strings = new ArrayList<>();
|
|
35
|
+
parse();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private void parse() {
|
|
39
|
+
if (data.length < 8) return;
|
|
40
|
+
|
|
41
|
+
ByteBuffer buffer = ByteBuffer.wrap(data);
|
|
42
|
+
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
|
43
|
+
|
|
44
|
+
// 检查魔数
|
|
45
|
+
int magic = buffer.getInt();
|
|
46
|
+
if (magic != AXML_MAGIC) return;
|
|
47
|
+
|
|
48
|
+
int fileSize = buffer.getInt();
|
|
49
|
+
|
|
50
|
+
// 查找字符串池
|
|
51
|
+
while (buffer.position() < data.length - 8) {
|
|
52
|
+
int chunkStart = buffer.position();
|
|
53
|
+
int chunkType = buffer.getShort() & 0xFFFF;
|
|
54
|
+
int headerSize = buffer.getShort() & 0xFFFF;
|
|
55
|
+
int chunkSize = buffer.getInt();
|
|
56
|
+
|
|
57
|
+
if (chunkType == STRING_POOL_TYPE) {
|
|
58
|
+
stringPoolStart = chunkStart;
|
|
59
|
+
stringPoolSize = chunkSize;
|
|
60
|
+
|
|
61
|
+
// 保存字符串池之前的数据
|
|
62
|
+
beforeStringPool = new byte[chunkStart];
|
|
63
|
+
System.arraycopy(data, 0, beforeStringPool, 0, chunkStart);
|
|
64
|
+
|
|
65
|
+
// 保存字符串池之后的数据
|
|
66
|
+
int afterStart = chunkStart + chunkSize;
|
|
67
|
+
if (afterStart < data.length) {
|
|
68
|
+
afterStringPool = new byte[data.length - afterStart];
|
|
69
|
+
System.arraycopy(data, afterStart, afterStringPool, 0, afterStringPool.length);
|
|
70
|
+
} else {
|
|
71
|
+
afterStringPool = new byte[0];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 解析字符串池
|
|
75
|
+
stringCount = buffer.getInt();
|
|
76
|
+
styleCount = buffer.getInt();
|
|
77
|
+
flags = buffer.getInt();
|
|
78
|
+
stringsOffset = buffer.getInt();
|
|
79
|
+
stylesOffset = buffer.getInt();
|
|
80
|
+
|
|
81
|
+
isUtf8 = (flags & 0x100) != 0;
|
|
82
|
+
|
|
83
|
+
// 读取字符串偏移表
|
|
84
|
+
int[] offsets = new int[stringCount];
|
|
85
|
+
for (int i = 0; i < stringCount; i++) {
|
|
86
|
+
offsets[i] = buffer.getInt();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 读取字符串
|
|
90
|
+
int stringsStart = chunkStart + stringsOffset;
|
|
91
|
+
for (int i = 0; i < stringCount; i++) {
|
|
92
|
+
int stringStart = stringsStart + offsets[i];
|
|
93
|
+
if (stringStart < data.length) {
|
|
94
|
+
strings.add(readStringAt(stringStart));
|
|
95
|
+
} else {
|
|
96
|
+
strings.add("");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
buffer.position(chunkStart + chunkSize);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private String readStringAt(int pos) {
|
|
108
|
+
try {
|
|
109
|
+
if (isUtf8) {
|
|
110
|
+
int charLen = data[pos] & 0xFF;
|
|
111
|
+
int byteLen;
|
|
112
|
+
int dataStart;
|
|
113
|
+
|
|
114
|
+
if ((charLen & 0x80) != 0) {
|
|
115
|
+
charLen = ((charLen & 0x7F) << 8) | (data[pos + 1] & 0xFF);
|
|
116
|
+
byteLen = data[pos + 2] & 0xFF;
|
|
117
|
+
if ((byteLen & 0x80) != 0) {
|
|
118
|
+
byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 3] & 0xFF);
|
|
119
|
+
dataStart = pos + 4;
|
|
120
|
+
} else {
|
|
121
|
+
dataStart = pos + 3;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
byteLen = data[pos + 1] & 0xFF;
|
|
125
|
+
if ((byteLen & 0x80) != 0) {
|
|
126
|
+
byteLen = ((byteLen & 0x7F) << 8) | (data[pos + 2] & 0xFF);
|
|
127
|
+
dataStart = pos + 3;
|
|
128
|
+
} else {
|
|
129
|
+
dataStart = pos + 2;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (dataStart + byteLen > data.length) {
|
|
134
|
+
byteLen = data.length - dataStart;
|
|
135
|
+
}
|
|
136
|
+
if (byteLen <= 0) return "";
|
|
137
|
+
|
|
138
|
+
return new String(data, dataStart, byteLen, StandardCharsets.UTF_8);
|
|
139
|
+
} else {
|
|
140
|
+
int charLen = (data[pos] & 0xFF) | ((data[pos + 1] & 0xFF) << 8);
|
|
141
|
+
if ((charLen & 0x8000) != 0) {
|
|
142
|
+
int high = (data[pos + 2] & 0xFF) | ((data[pos + 3] & 0xFF) << 8);
|
|
143
|
+
charLen = ((charLen & 0x7FFF) << 16) | high;
|
|
144
|
+
pos += 4;
|
|
145
|
+
} else {
|
|
146
|
+
pos += 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (pos + charLen * 2 > data.length) {
|
|
150
|
+
charLen = (data.length - pos) / 2;
|
|
151
|
+
}
|
|
152
|
+
if (charLen <= 0) return "";
|
|
153
|
+
|
|
154
|
+
char[] chars = new char[charLen];
|
|
155
|
+
for (int i = 0; i < charLen; i++) {
|
|
156
|
+
chars[i] = (char) ((data[pos + i * 2] & 0xFF) | ((data[pos + i * 2 + 1] & 0xFF) << 8));
|
|
157
|
+
}
|
|
158
|
+
return new String(chars);
|
|
159
|
+
}
|
|
160
|
+
} catch (Exception e) {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 替换字符串
|
|
167
|
+
* @return 替换的次数
|
|
168
|
+
*/
|
|
169
|
+
public int replaceString(String oldValue, String newValue) {
|
|
170
|
+
int count = 0;
|
|
171
|
+
for (int i = 0; i < strings.size(); i++) {
|
|
172
|
+
String str = strings.get(i);
|
|
173
|
+
if (str != null && str.contains(oldValue)) {
|
|
174
|
+
strings.set(i, str.replace(oldValue, newValue));
|
|
175
|
+
count++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 构建修改后的 AXML 数据
|
|
183
|
+
*/
|
|
184
|
+
public byte[] build() {
|
|
185
|
+
if (strings.isEmpty() || beforeStringPool == null) {
|
|
186
|
+
return data;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
191
|
+
|
|
192
|
+
// 写入字符串池之前的数据
|
|
193
|
+
baos.write(beforeStringPool);
|
|
194
|
+
|
|
195
|
+
// 构建新的字符串池
|
|
196
|
+
byte[] newStringPool = buildStringPool();
|
|
197
|
+
baos.write(newStringPool);
|
|
198
|
+
|
|
199
|
+
// 写入字符串池之后的数据
|
|
200
|
+
baos.write(afterStringPool);
|
|
201
|
+
|
|
202
|
+
// 更新文件大小
|
|
203
|
+
byte[] result = baos.toByteArray();
|
|
204
|
+
int newFileSize = result.length;
|
|
205
|
+
|
|
206
|
+
// 更新文件头中的大小
|
|
207
|
+
result[4] = (byte) (newFileSize & 0xFF);
|
|
208
|
+
result[5] = (byte) ((newFileSize >> 8) & 0xFF);
|
|
209
|
+
result[6] = (byte) ((newFileSize >> 16) & 0xFF);
|
|
210
|
+
result[7] = (byte) ((newFileSize >> 24) & 0xFF);
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
} catch (Exception e) {
|
|
214
|
+
return data;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private byte[] buildStringPool() throws Exception {
|
|
219
|
+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
220
|
+
|
|
221
|
+
// 先构建字符串数据
|
|
222
|
+
ByteArrayOutputStream stringData = new ByteArrayOutputStream();
|
|
223
|
+
int[] offsets = new int[strings.size()];
|
|
224
|
+
|
|
225
|
+
for (int i = 0; i < strings.size(); i++) {
|
|
226
|
+
offsets[i] = stringData.size();
|
|
227
|
+
String str = strings.get(i);
|
|
228
|
+
if (str == null) str = "";
|
|
229
|
+
|
|
230
|
+
if (isUtf8) {
|
|
231
|
+
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
|
|
232
|
+
int charLen = str.length();
|
|
233
|
+
int byteLen = bytes.length;
|
|
234
|
+
|
|
235
|
+
// 写入字符长度
|
|
236
|
+
if (charLen > 127) {
|
|
237
|
+
stringData.write((charLen >> 8) | 0x80);
|
|
238
|
+
stringData.write(charLen & 0xFF);
|
|
239
|
+
} else {
|
|
240
|
+
stringData.write(charLen);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 写入字节长度
|
|
244
|
+
if (byteLen > 127) {
|
|
245
|
+
stringData.write((byteLen >> 8) | 0x80);
|
|
246
|
+
stringData.write(byteLen & 0xFF);
|
|
247
|
+
} else {
|
|
248
|
+
stringData.write(byteLen);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 写入字符串数据
|
|
252
|
+
stringData.write(bytes);
|
|
253
|
+
stringData.write(0); // null terminator
|
|
254
|
+
} else {
|
|
255
|
+
// UTF-16
|
|
256
|
+
int charLen = str.length();
|
|
257
|
+
if (charLen > 0x7FFF) {
|
|
258
|
+
stringData.write((charLen & 0xFF) | 0x80);
|
|
259
|
+
stringData.write((charLen >> 8) | 0x80);
|
|
260
|
+
stringData.write((charLen >> 16) & 0xFF);
|
|
261
|
+
stringData.write((charLen >> 24) & 0xFF);
|
|
262
|
+
} else {
|
|
263
|
+
stringData.write(charLen & 0xFF);
|
|
264
|
+
stringData.write((charLen >> 8) & 0xFF);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (int j = 0; j < str.length(); j++) {
|
|
268
|
+
char c = str.charAt(j);
|
|
269
|
+
stringData.write(c & 0xFF);
|
|
270
|
+
stringData.write((c >> 8) & 0xFF);
|
|
271
|
+
}
|
|
272
|
+
// null terminator
|
|
273
|
+
stringData.write(0);
|
|
274
|
+
stringData.write(0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 计算头部大小
|
|
279
|
+
int headerSize = 28; // 固定头部
|
|
280
|
+
int offsetTableSize = strings.size() * 4;
|
|
281
|
+
int styleOffsetTableSize = styleCount * 4;
|
|
282
|
+
int stringsDataSize = stringData.size();
|
|
283
|
+
|
|
284
|
+
// 对齐到 4 字节
|
|
285
|
+
int padding = (4 - (stringsDataSize % 4)) % 4;
|
|
286
|
+
stringsDataSize += padding;
|
|
287
|
+
|
|
288
|
+
int newStringsOffset = headerSize + offsetTableSize + styleOffsetTableSize;
|
|
289
|
+
int totalSize = newStringsOffset + stringsDataSize;
|
|
290
|
+
|
|
291
|
+
// 写入 chunk 头
|
|
292
|
+
ByteBuffer header = ByteBuffer.allocate(headerSize);
|
|
293
|
+
header.order(ByteOrder.LITTLE_ENDIAN);
|
|
294
|
+
header.putShort((short) STRING_POOL_TYPE); // type
|
|
295
|
+
header.putShort((short) headerSize); // header size
|
|
296
|
+
header.putInt(totalSize); // chunk size
|
|
297
|
+
header.putInt(strings.size()); // string count
|
|
298
|
+
header.putInt(styleCount); // style count
|
|
299
|
+
header.putInt(flags); // flags
|
|
300
|
+
header.putInt(newStringsOffset); // strings offset
|
|
301
|
+
header.putInt(0); // styles offset (暂不支持)
|
|
302
|
+
|
|
303
|
+
baos.write(header.array());
|
|
304
|
+
|
|
305
|
+
// 写入偏移表
|
|
306
|
+
for (int offset : offsets) {
|
|
307
|
+
baos.write(offset & 0xFF);
|
|
308
|
+
baos.write((offset >> 8) & 0xFF);
|
|
309
|
+
baos.write((offset >> 16) & 0xFF);
|
|
310
|
+
baos.write((offset >> 24) & 0xFF);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 写入样式偏移表(空)
|
|
314
|
+
for (int i = 0; i < styleCount; i++) {
|
|
315
|
+
baos.write(0);
|
|
316
|
+
baos.write(0);
|
|
317
|
+
baos.write(0);
|
|
318
|
+
baos.write(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 写入字符串数据
|
|
322
|
+
baos.write(stringData.toByteArray());
|
|
323
|
+
|
|
324
|
+
// 写入对齐填充
|
|
325
|
+
for (int i = 0; i < padding; i++) {
|
|
326
|
+
baos.write(0);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return baos.toByteArray();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -538,6 +538,25 @@ public class DexEditorPluginPlugin extends Plugin {
|
|
|
538
538
|
));
|
|
539
539
|
break;
|
|
540
540
|
|
|
541
|
+
case "listApkFiles":
|
|
542
|
+
result.put("data", dexManager.listApkFiles(
|
|
543
|
+
params.getString("apkPath"),
|
|
544
|
+
params.optString("filter", ""),
|
|
545
|
+
params.optInt("limit", 100),
|
|
546
|
+
params.optInt("offset", 0)
|
|
547
|
+
));
|
|
548
|
+
break;
|
|
549
|
+
|
|
550
|
+
case "readApkFile":
|
|
551
|
+
result.put("data", dexManager.readApkFile(
|
|
552
|
+
params.getString("apkPath"),
|
|
553
|
+
params.getString("filePath"),
|
|
554
|
+
params.optBoolean("asBase64", false),
|
|
555
|
+
params.optInt("maxBytes", 0),
|
|
556
|
+
params.optInt("offset", 0)
|
|
557
|
+
));
|
|
558
|
+
break;
|
|
559
|
+
|
|
541
560
|
default:
|
|
542
561
|
result.put("success", false);
|
|
543
562
|
result.put("error", "Unknown action: " + action);
|
|
@@ -2407,31 +2407,39 @@ public class DexManager {
|
|
|
2407
2407
|
// ==================== XML/资源操作方法 ====================
|
|
2408
2408
|
|
|
2409
2409
|
/**
|
|
2410
|
-
* 获取 APK 的 AndroidManifest.xml
|
|
2410
|
+
* 获取 APK 的 AndroidManifest.xml(解码为可读 XML)
|
|
2411
2411
|
*/
|
|
2412
2412
|
public JSObject getManifestFromApk(String apkPath) throws Exception {
|
|
2413
2413
|
JSObject result = new JSObject();
|
|
2414
2414
|
|
|
2415
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2415
2416
|
try {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
com.reandroid.arsc.chunk.xml.AndroidManifestBlock manifest = apkModule.getAndroidManifest();
|
|
2417
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2418
|
+
java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
2419
2419
|
|
|
2420
|
-
if (
|
|
2421
|
-
apkModule.close();
|
|
2420
|
+
if (manifestEntry == null) {
|
|
2422
2421
|
throw new Exception("AndroidManifest.xml not found in APK");
|
|
2423
2422
|
}
|
|
2424
2423
|
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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);
|
|
2428
2436
|
|
|
2429
2437
|
result.put("manifest", xmlContent);
|
|
2430
2438
|
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2439
|
+
} finally {
|
|
2440
|
+
if (zipFile != null) {
|
|
2441
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
2442
|
+
}
|
|
2435
2443
|
}
|
|
2436
2444
|
|
|
2437
2445
|
return result;
|
|
@@ -2661,7 +2669,7 @@ public class DexManager {
|
|
|
2661
2669
|
}
|
|
2662
2670
|
|
|
2663
2671
|
/**
|
|
2664
|
-
* 精准替换 AndroidManifest.xml
|
|
2672
|
+
* 精准替换 AndroidManifest.xml 中的字符串(二进制替换,支持任意长度)
|
|
2665
2673
|
*/
|
|
2666
2674
|
public JSObject replaceInManifest(String apkPath, org.json.JSONArray replacements) throws Exception {
|
|
2667
2675
|
JSObject result = new JSObject();
|
|
@@ -2669,36 +2677,37 @@ public class DexManager {
|
|
|
2669
2677
|
int replacedCount = 0;
|
|
2670
2678
|
|
|
2671
2679
|
try {
|
|
2672
|
-
//
|
|
2673
|
-
|
|
2674
|
-
|
|
2680
|
+
// 读取 APK 中的 AndroidManifest.xml
|
|
2681
|
+
java.util.zip.ZipFile zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2682
|
+
java.util.zip.ZipEntry manifestEntry = zipFile.getEntry("AndroidManifest.xml");
|
|
2675
2683
|
|
|
2676
|
-
if (
|
|
2677
|
-
|
|
2684
|
+
if (manifestEntry == null) {
|
|
2685
|
+
zipFile.close();
|
|
2678
2686
|
throw new Exception("AndroidManifest.xml not found in APK");
|
|
2679
2687
|
}
|
|
2680
2688
|
|
|
2681
|
-
//
|
|
2682
|
-
|
|
2689
|
+
// 读取 AXML 数据
|
|
2690
|
+
java.io.InputStream is = zipFile.getInputStream(manifestEntry);
|
|
2691
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
2692
|
+
byte[] buffer = new byte[8192];
|
|
2693
|
+
int len;
|
|
2694
|
+
while ((len = is.read(buffer)) != -1) {
|
|
2695
|
+
baos.write(buffer, 0, len);
|
|
2696
|
+
}
|
|
2697
|
+
is.close();
|
|
2698
|
+
zipFile.close();
|
|
2699
|
+
|
|
2700
|
+
byte[] axmlData = baos.toByteArray();
|
|
2701
|
+
|
|
2702
|
+
// 使用 AxmlEditor 执行替换(支持任意长度)
|
|
2703
|
+
AxmlEditor editor = new AxmlEditor(axmlData);
|
|
2683
2704
|
|
|
2684
2705
|
for (int i = 0; i < replacements.length(); i++) {
|
|
2685
2706
|
org.json.JSONObject replacement = replacements.getJSONObject(i);
|
|
2686
2707
|
String oldValue = replacement.getString("oldValue");
|
|
2687
2708
|
String newValue = replacement.getString("newValue");
|
|
2688
2709
|
|
|
2689
|
-
int count =
|
|
2690
|
-
|
|
2691
|
-
// 遍历字符串池中的所有字符串
|
|
2692
|
-
for (com.reandroid.arsc.item.StringItem stringItem : stringPool.listItems()) {
|
|
2693
|
-
if (stringItem != null) {
|
|
2694
|
-
String str = stringItem.get();
|
|
2695
|
-
if (str != null && str.contains(oldValue)) {
|
|
2696
|
-
String newStr = str.replace(oldValue, newValue);
|
|
2697
|
-
stringItem.set(newStr);
|
|
2698
|
-
count++;
|
|
2699
|
-
}
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
2710
|
+
int count = editor.replaceString(oldValue, newValue);
|
|
2702
2711
|
|
|
2703
2712
|
JSObject detail = new JSObject();
|
|
2704
2713
|
detail.put("oldValue", oldValue);
|
|
@@ -2710,7 +2719,6 @@ public class DexManager {
|
|
|
2710
2719
|
}
|
|
2711
2720
|
|
|
2712
2721
|
if (replacedCount == 0) {
|
|
2713
|
-
apkModule.close();
|
|
2714
2722
|
result.put("success", true);
|
|
2715
2723
|
result.put("replacedCount", 0);
|
|
2716
2724
|
result.put("details", details);
|
|
@@ -2718,16 +2726,55 @@ public class DexManager {
|
|
|
2718
2726
|
return result;
|
|
2719
2727
|
}
|
|
2720
2728
|
|
|
2721
|
-
//
|
|
2722
|
-
|
|
2729
|
+
// 获取修改后的数据
|
|
2730
|
+
byte[] modifiedData = editor.build();
|
|
2723
2731
|
|
|
2724
|
-
//
|
|
2732
|
+
// 替换 APK 中的 AndroidManifest.xml
|
|
2733
|
+
java.io.File apkFile = new java.io.File(apkPath);
|
|
2725
2734
|
java.io.File tempApk = new java.io.File(apkPath + ".tmp");
|
|
2726
|
-
|
|
2727
|
-
|
|
2735
|
+
|
|
2736
|
+
java.util.zip.ZipInputStream zis = new java.util.zip.ZipInputStream(
|
|
2737
|
+
new java.io.BufferedInputStream(new java.io.FileInputStream(apkFile)));
|
|
2738
|
+
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(
|
|
2739
|
+
new java.io.BufferedOutputStream(new java.io.FileOutputStream(tempApk)));
|
|
2740
|
+
|
|
2741
|
+
java.util.zip.ZipEntry entry;
|
|
2742
|
+
while ((entry = zis.getNextEntry()) != null) {
|
|
2743
|
+
if (entry.getName().equals("AndroidManifest.xml")) {
|
|
2744
|
+
// 写入修改后的 Manifest
|
|
2745
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry("AndroidManifest.xml");
|
|
2746
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2747
|
+
zos.putNextEntry(newEntry);
|
|
2748
|
+
zos.write(modifiedData);
|
|
2749
|
+
zos.closeEntry();
|
|
2750
|
+
} else {
|
|
2751
|
+
// 复制其他文件
|
|
2752
|
+
java.util.zip.ZipEntry newEntry = new java.util.zip.ZipEntry(entry.getName());
|
|
2753
|
+
newEntry.setTime(entry.getTime());
|
|
2754
|
+
if (entry.getMethod() == java.util.zip.ZipEntry.STORED) {
|
|
2755
|
+
newEntry.setMethod(java.util.zip.ZipEntry.STORED);
|
|
2756
|
+
newEntry.setSize(entry.getSize());
|
|
2757
|
+
newEntry.setCrc(entry.getCrc());
|
|
2758
|
+
} else {
|
|
2759
|
+
newEntry.setMethod(java.util.zip.ZipEntry.DEFLATED);
|
|
2760
|
+
}
|
|
2761
|
+
zos.putNextEntry(newEntry);
|
|
2762
|
+
if (!entry.isDirectory()) {
|
|
2763
|
+
byte[] buf = new byte[8192];
|
|
2764
|
+
int n;
|
|
2765
|
+
while ((n = zis.read(buf)) != -1) {
|
|
2766
|
+
zos.write(buf, 0, n);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
zos.closeEntry();
|
|
2770
|
+
}
|
|
2771
|
+
zis.closeEntry();
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
zis.close();
|
|
2775
|
+
zos.close();
|
|
2728
2776
|
|
|
2729
2777
|
// 替换原文件
|
|
2730
|
-
java.io.File apkFile = new java.io.File(apkPath);
|
|
2731
2778
|
if (!apkFile.delete()) {
|
|
2732
2779
|
Log.e(TAG, "Failed to delete original APK");
|
|
2733
2780
|
}
|
|
@@ -2926,6 +2973,171 @@ public class DexManager {
|
|
|
2926
2973
|
return pos + 2;
|
|
2927
2974
|
}
|
|
2928
2975
|
|
|
2976
|
+
/**
|
|
2977
|
+
* 列出 APK 中的所有文件
|
|
2978
|
+
*/
|
|
2979
|
+
public JSObject listApkFiles(String apkPath, String filter, int limit, int offset) throws Exception {
|
|
2980
|
+
JSObject result = new JSObject();
|
|
2981
|
+
JSArray files = new JSArray();
|
|
2982
|
+
|
|
2983
|
+
java.util.zip.ZipFile zipFile = null;
|
|
2984
|
+
try {
|
|
2985
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
2986
|
+
java.util.Enumeration<? extends java.util.zip.ZipEntry> entries = zipFile.entries();
|
|
2987
|
+
|
|
2988
|
+
java.util.List<JSObject> allFiles = new java.util.ArrayList<>();
|
|
2989
|
+
|
|
2990
|
+
while (entries.hasMoreElements()) {
|
|
2991
|
+
java.util.zip.ZipEntry entry = entries.nextElement();
|
|
2992
|
+
String name = entry.getName();
|
|
2993
|
+
|
|
2994
|
+
// 应用过滤
|
|
2995
|
+
if (!filter.isEmpty() && !name.contains(filter)) {
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
JSObject fileInfo = new JSObject();
|
|
3000
|
+
fileInfo.put("path", name);
|
|
3001
|
+
fileInfo.put("size", entry.getSize());
|
|
3002
|
+
fileInfo.put("compressedSize", entry.getCompressedSize());
|
|
3003
|
+
fileInfo.put("isDirectory", entry.isDirectory());
|
|
3004
|
+
|
|
3005
|
+
// 判断文件类型
|
|
3006
|
+
String type = "unknown";
|
|
3007
|
+
if (name.endsWith(".dex")) type = "dex";
|
|
3008
|
+
else if (name.endsWith(".so")) type = "native";
|
|
3009
|
+
else if (name.endsWith(".xml")) type = "xml";
|
|
3010
|
+
else if (name.startsWith("res/")) type = "resource";
|
|
3011
|
+
else if (name.startsWith("assets/")) type = "asset";
|
|
3012
|
+
else if (name.startsWith("lib/")) type = "native";
|
|
3013
|
+
else if (name.startsWith("META-INF/")) type = "meta";
|
|
3014
|
+
else if (name.equals("AndroidManifest.xml")) type = "manifest";
|
|
3015
|
+
else if (name.equals("resources.arsc")) type = "arsc";
|
|
3016
|
+
fileInfo.put("type", type);
|
|
3017
|
+
|
|
3018
|
+
allFiles.add(fileInfo);
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
int total = allFiles.size();
|
|
3022
|
+
|
|
3023
|
+
// 分页
|
|
3024
|
+
int start = Math.min(offset, total);
|
|
3025
|
+
int end = Math.min(offset + limit, total);
|
|
3026
|
+
|
|
3027
|
+
for (int i = start; i < end; i++) {
|
|
3028
|
+
files.put(allFiles.get(i));
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
result.put("files", files);
|
|
3032
|
+
result.put("total", total);
|
|
3033
|
+
result.put("offset", offset);
|
|
3034
|
+
result.put("limit", limit);
|
|
3035
|
+
result.put("returned", files.length());
|
|
3036
|
+
result.put("hasMore", end < total);
|
|
3037
|
+
|
|
3038
|
+
} finally {
|
|
3039
|
+
if (zipFile != null) {
|
|
3040
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
return result;
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
/**
|
|
3048
|
+
* 读取 APK 中的任意文件
|
|
3049
|
+
*/
|
|
3050
|
+
public JSObject readApkFile(String apkPath, String filePath, boolean asBase64, int maxBytes, int offset) throws Exception {
|
|
3051
|
+
JSObject result = new JSObject();
|
|
3052
|
+
|
|
3053
|
+
java.util.zip.ZipFile zipFile = null;
|
|
3054
|
+
try {
|
|
3055
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
3056
|
+
java.util.zip.ZipEntry entry = zipFile.getEntry(filePath);
|
|
3057
|
+
|
|
3058
|
+
if (entry == null) {
|
|
3059
|
+
result.put("error", "File not found: " + filePath);
|
|
3060
|
+
return result;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
result.put("path", filePath);
|
|
3064
|
+
result.put("size", entry.getSize());
|
|
3065
|
+
result.put("compressedSize", entry.getCompressedSize());
|
|
3066
|
+
|
|
3067
|
+
java.io.InputStream is = zipFile.getInputStream(entry);
|
|
3068
|
+
|
|
3069
|
+
// 跳过偏移量
|
|
3070
|
+
if (offset > 0) {
|
|
3071
|
+
is.skip(offset);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// 读取数据
|
|
3075
|
+
int readSize = maxBytes > 0 ? maxBytes : (int) entry.getSize();
|
|
3076
|
+
if (readSize > 1024 * 1024) { // 限制最大 1MB
|
|
3077
|
+
readSize = 1024 * 1024;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
byte[] buffer = new byte[readSize];
|
|
3081
|
+
int totalRead = 0;
|
|
3082
|
+
int read;
|
|
3083
|
+
while (totalRead < readSize && (read = is.read(buffer, totalRead, readSize - totalRead)) != -1) {
|
|
3084
|
+
totalRead += read;
|
|
3085
|
+
}
|
|
3086
|
+
is.close();
|
|
3087
|
+
|
|
3088
|
+
byte[] data = new byte[totalRead];
|
|
3089
|
+
System.arraycopy(buffer, 0, data, 0, totalRead);
|
|
3090
|
+
|
|
3091
|
+
if (asBase64) {
|
|
3092
|
+
// Base64 编码返回
|
|
3093
|
+
result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
|
|
3094
|
+
result.put("encoding", "base64");
|
|
3095
|
+
} else {
|
|
3096
|
+
// 尝试作为文本返回
|
|
3097
|
+
String content = new String(data, java.nio.charset.StandardCharsets.UTF_8);
|
|
3098
|
+
|
|
3099
|
+
// 检查是否是二进制文件
|
|
3100
|
+
boolean isBinary = false;
|
|
3101
|
+
for (int i = 0; i < Math.min(100, data.length); i++) {
|
|
3102
|
+
if (data[i] == 0) {
|
|
3103
|
+
isBinary = true;
|
|
3104
|
+
break;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
if (isBinary && !filePath.endsWith(".xml")) {
|
|
3109
|
+
// 二进制文件自动使用 Base64
|
|
3110
|
+
result.put("content", android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP));
|
|
3111
|
+
result.put("encoding", "base64");
|
|
3112
|
+
result.put("note", "Binary file, auto-encoded as base64");
|
|
3113
|
+
} else {
|
|
3114
|
+
// 如果是 XML 文件,尝试解码 AXML
|
|
3115
|
+
if (filePath.endsWith(".xml") && data.length > 4) {
|
|
3116
|
+
int magic = (data[0] & 0xFF) | ((data[1] & 0xFF) << 8) |
|
|
3117
|
+
((data[2] & 0xFF) << 16) | ((data[3] & 0xFF) << 24);
|
|
3118
|
+
if (magic == 0x00080003) {
|
|
3119
|
+
// 是 AXML 格式,解码
|
|
3120
|
+
content = AxmlParser.decode(data);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
result.put("content", content);
|
|
3124
|
+
result.put("encoding", "text");
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
result.put("offset", offset);
|
|
3129
|
+
result.put("bytesRead", totalRead);
|
|
3130
|
+
result.put("hasMore", offset + totalRead < entry.getSize());
|
|
3131
|
+
|
|
3132
|
+
} finally {
|
|
3133
|
+
if (zipFile != null) {
|
|
3134
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
return result;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
2929
3141
|
/**
|
|
2930
3142
|
* 清理临时目录
|
|
2931
3143
|
*/
|