Skip to content

Commit

Permalink
feat: add option to disable anonymous class inline (#633)
Browse files Browse the repository at this point in the history
  • Loading branch information
skylot committed Apr 25, 2019
1 parent db7f2cf commit e7e7b66
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 73 deletions.
51 changes: 29 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,31 +54,38 @@ Run **jadx** on itself:

### Usage
```
jadx[-gui] [options] <input file> (.dex, .apk, .jar or .class)
jadx[-gui] [options] <input file> (.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

Expand Down
8 changes: 8 additions & 0 deletions jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -175,6 +178,7 @@ public JadxArgs toJadxArgs() {
args.setExportAsGradleProject(exportAsGradleProject);
args.setUseImports(useImports);
args.setDebugInfo(debugInfo);
args.setInlineAnonymousClasses(inlineAnonymousClasses);
args.setRenameCaseSensitive(isRenameCaseSensitive());
args.setRenameValid(isRenameValid());
args.setRenamePrintable(isRenamePrintable());
Expand Down Expand Up @@ -225,6 +229,10 @@ public boolean isDebugInfo() {
return debugInfo;
}

public boolean isInlineAnonymousClasses() {
return inlineAnonymousClasses;
}

public boolean isDeobfuscationOn() {
return deobfuscationOn;
}
Expand Down
9 changes: 9 additions & 0 deletions jadx-core/src/main/java/jadx/api/JadxArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -155,6 +156,14 @@ public void setDebugInfo(boolean debugInfo) {
this.debugInfo = debugInfo;
}

public boolean isInlineAnonymousClasses() {
return inlineAnonymousClasses;
}

public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
this.inlineAnonymousClasses = inlineAnonymousClasses;
}

public boolean isSkipResources() {
return skipResources;
}
Expand Down
2 changes: 2 additions & 0 deletions jadx-core/src/main/java/jadx/core/Jadx.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -104,6 +105,7 @@ public static List<IDexTreeVisitor> getPassesList(JadxArgs args) {

passes.add(new ExtractFieldInit());
passes.add(new FixAccessModifiers());
passes.add(new ProcessAnonymous());
passes.add(new ClassModifier());
passes.add(new MethodInlineVisitor());
passes.add(new EnumVisitor());
Expand Down
3 changes: 1 addition & 2 deletions jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ public void addClassBody(CodeWriter clsCode) throws CodegenException {

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());
Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ private void filledNewArray(FilledNewArrayNode insn, CodeWriter code) throws Cod
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;
}
Expand Down
36 changes: 1 addition & 35 deletions jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ public ClassNode(DexNode dex, ClassDef cls) {
accFlagsValue = cls.getAccessFlags();
}
this.accessFlags = new AccessInfo(accFlagsValue, AFType.CLASS);
markAnonymousClass();
buildCache();
} catch (Exception e) {
throw new JadxRuntimeException("Error decode class: " + clsInfo, e);
Expand Down Expand Up @@ -401,41 +400,8 @@ && getSuperClass() != null
&& 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
desc = "Remove synthetic classes, methods and fields",
runAfter = {
ModVisitor.class,
FixAccessModifiers.class
FixAccessModifiers.class,
ProcessAnonymous.class
}
)
public class ClassModifier extends AbstractVisitor {
Expand All @@ -51,7 +52,6 @@ public boolean visit(ClassNode cls) throws JadxException {
cls.add(AFlag.DONT_GENERATE);
return false;
}
cls.markAnonymousClass();
removeSyntheticFields(cls);
cls.getMethods().forEach(ClassModifier::removeSyntheticMethods);
cls.getMethods().forEach(ClassModifier::removeEmptyMethods);
Expand All @@ -73,7 +73,7 @@ private static void removeSyntheticFields(ClassNode cls) {
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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,8 @@ private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn
}

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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand Down Expand Up @@ -37,5 +38,17 @@ public void test() {
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()"));
}
}
11 changes: 6 additions & 5 deletions jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,7 +12,7 @@
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;
Expand Down Expand Up @@ -281,6 +278,10 @@ public void setUseImports(boolean useImports) {
this.useImports = useImports;
}

public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
this.inlineAnonymousClasses = inlineAnonymousClasses;
}

public boolean isAutoStartJobs() {
return autoStartJobs;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ private SettingsGroup makeDecompilationGroup() {
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"),
Expand All @@ -357,6 +364,7 @@ private SettingsGroup makeDecompilationGroup() {
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;
Expand Down
1 change: 1 addition & 0 deletions jadx-gui/src/main/resources/i18n/Messages_en_US.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit e7e7b66

Please sign in to comment.