capacitor-dex-editor 0.0.6 → 0.0.8
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.
|
@@ -401,6 +401,29 @@ public class DexEditorPluginPlugin extends Plugin {
|
|
|
401
401
|
));
|
|
402
402
|
break;
|
|
403
403
|
|
|
404
|
+
// ==================== DEX 编辑器操作 ====================
|
|
405
|
+
case "listDexClasses":
|
|
406
|
+
result.put("data", dexManager.listDexClassesFromApk(
|
|
407
|
+
params.getString("apkPath"),
|
|
408
|
+
params.getString("dexPath")
|
|
409
|
+
));
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case "getDexStrings":
|
|
413
|
+
result.put("data", dexManager.getDexStringsFromApk(
|
|
414
|
+
params.getString("apkPath"),
|
|
415
|
+
params.getString("dexPath")
|
|
416
|
+
));
|
|
417
|
+
break;
|
|
418
|
+
|
|
419
|
+
case "searchInDex":
|
|
420
|
+
result.put("data", dexManager.searchInDexFromApk(
|
|
421
|
+
params.getString("apkPath"),
|
|
422
|
+
params.getString("dexPath"),
|
|
423
|
+
params.getString("query")
|
|
424
|
+
));
|
|
425
|
+
break;
|
|
426
|
+
|
|
404
427
|
default:
|
|
405
428
|
result.put("success", false);
|
|
406
429
|
result.put("error", "Unknown action: " + action);
|
|
@@ -1214,4 +1214,237 @@ public class DexManager {
|
|
|
1214
1214
|
}
|
|
1215
1215
|
return count;
|
|
1216
1216
|
}
|
|
1217
|
+
|
|
1218
|
+
// ==================== APK 内 DEX 操作(无需会话) ====================
|
|
1219
|
+
|
|
1220
|
+
/**
|
|
1221
|
+
* 从 APK 中的 DEX 文件列出所有类
|
|
1222
|
+
* @param apkPath APK 文件路径
|
|
1223
|
+
* @param dexPath DEX 文件在 APK 中的路径(如 "classes.dex")
|
|
1224
|
+
*/
|
|
1225
|
+
public JSObject listDexClassesFromApk(String apkPath, String dexPath) throws Exception {
|
|
1226
|
+
JSObject result = new JSObject();
|
|
1227
|
+
JSArray classes = new JSArray();
|
|
1228
|
+
|
|
1229
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1230
|
+
java.io.InputStream dexInputStream = null;
|
|
1231
|
+
|
|
1232
|
+
try {
|
|
1233
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1234
|
+
java.util.zip.ZipEntry dexEntry = zipFile.getEntry(dexPath);
|
|
1235
|
+
|
|
1236
|
+
if (dexEntry == null) {
|
|
1237
|
+
throw new IOException("DEX file not found in APK: " + dexPath);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
dexInputStream = zipFile.getInputStream(dexEntry);
|
|
1241
|
+
|
|
1242
|
+
// 读取 DEX 文件到内存
|
|
1243
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
1244
|
+
byte[] buffer = new byte[8192];
|
|
1245
|
+
int len;
|
|
1246
|
+
while ((len = dexInputStream.read(buffer)) != -1) {
|
|
1247
|
+
baos.write(buffer, 0, len);
|
|
1248
|
+
}
|
|
1249
|
+
byte[] dexBytes = baos.toByteArray();
|
|
1250
|
+
|
|
1251
|
+
// 解析 DEX 文件
|
|
1252
|
+
DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexBytes);
|
|
1253
|
+
|
|
1254
|
+
// 收集所有类名
|
|
1255
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1256
|
+
String type = classDef.getType();
|
|
1257
|
+
// 转换 Lcom/example/Class; 格式为 com.example.Class
|
|
1258
|
+
String className = convertTypeToClassName(type);
|
|
1259
|
+
classes.put(className);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
result.put("classes", classes);
|
|
1263
|
+
result.put("count", classes.length());
|
|
1264
|
+
|
|
1265
|
+
} finally {
|
|
1266
|
+
if (dexInputStream != null) {
|
|
1267
|
+
try { dexInputStream.close(); } catch (Exception ignored) {}
|
|
1268
|
+
}
|
|
1269
|
+
if (zipFile != null) {
|
|
1270
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* 从 APK 中的 DEX 文件获取字符串常量池
|
|
1279
|
+
*/
|
|
1280
|
+
public JSObject getDexStringsFromApk(String apkPath, String dexPath) throws Exception {
|
|
1281
|
+
JSObject result = new JSObject();
|
|
1282
|
+
JSArray strings = new JSArray();
|
|
1283
|
+
|
|
1284
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1285
|
+
java.io.InputStream dexInputStream = null;
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1289
|
+
java.util.zip.ZipEntry dexEntry = zipFile.getEntry(dexPath);
|
|
1290
|
+
|
|
1291
|
+
if (dexEntry == null) {
|
|
1292
|
+
throw new IOException("DEX file not found in APK: " + dexPath);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
dexInputStream = zipFile.getInputStream(dexEntry);
|
|
1296
|
+
|
|
1297
|
+
// 读取 DEX 文件到内存
|
|
1298
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
1299
|
+
byte[] buffer = new byte[8192];
|
|
1300
|
+
int len;
|
|
1301
|
+
while ((len = dexInputStream.read(buffer)) != -1) {
|
|
1302
|
+
baos.write(buffer, 0, len);
|
|
1303
|
+
}
|
|
1304
|
+
byte[] dexBytes = baos.toByteArray();
|
|
1305
|
+
|
|
1306
|
+
// 解析 DEX 文件
|
|
1307
|
+
DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexBytes);
|
|
1308
|
+
|
|
1309
|
+
// 直接从 DEX 字符串表中读取所有字符串
|
|
1310
|
+
int stringCount = dexFile.getStringCount();
|
|
1311
|
+
for (int i = 0; i < stringCount; i++) {
|
|
1312
|
+
try {
|
|
1313
|
+
String str = dexFile.getStringSection().get(i);
|
|
1314
|
+
JSObject item = new JSObject();
|
|
1315
|
+
item.put("index", i);
|
|
1316
|
+
item.put("value", str != null ? str : "");
|
|
1317
|
+
strings.put(item);
|
|
1318
|
+
} catch (Exception e) {
|
|
1319
|
+
// 跳过无法读取的字符串
|
|
1320
|
+
Log.w(TAG, "Failed to read string at index " + i, e);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
result.put("strings", strings);
|
|
1325
|
+
result.put("count", strings.length());
|
|
1326
|
+
|
|
1327
|
+
} finally {
|
|
1328
|
+
if (dexInputStream != null) {
|
|
1329
|
+
try { dexInputStream.close(); } catch (Exception ignored) {}
|
|
1330
|
+
}
|
|
1331
|
+
if (zipFile != null) {
|
|
1332
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* 在 APK 中的 DEX 文件中搜索
|
|
1341
|
+
*/
|
|
1342
|
+
public JSObject searchInDexFromApk(String apkPath, String dexPath, String query) throws Exception {
|
|
1343
|
+
JSObject result = new JSObject();
|
|
1344
|
+
JSArray results = new JSArray();
|
|
1345
|
+
|
|
1346
|
+
if (query == null || query.isEmpty()) {
|
|
1347
|
+
result.put("results", results);
|
|
1348
|
+
result.put("count", 0);
|
|
1349
|
+
return result;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
String queryLower = query.toLowerCase();
|
|
1353
|
+
|
|
1354
|
+
java.util.zip.ZipFile zipFile = null;
|
|
1355
|
+
java.io.InputStream dexInputStream = null;
|
|
1356
|
+
|
|
1357
|
+
try {
|
|
1358
|
+
zipFile = new java.util.zip.ZipFile(apkPath);
|
|
1359
|
+
java.util.zip.ZipEntry dexEntry = zipFile.getEntry(dexPath);
|
|
1360
|
+
|
|
1361
|
+
if (dexEntry == null) {
|
|
1362
|
+
throw new IOException("DEX file not found in APK: " + dexPath);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
dexInputStream = zipFile.getInputStream(dexEntry);
|
|
1366
|
+
|
|
1367
|
+
// 读取 DEX 文件到内存
|
|
1368
|
+
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
|
1369
|
+
byte[] buffer = new byte[8192];
|
|
1370
|
+
int len;
|
|
1371
|
+
while ((len = dexInputStream.read(buffer)) != -1) {
|
|
1372
|
+
baos.write(buffer, 0, len);
|
|
1373
|
+
}
|
|
1374
|
+
byte[] dexBytes = baos.toByteArray();
|
|
1375
|
+
|
|
1376
|
+
// 解析 DEX 文件
|
|
1377
|
+
DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexBytes);
|
|
1378
|
+
|
|
1379
|
+
// 搜索类名和方法名
|
|
1380
|
+
for (ClassDef classDef : dexFile.getClasses()) {
|
|
1381
|
+
String className = convertTypeToClassName(classDef.getType());
|
|
1382
|
+
|
|
1383
|
+
// 搜索类名
|
|
1384
|
+
if (className.toLowerCase().contains(queryLower)) {
|
|
1385
|
+
JSObject item = new JSObject();
|
|
1386
|
+
item.put("className", className);
|
|
1387
|
+
item.put("type", "class");
|
|
1388
|
+
item.put("content", className);
|
|
1389
|
+
results.put(item);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// 搜索方法名
|
|
1393
|
+
for (Method method : classDef.getMethods()) {
|
|
1394
|
+
String methodName = method.getName();
|
|
1395
|
+
if (methodName.toLowerCase().contains(queryLower)) {
|
|
1396
|
+
JSObject item = new JSObject();
|
|
1397
|
+
item.put("className", className);
|
|
1398
|
+
item.put("methodName", methodName);
|
|
1399
|
+
item.put("type", "method");
|
|
1400
|
+
item.put("content", methodName + " in " + className);
|
|
1401
|
+
results.put(item);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// 搜索方法内的字符串
|
|
1405
|
+
MethodImplementation impl = method.getImplementation();
|
|
1406
|
+
if (impl != null) {
|
|
1407
|
+
for (Instruction instruction : impl.getInstructions()) {
|
|
1408
|
+
String instrStr = instruction.toString();
|
|
1409
|
+
if (instrStr.toLowerCase().contains(queryLower)) {
|
|
1410
|
+
JSObject item = new JSObject();
|
|
1411
|
+
item.put("className", className);
|
|
1412
|
+
item.put("methodName", methodName);
|
|
1413
|
+
item.put("type", "instruction");
|
|
1414
|
+
item.put("content", instrStr);
|
|
1415
|
+
results.put(item);
|
|
1416
|
+
break; // 每个方法只记录一次
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
result.put("results", results);
|
|
1424
|
+
result.put("count", results.length());
|
|
1425
|
+
|
|
1426
|
+
} finally {
|
|
1427
|
+
if (dexInputStream != null) {
|
|
1428
|
+
try { dexInputStream.close(); } catch (Exception ignored) {}
|
|
1429
|
+
}
|
|
1430
|
+
if (zipFile != null) {
|
|
1431
|
+
try { zipFile.close(); } catch (Exception ignored) {}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* 将 DEX 类型格式转换为 Java 类名格式
|
|
1440
|
+
* 例如: Lcom/example/Class; -> com.example.Class
|
|
1441
|
+
*/
|
|
1442
|
+
private String convertTypeToClassName(String type) {
|
|
1443
|
+
if (type == null) return "";
|
|
1444
|
+
String className = type;
|
|
1445
|
+
if (className.startsWith("L") && className.endsWith(";")) {
|
|
1446
|
+
className = className.substring(1, className.length() - 1);
|
|
1447
|
+
}
|
|
1448
|
+
return className.replace("/", ".");
|
|
1449
|
+
}
|
|
1217
1450
|
}
|