Commit e7e7b664 authored by Skylot's avatar Skylot

feat: add option to disable anonymous class inline (#633)

parent db7f2cf5
......@@ -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
......
......@@ -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;
}
......
......@@ -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;
}
......
......@@ -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());
......
......@@ -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());
......
......@@ -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;
}
......
......@@ -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
......
......@@ -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()) {
......
......@@ -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;
......
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;
}
}
......@@ -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()"));
}
}
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;
}
......
......@@ -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;
......
......@@ -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
......
......@@ -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
......
......@@ -90,6 +90,7 @@ preferences.escapeUnicode=将 Unicode 字符转义
preferences.replaceConsts=替换常量
preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符
preferences.useImports=使用 import 语句
#preferences.inlineAnonymous=
preferences.skipResourcesDecode=不反编译资源文件
#preferences.autoSave=
preferences.threads=并行线程数
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment