feat: add option to disable anonymous class inline (#633)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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=并行线程数
|
||||
|
||||
Reference in New Issue
Block a user