diff --git a/src/main/java/jadx/Consts.java b/src/main/java/jadx/Consts.java
new file mode 100644
index 000000000..821ca24a0
--- /dev/null
+++ b/src/main/java/jadx/Consts.java
@@ -0,0 +1,13 @@
+package jadx;
+
+public class Consts {
+ public static final String JADX_VERSION = "dev";
+
+ public static final boolean DEBUG = false;
+
+ public static final String CLASS_OBJECT = "java.lang.Object";
+ public static final String CLASS_STRING = "java.lang.String";
+ public static final String CLASS_CLASS = "java.lang.Class";
+ public static final String CLASS_THROWABLE = "java.lang.Throwable";
+ public static final String CLASS_ENUM = "java.lang.Enum";
+}
diff --git a/src/main/java/jadx/JadxArgs.java b/src/main/java/jadx/JadxArgs.java
new file mode 100644
index 000000000..69ff93319
--- /dev/null
+++ b/src/main/java/jadx/JadxArgs.java
@@ -0,0 +1,187 @@
+package jadx;
+
+import jadx.utils.exceptions.JadxException;
+import jadx.utils.files.InputFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterDescription;
+import com.beust.jcommander.ParameterException;
+
+public class JadxArgs {
+ private final static Logger LOG = LoggerFactory.getLogger(JadxArgs.class);
+
+ @Parameter(description = " (.dex, .apk, .jar or .class)", required = true)
+ protected List files;
+
+ @Parameter(names = { "-d", "--output-dir" }, description = "output directory")
+ protected String outDirName;
+
+ @Parameter(names = { "-j", "--threads-count" }, description = "processing threads count")
+ protected int threadsCount = Runtime.getRuntime().availableProcessors();
+
+ @Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)", help = true)
+ protected boolean fallbackMode = false;
+
+ @Parameter(names = { "--not-obfuscated" }, description = "set this flag if code not obfuscated")
+ protected boolean notObfuscated = false;
+
+ @Parameter(names = { "--cfg" }, description = "save methods control flow graph")
+ protected boolean cfgOutput = false;
+
+ @Parameter(names = { "--raw-cfg" }, description = "save methods control flow graph (use raw instructions)")
+ protected boolean rawCfgOutput = false;
+
+ @Parameter(names = { "-v", "--verbose" }, description = "verbose output")
+ protected boolean verbose = false;
+
+ @Parameter(names = { "-h", "--help" }, description = "print this help", help = true)
+ protected boolean printHelp = false;
+
+ private final List input = new ArrayList();
+ private File outputDir;
+
+ public void parse(String[] args) throws JadxException {
+ try {
+ new JCommander(this, args);
+ processArgs();
+ } catch (ParameterException e) {
+ System.out.println("Arguments parse error: " + e.getMessage());
+ System.out.println();
+ printHelp = true;
+ }
+ }
+
+ private void processArgs() throws JadxException {
+ if (printHelp)
+ return;
+
+ if (files == null || files.isEmpty())
+ throw new JadxException("Please specify at least one input file");
+
+ for (String fileName : files) {
+ File file = new File(fileName);
+ if (!file.exists())
+ throw new JadxException("File not found: " + file);
+
+ try {
+ input.add(new InputFile(file));
+ } catch (IOException e) {
+ throw new JadxException("File processing error: " + file, e);
+ }
+ }
+
+ if (input.isEmpty())
+ throw new JadxException("No files with correct extension (must be '.dex', '.class' or '.jar')");
+
+ if (threadsCount <= 0)
+ throw new JadxException("Threads count must be positive");
+
+ if (outDirName == null) {
+ File file = new File(files.get(0));
+ String name = file.getName();
+ int pos = name.lastIndexOf('.');
+ if (pos != -1)
+ outDirName = name.substring(0, pos);
+ else
+ outDirName = name + "-jadx-out";
+
+ LOG.info("output directory: " + outDirName);
+ }
+
+ outputDir = new File(outDirName);
+ if (!outputDir.exists() && !outputDir.mkdirs())
+ throw new JadxException("Can't create directory " + outputDir);
+ if (!outputDir.isDirectory())
+ throw new JadxException("Output file exists as file " + outputDir);
+ }
+
+ public void printUsage() {
+ JCommander jc = new JCommander(this);
+ // print usage in not sorted fields order (by default its sorted by description)
+ PrintStream out = System.out;
+ out.println("jadx - dex to java decompiler, version: '" + Consts.JADX_VERSION + "'");
+ out.println();
+ out.println("usage: jadx [options] " + jc.getMainParameterDescription());
+ out.println("options:");
+
+ List params = jc.getParameters();
+
+ int maxNamesLen = 0;
+ for (ParameterDescription p : params) {
+ int len = p.getNames().length();
+ if (len > maxNamesLen)
+ maxNamesLen = len;
+ }
+
+ Field[] fields = this.getClass().getDeclaredFields();
+ for (Field f : fields) {
+ for (ParameterDescription p : params) {
+ if (f.getName().equals(p.getParameterized().getName())) {
+ StringBuilder opt = new StringBuilder();
+ opt.append(' ').append(p.getNames());
+ addSpaces(opt, maxNamesLen - opt.length() + 2);
+ opt.append("- ").append(p.getDescription());
+ if (p.getParameter().required())
+ opt.append(" [required]");
+ out.println(opt.toString());
+ break;
+ }
+ }
+ }
+ out.println("Example:");
+ out.println(" jadx -d out classes.dex");
+ }
+
+ private static void addSpaces(StringBuilder str, int count) {
+ for (int i = 0; i < count; i++)
+ str.append(' ');
+ }
+
+ public File getOutDir() {
+ return outputDir;
+ }
+
+ public int getThreadsCount() {
+ return threadsCount;
+ }
+
+ public boolean isCFGOutput() {
+ return cfgOutput;
+ }
+
+ public boolean isRawCFGOutput() {
+ return rawCfgOutput;
+ }
+
+ public List getInput() {
+ return input;
+ }
+
+ public boolean isFallbackMode() {
+ return fallbackMode;
+ }
+
+ public boolean isNotObfuscated() {
+ return notObfuscated;
+ }
+
+ public boolean isVerbose() {
+ return verbose;
+ }
+
+ public boolean isPrintHelp() {
+ return printHelp;
+ }
+
+}
diff --git a/src/main/java/jadx/Main.java b/src/main/java/jadx/Main.java
new file mode 100644
index 000000000..5d4d1aed3
--- /dev/null
+++ b/src/main/java/jadx/Main.java
@@ -0,0 +1,143 @@
+package jadx;
+
+import jadx.codegen.CodeGen;
+import jadx.dex.info.ClassInfo;
+import jadx.dex.nodes.ClassNode;
+import jadx.dex.nodes.RootNode;
+import jadx.dex.visitors.BlockMakerVisitor;
+import jadx.dex.visitors.ClassCheck;
+import jadx.dex.visitors.CodeShrinker;
+import jadx.dex.visitors.ConstInlinerVisitor;
+import jadx.dex.visitors.DotGraphVisitor;
+import jadx.dex.visitors.EnumVisitor;
+import jadx.dex.visitors.FallbackModeVisitor;
+import jadx.dex.visitors.IDexTreeVisitor;
+import jadx.dex.visitors.ModVisitor;
+import jadx.dex.visitors.regions.CheckRegions;
+import jadx.dex.visitors.regions.PostRegionVisitor;
+import jadx.dex.visitors.regions.ProcessVariables;
+import jadx.dex.visitors.regions.RegionMakerVisitor;
+import jadx.dex.visitors.typeresolver.FinishTypeResolver;
+import jadx.dex.visitors.typeresolver.TypeResolver;
+import jadx.utils.ErrorsCounter;
+import jadx.utils.exceptions.JadxException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Main {
+ private static final Logger LOG = LoggerFactory.getLogger(Main.class);
+
+ static {
+ if (Consts.DEBUG)
+ LOG.info("debug enabled");
+ if (Main.class.desiredAssertionStatus())
+ LOG.info("assertions enabled");
+ }
+
+ public static int run(JadxArgs args) {
+ int errorCount;
+ try {
+ RootNode root = new RootNode(args);
+ LOG.info("loading ...");
+ root.load();
+ LOG.info("processing ...");
+ root.init();
+
+ int threadsCount = args.getThreadsCount();
+ LOG.debug("processing threads count: {}", threadsCount);
+
+ List passes = getPassesList(args);
+ if (threadsCount == 1) {
+ for (ClassNode cls : root.getClasses()) {
+ ProcessClass job = new ProcessClass(cls, passes);
+ job.run();
+ }
+ } else {
+ ExecutorService executor = Executors.newFixedThreadPool(threadsCount);
+ for (ClassNode cls : root.getClasses()) {
+ ProcessClass job = new ProcessClass(cls, passes);
+ executor.execute(job);
+ }
+ executor.shutdown();
+ executor.awaitTermination(100, TimeUnit.DAYS);
+ }
+ } catch (Throwable e) {
+ LOG.error("jadx error:", e);
+ } finally {
+ errorCount = ErrorsCounter.getErrorCount();
+ if (errorCount != 0)
+ ErrorsCounter.printReport();
+
+ // clear resources if we use jadx as a library
+ ClassInfo.clearCache();
+ ErrorsCounter.reset();
+ }
+ LOG.info("done");
+ return errorCount;
+ }
+
+ private static List getPassesList(JadxArgs args) {
+ List passes = new ArrayList();
+ if (args.isFallbackMode()) {
+ passes.add(new FallbackModeVisitor());
+ } else {
+ passes.add(new BlockMakerVisitor());
+
+ passes.add(new TypeResolver());
+ passes.add(new ConstInlinerVisitor());
+ passes.add(new FinishTypeResolver());
+
+ passes.add(new ClassCheck());
+
+ if (args.isRawCFGOutput())
+ passes.add(new DotGraphVisitor(args.getOutDir(), false, true));
+
+ passes.add(new ModVisitor());
+ passes.add(new EnumVisitor());
+
+ if (args.isCFGOutput())
+ passes.add(new DotGraphVisitor(args.getOutDir(), false));
+
+ passes.add(new RegionMakerVisitor());
+ passes.add(new PostRegionVisitor());
+
+ passes.add(new CodeShrinker());
+ passes.add(new ProcessVariables());
+ passes.add(new CheckRegions());
+ if (args.isCFGOutput())
+ passes.add(new DotGraphVisitor(args.getOutDir(), true));
+ }
+ passes.add(new CodeGen(args));
+ return passes;
+ }
+
+ public static void main(String[] args) {
+ JadxArgs jadxArgs = new JadxArgs();
+ try {
+ jadxArgs.parse(args);
+ if (jadxArgs.isPrintHelp()) {
+ jadxArgs.printUsage();
+ System.exit(0);
+ }
+ } catch (JadxException e) {
+ LOG.error("Error: " + e.getMessage());
+ System.exit(1);
+ }
+
+ if (jadxArgs.isVerbose()) {
+ ch.qos.logback.classic.Logger rootLogger =
+ (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+ rootLogger.setLevel(ch.qos.logback.classic.Level.DEBUG);
+ }
+
+ int result = run(jadxArgs);
+ System.exit(result);
+ }
+}
diff --git a/src/main/java/jadx/ProcessClass.java b/src/main/java/jadx/ProcessClass.java
new file mode 100644
index 000000000..313290cdd
--- /dev/null
+++ b/src/main/java/jadx/ProcessClass.java
@@ -0,0 +1,37 @@
+package jadx;
+
+import jadx.dex.nodes.ClassNode;
+import jadx.dex.visitors.DepthTraverser;
+import jadx.dex.visitors.IDexTreeVisitor;
+import jadx.utils.exceptions.DecodeException;
+
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class ProcessClass implements Runnable {
+ private final static Logger LOG = LoggerFactory.getLogger(ProcessClass.class);
+
+ private final ClassNode cls;
+ private final List passes;
+
+ ProcessClass(ClassNode cls, List passes) {
+ this.cls = cls;
+ this.passes = passes;
+ }
+
+ @Override
+ public void run() {
+ try {
+ cls.load();
+ for (IDexTreeVisitor visitor : passes) {
+ DepthTraverser.visit(visitor, cls);
+ }
+ } catch (DecodeException e) {
+ LOG.error("Decode exception: " + cls, e);
+ } finally {
+ cls.unload();
+ }
+ }
+}
diff --git a/src/main/java/jadx/codegen/AnnotationGen.java b/src/main/java/jadx/codegen/AnnotationGen.java
new file mode 100644
index 000000000..3bb8d1eb8
--- /dev/null
+++ b/src/main/java/jadx/codegen/AnnotationGen.java
@@ -0,0 +1,195 @@
+package jadx.codegen;
+
+import jadx.Consts;
+import jadx.dex.attributes.AttributeType;
+import jadx.dex.attributes.IAttributeNode;
+import jadx.dex.attributes.annotations.Annotation;
+import jadx.dex.attributes.annotations.AnnotationsList;
+import jadx.dex.attributes.annotations.MethodParameters;
+import jadx.dex.info.FieldInfo;
+import jadx.dex.instructions.args.ArgType;
+import jadx.dex.nodes.ClassNode;
+import jadx.dex.nodes.FieldNode;
+import jadx.dex.nodes.MethodNode;
+import jadx.utils.StringUtils;
+import jadx.utils.Utils;
+import jadx.utils.exceptions.JadxRuntimeException;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class AnnotationGen {
+
+ private final ClassNode cls;
+ private final ClassGen classGen;
+
+ public AnnotationGen(ClassNode cls, ClassGen classGen) {
+ this.cls = cls;
+ this.classGen = classGen;
+ }
+
+ public void addForClass(CodeWriter code) {
+ add(cls, code);
+ }
+
+ public void addForMethod(CodeWriter code, MethodNode mth) {
+ add(mth, code);
+ }
+
+ public void addForField(CodeWriter code, FieldNode field) {
+ add(field, code);
+ }
+
+ public void addForParameter(CodeWriter code, MethodParameters paramsAnnotations, int n) {
+ AnnotationsList aList = paramsAnnotations.getParamList().get(n);
+ if (aList == null || aList.size() == 0)
+ return;
+
+ for (Annotation a : aList.getAll()) {
+ code.add(formatAnnotation(a));
+ code.add(' ');
+ }
+ }
+
+ private void add(IAttributeNode node, CodeWriter code) {
+ AnnotationsList aList = (AnnotationsList) node.getAttributes().get(AttributeType.ANNOTATION_LIST);
+ if (aList == null || aList.size() == 0)
+ return;
+
+ for (Annotation a : aList.getAll()) {
+ String aCls = a.getAnnotationClass();
+ if (aCls.startsWith("dalvik.annotation.")) {
+ // skip
+ if (aCls.equals("dalvik.annotation.Signature"))
+ code.startLine("// signature: "
+ + Utils.mergeSignature((List) a.getValues().get("value")));
+ else if (Consts.DEBUG)
+ code.startLine("// " + a);
+ } else {
+ code.startLine();
+ code.add(formatAnnotation(a));
+ }
+ }
+ }
+
+ private CodeWriter formatAnnotation(Annotation a) {
+ CodeWriter code = new CodeWriter();
+ code.add('@');
+ code.add(classGen.useClass(a.getType()));
+ Map vl = a.getValues();
+ if (vl.size() != 0) {
+ code.add('(');
+ if (vl.size() == 1 && vl.containsKey("value")) {
+ code.add(encValueToString(vl.get("value")));
+ } else {
+ for (Iterator> it = vl.entrySet().iterator(); it.hasNext();) {
+ Entry e = it.next();
+ code.add(e.getKey());
+ code.add(" = ");
+ code.add(encValueToString(e.getValue()));
+ if (it.hasNext())
+ code.add(", ");
+ }
+ }
+ code.add(')');
+ }
+ return code;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void addThrows(MethodNode mth, CodeWriter code) {
+ AnnotationsList anList = (AnnotationsList) mth.getAttributes().get(AttributeType.ANNOTATION_LIST);
+ if (anList == null || anList.size() == 0)
+ return;
+
+ Annotation an = anList.get("dalvik.annotation.Throws");
+ if (an != null) {
+ Object exs = an.getValues().get("value");
+ code.add(" throws ");
+ for (Iterator it = ((List) exs).iterator(); it.hasNext();) {
+ ArgType ex = it.next();
+ code.add(TypeGen.translate(classGen, ex));
+ if (it.hasNext())
+ code.add(", ");
+ }
+ }
+ }
+
+ public Object getAnnotationDefaultValue(String name) {
+ AnnotationsList anList = (AnnotationsList) cls.getAttributes().get(AttributeType.ANNOTATION_LIST);
+ if (anList == null || anList.size() == 0)
+ return null;
+
+ Annotation an = anList.get("dalvik.annotation.AnnotationDefault");
+ if (an != null) {
+ Annotation defAnnotation = (Annotation) an.getValues().get("value");
+ return defAnnotation.getValues().get(name);
+ }
+ return null;
+ }
+
+ // TODO: refactor this boilerplate code
+ @SuppressWarnings("unchecked")
+ public String encValueToString(Object val) {
+ if (val == null)
+ return "null";
+
+ if (val instanceof String)
+ return StringUtils.unescapeString((String) val);
+ if (val instanceof Integer)
+ return TypeGen.formatInteger((Integer) val);
+ if (val instanceof Character)
+ return StringUtils.unescapeChar((Character) val);
+ if (val instanceof Boolean)
+ return Boolean.TRUE.equals(val) ? "true" : "false";
+ if (val instanceof Float)
+ return TypeGen.formatFloat((Float) val);
+ if (val instanceof Double)
+ return TypeGen.formatDouble((Double) val);
+ if (val instanceof Long)
+ return TypeGen.formatLong((Long) val);
+ if (val instanceof Short)
+ return TypeGen.formatShort((Short) val);
+ if (val instanceof Byte)
+ return TypeGen.formatByte((Byte) val);
+
+ if (val instanceof ArgType)
+ return TypeGen.translate(classGen, (ArgType) val) + ".class";
+
+ if (val instanceof FieldInfo) {
+ // must be a static field
+ FieldInfo field = (FieldInfo) val;
+ // FIXME: !!code from InsnGen.sfield
+ String thisClass = cls.getFullName();
+ if (field.getDeclClass().getFullName().equals(thisClass)) {
+ return field.getName();
+ } else {
+ return classGen.useClass(field.getDeclClass()) + '.' + field.getName();
+ }
+ }
+
+ if (val instanceof List) {
+ StringBuilder str = new StringBuilder();
+ str.append('{');
+ List