From ed385e8cf1dd431a5892cffa5babfea20aaecb41 Mon Sep 17 00:00:00 2001 From: skylot Date: Tue, 18 Jun 2019 16:06:56 +0300 Subject: [PATCH] feat: output decompilation results in json format (#676) --- README.md | 2 + .../main/java/jadx/cli/JCommanderWrapper.java | 10 + .../src/main/java/jadx/cli/JadxCLIArgs.java | 28 ++- jadx-core/build.gradle | 1 + .../src/main/java/jadx/api/CodePosition.java | 10 +- .../src/main/java/jadx/api/JadxArgs.java | 19 ++ .../main/java/jadx/api/JadxDecompiler.java | 2 +- .../main/java/jadx/core/codegen/ClassGen.java | 91 +++---- .../main/java/jadx/core/codegen/CodeGen.java | 57 +++-- .../java/jadx/core/codegen/CodeWriter.java | 23 +- .../main/java/jadx/core/codegen/InsnGen.java | 7 +- .../java/jadx/core/codegen/MethodGen.java | 90 ++++--- .../java/jadx/core/codegen/RegionGen.java | 13 +- .../jadx/core/codegen/json/JsonCodeGen.java | 224 ++++++++++++++++++ .../core/codegen/json/JsonMappingGen.java | 107 +++++++++ .../jadx/core/codegen/json/cls/JsonClass.java | 94 ++++++++ .../core/codegen/json/cls/JsonCodeLine.java | 33 +++ .../jadx/core/codegen/json/cls/JsonField.java | 5 + .../core/codegen/json/cls/JsonMethod.java | 51 ++++ .../jadx/core/codegen/json/cls/JsonNode.java | 40 ++++ .../codegen/json/mapping/JsonClsMapping.java | 71 ++++++ .../json/mapping/JsonFieldMapping.java | 22 ++ .../codegen/json/mapping/JsonMapping.java | 15 ++ .../codegen/json/mapping/JsonMthMapping.java | 40 ++++ .../java/jadx/core/deobf/Deobfuscator.java | 2 +- .../java/jadx/core/dex/info/AccessInfo.java | 4 + .../java/jadx/core/dex/info/FieldInfo.java | 6 + .../java/jadx/core/dex/info/MethodInfo.java | 2 +- .../java/jadx/core/dex/nodes/MethodNode.java | 4 + .../dex/visitors/FallbackModeVisitor.java | 9 + .../jadx/core/dex/visitors/RenameVisitor.java | 4 + .../java/jadx/core/dex/visitors/SaveCode.java | 22 +- .../java/jadx/core/utils/CodeGenUtils.java | 8 + .../java/jadx/core/utils/ImmutableList.java | 23 +- .../src/main/java/jadx/core/utils/Utils.java | 13 + .../java/jadx/core/utils/files/InputFile.java | 2 +- .../integration/others/TestJsonOutput.java | 62 +++++ jadx-gui/build.gradle | 1 - .../java/jadx/gui/settings/JadxSettings.java | 11 +- 39 files changed, 1087 insertions(+), 141 deletions(-) create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/JsonMappingGen.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonClass.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonCodeLine.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonField.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonMethod.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonNode.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonClsMapping.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonFieldMapping.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMapping.java create mode 100644 jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMthMapping.java create mode 100644 jadx-core/src/test/java/jadx/tests/integration/others/TestJsonOutput.java diff --git a/README.md b/README.md index 923c6a195..574cf590a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ options: -j, --threads-count - processing threads count -r, --no-res - do not decode resources -s, --no-src - do not decompile source code + --single-class - decompile a single class + --output-format - can be 'java' or 'json' (default: java) -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 diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index a24fdf7c0..f87f6a9bf 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -112,6 +112,16 @@ public class JCommanderWrapper { // ignore } } + if (fieldType == String.class) { + try { + String val = (String) f.get(args); + if (val != null) { + opt.append(" (default: ").append(val).append(')'); + } + } catch (Exception e) { + // ignore + } + } } private static void addSpaces(StringBuilder str, int count) { diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 55443a7a2..1b798f23d 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -46,6 +46,9 @@ public class JadxCLIArgs { @Parameter(names = { "--single-class" }, description = "decompile a single class") protected String singleClass = null; + @Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'") + protected String outputFormat = "java"; + @Parameter(names = { "-e", "--export-gradle" }, description = "save as android gradle project") protected boolean exportAsGradleProject = false; @@ -86,7 +89,18 @@ public class JadxCLIArgs { protected boolean deobfuscationForceSave = false; @Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias") - protected boolean deobfuscationUseSourceNameAsAlias = true; + protected boolean deobfuscationUseSourceNameAsAlias = false; + + @Parameter( + names = { "--rename-flags" }, + description = "what to rename, comma-separated," + + " 'case' for system case sensitivity," + + " 'valid' for java identifiers," + + " 'printable' characters," + + " 'none' or 'all' (default)", + converter = RenameConverter.class + ) + protected Set renameFlags = EnumSet.allOf(RenameEnum.class); @Parameter(names = { "--fs-case-sensitive" }, description = "treat filesystem as case sensitive, false by default") protected boolean fsCaseSensitive = false; @@ -100,17 +114,6 @@ public class JadxCLIArgs { @Parameter(names = { "-f", "--fallback" }, description = "make simple dump (using goto instead of 'if', 'for', etc)") protected boolean fallbackMode = false; - @Parameter( - names = { "--rename-flags" }, - description = "what to rename, comma-separated," - + " 'case' for system case sensitivity," - + " 'valid' for java identifiers," - + " 'printable' characters," - + " 'none' or 'all' (default)", - converter = RenameConverter.class - ) - protected Set renameFlags = EnumSet.allOf(RenameEnum.class); - @Parameter(names = { "-v", "--verbose" }, description = "verbose output") protected boolean verbose = false; @@ -178,6 +181,7 @@ public class JadxCLIArgs { args.setOutDir(FileUtils.toFile(outDir)); args.setOutDirSrc(FileUtils.toFile(outDirSrc)); args.setOutDirRes(FileUtils.toFile(outDirRes)); + args.setOutputFormat(JadxArgs.OutputFormatEnum.valueOf(outputFormat.toUpperCase())); args.setThreadsCount(threadsCount); args.setSkipSources(skipSources); if (singleClass != null) { diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index a2d8275f7..6c968887c 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -8,6 +8,7 @@ dependencies { compile 'org.ow2.asm:asm:7.1' compile 'org.jetbrains:annotations:17.0.0' compile 'uk.com.robust-it:cloning:1.9.12' + compile 'com.google.code.gson:gson:2.8.5' compile 'org.smali:baksmali:2.2.7' compile('org.smali:smali:2.2.7') { diff --git a/jadx-core/src/main/java/jadx/api/CodePosition.java b/jadx-core/src/main/java/jadx/api/CodePosition.java index e05e91425..678b12e9b 100644 --- a/jadx-core/src/main/java/jadx/api/CodePosition.java +++ b/jadx-core/src/main/java/jadx/api/CodePosition.java @@ -57,6 +57,14 @@ public final class CodePosition { @Override public String toString() { - return line + ':' + offset + (node != null ? " " + node : ""); + StringBuilder sb = new StringBuilder(); + sb.append(line); + if (offset != 0) { + sb.append(':').append(offset); + } + if (node != null) { + sb.append(' ').append(node); + } + return sb.toString(); } } diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index 6707a72ac..c484e33d4 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -62,6 +62,12 @@ public class JadxArgs { private Set renameFlags = EnumSet.allOf(RenameEnum.class); + public enum OutputFormatEnum { + JAVA, JSON + } + + private OutputFormatEnum outputFormat = OutputFormatEnum.JAVA; + public JadxArgs() { // use default options } @@ -308,6 +314,18 @@ public class JadxArgs { } } + public OutputFormatEnum getOutputFormat() { + return outputFormat; + } + + public boolean isJsonOutput() { + return outputFormat == OutputFormatEnum.JSON; + } + + public void setOutputFormat(OutputFormatEnum outputFormat) { + this.outputFormat = outputFormat; + } + @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles @@ -333,6 +351,7 @@ public class JadxArgs { + ", exportAsGradleProject=" + exportAsGradleProject + ", fsCaseSensitive=" + fsCaseSensitive + ", renameFlags=" + renameFlags + + ", outputFormat=" + outputFormat + '}'; } } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 7391e7adb..6bb3b84d6 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -215,7 +215,7 @@ public final class JadxDecompiler { executor.execute(() -> { try { cls.decompile(); - SaveCode.save(outDir, args, cls.getClassNode()); + SaveCode.save(outDir, cls.getClassNode()); } catch (Exception e) { LOG.error("Error saving class: {}", cls.getFullName(), e); } 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 7af1ce896..00610c444 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -19,7 +19,6 @@ import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr.EnumField; import jadx.core.dex.attributes.nodes.JadxError; import jadx.core.dex.attributes.nodes.LineAttrNode; -import jadx.core.dex.attributes.nodes.SourceFileAttr; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; @@ -128,8 +127,8 @@ public class ClassGen { } annotationGen.addForClass(clsCode); - insertSourceFileInfo(clsCode, cls); insertRenameInfo(clsCode, cls); + CodeGenUtils.addSourceFileInfo(clsCode, cls); clsCode.startLine(af.makeString()); if (af.isInterface()) { if (af.isAnnotation()) { @@ -290,29 +289,21 @@ public class ClassGen { return false; } - private void addMethod(CodeWriter code, MethodNode mth) throws CodegenException { + public void addMethod(CodeWriter code, MethodNode mth) throws CodegenException { + CodeGenUtils.addComments(code, mth); if (mth.getAccessFlags().isAbstract() || mth.getAccessFlags().isNative()) { MethodGen mthGen = new MethodGen(this, mth); mthGen.addDefinition(code); - if (cls.getAccessFlags().isAnnotation()) { - Object def = annotationGen.getAnnotationDefaultValue(mth.getName()); - if (def != null) { - code.add(" default "); - annotationGen.encodeValue(code, def); - } - } code.add(';'); } else { - CodeGenUtils.addComments(code, mth); insertDecompilationProblems(code, mth); boolean badCode = mth.contains(AFlag.INCONSISTENT_CODE); if (badCode && showInconsistentCode) { - code.startLine("/* Code decompiled incorrectly, please refer to instructions dump. */"); mth.remove(AFlag.INCONSISTENT_CODE); badCode = false; } MethodGen mthGen; - if (badCode || mth.contains(AType.JADX_ERROR) || fallback) { + if (badCode || fallback || mth.contains(AType.JADX_ERROR) || mth.getRegion() == null) { mthGen = MethodGen.getFallbackMethodGen(mth); } else { mthGen = new MethodGen(this, mth); @@ -322,12 +313,7 @@ public class ClassGen { } code.add('{'); code.incIndent(); - insertSourceFileInfo(code, mth); - if (fallback) { - mthGen.addFallbackMethodCode(code); - } else { - mthGen.addInstructions(code); - } + mthGen.addInstructions(code); code.decIndent(); code.startLine('}'); } @@ -357,37 +343,41 @@ public class ClassGen { private void addFields(CodeWriter code) throws CodegenException { addEnumFields(code); for (FieldNode f : cls.getFields()) { - if (f.contains(AFlag.DONT_GENERATE)) { - continue; - } - CodeGenUtils.addComments(code, f); - annotationGen.addForField(code, f); + addField(code, f); + } + } - if (f.getFieldInfo().isRenamed()) { - code.newLine(); - CodeGenUtils.addRenamedComment(code, f, f.getName()); - } - code.startLine(f.getAccessFlags().makeString()); - useType(code, f.getType()); - code.add(' '); - code.attachDefinition(f); - code.add(f.getAlias()); - FieldInitAttr fv = f.get(AType.FIELD_INIT); - if (fv != null) { - code.add(" = "); - if (fv.getValue() == null) { - code.add(TypeGen.literalToString(0, f.getType(), cls, fallback)); - } else { - if (fv.getValueType() == InitType.CONST) { - annotationGen.encodeValue(code, fv.getValue()); - } else if (fv.getValueType() == InitType.INSN) { - InsnGen insnGen = makeInsnGen(fv.getInsnMth()); - addInsnBody(insnGen, code, fv.getInsn()); - } + public void addField(CodeWriter code, FieldNode f) { + if (f.contains(AFlag.DONT_GENERATE)) { + return; + } + CodeGenUtils.addComments(code, f); + annotationGen.addForField(code, f); + + if (f.getFieldInfo().isRenamed()) { + code.newLine(); + CodeGenUtils.addRenamedComment(code, f, f.getName()); + } + code.startLine(f.getAccessFlags().makeString()); + useType(code, f.getType()); + code.add(' '); + code.attachDefinition(f); + code.add(f.getAlias()); + FieldInitAttr fv = f.get(AType.FIELD_INIT); + if (fv != null) { + code.add(" = "); + if (fv.getValue() == null) { + code.add(TypeGen.literalToString(0, f.getType(), cls, fallback)); + } else { + if (fv.getValueType() == InitType.CONST) { + annotationGen.encodeValue(code, fv.getValue()); + } else if (fv.getValueType() == InitType.INSN) { + InsnGen insnGen = makeInsnGen(fv.getInsnMth()); + addInsnBody(insnGen, code, fv.getInsn()); } } - code.add(';'); } + code.add(';'); } private boolean isFieldsPresents() { @@ -569,7 +559,7 @@ public class ClassGen { } } - private Set getImports() { + public Set getImports() { if (parentGen != null) { return parentGen.getImports(); } else { @@ -615,13 +605,6 @@ public class ClassGen { return searchCollision(dex, useCls.getParentClass(), searchCls); } - private void insertSourceFileInfo(CodeWriter code, AttrNode node) { - SourceFileAttr sourceFileAttr = node.get(AType.SOURCE_FILE); - if (sourceFileAttr != null) { - code.startLine("/* compiled from: ").add(sourceFileAttr.getFileName()).add(" */"); - } - } - private void insertRenameInfo(CodeWriter code, ClassNode cls) { ClassInfo classInfo = cls.getClassInfo(); if (classInfo.hasAlias()) { diff --git a/jadx-core/src/main/java/jadx/core/codegen/CodeGen.java b/jadx-core/src/main/java/jadx/core/codegen/CodeGen.java index 2cb7802bf..4d5d6bc8b 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/CodeGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/CodeGen.java @@ -1,29 +1,58 @@ package jadx.core.codegen; +import java.util.concurrent.Callable; + +import jadx.api.JadxArgs; +import jadx.core.codegen.json.JsonCodeGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; -import jadx.core.utils.exceptions.CodegenException; import jadx.core.utils.exceptions.JadxRuntimeException; public class CodeGen { - public static void generate(ClassNode cls) throws CodegenException { + public static void generate(ClassNode cls) { if (cls.contains(AFlag.DONT_GENERATE)) { cls.setCode(CodeWriter.EMPTY); } else { - ClassGen clsGen = new ClassGen(cls, cls.root().getArgs()); - CodeWriter code; - try { - code = clsGen.makeClass(); - } catch (Exception e) { - if (cls.contains(AFlag.RESTART_CODEGEN)) { - cls.remove(AFlag.RESTART_CODEGEN); - code = clsGen.makeClass(); - } else { - throw new JadxRuntimeException("Code generation error", e); - } + JadxArgs args = cls.root().getArgs(); + switch (args.getOutputFormat()) { + case JAVA: + generateJavaCode(cls, args); + break; + + case JSON: + generateJson(cls); + break; + } + } + } + + private static void generateJavaCode(ClassNode cls, JadxArgs args) { + ClassGen clsGen = new ClassGen(cls, args); + CodeWriter code = wrapCodeGen(cls, clsGen::makeClass); + cls.setCode(code); + } + + private static void generateJson(ClassNode cls) { + JsonCodeGen codeGen = new JsonCodeGen(cls); + String clsJson = wrapCodeGen(cls, codeGen::process); + cls.setCode(new CodeWriter(clsJson)); + } + + private static R wrapCodeGen(ClassNode cls, Callable codeGenFunc) { + try { + return codeGenFunc.call(); + } catch (Exception e) { + if (cls.contains(AFlag.RESTART_CODEGEN)) { + cls.remove(AFlag.RESTART_CODEGEN); + try { + return codeGenFunc.call(); + } catch (Exception ex) { + throw new JadxRuntimeException("Code generation error after restart", ex); + } + } else { + throw new JadxRuntimeException("Code generation error", e); } - cls.setCode(code); } } diff --git a/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java b/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java index c37bedb50..459575388 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java +++ b/jadx-core/src/main/java/jadx/core/codegen/CodeWriter.java @@ -4,7 +4,6 @@ import java.io.File; import java.io.PrintWriter; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import java.util.TreeMap; @@ -37,7 +36,7 @@ public class CodeWriter { INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR + INDENT_STR, }; - private StringBuilder buf = new StringBuilder(); + private StringBuilder buf; @Nullable private String code; private String indentStr; @@ -49,6 +48,7 @@ public class CodeWriter { private Map lineMap = Collections.emptyMap(); public CodeWriter() { + this.buf = new StringBuilder(); this.indent = 0; this.indentStr = ""; if (ADD_LINE_NUMBERS) { @@ -56,6 +56,12 @@ public class CodeWriter { } } + // create filled instance (just string wrapper) + public CodeWriter(String code) { + this.buf = null; + this.code = code; + } + public CodeWriter startLine() { addLine(); addLineIndent(); @@ -225,6 +231,10 @@ public class CodeWriter { attachAnnotation(obj, new CodePosition(line, offset + 1)); } + public void attachLineAnnotation(Object obj) { + attachAnnotation(obj, new CodePosition(line, 0)); + } + private Object attachAnnotation(Object obj, CodePosition pos) { if (annotations.isEmpty()) { annotations = new HashMap<>(); @@ -260,16 +270,15 @@ public class CodeWriter { code = buf.toString(); buf = null; - Iterator> it = annotations.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); + annotations.entrySet().removeIf(entry -> { Object v = entry.getValue(); if (v instanceof DefinitionWrapper) { LineAttrNode l = ((DefinitionWrapper) v).getNode(); l.setDecompiledLine(entry.getKey().getLine()); - it.remove(); + return true; } - } + return false; + }); return this; } 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 9483d698c..60e97876f 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -63,6 +63,7 @@ public class InsnGen { protected final MethodNode mth; protected final RootNode root; protected final boolean fallback; + protected final boolean attachInsns; protected enum Flags { BODY_ONLY, @@ -73,8 +74,9 @@ public class InsnGen { public InsnGen(MethodGen mgen, boolean fallback) { this.mgen = mgen; this.mth = mgen.getMethodNode(); - this.root = mth.dex().root(); + this.root = mth.root(); this.fallback = fallback; + this.attachInsns = root.getArgs().isJsonOutput(); } private boolean isFallback() { @@ -222,6 +224,9 @@ public class InsnGen { } else { if (flag != Flags.INLINE) { code.startLineWithNum(insn.getSourceLine()); + if (attachInsns) { + code.attachLineAnnotation(insn); + } } if (insn.getResult() != null) { SSAVar var = insn.getResult().getSVar(); diff --git a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java index b339ad3f2..42e4d2466 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -85,9 +85,14 @@ public class MethodGen { ai = ai.remove(AccessFlags.ACC_PUBLIC); } - if (mth.getMethodInfo().isRenamed() && !ai.isConstructor()) { + if (mth.getMethodInfo().hasAlias() && !ai.isConstructor()) { CodeGenUtils.addRenamedComment(code, mth, mth.getName()); } + CodeGenUtils.addSourceFileInfo(code, mth); + if (mth.contains(AFlag.INCONSISTENT_CODE)) { + code.startLine("/* Code decompiled incorrectly, please refer to instructions dump. */"); + } + code.startLineWithNum(mth.getSourceLine()); code.add(ai.makeString()); if (Consts.DEBUG) { @@ -125,6 +130,15 @@ public class MethodGen { code.add(')'); annotationGen.addThrows(mth, code); + + // add default value if in annotation class + if (mth.getParentClass().getAccessFlags().isAnnotation()) { + Object def = annotationGen.getAnnotationDefaultValue(mth.getName()); + if (def != null) { + code.add(" default "); + annotationGen.encodeValue(code, def); + } + } return true; } @@ -181,41 +195,49 @@ public class MethodGen { } public void addInstructions(CodeWriter code) throws CodegenException { - if (mth.contains(AType.JADX_ERROR) - || mth.contains(AFlag.INCONSISTENT_CODE) - || mth.getRegion() == null) { - code.startLine("/*"); + if (mth.root().getArgs().isFallbackMode()) { addFallbackMethodCode(code); - code.startLine("*/"); - - code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: ") - .add(mth.getParentClass().getClassInfo().getAliasFullName()) - .add('.') - .add(mth.getAlias()) - .add('(') - .add(Utils.listToString(mth.getMethodInfo().getArgumentsTypes())) - .add("):") - .add(mth.getMethodInfo().getReturnType().toString()) - .add("\");"); + } else if (classGen.isFallbackMode()) { + dumpInstructions(code); } else { - try { - RegionGen regionGen = new RegionGen(this); - regionGen.makeRegion(code, mth.getRegion()); - } catch (StackOverflowError | BootstrapMethodError e) { - mth.addError("Method code generation error", new JadxOverflowException("StackOverflow")); - classGen.insertDecompilationProblems(code, mth); - addInstructions(code); - } catch (Exception e) { - if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) { - throw e; - } - mth.addError("Method code generation error", e); - classGen.insertDecompilationProblems(code, mth); - addInstructions(code); - } + addRegionInsns(code); } } + public void addRegionInsns(CodeWriter code) throws CodegenException { + try { + RegionGen regionGen = new RegionGen(this); + regionGen.makeRegion(code, mth.getRegion()); + } catch (StackOverflowError | BootstrapMethodError e) { + mth.addError("Method code generation error", new JadxOverflowException("StackOverflow")); + classGen.insertDecompilationProblems(code, mth); + dumpInstructions(code); + } catch (Exception e) { + if (mth.getParentClass().getTopParentClass().contains(AFlag.RESTART_CODEGEN)) { + throw e; + } + mth.addError("Method code generation error", e); + classGen.insertDecompilationProblems(code, mth); + dumpInstructions(code); + } + } + + public void dumpInstructions(CodeWriter code) { + code.startLine("/*"); + addFallbackMethodCode(code); + code.startLine("*/"); + + code.startLine("throw new UnsupportedOperationException(\"Method not decompiled: ") + .add(mth.getParentClass().getClassInfo().getAliasFullName()) + .add('.') + .add(mth.getAlias()) + .add('(') + .add(Utils.listToString(mth.getMethodInfo().getArgumentsTypes())) + .add("):") + .add(mth.getMethodInfo().getReturnType().toString()) + .add("\");"); + } + public void addFallbackMethodCode(CodeWriter code) { if (mth.getInstructions() == null) { // load original instructions @@ -244,6 +266,7 @@ public class MethodGen { public static void addFallbackInsns(CodeWriter code, MethodNode mth, InsnNode[] insnArr, boolean addLabels) { InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true); + boolean attachInsns = mth.root().getArgs().isJsonOutput(); InsnNode prevInsn = null; for (InsnNode insn : insnArr) { if (insn == null) { @@ -259,6 +282,9 @@ public class MethodGen { } try { code.startLine(); + if (attachInsns) { + code.attachLineAnnotation(insn); + } RegisterArg resArg = insn.getResult(); if (resArg != null) { ArgType varType = resArg.getInitType(); @@ -304,7 +330,7 @@ public class MethodGen { * Return fallback variant of method codegen */ public static MethodGen getFallbackMethodGen(MethodNode mth) { - ClassGen clsGen = new ClassGen(mth.getParentClass(), null, true, true, true); + ClassGen clsGen = new ClassGen(mth.getParentClass(), null, false, true, true); return new MethodGen(clsGen, mth); } diff --git a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java index 28b72c61f..e4cd8a293 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -121,6 +121,17 @@ public class RegionGen extends InsnGen { } else { code.attachSourceLine(region.getSourceLine()); } + if (attachInsns) { + List conditionBlocks = region.getConditionBlocks(); + if (!conditionBlocks.isEmpty()) { + BlockNode blockNode = conditionBlocks.get(0); + InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); + if (lastInsn != null) { + code.attachLineAnnotation(lastInsn); + } + } + } + code.add("if ("); new ConditionGen(this).add(code, region.getCondition()); code.add(") {"); @@ -128,7 +139,7 @@ public class RegionGen extends InsnGen { code.startLine('}'); IContainer els = region.getElseRegion(); - if (els != null && RegionUtils.notEmpty(els)) { + if (RegionUtils.notEmpty(els)) { code.add(" else "); if (connectElseIf(code, els)) { return; diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java new file mode 100644 index 000000000..ddf29bfcc --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/JsonCodeGen.java @@ -0,0 +1,224 @@ +package jadx.core.codegen.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import jadx.api.CodePosition; +import jadx.api.JadxArgs; +import jadx.core.codegen.ClassGen; +import jadx.core.codegen.CodeWriter; +import jadx.core.codegen.MethodGen; +import jadx.core.codegen.json.cls.JsonClass; +import jadx.core.codegen.json.cls.JsonCodeLine; +import jadx.core.codegen.json.cls.JsonField; +import jadx.core.codegen.json.cls.JsonMethod; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.CodeGenUtils; +import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class JsonCodeGen { + + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES) + .disableHtmlEscaping() + .create(); + + private final ClassNode cls; + private final JadxArgs args; + private final RootNode root; + + public JsonCodeGen(ClassNode cls) { + this.cls = cls; + this.root = cls.root(); + this.args = root.getArgs(); + } + + public String process() { + JsonClass jsonCls = processCls(cls, null); + return GSON.toJson(jsonCls); + } + + private JsonClass processCls(ClassNode cls, @Nullable ClassGen parentCodeGen) { + ClassGen classGen; + if (parentCodeGen == null) { + classGen = new ClassGen(cls, args); + } else { + classGen = new ClassGen(cls, parentCodeGen); + } + ClassInfo classInfo = cls.getClassInfo(); + + JsonClass jsonCls = new JsonClass(); + jsonCls.setPkg(classInfo.getAliasPkg()); + jsonCls.setDex(cls.dex().getDexFile().getName()); + jsonCls.setName(classInfo.getFullName()); + if (classInfo.hasAlias()) { + jsonCls.setAlias(classInfo.getAliasFullName()); + } + jsonCls.setType(getClassTypeStr(cls)); + jsonCls.setAccessFlags(cls.getAccessFlags().rawValue()); + if (!Objects.equals(cls.getSuperClass(), ArgType.OBJECT)) { + jsonCls.setSuperClass(getTypeAlias(cls.getSuperClass())); + } + if (!cls.getInterfaces().isEmpty()) { + jsonCls.setInterfaces(Utils.collectionMap(cls.getInterfaces(), this::getTypeAlias)); + } + + CodeWriter cw = new CodeWriter(); + CodeGenUtils.addComments(cw, cls); + classGen.insertDecompilationProblems(cw, cls); + classGen.addClassDeclaration(cw); + jsonCls.setDeclaration(cw.finish().toString()); + + addFields(cls, jsonCls, classGen); + addMethods(cls, jsonCls, classGen); + addInnerClasses(cls, jsonCls, classGen); + + if (!cls.getClassInfo().isInner()) { + List imports = Utils.collectionMap(classGen.getImports(), ClassInfo::getAliasFullName); + Collections.sort(imports); + jsonCls.setImports(imports); + } + return jsonCls; + } + + private void addInnerClasses(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { + List innerClasses = cls.getInnerClasses(); + if (innerClasses.isEmpty()) { + return; + } + jsonCls.setInnerClasses(new ArrayList<>(innerClasses.size())); + for (ClassNode innerCls : innerClasses) { + if (innerCls.contains(AFlag.DONT_GENERATE)) { + continue; + } + JsonClass innerJsonCls = processCls(innerCls, classGen); + jsonCls.getInnerClasses().add(innerJsonCls); + } + } + + private void addFields(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { + jsonCls.setFields(new ArrayList<>()); + for (FieldNode field : cls.getFields()) { + if (field.contains(AFlag.DONT_GENERATE)) { + continue; + } + JsonField jsonField = new JsonField(); + jsonField.setName(field.getName()); + if (field.getFieldInfo().hasAlias()) { + jsonField.setAlias(field.getAlias()); + } + + CodeWriter cw = new CodeWriter(); + classGen.addField(cw, field); + jsonField.setDeclaration(cw.finish().toString()); + jsonField.setAccessFlags(field.getAccessFlags().rawValue()); + + jsonCls.getFields().add(jsonField); + } + } + + private void addMethods(ClassNode cls, JsonClass jsonCls, ClassGen classGen) { + jsonCls.setMethods(new ArrayList<>()); + for (MethodNode mth : cls.getMethods()) { + if (mth.contains(AFlag.DONT_GENERATE)) { + continue; + } + JsonMethod jsonMth = new JsonMethod(); + jsonMth.setName(mth.getName()); + if (mth.getMethodInfo().hasAlias()) { + jsonMth.setAlias(mth.getAlias()); + } + jsonMth.setSignature(mth.getMethodInfo().getShortId()); + jsonMth.setReturnType(getTypeAlias(mth.getReturnType())); + jsonMth.setArguments(Utils.collectionMap(mth.getMethodInfo().getArgumentsTypes(), this::getTypeAlias)); + + MethodGen mthGen = new MethodGen(classGen, mth); + CodeWriter cw = new CodeWriter(); + mthGen.addDefinition(cw); + jsonMth.setDeclaration(cw.finish().toString()); + jsonMth.setAccessFlags(mth.getAccessFlags().rawValue()); + jsonMth.setLines(fillMthCode(mth, mthGen)); + jsonMth.setOffset("0x" + Long.toHexString(mth.getMethodCodeOffset())); + jsonCls.getMethods().add(jsonMth); + } + } + + private List fillMthCode(MethodNode mth, MethodGen mthGen) { + if (mth.isNoCode()) { + return Collections.emptyList(); + } + + CodeWriter code = new CodeWriter(); + try { + mthGen.addInstructions(code); + } catch (Exception e) { + throw new JadxRuntimeException("Method generation error", e); + } + code.finish(); + String codeStr = code.toString(); + if (codeStr.isEmpty()) { + return Collections.emptyList(); + } + + String[] lines = codeStr.split(CodeWriter.NL); + Map lineMapping = code.getLineMapping(); + Map annotations = code.getAnnotations(); + long mthCodeOffset = mth.getMethodCodeOffset() + 16; + + int linesCount = lines.length; + List codeLines = new ArrayList<>(linesCount); + for (int i = 0; i < linesCount; i++) { + String codeLine = lines[i]; + int line = i + 2; + JsonCodeLine jsonCodeLine = new JsonCodeLine(); + jsonCodeLine.setCode(codeLine); + jsonCodeLine.setSourceLine(lineMapping.get(line)); + Object obj = annotations.get(new CodePosition(line, 0)); + if (obj instanceof InsnNode) { + int offset = ((InsnNode) obj).getOffset(); + jsonCodeLine.setOffset("0x" + Long.toHexString(mthCodeOffset + offset * 2)); + } + codeLines.add(jsonCodeLine); + } + return codeLines; + } + + private String getTypeAlias(ArgType clsType) { + if (Objects.equals(clsType, ArgType.OBJECT)) { + return ArgType.OBJECT.getObject(); + } + if (clsType.isObject()) { + ClassInfo classInfo = ClassInfo.fromType(root, clsType); + return classInfo.getAliasFullName(); + } + return clsType.toString(); + } + + private String getClassTypeStr(ClassNode cls) { + if (cls.isEnum()) { + return "enum"; + } + if (cls.getAccessFlags().isInterface()) { + return "interface"; + } + return "class"; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/JsonMappingGen.java b/jadx-core/src/main/java/jadx/core/codegen/json/JsonMappingGen.java new file mode 100644 index 000000000..86c8a9f9e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/JsonMappingGen.java @@ -0,0 +1,107 @@ +package jadx.core.codegen.json; + +import java.io.File; +import java.io.FileWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import jadx.api.JadxArgs; +import jadx.core.codegen.json.mapping.JsonClsMapping; +import jadx.core.codegen.json.mapping.JsonFieldMapping; +import jadx.core.codegen.json.mapping.JsonMapping; +import jadx.core.codegen.json.mapping.JsonMthMapping; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.info.MethodInfo; +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.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; + +public class JsonMappingGen { + private static final Logger LOG = LoggerFactory.getLogger(JsonMappingGen.class); + + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES) + .disableHtmlEscaping() + .create(); + + public static void dump(RootNode root) { + JsonMapping mapping = new JsonMapping(); + fillMapping(mapping, root); + + JadxArgs args = root.getArgs(); + File outDirSrc = args.getOutDirSrc().getAbsoluteFile(); + File mappingFile = new File(outDirSrc, "mapping.json"); + FileUtils.makeDirsForFile(mappingFile); + try (Writer writer = new FileWriter(mappingFile)) { + GSON.toJson(mapping, writer); + LOG.info("Save mappings to {}", mappingFile.getAbsolutePath()); + } catch (Exception e) { + throw new JadxRuntimeException("Failed to save mapping json", e); + } + } + + private static void fillMapping(JsonMapping mapping, RootNode root) { + List classes = root.getClasses(true); + mapping.setClasses(new ArrayList<>(classes.size())); + for (ClassNode cls : classes) { + ClassInfo classInfo = cls.getClassInfo(); + JsonClsMapping jsonCls = new JsonClsMapping(); + jsonCls.setName(classInfo.getRawName()); + jsonCls.setAlias(classInfo.getAliasFullName()); + jsonCls.setInner(classInfo.isInner()); + jsonCls.setJson(cls.getTopParentClass().getClassInfo().getAliasFullPath() + ".json"); + if (classInfo.isInner()) { + jsonCls.setTopClass(cls.getTopParentClass().getClassInfo().getFullName()); + } + addFields(cls, jsonCls); + addMethods(cls, jsonCls); + mapping.getClasses().add(jsonCls); + } + } + + private static void addMethods(ClassNode cls, JsonClsMapping jsonCls) { + List methods = cls.getMethods(); + if (methods.isEmpty()) { + return; + } + jsonCls.setMethods(new ArrayList<>(methods.size())); + for (MethodNode method : methods) { + JsonMthMapping jsonMethod = new JsonMthMapping(); + MethodInfo methodInfo = method.getMethodInfo(); + jsonMethod.setSignature(methodInfo.getShortId()); + jsonMethod.setName(methodInfo.getName()); + jsonMethod.setAlias(methodInfo.getAlias()); + jsonMethod.setOffset("0x" + Long.toHexString(method.getMethodCodeOffset())); + jsonCls.getMethods().add(jsonMethod); + } + } + + private static void addFields(ClassNode cls, JsonClsMapping jsonCls) { + List fields = cls.getFields(); + if (fields.isEmpty()) { + return; + } + jsonCls.setFields(new ArrayList<>(fields.size())); + for (FieldNode field : fields) { + JsonFieldMapping jsonField = new JsonFieldMapping(); + jsonField.setName(field.getName()); + jsonField.setAlias(field.getAlias()); + jsonCls.getFields().add(jsonField); + } + } + + private JsonMappingGen() { + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonClass.java b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonClass.java new file mode 100644 index 000000000..4ab0d05b2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonClass.java @@ -0,0 +1,94 @@ +package jadx.core.codegen.json.cls; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public class JsonClass extends JsonNode { + @SerializedName("package") + private String pkg; + private String type; // class, interface, enum + @SerializedName("extends") + private String superClass; + @SerializedName("implements") + private List interfaces; + private String dex; + + private List fields; + private List methods; + private List innerClasses; + + private List imports; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSuperClass() { + return superClass; + } + + public void setSuperClass(String superClass) { + this.superClass = superClass; + } + + public List getInterfaces() { + return interfaces; + } + + public void setInterfaces(List interfaces) { + this.interfaces = interfaces; + } + + public List getFields() { + return fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + + public List getMethods() { + return methods; + } + + public void setMethods(List methods) { + this.methods = methods; + } + + public List getInnerClasses() { + return innerClasses; + } + + public void setInnerClasses(List innerClasses) { + this.innerClasses = innerClasses; + } + + public String getPkg() { + return pkg; + } + + public void setPkg(String pkg) { + this.pkg = pkg; + } + + public String getDex() { + return dex; + } + + public void setDex(String dex) { + this.dex = dex; + } + + public List getImports() { + return imports; + } + + public void setImports(List imports) { + this.imports = imports; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonCodeLine.java b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonCodeLine.java new file mode 100644 index 000000000..c08cf4355 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonCodeLine.java @@ -0,0 +1,33 @@ +package jadx.core.codegen.json.cls; + +import org.jetbrains.annotations.Nullable; + +public class JsonCodeLine { + private String code; + private String offset; + private Integer sourceLine; + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getOffset() { + return offset; + } + + public void setOffset(String offset) { + this.offset = offset; + } + + public Integer getSourceLine() { + return sourceLine; + } + + public void setSourceLine(@Nullable Integer sourceLine) { + this.sourceLine = sourceLine; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonField.java b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonField.java new file mode 100644 index 000000000..0b243db5b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonField.java @@ -0,0 +1,5 @@ +package jadx.core.codegen.json.cls; + +public class JsonField extends JsonNode { + String type; +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonMethod.java b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonMethod.java new file mode 100644 index 000000000..cf21e11c2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonMethod.java @@ -0,0 +1,51 @@ +package jadx.core.codegen.json.cls; + +import java.util.List; + +public class JsonMethod extends JsonNode { + private String signature; + private String returnType; + private List arguments; + private List lines; + private String offset; + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public String getReturnType() { + return returnType; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public List getArguments() { + return arguments; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public List getLines() { + return lines; + } + + public void setLines(List lines) { + this.lines = lines; + } + + public String getOffset() { + return offset; + } + + public void setOffset(String offset) { + this.offset = offset; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonNode.java b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonNode.java new file mode 100644 index 000000000..fdb5d96f4 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/cls/JsonNode.java @@ -0,0 +1,40 @@ +package jadx.core.codegen.json.cls; + +public class JsonNode { + private String name; + private String alias; + private String declaration; + private int accessFlags; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getDeclaration() { + return declaration; + } + + public void setDeclaration(String declaration) { + this.declaration = declaration; + } + + public int getAccessFlags() { + return accessFlags; + } + + public void setAccessFlags(int accessFlags) { + this.accessFlags = accessFlags; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonClsMapping.java b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonClsMapping.java new file mode 100644 index 000000000..b7ab7e730 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonClsMapping.java @@ -0,0 +1,71 @@ +package jadx.core.codegen.json.mapping; + +import java.util.List; + +public class JsonClsMapping { + private String name; + private String alias; + + private String json; + private boolean inner; + private String topClass; + + private List fields; + private List methods; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getJson() { + return json; + } + + public void setJson(String json) { + this.json = json; + } + + public boolean isInner() { + return inner; + } + + public void setInner(boolean inner) { + this.inner = inner; + } + + public String getTopClass() { + return topClass; + } + + public void setTopClass(String topClass) { + this.topClass = topClass; + } + + public List getFields() { + return fields; + } + + public void setFields(List fields) { + this.fields = fields; + } + + public List getMethods() { + return methods; + } + + public void setMethods(List methods) { + this.methods = methods; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonFieldMapping.java b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonFieldMapping.java new file mode 100644 index 000000000..131d64510 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonFieldMapping.java @@ -0,0 +1,22 @@ +package jadx.core.codegen.json.mapping; + +public class JsonFieldMapping { + private String name; + private String alias; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMapping.java b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMapping.java new file mode 100644 index 000000000..16cf85e97 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMapping.java @@ -0,0 +1,15 @@ +package jadx.core.codegen.json.mapping; + +import java.util.List; + +public class JsonMapping { + private List classes; + + public List getClasses() { + return classes; + } + + public void setClasses(List classes) { + this.classes = classes; + } +} diff --git a/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMthMapping.java b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMthMapping.java new file mode 100644 index 000000000..68ec435f2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/codegen/json/mapping/JsonMthMapping.java @@ -0,0 +1,40 @@ +package jadx.core.codegen.json.mapping; + +public class JsonMthMapping { + private String signature; + private String name; + private String alias; + private String offset; + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAlias() { + return alias; + } + + public void setAlias(String alias) { + this.alias = alias; + } + + public String getOffset() { + return offset; + } + + public void setOffset(String offset) { + this.offset = offset; + } +} diff --git a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java index 9747d07ec..eafa33440 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java +++ b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java @@ -142,7 +142,7 @@ public class Deobfuscator { } for (MethodInfo mth : o.getMethods()) { if (aliasToUse == null) { - if (mth.isRenamed() && !mth.isAliasFromPreset()) { + if (mth.hasAlias() && !mth.isAliasFromPreset()) { mth.setAlias(String.format("mo%d%s", id, prepareNamePart(mth.getName()))); } aliasToUse = mth.getAlias(); diff --git a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java index b08485ce1..02bd80f9c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java @@ -201,6 +201,10 @@ public class AccessInfo { } } + public int rawValue() { + return accFlags; + } + @Override public String toString() { return "AccessInfo: " + type + " 0x" + Integer.toHexString(accFlags) + " (" + rawString() + ')'; diff --git a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java index 983412c6e..ba9a2e782 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/FieldInfo.java @@ -1,5 +1,7 @@ package jadx.core.dex.info; +import java.util.Objects; + import com.android.dex.FieldId; import jadx.core.codegen.TypeGen; @@ -53,6 +55,10 @@ public final class FieldInfo { this.alias = alias; } + public boolean hasAlias() { + return !Objects.equals(name, alias); + } + public String getFullId() { return declClass.getFullName() + '.' + name + ':' + TypeGen.signature(type); } diff --git a/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java index 73d5e4202..f46f62c17 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/MethodInfo.java @@ -130,7 +130,7 @@ public final class MethodInfo { this.alias = alias; } - public boolean isRenamed() { + public boolean hasAlias() { return !name.equals(alias); } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java index 1a446b728..91e59112d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java @@ -667,6 +667,10 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return mthInfo; } + public long getMethodCodeOffset() { + return noCode ? 0 : methodData.getCodeOffset(); + } + /** * Stat method. * Calculate instructions count as a measure of method size diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/FallbackModeVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/FallbackModeVisitor.java index 6ca8ef972..86689cacc 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/FallbackModeVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/FallbackModeVisitor.java @@ -1,13 +1,22 @@ package jadx.core.dex.visitors; +import jadx.core.codegen.json.JsonMappingGen; import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.utils.exceptions.JadxException; public class FallbackModeVisitor extends AbstractVisitor { + @Override + public void init(RootNode root) { + if (root.getArgs().isJsonOutput()) { + JsonMappingGen.dump(root); + } + } + @Override public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java index a5df5868b..9f2ca19b0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/RenameVisitor.java @@ -10,6 +10,7 @@ import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.core.Consts; +import jadx.core.codegen.json.JsonMappingGen; import jadx.core.deobf.Deobfuscator; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; @@ -51,6 +52,9 @@ public class RenameVisitor extends AbstractVisitor { deobfuscator.savePresets(); deobfuscator.clear(); } + if (args.isJsonOutput()) { + JsonMappingGen.dump(root); + } } private static void checkClasses(Deobfuscator deobfuscator, RootNode root, JadxArgs args) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java index b51fe54cf..224a8cc81 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java @@ -13,7 +13,7 @@ public class SaveCode { private SaveCode() { } - public static void save(File dir, JadxArgs args, ClassNode cls) { + public static void save(File dir, ClassNode cls) { if (cls.contains(AFlag.DONT_GENERATE)) { return; } @@ -21,10 +21,24 @@ public class SaveCode { if (clsCode == null) { throw new JadxRuntimeException("Code not generated for class " + cls.getFullName()); } - String fileName = cls.getClassInfo().getAliasFullPath() + ".java"; - if (args.isFallbackMode()) { - fileName += ".jadx"; + if (clsCode == CodeWriter.EMPTY) { + return; } + String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls); clsCode.save(dir, fileName); } + + private static String getFileExtension(ClassNode cls) { + JadxArgs.OutputFormatEnum outputFormat = cls.root().getArgs().getOutputFormat(); + switch (outputFormat) { + case JAVA: + return ".java"; + + case JSON: + return ".json"; + + default: + throw new JadxRuntimeException("Unknown output format: " + outputFormat); + } + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java index 9289c14bb..8aa639872 100644 --- a/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/CodeGenUtils.java @@ -6,6 +6,7 @@ import jadx.core.codegen.CodeWriter; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; import jadx.core.dex.attributes.nodes.RenameReasonAttr; +import jadx.core.dex.attributes.nodes.SourceFileAttr; public class CodeGenUtils { @@ -27,6 +28,13 @@ public class CodeGenUtils { code.add(" */"); } + public static void addSourceFileInfo(CodeWriter code, AttrNode node) { + SourceFileAttr sourceFileAttr = node.get(AType.SOURCE_FILE); + if (sourceFileAttr != null) { + code.startLine("/* compiled from: ").add(sourceFileAttr.getFileName()).add(" */"); + } + } + private CodeGenUtils() { } } diff --git a/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java b/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java index fe4146a73..c399e5d16 100644 --- a/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java +++ b/jadx-core/src/main/java/jadx/core/utils/ImmutableList.java @@ -210,11 +210,26 @@ public final class ImmutableList implements List, RandomAccess { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { - return false; + if (o instanceof ImmutableList) { + ImmutableList other = (ImmutableList) o; + return Arrays.equals(arr, other.arr); } - ImmutableList that = (ImmutableList) o; - return Arrays.equals(arr, that.arr); + if (o instanceof List) { + List other = (List) o; + int size = size(); + if (size != other.size()) { + return false; + } + for (int i = 0; i < size; i++) { + E e1 = arr[i]; + Object e2 = other.get(i); + if (!Objects.equals(e1, e2)) { + return false; + } + } + return true; + } + return false; } @Override diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index ad8c42fd6..796be6a6f 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -3,7 +3,9 @@ package jadx.core.utils; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -154,6 +156,17 @@ public class Utils { } } + public static List collectionMap(Collection list, Function mapFunc) { + if (list == null || list.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(list.size()); + for (T t : list) { + result.add(mapFunc.apply(t)); + } + return result; + } + public static List lockList(List list) { if (list.isEmpty()) { return Collections.emptyList(); diff --git a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java index e6a7cd029..e5b22039e 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/InputFile.java @@ -97,7 +97,7 @@ public class InputFile { } private void addDexFile(Path path) throws IOException { - addDexFile("", path); + addDexFile(path.getFileName().toString(), path); } private void addDexFile(String fileName, Path path) throws IOException { diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestJsonOutput.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestJsonOutput.java new file mode 100644 index 000000000..f4e0eaadc --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestJsonOutput.java @@ -0,0 +1,62 @@ +package jadx.tests.integration.others; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.api.JadxArgs; +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +public class TestJsonOutput extends IntegrationTest { + + public static class TestCls { + private final String prefix = "list: "; + + static { + System.out.println("test"); + } + + public void test(boolean b, List list) { + if (b) { + System.out.println(prefix + list); + } + } + + public static class Inner implements Runnable { + @Override + public void run() { + System.out.println("run"); + } + } + } + + @Test + public void test() { + disableCompilation(); + args.setOutputFormat(JadxArgs.OutputFormatEnum.JSON); + + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("\"offset\": \"0x")); + assertThat(code, containsOne("public static class Inner implements Runnable")); + } + + @Test + public void testFallback() { + disableCompilation(); + setFallback(); + args.setOutputFormat(JadxArgs.OutputFormatEnum.JSON); + + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("\"offset\": \"0x")); + assertThat(code, containsOne("public static class Inner implements java.lang.Runnable")); + } +} diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 26c570dc4..80ec315f0 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -15,7 +15,6 @@ dependencies { compile(project(":jadx-cli")) compile 'com.fifesoft:rsyntaxtextarea:3.0.2' - compile 'com.google.code.gson:gson:2.8.5' compile files('libs/jfontchooser-1.0.5.jar') compile 'hu.kazocsaba:image-viewer:1.2.3' 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 56284b594..bbe6abca5 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; @@ -42,7 +39,9 @@ public class JadxSettings extends JadxCLIArgs { private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont(); static final Set SKIP_FIELDS = new HashSet<>(Arrays.asList( - "files", "input", "outDir", "outDirSrc", "outDirRes", "verbose", "printVersion", "printHelp")); + "files", "input", "outDir", "outDirSrc", "outDirRes", "outputFormat", + "verbose", "printVersion", "printHelp")); + private Path lastSaveProjectPath = USER_HOME; private Path lastOpenFilePath = USER_HOME; private Path lastSaveFilePath = USER_HOME;