diff --git a/README.md b/README.md index cadd85d3d..958051c1b 100644 --- a/README.md +++ b/README.md @@ -54,31 +54,38 @@ Run **jadx** on itself: ### Usage ``` -jadx[-gui] [options] (.dex, .apk, .jar or .class) +jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc) options: - -d, --output-dir - output directory - -ds, --output-dir-src - output directory for sources - -dr, --output-dir-res - output directory for resources - -j, --threads-count - processing threads count - -r, --no-res - do not decode resources - -s, --no-src - do not decompile source code - -e, --export-gradle - save as android gradle project - --show-bad-code - show inconsistent code (incorrectly decompiled) - --no-imports - disable use of imports, always write entire package name - --no-replace-consts - don't replace constant value with matching constant field - --escape-unicode - escape non latin characters in strings (with \u) - --deobf - activate deobfuscation - --deobf-min - min length of name - --deobf-max - max length of name - --deobf-rewrite-cfg - force to save deobfuscation map - --deobf-use-sourcename - use source file name as class name alias - --cfg - save methods control flow graph to dot file - --raw-cfg - save methods control flow graph (use raw instructions) - -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc) - -v, --verbose - verbose output - -h, --help - print this help + -d, --output-dir - output directory + -ds, --output-dir-src - output directory for sources + -dr, --output-dir-res - output directory for resources + -j, --threads-count - processing threads count + -r, --no-res - do not decode resources + -s, --no-src - do not decompile source code + -e, --export-gradle - save as android gradle project + --show-bad-code - show inconsistent code (incorrectly decompiled) + --no-imports - disable use of imports, always write entire package name + --no-debug-info - disable debug info + --no-inline-anonymous - disable anonymous classes inline + --no-replace-consts - don't replace constant value with matching constant field + --escape-unicode - escape non latin characters in strings (with \u) + --respect-bytecode-access-modifiers - don't change original access modifiers + --deobf - activate deobfuscation + --deobf-min - min length of name, renamed if shorter (default: 3) + --deobf-max - max length of name, renamed if longer (default: 64) + --deobf-rewrite-cfg - force to save deobfuscation map + --deobf-use-sourcename - use source file name as class name alias + --rename-flags - what to rename, comma-separated, 'case' for system case sensitivity, 'valid' for java identifiers, 'printable' characters, 'none' or 'all' + --cfg - save methods control flow graph to dot file + --raw-cfg - save methods control flow graph (use raw instructions) + -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc) + -v, --verbose - verbose output + --version - print jadx version + -h, --help - print this help Example: jadx -d out classes.dex + jadx --rename-flags "none" classes.dex + jadx --rename-flags "valid,printable" classes.dex ``` These options also worked on jadx-gui running from command line and override options from preferences dialog diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 71b39ec8d..1131c5910 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -57,6 +57,9 @@ public class JadxCLIArgs { @Parameter(names = {"--no-debug-info"}, description = "disable debug info") protected boolean debugInfo = true; + @Parameter(names = { "--no-inline-anonymous" }, description = "disable anonymous classes inline") + protected boolean inlineAnonymousClasses = true; + @Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field") protected boolean replaceConsts = true; @@ -175,6 +178,7 @@ public class JadxCLIArgs { args.setExportAsGradleProject(exportAsGradleProject); args.setUseImports(useImports); args.setDebugInfo(debugInfo); + args.setInlineAnonymousClasses(inlineAnonymousClasses); args.setRenameCaseSensitive(isRenameCaseSensitive()); args.setRenameValid(isRenameValid()); args.setRenamePrintable(isRenamePrintable()); @@ -225,6 +229,10 @@ public class JadxCLIArgs { return debugInfo; } + public boolean isInlineAnonymousClasses() { + return inlineAnonymousClasses; + } + public boolean isDeobfuscationOn() { return deobfuscationOn; } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index df4de620d..8a832c8cb 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -31,6 +31,7 @@ public class JadxArgs { private boolean useImports = true; private boolean debugInfo = true; + private boolean inlineAnonymousClasses = true; private boolean skipResources = false; private boolean skipSources = false; @@ -155,6 +156,14 @@ public class JadxArgs { this.debugInfo = debugInfo; } + public boolean isInlineAnonymousClasses() { + return inlineAnonymousClasses; + } + + public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) { + this.inlineAnonymousClasses = inlineAnonymousClasses; + } + public boolean isSkipResources() { return skipResources; } diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index d961f1be9..91ea09058 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -26,6 +26,7 @@ import jadx.core.dex.visitors.MarkFinallyVisitor; import jadx.core.dex.visitors.MethodInlineVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.dex.visitors.PrepareForCodeGen; +import jadx.core.dex.visitors.ProcessAnonymous; import jadx.core.dex.visitors.ReSugarCode; import jadx.core.dex.visitors.RenameVisitor; import jadx.core.dex.visitors.SimplifyVisitor; @@ -104,6 +105,7 @@ public class Jadx { passes.add(new ExtractFieldInit()); passes.add(new FixAccessModifiers()); + passes.add(new ProcessAnonymous()); passes.add(new ClassModifier()); passes.add(new MethodInlineVisitor()); passes.add(new EnumVisitor()); diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java index 288f81c64..46043879d 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -228,8 +228,7 @@ public class ClassGen { private void addInnerClasses(CodeWriter code, ClassNode cls) throws CodegenException { for (ClassNode innerCls : cls.getInnerClasses()) { - if (innerCls.contains(AFlag.DONT_GENERATE) - || innerCls.contains(AFlag.ANONYMOUS_CLASS)) { + if (innerCls.contains(AFlag.DONT_GENERATE)) { continue; } ClassGen inClGen = new ClassGen(innerCls, getParentGen()); diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java index 5ad108b12..a1a2df307 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -556,7 +556,7 @@ public class InsnGen { private void makeConstructor(ConstructorInsn insn, CodeWriter code) throws CodegenException { ClassNode cls = mth.dex().resolveClass(insn.getClassType()); - if (cls != null && cls.contains(AFlag.ANONYMOUS_CLASS) && !fallback) { + if (cls != null && cls.isAnonymous() && !fallback) { inlineAnonymousConstructor(code, cls, insn); return; } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index 5c90d3c16..f440dbd8a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -122,7 +122,6 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { accFlagsValue = cls.getAccessFlags(); } this.accessFlags = new AccessInfo(accFlagsValue, AFType.CLASS); - markAnonymousClass(); buildCache(); } catch (Exception e) { throw new JadxRuntimeException("Error decode class: " + clsInfo, e); @@ -401,41 +400,8 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { && getSuperClass().getObject().equals(ArgType.ENUM.getObject()); } - public boolean markAnonymousClass() { - if (isAnonymous() || isLambdaCls()) { - add(AFlag.ANONYMOUS_CLASS); - add(AFlag.DONT_GENERATE); - - for (MethodNode mth : getMethods()) { - if (mth.isConstructor()) { - mth.add(AFlag.ANONYMOUS_CONSTRUCTOR); - } - } - return true; - } - return false; - } - public boolean isAnonymous() { - return clsInfo.isInner() - && Character.isDigit(clsInfo.getShortName().charAt(0)) - && methods.stream().filter(MethodNode::isConstructor).count() == 1; - } - - public boolean isLambdaCls() { - return accessFlags.isSynthetic() && accessFlags.isFinal() - && clsInfo.getType().getObject().contains(".-$$Lambda$") - && countStaticFields() == 0; - } - - private int countStaticFields() { - int c = 0; - for (FieldNode field : fields) { - if (field.getAccessFlags().isStatic()) { - c++; - } - } - return c; + return contains(AFlag.ANONYMOUS_CLASS); } @Nullable diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java index 74ec8533f..6bbde5741 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java @@ -37,7 +37,8 @@ import jadx.core.utils.exceptions.JadxException; desc = "Remove synthetic classes, methods and fields", runAfter = { ModVisitor.class, - FixAccessModifiers.class + FixAccessModifiers.class, + ProcessAnonymous.class } ) public class ClassModifier extends AbstractVisitor { @@ -51,7 +52,6 @@ public class ClassModifier extends AbstractVisitor { cls.add(AFlag.DONT_GENERATE); return false; } - cls.markAnonymousClass(); removeSyntheticFields(cls); cls.getMethods().forEach(ClassModifier::removeSyntheticMethods); cls.getMethods().forEach(ClassModifier::removeEmptyMethods); @@ -73,7 +73,7 @@ public class ClassModifier extends AbstractVisitor { if (cls.getAccessFlags().isStatic()) { return; } - boolean inline = cls.contains(AFlag.ANONYMOUS_CLASS); + boolean inline = cls.isAnonymous(); if (inline || cls.getClassInfo().isInner()) { for (FieldNode field : cls.getFields()) { if (field.getAccessFlags().isSynthetic() && field.getType().isObject()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java index f8bef6edd..ba1cfbd15 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java @@ -229,11 +229,8 @@ public class ModVisitor extends AbstractVisitor { } ClassNode classNode = callMthNode.getParentClass(); - if (!classNode.contains(AFlag.ANONYMOUS_CLASS)) { - // check if class can be anonymous but not yet marked due to dependency issues - if (!classNode.markAnonymousClass()) { - return; - } + if (!classNode.isAnonymous()) { + return; } if (!mth.getParentClass().getInnerClasses().contains(classNode)) { return; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java new file mode 100644 index 000000000..faa66b823 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java @@ -0,0 +1,66 @@ +package jadx.core.dex.visitors; + +import jadx.core.dex.attributes.AFlag; +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.dex.visitors.regions.RegionMakerVisitor; + +@JadxVisitor( + name = "ProcessAnonymous", + desc = "Mark anonymous and lambda classes (for future inline)", + runAfter = RegionMakerVisitor.class +) +public class ProcessAnonymous extends AbstractVisitor { + + @Override + public void init(RootNode root) { + if (!root.getArgs().isInlineAnonymousClasses()) { + return; + } + + for (ClassNode cls : root.getClasses(true)) { + markAnonymousClass(cls); + } + } + + private static boolean markAnonymousClass(ClassNode cls) { + if (isAnonymous(cls) || isLambdaCls(cls)) { + cls.add(AFlag.ANONYMOUS_CLASS); + cls.add(AFlag.DONT_GENERATE); + + for (MethodNode mth : cls.getMethods()) { + if (mth.isConstructor()) { + mth.add(AFlag.ANONYMOUS_CONSTRUCTOR); + } + } + return true; + } + return false; + } + + private static boolean isAnonymous(ClassNode cls) { + return cls.getClassInfo().isInner() + && Character.isDigit(cls.getClassInfo().getShortName().charAt(0)) + && cls.getMethods().stream().filter(MethodNode::isConstructor).count() == 1; + } + + private static boolean isLambdaCls(ClassNode cls) { + return cls.getAccessFlags().isSynthetic() + && cls.getAccessFlags().isFinal() + && cls.getClassInfo().getRawName().contains(".-$$Lambda$") + && countStaticFields(cls) == 0; + } + + private static int countStaticFields(ClassNode cls) { + int c = 0; + for (FieldNode field : cls.getFields()) { + if (field.getAccessFlags().isStatic()) { + c++; + } + } + return c; + } + +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java index 7ace92742..ef2f05d00 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; @@ -37,5 +38,17 @@ public class TestAnonymousClass extends IntegrationTest { assertThat(code, not(containsString("this"))); assertThat(code, not(containsString("null"))); assertThat(code, not(containsString("AnonymousClass_"))); + assertThat(code, not(containsString("class AnonymousClass"))); + } + + @Test + public void testNoInline() { + getArgs().setInlineAnonymousClasses(false); + + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("class AnonymousClass1 implements FilenameFilter {")); + assertThat(code, containsOne("new AnonymousClass1()")); } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 90d75401f..ca42113d7 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -1,9 +1,6 @@ package jadx.gui.settings; -import java.awt.Font; -import java.awt.GraphicsDevice; -import java.awt.GraphicsEnvironment; -import java.awt.Window; +import java.awt.*; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -15,7 +12,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import javax.swing.JFrame; +import javax.swing.*; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.jetbrains.annotations.Nullable; @@ -281,6 +278,10 @@ public class JadxSettings extends JadxCLIArgs { this.useImports = useImports; } + public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) { + this.inlineAnonymousClasses = inlineAnonymousClasses; + } + public boolean isAutoStartJobs() { return autoStartJobs; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 1dc4c3343..d335edc79 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -347,6 +347,13 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); + JCheckBox inlineAnonymous = new JCheckBox(); + inlineAnonymous.setSelected(settings.isInlineAnonymousClasses()); + inlineAnonymous.addItemListener(e -> { + settings.setInlineAnonymousClasses(e.getStateChange() == ItemEvent.SELECTED); + needReload(); + }); + SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile")); other.addRow(NLS.str("preferences.threads"), threadsCount); other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"), @@ -357,6 +364,7 @@ public class JadxSettingsWindow extends JDialog { other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts); other.addRow(NLS.str("preferences.respectBytecodeAccessModifiers"), respectBytecodeAccessModifiers); other.addRow(NLS.str("preferences.useImports"), useImports); + other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous); other.addRow(NLS.str("preferences.fallback"), fallback); other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode); return other; diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index ea1bb5524..bc1438ba5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -90,6 +90,7 @@ preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Replace constants preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers preferences.useImports=Use import statements +preferences.inlineAnonymous=Inline anonymous classes preferences.skipResourcesDecode=Don't decode resources preferences.autoSave=Auto save preferences.threads=Processing threads count diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 8f07b014d..81a460d69 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -90,6 +90,7 @@ preferences.escapeUnicode=Escape unicode preferences.replaceConsts=Reemplazar constantes #preferences.respectBytecodeAccessModifiers= #preferences.useImports= +#preferences.inlineAnonymous= preferences.skipResourcesDecode=No descodificar recursos #preferences.autoSave= preferences.threads=Número de hilos a procesar diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index c1f5ffbc0..b6dc7fb83 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -90,6 +90,7 @@ preferences.escapeUnicode=将 Unicode 字符转义 preferences.replaceConsts=替换常量 preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符 preferences.useImports=使用 import 语句 +#preferences.inlineAnonymous= preferences.skipResourcesDecode=不反编译资源文件 #preferences.autoSave= preferences.threads=并行线程数