Compare commits

...

26 Commits

Author SHA1 Message Date
Skylot 18070eb7a6 fix(gui): allow to select file on mapping export 2022-06-20 14:19:59 +01:00
Skylot 8486891728 fix(gui): resolve various minor issues 2022-06-20 13:17:50 +01:00
Skylot 4679172d4f fix(gui): correct set highlighted text in search (#1507) 2022-06-20 13:16:42 +01:00
Skylot 92a6c333d8 fix(gui): force jadx new version check by default 2022-06-20 12:55:15 +01:00
Skylot 358adbdd65 feat(gui): allow to disable jump on double click (#1540) 2022-06-19 17:19:08 +01:00
Skylot 65f7c80222 feat(gui): add reload and live reload actions (#1537) 2022-06-18 20:20:11 +01:00
Skylot d2e6bb236e fix: use wide move for long/double store/load java opcodes 2022-06-16 16:26:14 +01:00
Skylot eaeb114258 fix: check class name collisions (#1526) 2022-06-15 18:43:33 +01:00
Skylot 1533b7fe6e fix: keep types on duplicate cast remove (#1527) 2022-06-12 21:55:12 +01:00
Julian Burner a2cd8e1ead feat(gui): support export to deobfuscation mapping file formats (#1491)(PR #1505)
* Add option to export mappings as Tiny v2 file

* Comply with JADX's import order conventions

* Only use Java 8 features

* Only use Java 8 features (2)

* Export comments to mappings file

* Method args test (doesn't work)

* Make method arg mapping exports work now

* Use `getTopParentClass()` instead of `getParentClass()`

See https://github.com/skylot/jadx/pull/1505#issuecomment-1145064865

* Remove unneeded method load call

* Small code cleanup; initial (broken) support for method vars

* Fixes regarding inner classes

* Add option to export mappings as Enigma directory

* Add option to export mappings as Enigma file/directory

Temporarily move to my mapping-io fork until this PR gets merged: https://github.com/FabricMC/mapping-io/pull/19

* Fix method vars' lv-indices

* Use correct offset value for method var mappings

* Also supply lvt-index for method var mappings

* Clarify why we're using local mapping-io fork; comment out Fabric Maven for now

* Remove unnecessary `public` modifier

* Make an `if` condition less complicated

* Move mapping export code into jadx-gui (for now)

* Make mapping export async; make export menu only clickable when everything is loaded

* Fix export mappings menu field declaration position
2022-06-11 20:19:08 +01:00
Christian Jones 4edb512121 fix(cli): allow decoding resource-only APKs (#1517)(PR #1530)
* Process resource-only inputs
* Fix error, add testing
2022-06-11 15:40:39 +01:00
Skylot 702b88228c fix(gui): resolve popup menu action run (#1514, #1529) 2022-06-11 15:08:28 +01:00
Skylot 14fd88b2f8 fix(gui): add volatile and update sync for decompiler field in wrapper (#1518) 2022-06-08 21:06:57 +01:00
Skylot 20657e8bb5 doc(cli): improve plugins section formatting 2022-06-06 19:55:58 +01:00
Skylot 93d3194e3b doc: remove incorrect tokei badge 2022-06-06 19:54:15 +01:00
Skylot 39331d9120 fix: remove deprecated --deobf-rewrite-cfg (#1513) 2022-06-06 19:53:47 +01:00
Skylot b4fa6644bc doc: add more badges 2022-06-05 16:06:19 +01:00
Skylot 0b2e2ed034 fix: improve class search for super call (#1512) 2022-06-05 14:49:34 +01:00
Skylot 81231206f3 fix(gui): reset disk cache on new jadx version 2022-06-04 23:26:25 +01:00
Skylot 49d0e76272 fix: support all-catch in multi-catch (#1510) 2022-06-04 23:25:52 +01:00
CmP-lt 0809993b37 fix(gui): improve restoration of windows saved state (PR #1511)
* Fix restoration of windows saved state
* Don't skip restoration of window saved bounds when they intersect with screen bounds.
* Restore saved bounds of main window regardless of it's saved extended state (fixes divider location of main split pane being restored incorrectly when saved state of main window is maximized).
* Add handling for out-of-screen(s) window bounds
2022-06-04 17:41:00 +01:00
Skylot 0c3afcc24c fix(gui): try to prevent jadx node leaks in UI objects 2022-06-03 16:15:14 +01:00
Skylot d6c851eed4 test: fix method code extract 2022-06-02 19:33:16 +01:00
Skylot dcf4a7c4e3 fix(gui): try to resolve some causes of memory leak 2022-06-01 19:48:51 +01:00
Skylot 9ba07b986b fix(gui): reduce usage of nullable decompiler field in jadx wrapper (#1506) 2022-06-01 16:36:30 +01:00
Skylot e6b6b93cbb fix: improve blocks tree compare for finally extract (#1501) 2022-05-31 20:57:34 +01:00
89 changed files with 1880 additions and 407 deletions
+18 -13
View File
@@ -3,8 +3,10 @@
## JADX
[![Build status](https://github.com/skylot/jadx/workflows/Build/badge.svg)](https://github.com/skylot/jadx/actions?query=workflow%3ABuild)
[![Alerts from lgtm.com](https://img.shields.io/lgtm/alerts/g/skylot/jadx.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/skylot/jadx/alerts/)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
![GitHub contributors](https://img.shields.io/github/contributors/skylot/jadx)
![GitHub all releases](https://img.shields.io/github/downloads/skylot/jadx/total)
![GitHub release (latest by SemVer)](https://img.shields.io/github/downloads/skylot/jadx/latest/total)
![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.skylot/jadx-core)](https://search.maven.org/search?q=g:io.github.skylot%20AND%20jadx)
[![License](http://img.shields.io/:license-apache-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
@@ -35,7 +37,7 @@ See these features in action here: [jadx-gui features overview](https://github.c
### Download
- release
from [github: ![Latest release](https://img.shields.io/github/release/skylot/jadx.svg)](https://github.com/skylot/jadx/releases/latest)
- latest [unstable build](https://nightly.link/skylot/jadx/workflows/build-artifacts/master)
- latest [unstable build ![GitHub commits since tagged version (branch)](https://img.shields.io/github/commits-since/skylot/jadx/latest/master)](https://nightly.link/skylot/jadx/workflows/build-artifacts/master)
After download unpack zip file go to `bin` directory and run:
- `jadx` - command line version
@@ -46,14 +48,18 @@ On Windows run `.bat` files with double-click\
For Windows, you can download it from [oracle.com](https://www.oracle.com/java/technologies/downloads/#jdk17-windows) (select x64 Installer).
### Install
1. Arch linux
1. Arch linux ![Arch Linux package](https://img.shields.io/archlinux/v/community/any/jadx?label=)
```bash
sudo pacman -S jadx
sudo pacman -S jadx
```
2. macOS
2. macOS ![homebrew version](https://img.shields.io/homebrew/v/jadx?label=)
```bash
brew install jadx
brew install jadx
```
3. [Flathub ![Flathub](https://img.shields.io/flathub/v/com.github.skylot.jadx?label=)](https://flathub.org/apps/details/com.github.skylot.jadx)
```bash
flatpak install flathub com.github.skylot.jadx
```
### Use jadx as a library
You can use jadx in your java projects, check details on [wiki page](https://github.com/skylot/jadx/wiki/Use-jadx-as-a-library)
@@ -108,7 +114,6 @@ options:
'read-or-save' - read if found, save otherwise (don't overwrite)
'overwrite' - don't read, always save
'ignore' - don't read and don't save
--deobf-rewrite-cfg - set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
@@ -131,11 +136,11 @@ options:
-h, --help - print this help
Plugin options (-P<name>=<value>):
1) dex-input (Load .dex and .apk files)
-Pdex-input.verify-checksum - Verify dex file checksum before load, values: [yes, no], default: yes
2) java-convert (Convert .jar and .class files to dex)
-Pjava-convert.mode - Convert mode, values: [dx, d8, both], default: both
-Pjava-convert.d8-desugar - Use desugar in d8, values: [yes, no], default: no
1) dex-input: Load .dex and .apk files
- dex-input.verify-checksum - verify dex file checksum before load, values: [yes, no], default: yes
2) java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
Examples:
jadx -d out classes.dex
+5
View File
@@ -49,6 +49,11 @@ allprojects {
mavenLocal()
mavenCentral()
google()
// Commented out for now since we're using a local mapping-io fork atm.
// maven {
// name 'FabricMC'
// url 'https://maven.fabricmc.net/'
// }
}
}
@@ -193,11 +193,11 @@ public class JCommanderWrapper<T> {
return false;
}
JadxPluginInfo pluginInfo = plugin.getPluginInfo();
out.append("\n ").append(k).append(") ");
out.append(pluginInfo.getPluginId()).append(" (").append(pluginInfo.getDescription()).append(") ");
out.append("\n ").append(k).append(") ");
out.append(pluginInfo.getPluginId()).append(": ").append(pluginInfo.getDescription());
for (OptionDescription desc : descs) {
StringBuilder opt = new StringBuilder();
opt.append(" -P").append(desc.name());
opt.append(" - ").append(desc.name());
addSpaces(opt, maxNamesLen - opt.length());
opt.append("- ").append(desc.description());
if (!desc.values().isEmpty()) {
+8 -2
View File
@@ -66,8 +66,14 @@ public class JadxCLI {
private static boolean checkForErrors(JadxDecompiler jadx) {
if (jadx.getRoot().getClasses().isEmpty()) {
LOG.error("Load failed! No classes for decompile!");
return true;
if (jadx.getArgs().isSkipResources()) {
LOG.error("Load failed! No classes for decompile!");
return true;
}
if (!jadx.getArgs().isSkipSources()) {
LOG.warn("No classes to decompile; decoding resources only");
jadx.getArgs().setSkipSources(true);
}
}
if (jadx.getErrorsCount() > 0) {
LOG.error("Load with errors! Check log for details");
@@ -123,9 +123,6 @@ public class JadxCLIArgs {
)
protected DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
@Parameter(names = { "--deobf-rewrite-cfg" }, description = "set '--deobf-cfg-file-mode' to 'overwrite' (deprecated)")
protected boolean deobfuscationForceSave = false;
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = false;
@@ -259,11 +256,7 @@ public class JadxCLIArgs {
args.setReplaceConsts(replaceConsts);
args.setDeobfuscationOn(deobfuscationOn);
args.setDeobfuscationMapFile(FileUtils.toFile(deobfuscationMapFile));
if (deobfuscationForceSave) {
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
} else {
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
}
args.setDeobfuscationMapFileMode(deobfuscationMapFileMode);
args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
@@ -377,10 +370,6 @@ public class JadxCLIArgs {
return deobfuscationMapFileMode;
}
public boolean isDeobfuscationForceSave() {
return deobfuscationForceSave;
}
public boolean isDeobfuscationUseSourceNameAsAlias() {
return deobfuscationUseSourceNameAsAlias;
}
@@ -44,6 +44,33 @@ public class TestInput {
decompile("multi", "samples/hello.dex", "samples/HelloWorld.smali");
}
@Test
public void testResourceOnly() throws Exception {
decode("resourceOnly", "samples/resources-only.apk");
}
private void decode(String tmpDirName, String apkSample) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.add("-v");
args.add("-d");
args.add(tempDir.toAbsolutePath().toString());
URL resource = getClass().getClassLoader().getResource(apkSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.add(sampleFile);
int result = JadxCLI.execute(args.toArray(new String[0]));
assertThat(result).isEqualTo(0);
List<Path> files = Files.find(
tempDir,
3,
(file, attr) -> file.getFileName().toString().equalsIgnoreCase("AndroidManifest.xml"))
.collect(Collectors.toList());
assertThat(files.isEmpty()).isFalse();
}
private void decompile(String tmpDirName, String... inputSamples) throws URISyntaxException, IOException {
List<String> args = new ArrayList<>();
Path tempDir = FileUtils.createTempDir(tmpDirName);
@@ -128,7 +128,7 @@ public class JadxArgs {
public void close() {
try {
inputFiles.clear();
inputFiles = null;
if (codeCache != null) {
codeCache.close();
}
@@ -7,6 +7,7 @@ public class Consts {
public static final boolean DEBUG_TYPE_INFERENCE = false;
public static final boolean DEBUG_OVERLOADED_CASTS = false;
public static final boolean DEBUG_EXC_HANDLERS = false;
public static final boolean DEBUG_FINALLY = false;
public static final String CLASS_OBJECT = "java.lang.Object";
public static final String CLASS_STRING = "java.lang.String";
+10 -3
View File
@@ -238,9 +238,17 @@ public class Jadx {
private static String version;
public static String getVersion() {
if (version != null) {
return version;
if (version == null) {
version = searchJadxVersion();
}
return version;
}
public static boolean isDevVersion() {
return getVersion().equals(VERSION_DEV);
}
private static String searchJadxVersion() {
try {
ClassLoader classLoader = Jadx.class.getClassLoader();
if (classLoader != null) {
@@ -250,7 +258,6 @@ public class Jadx {
Manifest manifest = new Manifest(is);
String ver = manifest.getMainAttributes().getValue("jadx-version");
if (ver != null) {
version = ver;
return ver;
}
}
@@ -203,6 +203,9 @@ public class ClspGraph {
if (isNew) {
addSuperTypes(parentCls, result);
}
} else {
// parent type is unknown
result.add(parentType.getObject());
}
}
}
@@ -614,21 +614,23 @@ public class ClassGen {
if (useCls.equals(extClsInfo)) {
return shortName;
}
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
return shortName;
}
if (isClassInnerFor(useCls, extClsInfo)) {
return shortName;
}
if (extClsInfo.isInner()) {
return expandInnerClassName(useCls, extClsInfo);
}
if (searchCollision(cls.root(), useCls, extClsInfo)) {
if (checkInnerCollision(cls.root(), useCls, extClsInfo)
|| checkInPackageCollision(cls.root(), useCls, extClsInfo)) {
return fullName;
}
if (isBothClassesInOneTopClass(useCls, extClsInfo)) {
return shortName;
}
// don't add import for top classes from 'java.lang' package (subpackages excluded)
if (extClsInfo.getPackage().equals("java.lang") && extClsInfo.getParentClass() == null) {
return shortName;
}
// don't add import if this class from same package
if (extClsInfo.getPackage().equals(useCls.getPackage()) && !extClsInfo.isInner()) {
return shortName;
@@ -709,7 +711,7 @@ public class ClassGen {
return false;
}
private static boolean searchCollision(RootNode root, ClassInfo useCls, ClassInfo searchCls) {
private static boolean checkInnerCollision(RootNode root, @Nullable ClassInfo useCls, ClassInfo searchCls) {
if (useCls == null) {
return false;
}
@@ -726,7 +728,20 @@ public class ClassGen {
}
}
}
return searchCollision(root, useCls.getParentClass(), searchCls);
return checkInnerCollision(root, useCls.getParentClass(), searchCls);
}
/**
* Check if class with same name exists in current package
*/
private static boolean checkInPackageCollision(RootNode root, ClassInfo useCls, ClassInfo searchCls) {
String currentPkg = useCls.getAliasPkg();
if (currentPkg.equals(searchCls.getAliasPkg())) {
// search class already from current package
return false;
}
String shortName = searchCls.getAliasShortName();
return root.getClsp().isClsKnown(currentPkg + '.' + shortName);
}
private void insertRenameInfo(ICodeWriter code, ClassNode cls) {
@@ -807,14 +807,9 @@ public class InsnGen {
break;
case SUPER:
ClassInfo superCallCls = getClassForSuperCall(code, callMth);
if (superCallCls != null) {
useClass(code, superCallCls);
code.add('.');
}
// use 'super' instead 'this' in 0 arg
code.add("super").add('.');
k++;
callSuper(code, callMth);
k++; // use 'super' instead 'this' in 0 arg
code.add('.');
break;
case STATIC:
@@ -965,34 +960,43 @@ public class InsnGen {
code.startLine('}');
}
@Nullable
private ClassInfo getClassForSuperCall(ICodeWriter code, MethodInfo callMth) {
ClassNode useCls = mth.getParentClass();
ClassInfo insnCls = useCls.getClassInfo();
ClassInfo declClass = callMth.getDeclClass();
if (insnCls.equals(declClass)) {
return null;
private void callSuper(ICodeWriter code, MethodInfo callMth) {
ClassInfo superCallCls = getClassForSuperCall(callMth);
if (superCallCls == null) {
// unknown class, add comment to keep that info
code.add("super/*").add(callMth.getDeclClass().getFullName()).add("*/");
return;
}
ClassNode topClass = useCls.getTopParentClass();
if (topClass.getClassInfo().equals(declClass)) {
return declClass;
ClassInfo curClass = mth.getParentClass().getClassInfo();
if (superCallCls.equals(curClass)) {
code.add("super");
return;
}
// search call class
ClassNode nextParent = useCls;
do {
ClassInfo nextClsInfo = nextParent.getClassInfo();
if (nextClsInfo.equals(declClass)
|| ArgType.isInstanceOf(mth.root(), nextClsInfo.getType(), declClass.getType())) {
if (nextParent == useCls) {
return null;
}
return nextClsInfo;
}
nextParent = nextParent.getParentClass();
} while (nextParent != null && nextParent != topClass);
// use custom class
useClass(code, superCallCls);
code.add(".super");
}
// search failed, just return parent class
return useCls.getParentClass().getClassInfo();
/**
* Search call class in super types of this
* and all parent classes (needed for inlined synthetic calls)
*/
@Nullable
private ClassInfo getClassForSuperCall(MethodInfo callMth) {
ArgType declClsType = callMth.getDeclClass().getType();
ClassNode parentNode = mth.getParentClass();
while (true) {
ClassInfo parentCls = parentNode.getClassInfo();
if (ArgType.isInstanceOf(root, parentCls.getType(), declClsType)) {
return parentCls;
}
ClassNode nextParent = parentNode.getParentClass();
if (nextParent == parentNode) {
// no parent, class not found
return null;
}
parentNode = nextParent;
}
}
void generateMethodArguments(ICodeWriter code, BaseInvokeNode insn, int startArgNum,
@@ -180,12 +180,12 @@ public final class ClassInfo implements Comparable<ClassInfo> {
return makeFullClsName(pkg, name, parentClass, false, true);
}
private String makeAliasFullName() {
public String makeAliasFullName() {
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false);
}
private String makeAliasRawFullName() {
return makeFullClsName(pkg, name, parentClass, true, true);
public String makeAliasRawFullName() {
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, true);
}
public String getAliasFullPath() {
@@ -170,6 +170,10 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return contains(AFlag.RETURN);
}
public boolean isEmpty() {
return instructions.isEmpty();
}
@Override
public int hashCode() {
return startOffset;
@@ -461,6 +461,10 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
return regsCount;
}
public int getArgsStartReg() {
return argsStartReg;
}
public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) {
int regNum = assignArg.getRegNum();
return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg);
@@ -269,6 +269,7 @@ public class RootNode {
public void runPreDecompileStage() {
boolean debugEnabled = LOG.isDebugEnabled();
for (IDexTreeVisitor pass : preDecompilePasses) {
Utils.checkThreadInterrupt();
long start = debugEnabled ? System.currentTimeMillis() : 0;
try {
pass.init(this);
@@ -14,9 +14,9 @@ import jadx.core.dex.instructions.args.ArgType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.InsnUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
public class ExceptionHandler {
@@ -33,9 +33,14 @@ public class ExceptionHandler {
private boolean removed = false;
public ExceptionHandler(int addr, @Nullable ClassInfo type) {
public static ExceptionHandler build(MethodNode mth, int addr, @Nullable ClassInfo type) {
ExceptionHandler eh = new ExceptionHandler(addr);
eh.addCatchType(mth, type);
return eh;
}
private ExceptionHandler(int addr) {
this.handlerOffset = addr;
addCatchType(type);
}
/**
@@ -43,7 +48,7 @@ public class ExceptionHandler {
*
* @param type - null for 'all' or 'Throwable' handler
*/
public boolean addCatchType(@Nullable ClassInfo type) {
public boolean addCatchType(MethodNode mth, @Nullable ClassInfo type) {
if (type != null) {
if (catchTypes.contains(type)) {
return false;
@@ -51,14 +56,16 @@ public class ExceptionHandler {
return catchTypes.add(type);
}
if (!this.catchTypes.isEmpty()) {
throw new JadxRuntimeException("Null type added to not empty exception handler: " + this);
mth.addDebugComment("Throwable added to exception handler: '" + catchTypeStr() + "', keep only Throwable");
catchTypes.clear();
return true;
}
return false;
}
public void addCatchTypes(Collection<ClassInfo> types) {
public void addCatchTypes(MethodNode mth, Collection<ClassInfo> types) {
for (ClassInfo type : types) {
addCatchType(type);
addCatchType(mth, type);
}
}
@@ -133,7 +133,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER);
if (excHandlerAttr != null) {
ExceptionHandler handler = excHandlerAttr.getHandler();
if (handler.addCatchType(type)) {
if (handler.addCatchType(mth, type)) {
// exist handler updated (assume from same try block) - don't add again
return null;
}
@@ -143,7 +143,7 @@ public class AttachTryCatchVisitor extends AbstractVisitor {
} else {
insn = insertNOP(insnByOffset, handlerOffset);
}
ExceptionHandler handler = new ExceptionHandler(handlerOffset, type);
ExceptionHandler handler = ExceptionHandler.build(mth, handlerOffset, type);
mth.addExceptionHandler(handler);
insn.addAttr(new ExcHandlerAttr(handler));
return handler;
@@ -362,8 +362,7 @@ public class ModVisitor extends AbstractVisitor {
private static void removeCheckCast(MethodNode mth, BlockNode block, int i, IndexInsnNode insn) {
InsnArg castArg = insn.getArg(0);
ArgType castType = (ArgType) insn.getIndex();
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)
|| isCastDuplicate(insn)) {
if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) {
RegisterArg result = insn.getResult();
result.setType(castArg.getType());
@@ -371,10 +370,19 @@ public class ModVisitor extends AbstractVisitor {
move.setResult(result);
move.addArg(castArg);
replaceInsn(mth, block, i, move);
return;
}
InsnNode prevCast = isCastDuplicate(insn);
if (prevCast != null) {
// replace previous cast with move
InsnNode move = new InsnNode(InsnType.MOVE, 1);
move.setResult(prevCast.getResult());
move.addArg(prevCast.getArg(0));
replaceInsn(mth, block, prevCast, move);
}
}
private static boolean isCastDuplicate(IndexInsnNode castInsn) {
private static @Nullable InsnNode isCastDuplicate(IndexInsnNode castInsn) {
InsnArg arg = castInsn.getArg(0);
if (arg.isRegister()) {
SSAVar sVar = ((RegisterArg) arg).getSVar();
@@ -382,11 +390,13 @@ public class ModVisitor extends AbstractVisitor {
InsnNode assignInsn = sVar.getAssign().getParentInsn();
if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) {
ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex();
return assignCastType.equals(castInsn.getIndex());
if (assignCastType.equals(castInsn.getIndex())) {
return assignInsn;
}
}
}
}
return false;
return null;
}
/**
@@ -549,7 +549,7 @@ public class BlockExceptionHandler {
if (handler == resultHandler) {
return false;
}
resultHandler.addCatchTypes(handler.getCatchTypes());
resultHandler.addCatchTypes(mth, handler.getCatchTypes());
handler.markForRemove();
return true;
});
@@ -6,9 +6,12 @@ import java.util.List;
import java.util.Set;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.Utils;
public class FinallyExtractInfo {
private final MethodNode mth;
private final ExceptionHandler finallyHandler;
private final List<BlockNode> allHandlerBlocks;
private final List<InsnsSlice> duplicateSlices = new ArrayList<>();
@@ -16,12 +19,17 @@ public class FinallyExtractInfo {
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
private final BlockNode startBlock;
public FinallyExtractInfo(ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
this.mth = mth;
this.finallyHandler = finallyHandler;
this.startBlock = startBlock;
this.allHandlerBlocks = allHandlerBlocks;
}
public MethodNode getMth() {
return mth;
}
public ExceptionHandler getFinallyHandler() {
return finallyHandler;
}
@@ -45,4 +53,12 @@ public class FinallyExtractInfo {
public BlockNode getStartBlock() {
return startBlock;
}
@Override
public String toString() {
return "FinallyExtractInfo{"
+ "\n finally:\n " + finallyInsnsSlice
+ "\n dups:\n " + Utils.listToString(duplicateSlices, "\n ")
+ "\n}";
}
}
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.instructions.InsnType;
@@ -38,6 +39,7 @@ import jadx.core.utils.Utils;
)
public class MarkFinallyVisitor extends AbstractVisitor {
private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
// private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class);
@Override
public void visit(MethodNode mth) {
@@ -60,7 +62,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
}
}
} catch (Exception e) {
LOG.warn("Undo finally extract visitor, mth: {}", mth, e);
mth.addWarnComment("Undo finally extract visitor", e);
undoFinallyVisitor(mth);
}
}
@@ -100,20 +102,23 @@ public class MarkFinallyVisitor extends AbstractVisitor {
List<BlockNode> handlerBlocks =
new ArrayList<>(BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, handlerBlock, handlerBlock));
handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception'
handlerBlocks.removeIf(b -> BlockUtils.checkLastInsnType(b, InsnType.THROW));
cutPathEnds(mth, handlerBlocks);
if (handlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(handlerBlocks)) {
// remove empty catch
allHandler.getTryBlock().removeHandler(allHandler);
return true;
}
BlockNode startBlock = Utils.getOne(handlerBlock.getCleanSuccessors());
FinallyExtractInfo extractInfo = new FinallyExtractInfo(allHandler, startBlock, handlerBlocks);
FinallyExtractInfo extractInfo = new FinallyExtractInfo(mth, allHandler, startBlock, handlerBlocks);
if (Consts.DEBUG_FINALLY) {
LOG.debug("Finally info: handler=({}), start={}, blocks={}", allHandler, startBlock, handlerBlocks);
}
boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty();
List<ExceptionHandler> handlers;
if (hasInnerBlocks) {
// collect handlers from this and all inner blocks (intentionally not using recursive collect for
// now)
// collect handlers from this and all inner blocks
// (intentionally not using recursive collect for now)
handlers = new ArrayList<>(tryBlock.getHandlers());
for (TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) {
handlers.addAll(innerTryBlock.getHandlers());
@@ -137,10 +142,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
}
}
}
if (Consts.DEBUG_FINALLY) {
LOG.debug("Handlers slices:\n{}", extractInfo);
}
boolean mergeInnerTryBlocks;
int duplicatesCount = extractInfo.getDuplicateSlices().size();
boolean fullTryBlock = duplicatesCount == (handlers.size() - 1);
if (fullTryBlock) {
if (duplicatesCount == (handlers.size() - 1)) {
// all collected handlers have duplicate block
mergeInnerTryBlocks = hasInnerBlocks;
} else {
@@ -170,15 +177,24 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (upPath.size() < handlerBlocks.size()) {
continue;
}
if (Consts.DEBUG_FINALLY) {
LOG.debug("Checking dup path starts: {} from {}", upPath, pred);
}
for (BlockNode block : upPath) {
if (searchDuplicateInsns(block, extractInfo)) {
found = true;
if (Consts.DEBUG_FINALLY) {
LOG.debug("Found dup in: {} from {}", block, pred);
}
break;
} else {
extractInfo.getFinallyInsnsSlice().resetIncomplete();
}
}
}
if (Consts.DEBUG_FINALLY) {
LOG.debug("Result slices:\n{}", extractInfo);
}
if (!found) {
return false;
}
@@ -204,6 +220,28 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return true;
}
private static void cutPathEnds(MethodNode mth, List<BlockNode> handlerBlocks) {
List<BlockNode> throwBlocks = ListUtils.filter(handlerBlocks,
b -> BlockUtils.checkLastInsnType(b, InsnType.THROW));
if (throwBlocks.size() != 1) {
mth.addDebugComment("Finally have unexpected throw blocks count: " + throwBlocks.size() + ", expect 1");
return;
}
BlockNode throwBlock = throwBlocks.get(0);
handlerBlocks.remove(throwBlock);
removeEmptyUpPath(handlerBlocks, throwBlock);
}
private static void removeEmptyUpPath(List<BlockNode> handlerBlocks, BlockNode startBlock) {
for (BlockNode pred : startBlock.getPredecessors()) {
if (pred.isEmpty()) {
if (handlerBlocks.remove(pred) && !BlockUtils.isBackEdge(pred, startBlock)) {
removeEmptyUpPath(handlerBlocks, pred);
}
}
}
}
private static List<BlockNode> getPathStarts(MethodNode mth, BlockNode bottom, BlockNode bottomFinallyBlock) {
Stream<BlockNode> preds = bottom.getPredecessors().stream().filter(b -> b != bottomFinallyBlock);
if (bottom == mth.getExitBlock()) {
@@ -219,9 +257,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) {
List<InsnNode> dupInsnsList = dupSlice.getInsnsList();
if (dupInsnsList.size() != finallyInsnsList.size()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Incorrect finally slice size: {}, expected: {}", dupSlice, finallySlice);
}
extractInfo.getMth().addDebugComment(
"Incorrect finally slice size: " + dupSlice + ", expected: " + finallySlice);
return false;
}
}
@@ -231,9 +268,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
List<InsnNode> insnsList = dupSlice.getInsnsList();
InsnNode dupInsn = insnsList.get(i);
if (finallyInsn.getType() != dupInsn.getType()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Incorrect finally slice insn: {}, expected: {}", dupInsn, finallyInsn);
}
extractInfo.getMth().addDebugComment(
"Incorrect finally slice insn: " + dupInsn + ", expected: " + finallyInsn);
return false;
}
}
@@ -342,24 +378,29 @@ public class MarkFinallyVisitor extends AbstractVisitor {
private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) {
List<InsnNode> dupInsns = dupBlock.getInstructions();
List<InsnNode> finallyInsns = finallyBlock.getInstructions();
if (dupInsns.size() < finallyInsns.size()) {
int dupSize = dupInsns.size();
int finSize = finallyInsns.size();
if (dupSize < finSize) {
return null;
}
int startPos = dupInsns.size() - finallyInsns.size();
int startPos;
int endPos = 0;
// fast check from end of block
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
// check from block start
if (checkInsns(dupInsns, finallyInsns, 0)) {
startPos = 0;
endPos = finallyInsns.size();
} else {
if (dupSize == finSize) {
if (!checkInsns(dupInsns, finallyInsns, 0)) {
return null;
}
startPos = 0;
} else {
// dupSize > finSize
startPos = dupSize - finSize;
// fast check from end of block
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
// search start insn
boolean found = false;
for (int i = 1; i < startPos; i++) {
if (checkInsns(dupInsns, finallyInsns, i)) {
startPos = i;
endPos = finallyInsns.size() + i;
endPos = finSize + i;
found = true;
break;
}
@@ -379,7 +420,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// both slices completed
complete = true;
} else {
endIndex = dupInsns.size();
endIndex = dupSize;
complete = false;
}
@@ -393,9 +434,8 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (finallySlice.isComplete()) {
// compare slices
if (finallySlice.getInsnsList().size() != slice.getInsnsList().size()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Another duplicated slice has different insns count: {}, finally: {}", slice, finallySlice);
}
extractInfo.getMth().addDebugComment(
"Another duplicated slice has different insns count: " + slice + ", finally: " + finallySlice);
return null;
}
// TODO: add additional slices checks
@@ -522,7 +562,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
DepthTraversal.visit(visitor, mth);
}
} catch (Exception e) {
LOG.error("Undo finally extract failed, mth: {}", mth, e);
mth.addError("Undo finally extract failed", e);
}
}
}
@@ -428,7 +428,7 @@ public class Utils {
}
public static void checkThreadInterrupt() {
if (Thread.interrupted()) {
if (Thread.currentThread().isInterrupted()) {
throw new JadxRuntimeException("Thread interrupted");
}
}
@@ -43,9 +43,6 @@ import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
@@ -63,13 +60,11 @@ import jadx.tests.api.utils.TestUtils;
import static org.apache.commons.lang3.StringUtils.leftPad;
import static org.apache.commons.lang3.StringUtils.rightPad;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
@@ -79,8 +74,6 @@ public abstract class IntegrationTest extends TestUtils {
private static final String TEST_DIRECTORY = "src/test/java";
private static final String TEST_DIRECTORY2 = "jadx-core/" + TEST_DIRECTORY;
private static final String OUT_DIR = "test-out-tmp";
private static final String DEFAULT_INPUT_PLUGIN = "dx";
/**
* Set 'TEST_INPUT_PLUGIN' env variable to use 'java' or 'dx' input in tests
@@ -132,7 +125,7 @@ public abstract class IntegrationTest extends TestUtils {
this.useJavaInput = null;
args = new JadxArgs();
args.setOutDir(new File(OUT_DIR));
args.setOutDir(new File("test-out-tmp"));
args.setShowInconsistentCode(true);
args.setThreadsCount(1);
args.setSkipResources(true);
@@ -156,6 +149,10 @@ public abstract class IntegrationTest extends TestUtils {
}
}
public void setOutDirSuffix(String suffix) {
args.setOutDir(new File("test-out-" + suffix + "-tmp"));
}
public String getTestName() {
return this.getClass().getSimpleName();
}
@@ -287,7 +284,7 @@ public abstract class IntegrationTest extends TestUtils {
}
protected void runChecks(List<ClassNode> clsList) {
clsList.forEach(this::checkCode);
clsList.forEach(cls -> checkCode(cls, allowWarnInCode));
compileClassNode(clsList);
clsList.forEach(this::runAutoCheck);
}
@@ -345,33 +342,6 @@ public abstract class IntegrationTest extends TestUtils {
root.processResources(resStorage);
}
protected void checkCode(ClassNode cls) {
assertFalse(hasErrors(cls), "Inconsistent cls: " + cls);
for (MethodNode mthNode : cls.getMethods()) {
if (hasErrors(mthNode)) {
fail("Method with problems: " + mthNode
+ "\n " + Utils.listToString(mthNode.getAttributesStringsList(), "\n "));
}
}
String code = cls.getCode().getCodeStr();
assertThat(code, not(containsString("inconsistent")));
assertThat(code, not(containsString("JADX ERROR")));
}
private boolean hasErrors(IAttributeNode node) {
if (node.contains(AFlag.INCONSISTENT_CODE) || node.contains(AType.JADX_ERROR)) {
return true;
}
if (!allowWarnInCode) {
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
if (commentsAttr != null) {
return commentsAttr.getComments().get(CommentsLevel.WARN) != null;
}
}
return false;
}
private void runAutoCheck(ClassNode cls) {
String clsName = cls.getClassInfo().getRawName().replace('/', '.');
try {
@@ -11,7 +11,6 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.tools.DiagnosticListener;
import javax.tools.JavaCompiler;
@@ -82,7 +81,7 @@ public class TestCompiler implements Closeable {
arguments.addAll(options.getArguments());
DiagnosticListener<? super JavaFileObject> diagnostic =
diagObj -> System.out.println("Compiler diagnostic: " + diagObj.getMessage(Locale.ROOT));
diagObj -> System.out.println("Compiler diagnostic: " + diagObj);
Writer out = new PrintWriter(System.out);
CompilationTask compilerTask = compiler.getTask(out, fileManager, diagnostic, arguments, null, jfObjects);
if (Boolean.FALSE.equals(compilerTask.call())) {
@@ -31,6 +31,10 @@ public enum TestProfile implements Consumer<IntegrationTest> {
test.useTargetJavaVersion(11);
test.useJavaInput();
}),
JAVA17("java-17", test -> {
test.useTargetJavaVersion(17);
test.useJavaInput();
}),
ECJ_DX_J8("ecj-dx-j8", test -> {
test.useEclipseCompiler();
test.useTargetJavaVersion(8);
@@ -52,8 +56,9 @@ public enum TestProfile implements Consumer<IntegrationTest> {
}
@Override
public void accept(IntegrationTest integrationTest) {
this.setup.accept(integrationTest);
public void accept(IntegrationTest test) {
this.setup.accept(test);
test.setOutDirSuffix(description);
}
public String getDescription() {
@@ -3,7 +3,21 @@ package jadx.tests.api.utils;
import org.junit.jupiter.api.extension.ExtendWith;
import jadx.NotYetImplementedExtension;
import jadx.api.CommentsLevel;
import jadx.api.ICodeWriter;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.attributes.nodes.JadxCommentsAttr;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.utils.Utils;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail;
@ExtendWith(NotYetImplementedExtension.class)
public class TestUtils {
@@ -35,4 +49,31 @@ public class TestUtils {
}
return count;
}
protected static void checkCode(ClassNode cls, boolean allowWarnInCode) {
assertFalse(hasErrors(cls, allowWarnInCode), "Inconsistent cls: " + cls);
for (MethodNode mthNode : cls.getMethods()) {
if (hasErrors(mthNode, allowWarnInCode)) {
fail("Method with problems: " + mthNode
+ "\n " + Utils.listToString(mthNode.getAttributesStringsList(), "\n "));
}
}
String code = cls.getCode().getCodeStr();
assertThat(code, not(containsString("inconsistent")));
assertThat(code, not(containsString("JADX ERROR")));
}
protected static boolean hasErrors(IAttributeNode node, boolean allowWarnInCode) {
if (node.contains(AFlag.INCONSISTENT_CODE) || node.contains(AType.JADX_ERROR)) {
return true;
}
if (!allowWarnInCode) {
JadxCommentsAttr commentsAttr = node.get(AType.JADX_COMMENTS);
if (commentsAttr != null) {
return commentsAttr.getComments().get(CommentsLevel.WARN) != null;
}
}
return false;
}
}
@@ -13,21 +13,24 @@ import jadx.api.ICodeWriter;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess;
import jadx.api.metadata.ICodeAnnotation;
import jadx.api.metadata.ICodeNodeRef;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.DebugChecks;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.utils.TestUtils;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
public abstract class BaseExternalTest extends IntegrationTest {
public abstract class BaseExternalTest extends TestUtils {
private static final Logger LOG = LoggerFactory.getLogger(BaseExternalTest.class);
protected JadxDecompiler decompiler;
protected abstract String getSamplesDir();
protected JadxArgs prepare(String inputFile) {
@@ -55,16 +58,16 @@ public abstract class BaseExternalTest extends IntegrationTest {
}
protected JadxDecompiler decompile(JadxArgs jadxArgs, @Nullable String clsPatternStr, @Nullable String mthPatternStr) {
JadxDecompiler jadx = new JadxDecompiler(jadxArgs);
jadx.load();
decompiler = new JadxDecompiler(jadxArgs);
decompiler.load();
if (clsPatternStr == null) {
jadx.save();
decompiler.save();
} else {
processByPatterns(jadx, clsPatternStr, mthPatternStr);
processByPatterns(decompiler, clsPatternStr, mthPatternStr);
}
printErrorReport(jadx);
return jadx;
printErrorReport(decompiler);
return decompiler;
}
private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) {
@@ -109,7 +112,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
} else {
LOG.info("Code: \n{}", classNode.getCode());
}
checkCode(classNode);
checkCode(classNode, false);
return true;
}
@@ -134,7 +137,7 @@ public abstract class BaseExternalTest extends IntegrationTest {
String dashLine = "======================================================================================";
for (MethodNode mth : classNode.getMethods()) {
if (isMthMatch(mth, mthPattern)) {
String mthCode = cutMethodCode(codeInfo, code, mth);
String mthCode = cutMethodCode(codeInfo, mth);
LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getShortId(),
dashLine,
mthCode,
@@ -143,36 +146,33 @@ public abstract class BaseExternalTest extends IntegrationTest {
}
}
@NotNull
private String cutMethodCode(ICodeInfo codeInfo, String code, MethodNode mth) {
int defPos = mth.getDefPosition();
int startPos = getCommentStartPos(code, defPos);
ICodeNodeRef nodeBelow = codeInfo.getCodeMetadata().getNodeBelow(defPos);
int stopPos = nodeBelow == null ? code.length() : nodeBelow.getDefPosition();
int brackets = 0;
StringBuilder mthCode = new StringBuilder();
for (int i = startPos; i > 0 && i < stopPos;) {
int codePoint = code.codePointAt(i);
mthCode.appendCodePoint(codePoint);
if (i >= defPos) {
// also count brackets for detect method end
if (codePoint == '{') {
brackets++;
} else if (codePoint == '}') {
brackets--;
if (brackets <= 0) {
break;
}
}
}
i += Character.charCount(codePoint);
}
return mthCode.toString();
private String cutMethodCode(ICodeInfo codeInfo, MethodNode mth) {
int startPos = getCommentStartPos(codeInfo, mth.getDefPosition());
int stopPos = getNextNodePos(mth, codeInfo);
return codeInfo.getCodeStr().substring(startPos, stopPos);
}
protected int getCommentStartPos(String code, int pos) {
private int getNextNodePos(MethodNode mth, ICodeInfo codeInfo) {
int pos = mth.getDefPosition() + 1;
while (true) {
ICodeNodeRef nodeBelow = codeInfo.getCodeMetadata().getNodeBelow(pos);
if (nodeBelow == null) {
return codeInfo.getCodeStr().length();
}
if (nodeBelow.getAnnType() != ICodeAnnotation.AnnType.METHOD) {
return nodeBelow.getDefPosition();
}
MethodNode nodeMth = (MethodNode) nodeBelow;
if (nodeMth.getParentClass().equals(mth.getParentClass())) { // skip methods from anonymous classes
return getCommentStartPos(codeInfo, nodeMth.getDefPosition());
}
pos = nodeMth.getDefPosition() + 1;
}
}
protected int getCommentStartPos(ICodeInfo codeInfo, int pos) {
String emptyLine = ICodeWriter.NL + ICodeWriter.NL;
int emptyLinePos = code.lastIndexOf(emptyLine, pos);
int emptyLinePos = codeInfo.getCodeStr().lastIndexOf(emptyLine, pos);
return emptyLinePos == -1 ? pos : emptyLinePos + emptyLine.length();
}
@@ -0,0 +1,42 @@
package jadx.tests.integration.invoke;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestSuperInvokeUnknown extends IntegrationTest {
public static class TestCls {
public static class BaseClass {
public int doSomething() {
return 0;
}
}
public static class NestedClass extends BaseClass {
@Override
public int doSomething() {
return super.doSomething();
}
}
}
@Test
public void test() {
disableCompilation();
noDebugInfo();
assertThat(getClassNode(TestCls.NestedClass.class)) // BaseClass unknown
.code()
.containsOne("return super.doSomething();");
}
@Test
public void testTopCls() {
noDebugInfo();
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("return super.doSomething();");
}
}
@@ -0,0 +1,59 @@
package jadx.tests.integration.names;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestCollisionWithJavaLangClasses extends IntegrationTest {
public static class TestCls1 {
public static class System {
public static void main(String[] args) {
java.lang.System.out.println("Hello world");
}
}
}
@Test
public void test1() {
assertThat(getClassNode(TestCls1.class))
.code()
.containsOne("java.lang.System.out.println");
}
public static class TestCls2 {
public void doSomething() {
System.doSomething();
java.lang.System.out.println("Hello World");
}
public static class System {
public static void doSomething() {
}
}
}
@Test
public void test2() {
assertThat(getClassNode(TestCls2.class))
.code()
.containsLine(2, "System.doSomething();")
.containsOne("java.lang.System.out.println");
}
@Test
public void test3() {
List<ClassNode> classes = getClassNodes(
jadx.tests.integration.names.pkg2.System.class,
jadx.tests.integration.names.pkg2.TestCls.class);
assertThat(searchCls(classes, "TestCls"))
.code()
.containsLine(2, "System.doSomething();")
.containsOne("java.lang.System.out.println");
}
}
@@ -0,0 +1,6 @@
package jadx.tests.integration.names.pkg2;
public class System {
public static void doSomething() {
}
}
@@ -0,0 +1,8 @@
package jadx.tests.integration.names.pkg2;
public class TestCls {
public void doSomething() {
System.doSomething();
java.lang.System.out.println("Hello World");
}
}
@@ -0,0 +1,45 @@
package jadx.tests.integration.trycatch;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTryCatchFinally14 extends IntegrationTest {
@SuppressWarnings("unused")
public static class TestCls {
private TCls t;
public void test() {
try {
if (t != null) {
t.doSomething();
}
} finally {
if (t != null) {
t.doFinally();
}
}
}
private static class TCls {
public void doSomething() {
}
public void doFinally() {
}
}
}
@TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne(".doSomething();")
.containsOne("} finally {")
.containsOne(".doFinally();")
.countString(2, "!= null) {");
}
}
@@ -25,8 +25,6 @@ public class TestTryCatchMultiException extends IntegrationTest {
@Test
public void test() {
// printDisassemble();
// setFallback();
noDebugInfo();
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
@@ -0,0 +1,33 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
@SuppressWarnings("CommentedOutCode")
public class TestTryCatchMultiException2 extends SmaliTest {
// @formatter:off
/*
public static boolean test() {
try {
Class<?> cls = Class.forName("c");
return ((Boolean) cls.getMethod("b", new Class[0]).invoke(cls, new Object[0])).booleanValue();
} catch (ClassNotFoundException | NoSuchMethodException | Exception | Throwable unused) {
// java compiler don't allow shadow subclasses in multi-catch
// in this case leave only Throwable
return false;
}
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("} catch (Throwable unused) {");
}
}
@@ -0,0 +1,29 @@
package jadx.tests.integration.types;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Issue 1527
*/
@SuppressWarnings("CommentedOutCode")
public class TestTypeResolver21 extends SmaliTest {
// @formatter:off
/*
public Number test(Object objectArray) {
Object[] arr = (Object[]) objectArray;
return (Number) arr[0];
}
*/
// @formatter:on
@Test
public void test() {
assertThat(getClassNodeFromSmali())
.code()
.containsOne("Object[] arr = (Object[]) objectArray;");
}
}
@@ -0,0 +1,29 @@
package jadx.tests.integration.types;
import java.io.IOException;
import java.io.InputStream;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTypeResolver22 extends IntegrationTest {
public static class TestCls {
public void test(InputStream input, long count) throws IOException {
long pos = input.skip(count);
while (pos < count) {
pos += input.skip(count - pos);
}
}
}
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("long pos = ");
}
}
@@ -0,0 +1,30 @@
package jadx.tests.integration.types;
import jadx.tests.api.IntegrationTest;
import jadx.tests.api.extensions.profiles.TestProfile;
import jadx.tests.api.extensions.profiles.TestWithProfiles;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestTypeResolver23 extends IntegrationTest {
public static class TestCls {
public long test(int a) {
long v = 1L;
if (a == 2) {
v = 2L;
} else if (a == 3) {
v = 3L;
}
System.out.println(v);
return v;
}
}
@TestWithProfiles({ TestProfile.JAVA8, TestProfile.DX_J8 })
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("long v");
}
}
@@ -0,0 +1,38 @@
.class public Ltrycatch/TestTryCatchMultiException2;
.super Ljava/lang/Object;
.method public static test()Z
.registers 5
:try_start_b
const-string v0, "c"
invoke-static {v0}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
move-result-object v1
const/4 v0, 0x0
const-string v2, "b"
new-array v3, v0, [Ljava/lang/Class;
invoke-virtual {v1, v2, v3}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
move-result-object v2
new-array v3, v0, [Ljava/lang/Object;
invoke-virtual {v2, v1, v3}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/Boolean;
invoke-virtual {v1}, Ljava/lang/Boolean;->booleanValue()Z
move-result v1
:try_end_2f
.catch Ljava/lang/ClassNotFoundException; {:try_start_b .. :try_end_2f} :catch_30
.catch Ljava/lang/NoSuchMethodException; {:try_start_b .. :try_end_2f} :catch_30
.catch Ljava/lang/Exception; {:try_start_b .. :try_end_2f} :catch_30
.catchall {:try_start_b .. :try_end_2f} :catchall_30
return v1
:catch_30
:catchall_30
return v0
.end method
@@ -0,0 +1,23 @@
.class public Ltypes/TestTypeResolver21;
.super Ljava/lang/Object;
.source "TestTypeResolver21.java"
.method public test(Ljava/lang/Object;)Ljava/lang/Number;
.registers 4
.param p1, "objectArray" # Ljava/lang/Object;
.prologue
.line 16
check-cast p1, [Ljava/lang/Object;
.end local p1 # "objectArray":Ljava/lang/Object;
move-object v0, p1
check-cast v0, [Ljava/lang/Object;
.line 17
.local v0, "arr":[Ljava/lang/Object;
const/4 v1, 0x0
aget-object v1, v0, v1
check-cast v1, Ljava/lang/Number;
return-object v1
.end method
+5 -1
View File
@@ -7,7 +7,6 @@ plugins {
dependencies {
implementation(project(':jadx-core'))
implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
@@ -30,6 +29,11 @@ dependencies {
implementation 'com.android.tools.build:apksig:4.2.1'
implementation 'io.github.hqktech:jdwp:1.0'
// TODO: Switch back to upstream once this PR gets merged:
// https://github.com/FabricMC/mapping-io/pull/19
// implementation 'net.fabricmc:mapping-io:0.3.0'
implementation files('libs/mapping-io-0.4.0-SNAPSHOT.jar')
testImplementation project(":jadx-core").sourceSets.test.output
}
Binary file not shown.
@@ -6,18 +6,27 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.JavaPackage;
import jadx.api.ResourceFile;
import jadx.api.impl.InMemoryCodeCache;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginManager;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.ProcessState;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.rename.RenameVisitor;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
@@ -30,11 +39,14 @@ import static jadx.core.dex.nodes.ProcessState.GENERATED_AND_UNLOADED;
import static jadx.core.dex.nodes.ProcessState.NOT_LOADED;
import static jadx.core.dex.nodes.ProcessState.PROCESS_COMPLETE;
@SuppressWarnings("ConstantConditions")
public class JadxWrapper {
private static final Logger LOG = LoggerFactory.getLogger(JadxWrapper.class);
private static final Object DECOMPILER_UPDATE_SYNC = new Object();
private final MainWindow mainWindow;
private JadxDecompiler decompiler;
private volatile @Nullable JadxDecompiler decompiler;
public JadxWrapper(MainWindow mainWindow) {
this.mainWindow = mainWindow;
@@ -43,23 +55,25 @@ public class JadxWrapper {
public void open() {
close();
try {
JadxProject project = getProject();
JadxArgs jadxArgs = getSettings().toJadxArgs();
jadxArgs.setInputFiles(FileUtils.toFiles(project.getFilePaths()));
jadxArgs.setCodeData(project.getCodeData());
synchronized (DECOMPILER_UPDATE_SYNC) {
JadxProject project = getProject();
JadxArgs jadxArgs = getSettings().toJadxArgs();
jadxArgs.setInputFiles(FileUtils.toFiles(project.getFilePaths()));
jadxArgs.setCodeData(project.getCodeData());
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
initCodeCache();
this.decompiler = new JadxDecompiler(jadxArgs);
this.decompiler.load();
initCodeCache();
}
} catch (Exception e) {
LOG.error("Jadx init error", e);
LOG.error("Jadx decompiler wrapper init error", e);
close();
}
}
// TODO: check and move into core package
public void unloadClasses() {
for (ClassNode cls : decompiler.getRoot().getClasses()) {
for (ClassNode cls : getDecompiler().getRoot().getClasses()) {
ProcessState clsState = cls.getState();
cls.unload();
cls.setState(clsState == PROCESS_COMPLETE ? GENERATED_AND_UNLOADED : NOT_LOADED);
@@ -68,14 +82,16 @@ public class JadxWrapper {
public void close() {
try {
if (decompiler != null) {
decompiler.close();
mainWindow.getCacheObject().reset();
synchronized (DECOMPILER_UPDATE_SYNC) {
if (decompiler != null) {
decompiler.close();
decompiler = null;
}
}
} catch (Exception e) {
LOG.error("Jadx decompiler close error", e);
} finally {
decompiler = null;
mainWindow.getCacheObject().reset();
}
}
@@ -94,7 +110,7 @@ public class JadxWrapper {
}
private BufferCodeCache buildBufferedDiskCache() {
DiskCodeCache diskCache = new DiskCodeCache(decompiler.getRoot(), getProject().getCacheDir());
DiskCodeCache diskCache = new DiskCodeCache(getDecompiler().getRoot(), getProject().getCacheDir());
return new BufferCodeCache(diskCache);
}
@@ -102,14 +118,14 @@ public class JadxWrapper {
* Get the complete list of classes
*/
public List<JavaClass> getClasses() {
return decompiler.getClasses();
return getDecompiler().getClasses();
}
/**
* Get all classes that are not excluded by the excluded packages settings
*/
public List<JavaClass> getIncludedClasses() {
List<JavaClass> classList = decompiler.getClasses();
List<JavaClass> classList = getDecompiler().getClasses();
List<String> excludedPackages = getExcludedPackages();
if (excludedPackages.isEmpty()) {
return classList;
@@ -123,7 +139,7 @@ public class JadxWrapper {
* Get all classes that are not excluded by the excluded packages settings including inner classes
*/
public List<JavaClass> getIncludedClassesWithInners() {
List<JavaClass> classes = decompiler.getClassesWithInners();
List<JavaClass> classes = getDecompiler().getClassesWithInners();
List<String> excludedPackages = getExcludedPackages();
if (excludedPackages.isEmpty()) {
return classes;
@@ -145,7 +161,7 @@ public class JadxWrapper {
}
public List<List<JavaClass>> buildDecompileBatches(List<JavaClass> classes) {
return decompiler.getDecompileScheduler().buildBatches(classes);
return getDecompiler().getDecompileScheduler().buildBatches(classes);
}
// TODO: move to CLI and filter classes in JadxDecompiler
@@ -175,20 +191,61 @@ public class JadxWrapper {
getSettings().sync();
}
public List<JavaPackage> getPackages() {
return decompiler.getPackages();
public List<JadxPlugin> getAllPlugins() {
if (decompiler != null) {
return decompiler.getPluginManager().getAllPlugins();
}
JadxPluginManager pluginManager = new JadxPluginManager();
pluginManager.load();
return pluginManager.getAllPlugins();
}
public List<ResourceFile> getResources() {
return decompiler.getResources();
}
public JadxDecompiler getDecompiler() {
/**
* TODO: make method private
* Do not store JadxDecompiler in fields to not leak old instances
*/
public @NotNull JadxDecompiler getDecompiler() {
if (decompiler == null || decompiler.getRoot() == null) {
throw new JadxRuntimeException("Decompiler not yet loaded");
}
return decompiler;
}
// TODO: forbid usage of this method
public RootNode getRootNode() {
return getDecompiler().getRoot();
}
public void reInitRenameVisitor() {
new RenameVisitor().init(getRootNode());
}
public void reloadCodeData() {
getDecompiler().reloadCodeData();
}
public JavaNode getJavaNodeByRef(ICodeNodeRef nodeRef) {
return getDecompiler().getJavaNodeByRef(nodeRef);
}
public JavaNode getEnclosingNode(ICodeInfo codeInfo, int pos) {
return getDecompiler().getEnclosingNode(codeInfo, pos);
}
public List<Runnable> getSaveTasks() {
return getDecompiler().getSaveTasks();
}
public List<JavaPackage> getPackages() {
return getDecompiler().getPackages();
}
public List<ResourceFile> getResources() {
return getDecompiler().getResources();
}
public JadxArgs getArgs() {
return decompiler.getArgs();
return getDecompiler().getArgs();
}
public JadxProject getProject() {
@@ -204,14 +261,14 @@ public class JadxWrapper {
* Full name of an outer class. Inner classes are not supported.
*/
public @Nullable JavaClass searchJavaClassByFullAlias(String fullName) {
return decompiler.getClasses().stream()
return getDecompiler().getClasses().stream()
.filter(cls -> cls.getFullName().equals(fullName))
.findFirst()
.orElse(null);
}
public @Nullable JavaClass searchJavaClassByOrigClassName(String fullName) {
return decompiler.searchJavaClassByOrigFullName(fullName);
return getDecompiler().searchJavaClassByOrigFullName(fullName);
}
/**
@@ -219,7 +276,7 @@ public class JadxWrapper {
* Full raw name of an outer class. Inner classes are not supported.
*/
public @Nullable JavaClass searchJavaClassByRawName(String rawName) {
return decompiler.getClasses().stream()
return getDecompiler().getClasses().stream()
.filter(cls -> cls.getRawName().equals(rawName))
.findFirst()
.orElse(null);
@@ -160,7 +160,7 @@ public class DbgUtils {
// TODO: parse AndroidManifest.xml instead of looking for keywords
private static String getManifestContent(MainWindow mainWindow) {
try {
ResourceFile androidManifest = mainWindow.getWrapper().getDecompiler().getResources()
ResourceFile androidManifest = mainWindow.getWrapper().getResources()
.stream()
.filter(res -> res.getType() == ResourceType.MANIFEST)
.findFirst()
@@ -3,6 +3,7 @@ package jadx.gui.jobs;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -11,6 +12,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
@@ -19,6 +21,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.utils.NLS;
@@ -33,16 +36,16 @@ import static jadx.gui.utils.UiUtils.calcProgress;
public class BackgroundExecutor {
private static final Logger LOG = LoggerFactory.getLogger(BackgroundExecutor.class);
private final MainWindow mainWindow;
private final JadxSettings settings;
private final ProgressPanel progressPane;
private ThreadPoolExecutor taskQueueExecutor;
private final Map<Long, IBackgroundTask> taskRunning = new ConcurrentHashMap<>();
private final AtomicLong idSupplier = new AtomicLong(0);
public BackgroundExecutor(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.progressPane = mainWindow.getProgressPane();
public BackgroundExecutor(JadxSettings settings, ProgressPanel progressPane) {
this.settings = Objects.requireNonNull(settings);
this.progressPane = Objects.requireNonNull(progressPane);
reset();
}
@@ -68,9 +71,16 @@ public class BackgroundExecutor {
public synchronized void cancelAll() {
try {
taskRunning.values().forEach(Cancelable::cancel);
taskQueueExecutor.shutdown();
boolean complete = taskQueueExecutor.awaitTermination(30, TimeUnit.SECONDS);
LOG.debug("Background task executor terminated with status: {}", complete ? "complete" : "interrupted");
taskQueueExecutor.shutdownNow();
boolean complete = taskQueueExecutor.awaitTermination(3, TimeUnit.SECONDS);
if (complete) {
LOG.debug("Background task executor canceled successfully");
} else {
String taskNames = taskRunning.values().stream()
.map(IBackgroundTask::getTitle)
.collect(Collectors.joining(", "));
LOG.debug("Background task executor cancel failed. Running tasks: {}", taskNames);
}
} catch (Exception e) {
LOG.error("Error terminating task executor", e);
} finally {
@@ -131,8 +141,17 @@ public class BackgroundExecutor {
try {
runJobs();
} finally {
taskComplete(id);
task.onDone(this);
try {
task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
task.onFinish(this);
progressPane.setVisible(false);
});
} finally {
taskComplete(id);
progressPane.changeVisibility(this, false);
}
}
return status;
}
@@ -146,7 +165,7 @@ public class BackgroundExecutor {
progressPane.changeVisibility(this, true);
}
status = TaskStatus.STARTED;
int threadsCount = mainWindow.getSettings().getThreadsCount();
int threadsCount = settings.getThreadsCount();
executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount);
for (Runnable job : jobs) {
executor.execute(job);
@@ -212,8 +231,15 @@ public class BackgroundExecutor {
// force termination
task.cancel();
executor.shutdown();
if (executor.awaitTermination(2, TimeUnit.SECONDS)) {
LOG.debug("Task cancel complete");
return;
}
LOG.debug("Forcing tasks cancel");
executor.shutdownNow();
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
LOG.debug("Task cancel complete: {}", complete ? "success" : "aborted");
LOG.debug("Forced task cancel status: {}",
complete ? "success" : "fail, still active: " + executor.getActiveCount());
}
private Supplier<TaskStatus> buildCancelCheck(long startTime) {
@@ -251,12 +277,6 @@ public class BackgroundExecutor {
};
}
@Override
protected void done() {
progressPane.setVisible(false);
task.onFinish(this);
}
@Override
public TaskStatus getStatus() {
return status;
@@ -6,7 +6,6 @@ import java.util.List;
import javax.swing.JOptionPane;
import jadx.api.ICodeCache;
import jadx.api.JadxDecompiler;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
@@ -35,9 +34,8 @@ public class ExportTask extends CancelableBackgroundTask {
@Override
public List<Runnable> scheduleJobs() {
wrapCodeCache();
JadxDecompiler decompiler = wrapper.getDecompiler();
decompiler.getArgs().setRootDir(saveDir);
List<Runnable> saveTasks = decompiler.getSaveTasks();
wrapper.getArgs().setRootDir(saveDir);
List<Runnable> saveTasks = wrapper.getSaveTasks();
this.timeLimit = DecompileTask.calcDecompileTimeLimit(saveTasks.size());
return saveTasks;
}
@@ -0,0 +1,285 @@
package jadx.gui.plugins.mappings;
import java.io.IOException;
import java.nio.file.Path;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.mappingio.MappedElementKind;
import net.fabricmc.mappingio.MappingWriter;
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
import jadx.api.ICodeInfo;
import jadx.api.data.ICodeComment;
import jadx.api.data.ICodeRename;
import jadx.api.data.IJavaNodeRef.RefType;
import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxCodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.utils.CodeUtils;
import jadx.core.Consts;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;
public class MappingExporter {
private static final Logger LOG = LoggerFactory.getLogger(MappingExporter.class);
private final RootNode root;
public MappingExporter(RootNode rootNode) {
this.root = rootNode;
}
private List<VarNode> collectMethodArgs(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
List<VarNode> args = new ArrayList<>();
codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> {
if (pos > lineEndPos) {
// Stop at line end
return Boolean.TRUE;
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
args.add(varNode);
}
}
return null;
});
return args;
}
private List<SimpleEntry<VarNode, Integer>> collectMethodVars(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int mthLineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
List<SimpleEntry<VarNode, Integer>> vars = new ArrayList<>();
AtomicInteger lastOffset = new AtomicInteger(-1);
codeInfo.getCodeMetadata().searchDown(mthLineEndPos, (pos, ann) -> {
if (ann instanceof InsnCodeOffset) {
lastOffset.set(((InsnCodeOffset) ann).getOffset());
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
if (lastOffset.get() != -1) {
vars.add(new SimpleEntry<VarNode, Integer>(varNode, lastOffset.get()));
} else {
LOG.warn("Local variable not present in bytecode, skipping: "
+ methodNode.getMethodInfo().getRawFullId() + "#" + varNode.getName());
}
lastOffset.set(-1);
}
}
return null;
});
return vars;
}
public void exportMappings(Path path, JadxCodeData codeData, MappingFormat mappingFormat) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
// Map < SrcName >
Set<String> mappedClasses = new HashSet<>();
// Map < DeclClass + ShortId >
Set<String> mappedFields = new HashSet<>();
Set<String> mappedMethods = new HashSet<>();
Set<String> methodsWithMappedElements = new HashSet<>();
// Map < DeclClass + MethodShortId + CodeRef, NewName >
Map<String, String> mappedMethodArgsAndVars = new HashMap<>();
// Map < DeclClass + *ShortId + *CodeRef, Comment >
Map<String, String> comments = new HashMap<>();
// We have to do this so we know for sure which elements are *manually* renamed
for (ICodeRename codeRename : codeData.getRenames()) {
if (codeRename.getNodeRef().getType().equals(RefType.CLASS)) {
mappedClasses.add(codeRename.getNodeRef().getDeclaringClass());
} else if (codeRename.getNodeRef().getType().equals(RefType.FIELD)) {
mappedFields.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else if (codeRename.getNodeRef().getType().equals(RefType.METHOD)) {
if (codeRename.getCodeRef() == null) {
mappedMethods.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else {
methodsWithMappedElements.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
mappedMethodArgsAndVars.put(codeRename.getNodeRef().getDeclaringClass()
+ codeRename.getNodeRef().getShortId()
+ codeRename.getCodeRef(),
codeRename.getNewName());
}
}
}
for (ICodeComment codeComment : codeData.getComments()) {
comments.put(codeComment.getNodeRef().getDeclaringClass()
+ (codeComment.getNodeRef().getShortId() == null ? "" : codeComment.getNodeRef().getShortId())
+ (codeComment.getCodeRef() == null ? "" : codeComment.getCodeRef()),
codeComment.getComment());
if (codeComment.getCodeRef() != null) {
methodsWithMappedElements.add(codeComment.getNodeRef().getDeclaringClass() + codeComment.getNodeRef().getShortId());
}
}
try {
if (mappingFormat.hasSingleFile()) {
if (path.toFile().exists()) {
path.toFile().delete();
}
path.toFile().createNewFile();
} else {
FileUtils.makeDirs(path);
}
mappingTree.visitHeader();
mappingTree.visitNamespaces("official", Arrays.asList("named"));
mappingTree.visitContent();
for (ClassNode cls : root.getClasses()) {
ClassInfo classInfo = cls.getClassInfo();
String classPath = classInfo.makeRawFullName().replace('.', '/');
String rawClassName = classInfo.getRawName();
if (classInfo.hasAlias()
&& !classInfo.getAliasShortName().equals(classInfo.getShortName())
&& mappedClasses.contains(rawClassName)) {
mappingTree.visitClass(classPath);
String alias = classInfo.makeAliasRawFullName().replace('.', '/');
if (alias.startsWith(Consts.DEFAULT_PACKAGE_NAME)) {
alias = alias.substring(Consts.DEFAULT_PACKAGE_NAME.length() + 1);
}
mappingTree.visitDstName(MappedElementKind.CLASS, 0, alias);
}
if (comments.containsKey(rawClassName)) {
mappingTree.visitClass(classPath);
mappingTree.visitComment(MappedElementKind.CLASS, comments.get(rawClassName));
}
for (FieldNode fld : cls.getFields()) {
FieldInfo fieldInfo = fld.getFieldInfo();
if (fieldInfo.hasAlias() && mappedFields.contains(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitDstName(MappedElementKind.FIELD, 0, fieldInfo.getAlias());
}
if (comments.containsKey(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitComment(MappedElementKind.FIELD, comments.get(rawClassName + fieldInfo.getShortId()));
}
}
for (MethodNode mth : cls.getMethods()) {
MethodInfo methodInfo = mth.getMethodInfo();
String methodName = methodInfo.getName();
String methodDesc = methodInfo.getShortId().substring(methodName.length());
if (methodInfo.hasAlias() && mappedMethods.contains(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitDstName(MappedElementKind.METHOD, 0, methodInfo.getAlias());
}
if (comments.containsKey(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitComment(MappedElementKind.METHOD, comments.get(rawClassName + methodInfo.getShortId()));
}
if (!methodsWithMappedElements.contains(rawClassName + methodInfo.getShortId())) {
continue;
}
// Method args
int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1;
int lastArgLvIndex = lvtIndex - 1;
List<VarNode> args = collectMethodArgs(mth);
for (VarNode arg : args) {
int lvIndex = arg.getReg() - args.get(0).getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(arg.getReg(), arg.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodArg(mappingTree, classPath, methodName, methodDesc, args.indexOf(arg), lvIndex);
mappingTree.visitDstName(MappedElementKind.METHOD_ARG, 0, mappedMethodArgsAndVars.get(key));
mappedMethodArgsAndVars.remove(key);
}
lastArgLvIndex = lvIndex;
lvtIndex++;
// Not checking for comments since method args can't have any
}
// Method vars
List<SimpleEntry<VarNode, Integer>> vars = collectMethodVars(mth);
for (SimpleEntry<VarNode, Integer> entry : vars) {
VarNode var = entry.getKey();
int offset = entry.getValue();
int lvIndex = lastArgLvIndex + var.getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(var.getReg(), var.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitDstName(MappedElementKind.METHOD_VAR, 0, mappedMethodArgsAndVars.get(key));
}
key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forInsn(offset);
if (comments.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitComment(MappedElementKind.METHOD_VAR, comments.get(key));
}
lvtIndex++;
}
}
}
MappingWriter writer = MappingWriter.create(path, mappingFormat);
mappingTree.accept(writer);
mappingTree.visitEnd();
writer.close();
} catch (IOException e) {
LOG.error("Failed to save deobfuscation map file '{}'", path.toAbsolutePath(), e);
}
}
private void visitField(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitField(srcName, srcDesc);
}
private void visitMethod(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitMethod(srcName, srcDesc);
}
private void visitMethodArg(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int argPosition,
int lvIndex) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodArg(argPosition, lvIndex, null);
}
private void visitMethodVar(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int lvtIndex,
int lvIndex, int startOpIdx) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodVar(lvtIndex, lvIndex, startOpIdx, null);
}
}
@@ -8,11 +8,11 @@ import org.slf4j.LoggerFactory;
import jadx.api.ICodeCache;
import jadx.api.ICodeWriter;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.Cancelable;
import jadx.gui.search.SearchSettings;
import jadx.gui.treemodel.CodeNode;
@@ -23,7 +23,7 @@ public final class CodeSearchProvider extends BaseSearchProvider {
private static final Logger LOG = LoggerFactory.getLogger(CodeSearchProvider.class);
private final ICodeCache codeCache;
private final JadxDecompiler decompiler;
private final JadxWrapper wrapper;
private @Nullable String code;
private int clsNum = 0;
@@ -32,7 +32,7 @@ public final class CodeSearchProvider extends BaseSearchProvider {
public CodeSearchProvider(MainWindow mw, SearchSettings searchSettings, List<JavaClass> classes) {
super(mw, searchSettings, classes);
this.codeCache = mw.getWrapper().getArgs().getCodeCache();
this.decompiler = mw.getWrapper().getDecompiler();
this.wrapper = mw.getWrapper();
}
@Override
@@ -75,7 +75,7 @@ public final class CodeSearchProvider extends BaseSearchProvider {
try {
ICodeMetadata metadata = javaCls.getCodeInfo().getCodeMetadata();
ICodeNodeRef nodeRef = metadata.getNodeAt(pos);
JavaNode encNode = decompiler.getJavaNodeByRef(nodeRef);
JavaNode encNode = wrapper.getJavaNodeByRef(nodeRef);
if (encNode != null) {
return convert(encNode);
}
@@ -57,6 +57,18 @@ public class JadxProject {
this.mainWindow = mainWindow;
}
public @Nullable Path getWorkingDir() {
if (projectPath != null) {
return projectPath.toAbsolutePath().getParent();
}
List<Path> files = data.getFiles();
if (!files.isEmpty()) {
Path path = files.get(0);
return path.toAbsolutePath().getParent();
}
return null;
}
@Nullable
public Path getProjectPath() {
return projectPath;
@@ -166,7 +178,18 @@ public class JadxProject {
Path path = files.get(0);
return path.resolveSibling(path.getFileName() + ".cache");
}
throw new JadxRuntimeException("Can't get working dir");
throw new JadxRuntimeException("Failed to build cache dir");
}
public boolean isEnableLiveReload() {
return data.isEnableLiveReload();
}
public void setEnableLiveReload(boolean newValue) {
if (newValue != data.isEnableLiveReload()) {
data.setEnableLiveReload(newValue);
changed();
}
}
private void changed() {
@@ -45,7 +45,7 @@ public class JadxSettings extends JadxCLIArgs {
private static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
private static final int RECENT_PROJECTS_COUNT = 15;
private static final int CURRENT_SETTINGS_VERSION = 17;
private static final int CURRENT_SETTINGS_VERSION = 18;
private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont();
@@ -59,7 +59,7 @@ public class JadxSettings extends JadxCLIArgs {
private Path lastOpenFilePath = USER_HOME;
private Path lastSaveFilePath = USER_HOME;
private boolean flattenPackage = false;
private boolean checkForUpdates = false;
private boolean checkForUpdates = true;
private List<Path> recentProjects = new ArrayList<>();
private String fontStr = "";
private String smaliFontStr = "";
@@ -91,6 +91,7 @@ public class JadxSettings extends JadxCLIArgs {
private String adbDialogPort = "5037";
private CodeCacheMode codeCacheMode = CodeCacheMode.DISK_WITH_CACHE;
private boolean jumpOnDoubleClick = true;
/**
* UI setting: the width of the tree showing the classes, resources, ...
@@ -219,29 +220,22 @@ public class JadxSettings extends JadxCLIArgs {
if (pos == null || pos.getBounds() == null) {
return false;
}
if (window instanceof MainWindow) {
int extendedState = getMainWindowExtendedState();
if (extendedState != JFrame.NORMAL) {
((JFrame) window).setExtendedState(extendedState);
return true;
}
}
if (!isContainedInAnyScreen(pos)) {
if (!isAccessibleInAnyScreen(pos)) {
return false;
}
window.setBounds(pos.getBounds());
if (window instanceof MainWindow) {
((JFrame) window).setExtendedState(getMainWindowExtendedState());
}
return true;
}
private static boolean isContainedInAnyScreen(WindowLocation pos) {
Rectangle bounds = pos.getBounds();
if (bounds.getX() > 0 && bounds.getY() > 0) {
for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
if (gd.getDefaultConfiguration().getBounds().contains(bounds)) {
return true;
}
private static boolean isAccessibleInAnyScreen(WindowLocation pos) {
Rectangle windowBounds = pos.getBounds();
for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
Rectangle screenBounds = gd.getDefaultConfiguration().getBounds();
if (screenBounds.intersects(windowBounds)) {
return true;
}
}
LOG.debug("Window saved position was ignored: {}", pos);
@@ -623,6 +617,14 @@ public class JadxSettings extends JadxCLIArgs {
this.codeCacheMode = codeCacheMode;
}
public boolean isJumpOnDoubleClick() {
return jumpOnDoubleClick;
}
public void setJumpOnDoubleClick(boolean jumpOnDoubleClick) {
this.jumpOnDoubleClick = jumpOnDoubleClick;
}
private void upgradeSettings(int fromVersion) {
LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION);
if (fromVersion == 0) {
@@ -702,11 +704,7 @@ public class JadxSettings extends JadxCLIArgs {
fromVersion++;
}
if (fromVersion == 15) {
if (deobfuscationForceSave) {
deobfuscationMapFileMode = DeobfuscationMapFileMode.OVERWRITE;
} else {
deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
}
deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
fromVersion++;
}
if (fromVersion == 16) {
@@ -717,6 +715,10 @@ public class JadxSettings extends JadxCLIArgs {
}
fromVersion++;
}
if (fromVersion == 17) {
checkForUpdates = true;
fromVersion++;
}
if (fromVersion != CURRENT_SETTINGS_VERSION) {
LOG.warn("Incorrect settings upgrade. Expected version: {}, got: {}", CURRENT_SETTINGS_VERSION, fromVersion);
}
@@ -65,7 +65,6 @@ import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.JadxPluginManager;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
import jadx.gui.ui.MainWindow;
@@ -580,8 +579,7 @@ public class JadxSettingsWindow extends JDialog {
private SettingsGroup makePluginOptionsGroup() {
SettingsGroup pluginsGroup = new SettingsGroup(NLS.str("preferences.plugins"));
JadxPluginManager pluginManager = mainWindow.getWrapper().getDecompiler().getPluginManager();
for (JadxPlugin plugin : pluginManager.getAllPlugins()) {
for (JadxPlugin plugin : mainWindow.getWrapper().getAllPlugins()) {
if (!(plugin instanceof JadxPluginOptions)) {
continue;
}
@@ -628,6 +626,10 @@ public class JadxSettingsWindow extends JDialog {
mainWindow.loadSettings();
});
JCheckBox jumpOnDoubleClick = new JCheckBox();
jumpOnDoubleClick.setSelected(settings.isJumpOnDoubleClick());
jumpOnDoubleClick.addItemListener(e -> settings.setJumpOnDoubleClick(e.getStateChange() == ItemEvent.SELECTED));
JCheckBox update = new JCheckBox();
update.setSelected(settings.isCheckForUpdates());
update.addItemListener(e -> settings.setCheckForUpdates(e.getStateChange() == ItemEvent.SELECTED));
@@ -649,6 +651,7 @@ public class JadxSettingsWindow extends JDialog {
SettingsGroup group = new SettingsGroup(NLS.str("preferences.other"));
group.addRow(NLS.str("preferences.language"), languageCbx);
group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode);
group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick);
group.addRow(NLS.str("preferences.check_for_updates"), update);
group.addRow(NLS.str("preferences.cfg"), cfg);
group.addRow(NLS.str("preferences.raw_cfg"), rawCfg);
@@ -19,6 +19,7 @@ public class ProjectData {
private List<TabViewState> openTabs = Collections.emptyList();
private int activeTab = -1;
private @Nullable Path cacheDir;
private boolean enableLiveReload = false;
public List<Path> getFiles() {
return files;
@@ -94,4 +95,12 @@ public class ProjectData {
public void setCacheDir(Path cacheDir) {
this.cacheDir = cacheDir;
}
public boolean isEnableLiveReload() {
return enableLiveReload;
}
public void setEnableLiveReload(boolean enableLiveReload) {
this.enableLiveReload = enableLiveReload;
}
}
@@ -16,6 +16,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
@@ -43,6 +44,7 @@ import javax.swing.Action;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
@@ -77,6 +79,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import net.fabricmc.mappingio.format.MappingFormat;
import jadx.api.JadxArgs;
import jadx.api.JavaNode;
@@ -93,6 +96,7 @@ import jadx.gui.jobs.DecompileTask;
import jadx.gui.jobs.ExportTask;
import jadx.gui.jobs.ProcessResult;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.plugins.mappings.MappingExporter;
import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
@@ -132,7 +136,9 @@ import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.fileswatcher.LiveReloadWorker;
import jadx.gui.utils.logs.LogCollector;
import jadx.gui.utils.ui.ActionHandler;
import static io.reactivex.internal.functions.Functions.EMPTY_RUNNABLE;
import static javax.swing.KeyStroke.getKeyStroke;
@@ -149,6 +155,7 @@ public class MainWindow extends JFrame {
private static final ImageIcon ICON_OPEN = UiUtils.openSvgIcon("ui/openDisk");
private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile");
private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall");
private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh");
private static final ImageIcon ICON_EXPORT = UiUtils.openSvgIcon("ui/export");
private static final ImageIcon ICON_EXIT = UiUtils.openSvgIcon("ui/exit");
private static final ImageIcon ICON_SYNC = UiUtils.openSvgIcon("ui/pagination");
@@ -174,6 +181,7 @@ public class MainWindow extends JFrame {
private transient Action newProjectAction;
private transient Action saveProjectAction;
private transient JMenu exportMappingsMenu;
private JPanel mainPanel;
private JSplitPane splitPane;
@@ -192,6 +200,9 @@ public class MainWindow extends JFrame {
private JToggleButton deobfToggleBtn;
private JCheckBoxMenuItem deobfMenuItem;
private JCheckBoxMenuItem liveReloadMenuItem;
private final LiveReloadWorker liveReloadWorker;
private transient Link updateLink;
private transient ProgressPanel progressPane;
private transient Theme editorTheme;
@@ -204,18 +215,18 @@ public class MainWindow extends JFrame {
this.cacheObject = new CacheObject();
this.project = new JadxProject(this);
this.wrapper = new JadxWrapper(this);
this.liveReloadWorker = new LiveReloadWorker(this);
resetCache();
FontUtils.registerBundledFonts();
initUI();
this.backgroundExecutor = new BackgroundExecutor(settings, progressPane);
initMenuAndToolbar();
registerMouseNavigationButtons();
UiUtils.setWindowIcons(this);
loadSettings();
update();
this.backgroundExecutor = new BackgroundExecutor(this);
checkForUpdate();
}
@@ -306,6 +317,7 @@ public class MainWindow extends JFrame {
return;
}
closeAll();
exportMappingsMenu.setEnabled(false);
updateProject(new JadxProject(this));
}
@@ -349,6 +361,31 @@ public class MainWindow extends JFrame {
update();
}
private void exportMappings(MappingFormat mappingFormat) {
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.CUSTOM_SAVE);
fileDialog.setTitle(NLS.str("file.export_mappings_as"));
Path workingDir = project.getWorkingDir();
Path baseDir = workingDir != null ? workingDir : settings.getLastSaveFilePath();
if (mappingFormat.hasSingleFile()) {
fileDialog.setSelectedFile(baseDir.resolve("mappings." + mappingFormat.fileExt));
fileDialog.setFileExtList(Collections.singletonList(mappingFormat.fileExt));
fileDialog.setSelectionMode(JFileChooser.FILES_ONLY);
} else {
fileDialog.setCurrentDir(baseDir);
fileDialog.setSelectionMode(JFileChooser.DIRECTORIES_ONLY);
}
List<Path> paths = fileDialog.show();
if (paths.size() != 1) {
return;
}
Path savePath = paths.get(0);
LOG.info("Export mappings to: {}", savePath.toAbsolutePath());
backgroundExecutor.execute(NLS.str("progress.export_mappings"),
() -> new MappingExporter(wrapper.getDecompiler().getRoot())
.exportMappings(savePath, project.getCodeData(), mappingFormat),
s -> update());
}
void open(List<Path> paths) {
open(paths, EMPTY_RUNNABLE);
}
@@ -386,7 +423,7 @@ public class MainWindow extends JFrame {
return loadedFile.resolveSibling(fileName);
}
public void reopen() {
public synchronized void reopen() {
saveAll();
closeAll();
loadFiles(EMPTY_RUNNABLE);
@@ -408,6 +445,7 @@ public class MainWindow extends JFrame {
}
private void loadFiles(Runnable onFinish) {
exportMappingsMenu.setEnabled(false);
if (project.getFilePaths().isEmpty()) {
return;
}
@@ -419,8 +457,13 @@ public class MainWindow extends JFrame {
UiUtils.errorMessage(this, NLS.str("message.memoryLow"));
return;
}
if (status != TaskStatus.COMPLETE) {
LOG.warn("Loading task incomplete, status: {}", status);
return;
}
checkLoadedStatus();
onOpen();
exportMappingsMenu.setEnabled(true);
onFinish.run();
});
}
@@ -437,6 +480,7 @@ public class MainWindow extends JFrame {
LogCollector.getInstance().reset();
wrapper.close();
tabbedPane.closeAllTabs();
UiUtils.resetClipboardOwner();
System.gc();
}
@@ -463,6 +507,7 @@ public class MainWindow extends JFrame {
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
initTree();
update();
updateLiveReload(project.isEnableLiveReload());
BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent());
backgroundExecutor.execute(NLS.str("progress.load"),
@@ -470,6 +515,21 @@ public class MainWindow extends JFrame {
status -> runInitialBackgroundJobs());
}
public void updateLiveReload(boolean state) {
if (liveReloadWorker.isStarted() == state) {
return;
}
project.setEnableLiveReload(state);
liveReloadMenuItem.setEnabled(false);
backgroundExecutor.execute(
(state ? "Starting" : "Stopping") + " live reload",
() -> liveReloadWorker.updateState(state),
s -> {
liveReloadMenuItem.setState(state);
liveReloadMenuItem.setEnabled(true);
});
}
private void addTreeCustomNodes() {
treeRoot.replaceCustomNode(ApkSignature.getApkSignature(wrapper));
treeRoot.replaceCustomNode(new SummaryNode(this));
@@ -765,7 +825,6 @@ public class MainWindow extends JFrame {
}
private void initMenuAndToolbar() {
final boolean devVersion = (Jadx.VERSION_DEV.equals(Jadx.getVersion()));
Action openAction = new AbstractAction(NLS.str("file.open_action"), ICON_OPEN) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -807,6 +866,49 @@ public class MainWindow extends JFrame {
};
saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as"));
ActionHandler reload = new ActionHandler(ev -> UiUtils.uiRun(this::reopen));
reload.setNameAndDesc(NLS.str("file.reload"));
reload.setIcon(ICON_RELOAD);
reload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, 0));
ActionHandler liveReload = new ActionHandler(ev -> updateLiveReload(!project.isEnableLiveReload()));
liveReload.setName(NLS.str("file.live_reload"));
liveReload.setShortDescription(NLS.str("file.live_reload_desc"));
liveReload.setKeyBinding(getKeyStroke(KeyEvent.VK_F5, InputEvent.SHIFT_DOWN_MASK));
liveReloadMenuItem = new JCheckBoxMenuItem(liveReload);
liveReloadMenuItem.setState(project.isEnableLiveReload());
Action exportMappingsAsTiny2 = new AbstractAction("Tiny v2 file") {
@Override
public void actionPerformed(ActionEvent e) {
exportMappings(MappingFormat.TINY_2);
}
};
exportMappingsAsTiny2.putValue(Action.SHORT_DESCRIPTION, "Tiny v2 file");
Action exportMappingsAsEnigma = new AbstractAction("Enigma file") {
@Override
public void actionPerformed(ActionEvent e) {
exportMappings(MappingFormat.ENIGMA);
}
};
exportMappingsAsEnigma.putValue(Action.SHORT_DESCRIPTION, "Enigma file");
Action exportMappingsAsEnigmaDir = new AbstractAction("Enigma directory") {
@Override
public void actionPerformed(ActionEvent e) {
exportMappings(MappingFormat.ENIGMA_DIR);
}
};
exportMappingsAsEnigmaDir.putValue(Action.SHORT_DESCRIPTION, "Enigma directory");
exportMappingsMenu = new JMenu(NLS.str("file.export_mappings_as"));
exportMappingsMenu.add(exportMappingsAsTiny2);
exportMappingsMenu.add(exportMappingsAsEnigma);
exportMappingsMenu.add(exportMappingsAsEnigmaDir);
exportMappingsMenu.setEnabled(false);
Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) {
@Override
public void actionPerformed(ActionEvent e) {
@@ -993,6 +1095,11 @@ public class MainWindow extends JFrame {
file.add(saveProjectAction);
file.add(saveProjectAsAction);
file.addSeparator();
file.add(reload);
file.add(liveReloadMenuItem);
file.addSeparator();
file.add(exportMappingsMenu);
file.addSeparator();
file.add(saveAllAction);
file.add(exportAction);
file.addSeparator();
@@ -1027,7 +1134,7 @@ public class MainWindow extends JFrame {
JMenu help = new JMenu(NLS.str("menu.help"));
help.setMnemonic(KeyEvent.VK_H);
help.add(logAction);
if (devVersion) {
if (Jadx.isDevVersion()) {
help.add(new AbstractAction("Show sample error report") {
@Override
public void actionPerformed(ActionEvent e) {
@@ -1060,6 +1167,8 @@ public class MainWindow extends JFrame {
toolbar.add(openAction);
toolbar.add(addFilesAction);
toolbar.addSeparator();
toolbar.add(reload);
toolbar.addSeparator();
toolbar.add(saveAllAction);
toolbar.add(exportAction);
toolbar.addSeparator();
@@ -1398,10 +1507,6 @@ public class MainWindow extends JFrame {
return backgroundExecutor;
}
public ProgressPanel getProgressPane() {
return progressPane;
}
public JRoot getTreeRoot() {
return treeRoot;
}
@@ -309,6 +309,7 @@ public class TabbedPane extends JTabbedPane {
public void closeCodePanel(ContentPanel contentPanel) {
openTabs.remove(contentPanel.getNode());
remove(contentPanel);
contentPanel.dispose();
}
@Nullable
@@ -373,49 +374,56 @@ public class TabbedPane extends JTabbedPane {
jumps.reset();
curTab = null;
lastTab = null;
FocusManager.reset();
}
@Nullable
public Component getFocusedComp() {
return FocusManager.isActive() ? FocusManager.focusedComp : null;
return FocusManager.getFocusedComp();
}
private static class FocusManager implements FocusListener {
static boolean active = false;
static FocusManager listener = new FocusManager();
static Component focusedComp;
private static final FocusManager INSTANCE = new FocusManager();
private static @Nullable Component focusedComp;
static boolean isActive() {
return active;
return focusedComp != null;
}
static void reset() {
focusedComp = null;
}
static Component getFocusedComp() {
return focusedComp;
}
@Override
public void focusGained(FocusEvent e) {
active = true;
focusedComp = (Component) e.getSource();
}
@Override
public void focusLost(FocusEvent e) {
active = false;
focusedComp = null;
}
static void listen(ContentPanel pane) {
if (pane instanceof ClassCodeContentPanel) {
((ClassCodeContentPanel) pane).getCodeArea().addFocusListener(listener);
((ClassCodeContentPanel) pane).getSmaliCodeArea().addFocusListener(listener);
((ClassCodeContentPanel) pane).getCodeArea().addFocusListener(INSTANCE);
((ClassCodeContentPanel) pane).getSmaliCodeArea().addFocusListener(INSTANCE);
return;
}
if (pane instanceof AbstractCodeContentPanel) {
((AbstractCodeContentPanel) pane).getCodeArea().addFocusListener(listener);
((AbstractCodeContentPanel) pane).getCodeArea().addFocusListener(INSTANCE);
return;
}
if (pane instanceof HtmlPanel) {
((HtmlPanel) pane).getHtmlArea().addFocusListener(listener);
((HtmlPanel) pane).getHtmlArea().addFocusListener(INSTANCE);
return;
}
if (pane instanceof ImagePanel) {
pane.addFocusListener(listener);
pane.addFocusListener(INSTANCE);
return;
}
// throw new JadxRuntimeException("Add the new ContentPanel to TabbedPane.FocusManager: " + pane);
@@ -1,5 +1,6 @@
package jadx.gui.ui.codearea;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
@@ -8,15 +9,21 @@ import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.Objects;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.DefaultCaret;
@@ -64,12 +71,12 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
}
protected final ContentPanel contentPanel;
protected final JNode node;
protected ContentPanel contentPanel;
protected JNode node;
public AbstractCodeArea(ContentPanel contentPanel, JNode node) {
this.contentPanel = contentPanel;
this.node = node;
this.node = Objects.requireNonNull(node);
setMarkOccurrences(false);
setEditable(false);
@@ -348,4 +355,41 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea {
}
return null;
}
public boolean isDisposed() {
return node == null;
}
public void dispose() {
// code area reference can still be used somewhere in UI objects,
// reset node reference to allow to GC jadx objects tree
node = null;
contentPanel = null;
// also clear internals
setIgnoreRepaint(true);
setText("");
setEnabled(false);
setSyntaxEditingStyle(SYNTAX_STYLE_NONE);
setLinkGenerator(null);
for (MouseListener mouseListener : getMouseListeners()) {
removeMouseListener(mouseListener);
}
for (MouseMotionListener mouseMotionListener : getMouseMotionListeners()) {
removeMouseMotionListener(mouseMotionListener);
}
JPopupMenu popupMenu = getPopupMenu();
for (PopupMenuListener popupMenuListener : popupMenu.getPopupMenuListeners()) {
popupMenu.removePopupMenuListener(popupMenuListener);
}
for (Component component : popupMenu.getComponents()) {
if (component instanceof JMenuItem) {
Action action = ((JMenuItem) component).getAction();
if (action instanceof JNodeAction) {
((JNodeAction) action).dispose();
}
}
}
popupMenu.removeAll();
}
}
@@ -1,6 +1,7 @@
package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
@@ -164,10 +165,27 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem
} catch (Exception e) {
LOG.debug("Failed to restore view position: {}", viewState.getViewPoint(), e);
}
int caretPos = viewState.getCaretPos();
try {
activePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
AbstractCodeArea codeArea = activePanel.getCodeArea();
int codeLen = codeArea.getDocument().getLength();
if (caretPos >= 0 && caretPos < codeLen) {
codeArea.setCaretPosition(caretPos);
}
} catch (Exception e) {
LOG.debug("Failed to restore caret position: {}", viewState.getCaretPos(), e);
LOG.debug("Failed to restore caret position: {}", caretPos, e);
}
}
@Override
public void dispose() {
javaCodePanel.dispose();
smaliCodePanel.dispose();
for (Component component : areaTabbedPane.getComponents()) {
if (component instanceof CodePanel) {
((CodePanel) component).dispose();
}
}
super.dispose();
}
}
@@ -16,10 +16,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
import jadx.api.metadata.ICodeAnnotation;
import jadx.gui.JadxWrapper;
import jadx.gui.settings.JadxProject;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
@@ -58,7 +58,7 @@ public final class CodeArea extends AbstractCodeArea {
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() % 2 == 0 || e.isControlDown()) {
if (e.isControlDown() || jumpOnDoubleClick(e)) {
navToDecl(e.getPoint(), codeLinkGenerator);
}
}
@@ -69,6 +69,10 @@ public final class CodeArea extends AbstractCodeArea {
}
}
private boolean jumpOnDoubleClick(MouseEvent e) {
return e.getClickCount() == 2 && getMainWindow().getSettings().isJumpOnDoubleClick();
}
@SuppressWarnings("deprecation")
private void navToDecl(Point point, CodeLinkGenerator codeLinkGenerator) {
int offs = viewToModel(point);
@@ -81,6 +85,10 @@ public final class CodeArea extends AbstractCodeArea {
@Override
public ICodeInfo getCodeInfo() {
if (cachedCodeInfo == null) {
if (isDisposed()) {
LOG.debug("CodeArea used after dispose!");
return ICodeInfo.EMPTY;
}
cachedCodeInfo = Objects.requireNonNull(node.getCodeInfo());
}
return cachedCodeInfo;
@@ -222,7 +230,7 @@ public final class CodeArea extends AbstractCodeArea {
return null;
}
try {
return getDecompiler().getJavaNodeAtPosition(getCodeInfo(), offset);
return getJadxWrapper().getDecompiler().getJavaNodeAtPosition(getCodeInfo(), offset);
} catch (Exception e) {
LOG.error("Can't get java node by offset: {}", offset, e);
}
@@ -231,7 +239,7 @@ public final class CodeArea extends AbstractCodeArea {
public JavaNode getClosestJavaNode(int offset) {
try {
return getDecompiler().getClosestJavaNode(getCodeInfo(), offset);
return getJadxWrapper().getDecompiler().getClosestJavaNode(getCodeInfo(), offset);
} catch (Exception e) {
LOG.error("Can't get java node by offset: {}", offset, e);
return null;
@@ -244,7 +252,7 @@ public final class CodeArea extends AbstractCodeArea {
if (codeInfo.hasMetadata()) {
ICodeAnnotation ann = codeInfo.getCodeMetadata().getAt(pos);
if (ann != null && ann.getAnnType() == ICodeAnnotation.AnnType.CLASS) {
return (JavaClass) getDecompiler().getJavaNodeByCodeAnnotation(codeInfo, ann);
return (JavaClass) getJadxWrapper().getDecompiler().getJavaNodeByCodeAnnotation(codeInfo, ann);
}
}
} catch (Exception e) {
@@ -275,11 +283,17 @@ public final class CodeArea extends AbstractCodeArea {
return contentPanel.getTabbedPane().getMainWindow();
}
public JadxDecompiler getDecompiler() {
return getMainWindow().getWrapper().getDecompiler();
public JadxWrapper getJadxWrapper() {
return getMainWindow().getWrapper();
}
public JadxProject getProject() {
return getMainWindow().getProject();
}
@Override
public void dispose() {
super.dispose();
cachedCodeInfo = null;
}
}
@@ -62,4 +62,9 @@ public final class CodeContentPanel extends AbstractCodeContentPanel implements
codePanel.getCodeScrollPane().getViewport().setViewPosition(viewState.getViewPoint());
codePanel.getCodeArea().setCaretPosition(viewState.getCaretPos());
}
@Override
public void dispose() {
codePanel.dispose();
}
}
@@ -195,4 +195,8 @@ public class CodePanel extends JPanel {
return this.codeArea.getContentPanel().getTabbedPane()
.getMainWindow().getSettings();
}
public void dispose() {
codeArea.dispose();
}
}
@@ -11,8 +11,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.data.ICodeComment;
@@ -25,8 +23,8 @@ import jadx.api.metadata.ICodeMetadata;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.dialog.CommentDialog;
import jadx.gui.utils.DefaultPopupMenuListener;
import jadx.gui.utils.NLS;
@@ -39,28 +37,29 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class);
private final CodeArea codeArea;
private final JavaClass topCls;
private final boolean enabled;
private ICodeComment actionComment;
public CommentAction(CodeArea codeArea) {
super(NLS.str("popup.add_comment") + " (;)");
this.codeArea = codeArea;
JNode topNode = codeArea.getNode();
if (topNode instanceof JClass) {
this.topCls = ((JClass) topNode).getCls();
} else {
this.topCls = null;
this.enabled = codeArea.getNode() instanceof JClass;
if (enabled) {
UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment",
() -> showCommentDialog(getCommentRef(codeArea.getCaretPosition())));
}
UiUtils.addKeyBinding(codeArea, getKeyStroke(KeyEvent.VK_SEMICOLON, 0), "popup.add_comment",
() -> showCommentDialog(getCommentRef(codeArea.getCaretPosition())));
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
ICodeComment codeComment = getCommentRef(UiUtils.getOffsetAtMousePosition(codeArea));
setEnabled(codeComment != null);
this.actionComment = codeComment;
if (enabled) {
ICodeComment codeComment = getCommentRef(UiUtils.getOffsetAtMousePosition(codeArea));
setEnabled(codeComment != null);
this.actionComment = codeComment;
} else {
setEnabled(false);
}
}
@Override
@@ -83,11 +82,11 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
*/
@Nullable
private ICodeComment getCommentRef(int pos) {
if (pos == -1 || this.topCls == null) {
if (pos == -1) {
return null;
}
try {
JadxDecompiler decompiler = codeArea.getDecompiler();
JadxWrapper wrapper = codeArea.getJadxWrapper();
ICodeInfo codeInfo = codeArea.getCodeInfo();
ICodeMetadata metadata = codeInfo.getCodeMetadata();
int lineStartPos = codeArea.getLineStartFor(pos);
@@ -95,7 +94,7 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
// add method line comment by instruction offset
ICodeAnnotation offsetAnn = metadata.searchUp(pos, lineStartPos, AnnType.OFFSET);
if (offsetAnn instanceof InsnCodeOffset) {
JavaNode node = decompiler.getJavaNodeByRef(metadata.getNodeAt(pos));
JavaNode node = wrapper.getJavaNodeByRef(metadata.getNodeAt(pos));
if (node instanceof JavaMethod) {
int rawOffset = ((InsnCodeOffset) offsetAnn).getOffset();
JadxNodeRef nodeRef = JadxNodeRef.forMth((JavaMethod) node);
@@ -114,7 +113,7 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
return null;
});
if (nodeDef != null) {
JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(decompiler.getJavaNodeByRef(nodeDef));
JadxNodeRef nodeRef = JadxNodeRef.forJavaNode(wrapper.getJavaNodeByRef(nodeDef));
return new JadxCodeComment(nodeRef, "");
}
@@ -128,7 +127,7 @@ public class CommentAction extends AbstractAction implements DefaultPopupMenuLis
return null;
});
if (nodeRef != null) {
JavaNode defNode = decompiler.getJavaNodeByRef(nodeRef);
JavaNode defNode = wrapper.getJavaNodeByRef(nodeRef);
return new JadxCodeComment(JadxNodeRef.forJavaNode(defNode), "");
}
}
@@ -1,6 +1,7 @@
package jadx.gui.ui.codearea;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import javax.swing.KeyStroke;
@@ -16,7 +17,7 @@ import jadx.gui.utils.UiUtils;
public abstract class JNodeAction extends AbstractAction {
private static final long serialVersionUID = -2600154727884853550L;
private final transient CodeArea codeArea;
private transient CodeArea codeArea;
private transient @Nullable JNode node;
public JNodeAction(String name, CodeArea codeArea) {
@@ -26,7 +27,7 @@ public abstract class JNodeAction extends AbstractAction {
public abstract void runAction(JNode node);
public boolean isActionEnabled(JNode node) {
public boolean isActionEnabled(@Nullable JNode node) {
return node != null;
}
@@ -47,7 +48,7 @@ public abstract class JNodeAction extends AbstractAction {
runAction(node);
}
public void changeNode(JNode node) {
public void changeNode(@Nullable JNode node) {
this.node = node;
setEnabled(isActionEnabled(node));
}
@@ -55,4 +56,12 @@ public abstract class JNodeAction extends AbstractAction {
public CodeArea getCodeArea() {
return codeArea;
}
public void dispose() {
node = null;
codeArea = null;
for (PropertyChangeListener changeListener : getPropertyChangeListeners()) {
removePropertyChangeListener(changeListener);
}
}
}
@@ -4,11 +4,11 @@ import java.util.ArrayList;
import java.util.List;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import jadx.gui.treemodel.JNode;
import jadx.gui.utils.DefaultPopupMenuListener;
public final class JNodePopupListener implements DefaultPopupMenuListener {
public final class JNodePopupListener implements PopupMenuListener {
private final CodeArea codeArea;
private final List<JNodeAction> actions = new ArrayList<>();
@@ -16,13 +16,28 @@ public final class JNodePopupListener implements DefaultPopupMenuListener {
this.codeArea = codeArea;
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
JNode node = codeArea.getNodeUnderMouse();
actions.forEach(action -> action.changeNode(node));
}
public void addActions(JNodeAction action) {
actions.add(action);
}
private void updateNode(JNode node) {
for (JNodeAction action : actions) {
action.changeNode(node);
}
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
updateNode(codeArea.getNodeUnderMouse());
}
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// this event can be called just before running action, so can't reset node here
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
updateNode(null);
}
}
@@ -29,9 +29,12 @@ public final class JadxTokenMaker extends JavaTokenMaker {
@Override
public Token getTokenList(Segment text, int initialTokenType, int startOffset) {
if (codeArea.isDisposed()) {
return new TokenImpl();
}
try {
Token tokens = super.getTokenList(text, initialTokenType, startOffset);
if (tokens.getType() != TokenTypes.NULL) {
if (tokens != null && tokens.getType() != TokenTypes.NULL) {
processTokens(tokens);
}
return tokens;
@@ -65,7 +65,7 @@ public class CommentDialog extends JDialog {
Collections.sort(list);
codeData.setComments(list);
project.setCodeData(codeData);
codeArea.getMainWindow().getWrapper().getDecompiler().reloadCodeData();
codeArea.getMainWindow().getWrapper().reloadCodeData();
} catch (Exception e) {
LOG.error("Comment action failed", e);
}
@@ -121,7 +121,7 @@ public class ExcludePkgDialog extends JDialog {
}
private void initPackageList() {
List<String> pkgs = mainWindow.getWrapper().getDecompiler().getPackages()
List<String> pkgs = mainWindow.getWrapper().getPackages()
.stream()
.map(JavaPackage::getFullName)
.collect(Collectors.toList());
@@ -27,7 +27,12 @@ import jadx.gui.utils.NLS;
public class FileDialog {
public enum OpenMode {
OPEN, ADD, SAVE_PROJECT, EXPORT
OPEN,
ADD,
SAVE_PROJECT,
EXPORT,
CUSTOM_SAVE,
CUSTOM_OPEN
}
private final MainWindow mainWindow;
@@ -44,6 +49,26 @@ public class FileDialog {
initForMode(mode);
}
public void setTitle(String title) {
this.title = title;
}
public void setFileExtList(List<String> fileExtList) {
this.fileExtList = fileExtList;
}
public void setSelectionMode(int selectionMode) {
this.selectionMode = selectionMode;
}
public void setSelectedFile(Path path) {
this.selectedFile = path;
}
public void setCurrentDir(Path currentDir) {
this.currentDir = currentDir;
}
public List<Path> show() {
FileChooser fileChooser = buildFileChooser();
int ret = isOpen ? fileChooser.showOpenDialog(mainWindow) : fileChooser.showSaveDialog(mainWindow);
@@ -66,10 +91,6 @@ public class FileDialog {
return currentDir;
}
public void setSelectedFile(Path path) {
this.selectedFile = path;
}
private void initForMode(OpenMode mode) {
switch (mode) {
case OPEN:
@@ -101,6 +122,14 @@ public class FileDialog {
currentDir = mainWindow.getSettings().getLastSaveFilePath();
isOpen = false;
break;
case CUSTOM_SAVE:
isOpen = false;
break;
case CUSTOM_OPEN:
isOpen = true;
break;
}
}
@@ -110,7 +139,7 @@ public class FileDialog {
fileChooser.setFileSelectionMode(selectionMode);
fileChooser.setMultiSelectionEnabled(isOpen);
fileChooser.setAcceptAllFileFilterUsed(true);
if (!fileExtList.isEmpty()) {
if (Utils.notEmpty(fileExtList)) {
String description = NLS.str("file_dialog.supported_files") + ": (" + Utils.listToString(fileExtList) + ')';
fileChooser.setFileFilter(new FileNameExtensionFilter(description, fileExtList.toArray(new String[0])));
}
@@ -38,8 +38,6 @@ import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxCodeRef;
import jadx.api.data.impl.JadxCodeRename;
import jadx.api.data.impl.JadxNodeRef;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.rename.RenameVisitor;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.gui.jobs.TaskStatus;
@@ -168,12 +166,11 @@ public class RenameDialog extends JDialog {
Collections.sort(list);
codeData.setRenames(list);
project.setCodeData(codeData);
mainWindow.getWrapper().getDecompiler().reloadCodeData();
mainWindow.getWrapper().reloadCodeData();
}
private void refreshState() {
RootNode rootNode = mainWindow.getWrapper().getDecompiler().getRoot();
new RenameVisitor().init(rootNode);
mainWindow.getWrapper().reInitRenameVisitor();
JNodeCache nodeCache = cache.getNodeCache();
JavaNode javaNode = node.getJavaNode();
@@ -143,11 +143,18 @@ public class SearchDialog extends CommonSearchDialog {
@Override
public void dispose() {
stopSearchTask();
if (searchDisposable != null && !searchDisposable.isDisposed()) {
searchDisposable.dispose();
}
resultsModel.clear();
removeActiveTabListener();
if (searchTask != null) {
searchTask.cancel();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), () -> {
stopSearchTask();
unloadTempData();
});
}
super.dispose();
}
@@ -300,7 +307,7 @@ public class SearchDialog extends CommonSearchDialog {
Flowable<String> textChanges = onTextFieldChanges(searchField);
Flowable<String> searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable());
searchDisposable = searchEvents
.debounce(100, TimeUnit.MILLISECONDS)
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(SwingSchedulers.edt())
.subscribe(this::search);
}
@@ -335,6 +342,7 @@ public class SearchDialog extends CommonSearchDialog {
return;
}
updateTableHighlight();
startSearch();
searchTask.setResultsLimit(100);
searchTask.setProgressListener(this::updateProgress);
@@ -399,6 +407,7 @@ public class SearchDialog extends CommonSearchDialog {
if (searchTask != null) {
searchTask.cancel();
searchTask.waitTask();
searchTask = null;
}
}
@@ -468,16 +477,21 @@ public class SearchDialog extends CommonSearchDialog {
private synchronized void searchComplete() {
UiUtils.uiThreadGuard();
LOG.debug("Search complete");
updateTableHighlight();
updateTable();
boolean complete = searchTask == null || searchTask.isSearchComplete();
loadAllButton.setEnabled(!complete);
loadMoreButton.setEnabled(!complete);
updateProgressLabel(complete);
unloadTempData();
progressFinishedCommon();
}
private void unloadTempData() {
mainWindow.getWrapper().unloadClasses();
System.gc();
}
private static Flowable<String> onTextFieldChanges(final JTextField textField) {
return Flowable.<String>create(emitter -> {
DocumentListener listener = new DocumentListener() {
@@ -16,11 +16,11 @@ import javax.swing.SwingConstants;
import javax.swing.WindowConstants;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
import jadx.api.JavaMethod;
import jadx.api.JavaNode;
import jadx.api.utils.CodeUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.jobs.TaskStatus;
import jadx.gui.treemodel.CodeNode;
import jadx.gui.treemodel.JMethod;
@@ -97,14 +97,14 @@ public class UsageDialog extends CommonSearchDialog {
private void processUsage(JavaNode searchNode, JavaClass topUseClass) {
ICodeInfo codeInfo = topUseClass.getCodeInfo();
String code = codeInfo.getCodeStr();
JadxDecompiler decompiler = mainWindow.getWrapper().getDecompiler();
JadxWrapper wrapper = mainWindow.getWrapper();
List<Integer> usePositions = topUseClass.getUsePlacesFor(codeInfo, searchNode);
for (int pos : usePositions) {
String line = CodeUtils.getLineForPos(code, pos);
if (line.startsWith("import ")) {
continue;
}
JavaNode enclosingNode = decompiler.getEnclosingNode(codeInfo, pos);
JavaNode enclosingNode = wrapper.getEnclosingNode(codeInfo, pos);
JavaNode usageNode = enclosingNode == null ? topUseClass : enclosingNode;
usageList.add(new CodeNode(getNodeCache().makeFrom(usageNode), line.trim(), pos));
}
@@ -12,8 +12,8 @@ public abstract class ContentPanel extends JPanel {
private static final long serialVersionUID = 3237031760631677822L;
protected final TabbedPane tabbedPane;
protected final JNode node;
protected TabbedPane tabbedPane;
protected JNode node;
protected ContentPanel(TabbedPane panel, JNode jnode) {
tabbedPane = panel;
@@ -44,4 +44,9 @@ public abstract class ContentPanel extends JPanel {
}
return node.getName();
}
public void dispose() {
tabbedPane = null;
node = null;
}
}
@@ -13,7 +13,6 @@ import javax.swing.ImageIcon;
import org.apache.commons.text.StringEscapeUtils;
import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.nodes.ClassNode;
@@ -21,6 +20,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.ProcessState;
import jadx.core.utils.ErrorsCounter;
import jadx.core.utils.Utils;
import jadx.gui.JadxWrapper;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
@@ -35,9 +35,11 @@ public class SummaryNode extends JNode {
private static final ImageIcon ICON = UiUtils.openSvgIcon("nodes/detailView");
private final MainWindow mainWindow;
private final JadxWrapper wrapper;
public SummaryNode(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.wrapper = mainWindow.getWrapper();
}
@Override
@@ -60,17 +62,16 @@ public class SummaryNode extends JNode {
private void writeInputSummary(StringEscapeUtils.Builder builder) throws IOException {
builder.append("<h2>Input</h2>");
JadxDecompiler jadx = mainWindow.getWrapper().getDecompiler();
builder.append("<h3>Files</h3>");
builder.append("<ul>");
for (File inputFile : jadx.getArgs().getInputFiles()) {
for (File inputFile : wrapper.getArgs().getInputFiles()) {
builder.append("<li>");
builder.escape(inputFile.getCanonicalFile().getAbsolutePath());
builder.append("</li>");
}
builder.append("</ul>");
List<ClassNode> classes = jadx.getRoot().getClasses(true);
List<ClassNode> classes = wrapper.getRootNode().getClasses(true);
List<String> codeSources = classes.stream()
.map(ClassNode::getInputFileName)
.distinct()
@@ -108,18 +109,21 @@ public class SummaryNode extends JNode {
private void writeDecompilationSummary(StringEscapeUtils.Builder builder) {
builder.append("<h2>Decompilation</h2>");
JadxDecompiler jadx = mainWindow.getWrapper().getDecompiler();
List<ClassNode> classes = jadx.getRoot().getClasses(false);
List<ClassNode> classes = wrapper.getRootNode().getClassesWithoutInner();
int classesCount = classes.size();
long notLoadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.NOT_LOADED).count();
long loadedClasses = classes.stream().filter(c -> c.getState() == ProcessState.LOADED).count();
long processedClasses = classes.stream().filter(c -> c.getState() == ProcessState.PROCESS_COMPLETE).count();
long generatedClasses = classes.stream().filter(c -> c.getState() == ProcessState.GENERATED_AND_UNLOADED).count();
builder.append("<ul>");
builder.append("<li>Top level classes: " + classesCount + "</li>");
builder.append("<li>At process stage: " + valueAndPercent(processedClasses, classesCount) + "</li>");
builder.append("<li>Not loaded: " + valueAndPercent(notLoadedClasses, classesCount) + "</li>");
builder.append("<li>Loaded: " + valueAndPercent(loadedClasses, classesCount) + "</li>");
builder.append("<li>Processed: " + valueAndPercent(processedClasses, classesCount) + "</li>");
builder.append("<li>Code generated: " + valueAndPercent(generatedClasses, classesCount) + "</li>");
builder.append("</ul>");
ErrorsCounter counter = jadx.getRoot().getErrorsCounter();
ErrorsCounter counter = wrapper.getRootNode().getErrorsCounter();
Set<IAttributeNode> problemNodes = new HashSet<>();
problemNodes.addAll(counter.getErrorNodes());
problemNodes.addAll(counter.getWarnNodes());
@@ -15,6 +15,7 @@ import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import jadx.api.JadxDecompiler;
import jadx.core.Jadx;
import jadx.gui.update.data.Release;
@SuppressWarnings("SameParameterValue")
@@ -56,8 +57,7 @@ public class JadxUpdate {
}
private static Release checkForNewRelease() throws IOException {
String version = JadxDecompiler.getVersion();
if (version.contains("dev")) {
if (Jadx.isDevVersion()) {
LOG.debug("Ignore check for update: development version");
return null;
}
@@ -65,11 +65,12 @@ public class JadxUpdate {
if (latest == null) {
return null;
}
String currentVersion = JadxDecompiler.getVersion();
String latestName = latest.getName();
if (latestName.equalsIgnoreCase(version)) {
if (latestName.equalsIgnoreCase(currentVersion)) {
return null;
}
if (VersionComparator.checkAndCompare(version, latestName) >= 0) {
if (VersionComparator.checkAndCompare(currentVersion, latestName) >= 0) {
return null;
}
LOG.info("Found new jadx version: {}", latest);
@@ -329,6 +329,23 @@ public class UiUtils {
}
}
/**
* Owner field in Clipboard class can store reference to CodeArea.
* This prevents from garbage collection whole jadx object tree and cause memory leak.
* Trying to lost ownership by new empty selection.
*/
public static void resetClipboardOwner() {
try {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemSelection();
if (clipboard != null) {
StringSelection selection = new StringSelection("");
clipboard.setContents(selection, selection);
}
} catch (Exception e) {
LOG.error("Failed to reset clipboard owner", e);
}
}
public static int calcProgress(ITaskProgress taskProgress) {
return calcProgress(taskProgress.progress(), taskProgress.total());
}
@@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.core.Jadx;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -190,6 +191,7 @@ public class DiskCodeCache implements ICodeCache {
private String buildCodeVersion(JadxArgs args) {
return DATA_FORMAT_VERSION
+ ":" + Jadx.getVersion()
+ ":" + args.makeCodeArgsHash()
+ ":" + buildInputsHash(args.getInputFiles());
}
@@ -0,0 +1,125 @@
package jadx.gui.utils.fileswatcher;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.Utils;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
public class FilesWatcher {
private static final Logger LOG = LoggerFactory.getLogger(FilesWatcher.class);
private final WatchService watcher = FileSystems.getDefault().newWatchService();
private final Map<WatchKey, Path> keys = new HashMap<>();
private final Map<Path, Set<Path>> files = new HashMap<>();
private final AtomicBoolean cancel = new AtomicBoolean(false);
private final BiConsumer<Path, WatchEvent.Kind<Path>> listener;
public FilesWatcher(List<Path> paths, BiConsumer<Path, WatchEvent.Kind<Path>> listener) throws IOException {
this.listener = listener;
for (Path path : paths) {
if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
registerDirs(path);
} else {
Path parentDir = path.toAbsolutePath().getParent();
register(parentDir);
files.merge(parentDir, Collections.singleton(path), Utils::mergeSets);
}
}
}
public void cancel() {
cancel.set(true);
}
@SuppressWarnings("unchecked")
public void watch() {
cancel.set(false);
LOG.debug("File watcher started for {} dirs", keys.size());
while (!cancel.get()) {
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException e) {
LOG.debug("File watcher interrupted");
return;
}
Path dir = keys.get(key);
if (dir == null) {
LOG.warn("Unknown directory key: {}", key);
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
if (cancel.get() || Thread.interrupted()) {
return;
}
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
Path fileName = ((WatchEvent<Path>) event).context();
Path path = dir.resolve(fileName);
Set<Path> files = this.files.get(dir);
if (files == null || files.contains(path)) {
listener.accept(path, (WatchEvent.Kind<Path>) kind);
}
if (kind == ENTRY_CREATE) {
try {
if (Files.isDirectory(path, NOFOLLOW_LINKS)) {
registerDirs(path);
}
} catch (Exception e) {
LOG.warn("Failed to update directory watch: {}", path, e);
}
}
}
boolean valid = key.reset();
if (!valid) {
keys.remove(key);
if (keys.isEmpty()) {
LOG.debug("File watcher stopped: all watch keys removed");
return;
}
}
}
}
private void registerDirs(Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
keys.put(key, dir);
}
}
@@ -0,0 +1,92 @@
package jadx.gui.utils.fileswatcher;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.reactivex.processors.PublishProcessor;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.UiUtils;
public class LiveReloadWorker {
private static final Logger LOG = LoggerFactory.getLogger(LiveReloadWorker.class);
private final MainWindow mainWindow;
private final PublishProcessor<Path> processor;
private volatile boolean started = false;
private ExecutorService executor;
private FilesWatcher watcher;
@SuppressWarnings("ResultOfMethodCallIgnored")
public LiveReloadWorker(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.processor = PublishProcessor.create();
this.processor
.debounce(1, TimeUnit.SECONDS)
.subscribe(path -> {
LOG.debug("Reload triggered");
UiUtils.uiRun(mainWindow::reopen);
});
}
public boolean isStarted() {
return started;
}
public synchronized void updateState(boolean enabled) {
if (this.started == enabled) {
return;
}
if (enabled) {
LOG.debug("Starting live reload worker");
start();
} else {
LOG.debug("Stopping live reload worker");
stop();
}
}
private void onUpdate(Path path, WatchEvent.Kind<Path> pathKind) {
LOG.debug("Path updated: {}", path);
processor.onNext(path);
}
private synchronized void start() {
try {
watcher = new FilesWatcher(mainWindow.getProject().getFilePaths(), this::onUpdate);
executor = Executors.newSingleThreadExecutor();
started = true;
executor.submit(watcher::watch);
} catch (Exception e) {
LOG.warn("Failed to start live reload worker", e);
resetState();
}
}
private synchronized void stop() {
try {
watcher.cancel();
executor.shutdownNow();
boolean canceled = executor.awaitTermination(5, TimeUnit.SECONDS);
if (!canceled) {
LOG.warn("Failed to cancel live reload worker");
}
} catch (Exception e) {
LOG.warn("Failed to stop live reload worker", e);
} finally {
resetState();
}
}
private void resetState() {
started = false;
executor = null;
watcher = null;
}
}
@@ -4,6 +4,8 @@ import java.awt.event.ActionEvent;
import java.util.function.Consumer;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.KeyStroke;
public class ActionHandler extends AbstractAction {
@@ -13,6 +15,27 @@ public class ActionHandler extends AbstractAction {
this.consumer = consumer;
}
public void setName(String name) {
putValue(NAME, name);
}
public void setNameAndDesc(String name) {
setName(name);
setShortDescription(name);
}
public void setShortDescription(String desc) {
putValue(SHORT_DESCRIPTION, desc);
}
public void setIcon(ImageIcon icon) {
putValue(SMALL_ICON, icon);
}
public void setKeyBinding(KeyStroke keyStroke) {
putValue(ACCELERATOR_KEY, keyStroke);
}
@Override
public void actionPerformed(ActionEvent e) {
consumer.accept(e);
@@ -26,6 +26,10 @@ file.open_title=Datei öffnen
file.new_project=Neues Projekt
file.save_project=Projekt speichern
file.save_project_as=Projekt speichern als…
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=Alles speichern
file.export_gradle=Als Gradle-Projekt speichern
file.save_all_msg=Verzeichnis für das Speichern dekompilierter Ressourcen auswählen
@@ -36,6 +40,7 @@ tree.resources_title=Ressourcen
tree.loading=Laden…
progress.load=Laden
#progress.export_mappings=
progress.decompile=Dekompilieren
#progress.canceling=Canceling
@@ -128,6 +133,7 @@ preferences.project=Projekt
preferences.other=Andere
preferences.language=Sprache
preferences.lineNumbersMode=Editor Zeilennummern-Modus
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=Nach Updates beim Start suchen
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
@@ -26,6 +26,10 @@ file.open_title=Open file
file.new_project=New project
file.save_project=Save project
file.save_project_as=Save project as...
file.reload=Reload files
file.live_reload=Live reload
file.live_reload_desc=Auto reload files on changes
file.export_mappings_as=Export mappings as...
file.save_all=Save all
file.export_gradle=Save as gradle project
file.save_all_msg=Select directory for save decompiled sources
@@ -36,6 +40,7 @@ tree.resources_title=Resources
tree.loading=Loading...
progress.load=Loading
progress.export_mappings=Exporting mappings
progress.decompile=Decompiling
progress.canceling=Canceling
@@ -128,6 +133,7 @@ preferences.project=Project
preferences.other=Other
preferences.language=Language
preferences.lineNumbersMode=Editor line numbers mode
preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=Check for updates on startup
preferences.useDx=Use dx/d8 to convert java bytecode
preferences.decompilationMode=Decompilation mode
@@ -26,6 +26,10 @@ file.open_title=Abrir archivo
#file.new_project=
#file.save_project=
#file.save_project_as=
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=Guardar todo
file.export_gradle=Guardar como proyecto Gradle
file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas
@@ -36,6 +40,7 @@ tree.resources_title=Recursos
tree.loading=Cargando...
progress.load=Cargando
#progress.export_mappings=
progress.decompile=Decompiling
#progress.canceling=Canceling
@@ -128,6 +133,7 @@ preferences.decompile=Descompilación
preferences.other=Otros
preferences.language=Idioma
#preferences.lineNumbersMode=Editor line numbers mode
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=Buscar actualizaciones al iniciar
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
@@ -26,6 +26,10 @@ file.open_title=파일 열기
file.new_project=새 프로젝트
file.save_project=프로젝트 저장
file.save_project_as=다른 이름으로 프로젝트 저장...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=모두 저장
file.export_gradle=Gradle 프로젝트로 저장
file.save_all_msg=디컴파일된 소스를 저장할 디렉토리 선택
@@ -36,6 +40,7 @@ tree.resources_title=리소스
tree.loading=로딩중...
progress.load=로딩중
#progress.export_mappings=
progress.decompile=디컴파일 중
#progress.canceling=Canceling
@@ -128,6 +133,7 @@ preferences.project=프로젝트
preferences.other=기타
preferences.language=언어
preferences.lineNumbersMode=편집기 줄 번호 모드
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=시작시 업데이트 확인
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
@@ -26,6 +26,10 @@ file.open_title=打开文件
file.new_project=新建项目
file.save_project=保存项目
file.save_project_as=另存项目为...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=全部保存
file.export_gradle=另存为 Gradle 项目
file.save_all_msg=请选择保存反编译资源的目录
@@ -36,6 +40,7 @@ tree.resources_title=资源文件
tree.loading=加载中...
progress.load=正在加载
#progress.export_mappings=
progress.decompile=反编译中
progress.canceling=正在取消
@@ -128,6 +133,7 @@ preferences.project=项目
preferences.other=其他
preferences.language=语言
preferences.lineNumbersMode=编辑器行号模式
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=启动时检查更新
preferences.useDx=使用 dx/d8 来转换java字节码
preferences.decompilationMode=反编译模式
@@ -26,6 +26,10 @@ file.open_title=開啟檔案
file.new_project=新建專案
file.save_project=儲存專案
file.save_project_as=另存專案...
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.save_all=全部儲存
file.export_gradle=另存為 gradle 專案
file.save_all_msg=選擇儲存反編譯原始碼的路徑
@@ -36,6 +40,7 @@ tree.resources_title=資源
tree.loading=載入中...
progress.load=載入中
#progress.export_mappings=
progress.decompile=正在反編譯
progress.canceling=正在取消
@@ -128,6 +133,7 @@ preferences.project=專案
preferences.other=其他
preferences.language=語言
preferences.lineNumbersMode=編輯器行號模式
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.check_for_updates=啟動時檢查更新
preferences.useDx=使用 dx/d8 來轉換 Java 位元組碼
preferences.decompilationMode=反編譯模式
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#6E6E6E" fill-rule="evenodd" d="M12.5747152,11.8852806 C11.4741474,13.1817355 9.83247882,14.0044386 7.99865879,14.0044386 C5.03907292,14.0044386 2.57997332,11.8615894 2.08820756,9.0427473 L3.94774327,9.10768372 C4.43372186,10.8898575 6.06393114,12.2000519 8.00015362,12.2000519 C9.30149237,12.2000519 10.4645985,11.6082097 11.2349873,10.6790094 L9.05000019,8.71167959 L14.0431479,8.44999981 L14.3048222,13.4430431 L12.5747152,11.8852806 Z M3.42785637,4.11741586 C4.52839138,2.82452748 6.16775464,2.00443857 7.99865879,2.00443857 C10.918604,2.00443857 13.3513802,4.09026967 13.8882946,6.8532307 L12.0226389,6.78808057 C11.5024872,5.05935553 9.89838095,3.8000774 8.00015362,3.8000774 C6.69867367,3.8000774 5.53545628,4.39204806 4.76506921,5.32142241 L6.95482203,7.29304326 L1.96167436,7.55472304 L1.70000005,2.56167973 L3.42785637,4.11741586 Z" transform="rotate(3 8.002 8.004)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -23,7 +23,7 @@ public class DexInputOptions extends BaseOptionsParser {
return Collections.singletonList(
new JadxOptionDescription(
VERIFY_CHECKSUM_OPT,
"Verify dex file checksum before load",
"verify dex file checksum before load",
"yes",
Arrays.asList("yes", "no")));
}
@@ -30,12 +30,12 @@ public class JavaConvertOptions extends BaseOptionsParser {
return Arrays.asList(
new JadxOptionDescription(
MODE_OPT,
"Convert mode",
"convert mode",
"both",
Arrays.asList("dx", "d8", "both")),
new JadxOptionDescription(
D8_DESUGAR_OPT,
"Use desugar in d8",
"use desugar in d8",
"no",
Arrays.asList("yes", "no")));
}
@@ -25,7 +25,7 @@ public class JavaConvertPlugin implements JadxInputPlugin, JadxPluginOptions {
return new JadxPluginInfo(
PLUGIN_ID,
"JavaConvert",
"Convert .jar and .class files to dex",
"Convert .class, .jar and .aar files to dex",
"java-input");
}
@@ -59,9 +59,9 @@ public class JavaInsnsRegister {
loadConst(arr, 0x14, "ldc2_w", true);
register(arr, 0x15, "iload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0));
register(arr, 0x16, "lload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).pushWide(0));
register(arr, 0x16, "lload", 1, 2, Opcode.MOVE_WIDE, s -> s.local(1, s.u1()).pushWide(0));
register(arr, 0x17, "fload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0));
register(arr, 0x18, "dload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).pushWide(0));
register(arr, 0x18, "dload", 1, 2, Opcode.MOVE_WIDE, s -> s.local(1, s.u1()).pushWide(0));
register(arr, 0x19, "aload", 1, 2, Opcode.MOVE, s -> s.local(1, s.u1()).push(0));
register(arr, 0x1a, "iload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0));
@@ -69,20 +69,20 @@ public class JavaInsnsRegister {
register(arr, 0x1c, "iload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).push(0));
register(arr, 0x1d, "iload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).push(0));
register(arr, 0x1e, "lload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).pushWide(0));
register(arr, 0x1f, "lload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).pushWide(0));
register(arr, 0x20, "lload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).pushWide(0));
register(arr, 0x21, "lload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).pushWide(0));
register(arr, 0x1e, "lload_0", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 0).pushWide(0));
register(arr, 0x1f, "lload_1", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 1).pushWide(0));
register(arr, 0x20, "lload_2", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 2).pushWide(0));
register(arr, 0x21, "lload_3", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 3).pushWide(0));
register(arr, 0x22, "fload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0));
register(arr, 0x23, "fload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).push(0));
register(arr, 0x24, "fload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).push(0));
register(arr, 0x25, "fload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).push(0));
register(arr, 0x26, "dload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).pushWide(0));
register(arr, 0x27, "dload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).pushWide(0));
register(arr, 0x28, "dload_2", 0, 2, Opcode.MOVE, s -> s.local(1, 2).pushWide(0));
register(arr, 0x29, "dload_3", 0, 2, Opcode.MOVE, s -> s.local(1, 3).pushWide(0));
register(arr, 0x26, "dload_0", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 0).pushWide(0));
register(arr, 0x27, "dload_1", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 1).pushWide(0));
register(arr, 0x28, "dload_2", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 2).pushWide(0));
register(arr, 0x29, "dload_3", 0, 2, Opcode.MOVE_WIDE, s -> s.local(1, 3).pushWide(0));
register(arr, 0x2a, "aload_0", 0, 2, Opcode.MOVE, s -> s.local(1, 0).push(0));
register(arr, 0x2b, "aload_1", 0, 2, Opcode.MOVE, s -> s.local(1, 1).push(0));
@@ -99,9 +99,9 @@ public class JavaInsnsRegister {
register(arr, 0x35, "saload", 0, 3, Opcode.AGET_SHORT, aget());
register(arr, 0x36, "istore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x37, "lstore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x37, "lstore", 1, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x38, "fstore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x39, "dstore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x39, "dstore", 1, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x3a, "astore", 1, 2, Opcode.MOVE, s -> s.pop(1).local(0, s.u1()));
register(arr, 0x3b, "istore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0));
@@ -109,20 +109,20 @@ public class JavaInsnsRegister {
register(arr, 0x3d, "istore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2));
register(arr, 0x3e, "istore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3));
register(arr, 0x3f, "lstore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0));
register(arr, 0x40, "lstore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1));
register(arr, 0x41, "lstore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2));
register(arr, 0x42, "lstore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3));
register(arr, 0x3f, "lstore_0", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 0));
register(arr, 0x40, "lstore_1", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 1));
register(arr, 0x41, "lstore_2", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 2));
register(arr, 0x42, "lstore_3", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 3));
register(arr, 0x43, "fstore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0));
register(arr, 0x44, "fstore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1));
register(arr, 0x45, "fstore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2));
register(arr, 0x46, "fstore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3));
register(arr, 0x47, "dstore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0));
register(arr, 0x48, "dstore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1));
register(arr, 0x49, "dstore_2", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 2));
register(arr, 0x4a, "dstore_3", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 3));
register(arr, 0x47, "dstore_0", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 0));
register(arr, 0x48, "dstore_1", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 1));
register(arr, 0x49, "dstore_2", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 2));
register(arr, 0x4a, "dstore_3", 0, 2, Opcode.MOVE_WIDE, s -> s.pop(1).local(0, 3));
register(arr, 0x4b, "astore_0", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 0));
register(arr, 0x4c, "astore_1", 0, 2, Opcode.MOVE, s -> s.pop(1).local(0, 1));