diff --git a/.gitignore b/.gitignore index d8906bd22..71fea2b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ jadx-output/ *.dump *.log *.cfg +*.orig diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index d66b929fc..54711f29c 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -110,7 +110,7 @@ public class JadxCLIArgs { return process(jcw); } - private boolean process(JCommanderWrapper jcw) { + private boolean process(JCommanderWrapper jcw) { if (printHelp) { jcw.printUsage(); return false; diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index ccd40ab34..66881b1af 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -294,6 +294,10 @@ public final class JadxDecompiler { return root; } + List getPasses() { + return passes; + } + synchronized BinaryXMLParser getXmlParser() { if (xmlParser == null) { xmlParser = new BinaryXMLParser(root); diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index abeb17f0e..61de7ab72 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -11,9 +11,8 @@ import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; import jadx.core.dex.visitors.ClassModifier; -import jadx.core.dex.visitors.CodeShrinker; import jadx.core.dex.visitors.ConstInlineVisitor; -import jadx.core.dex.visitors.DebugInfoVisitor; +import jadx.core.dex.visitors.ConstructorVisitor; import jadx.core.dex.visitors.DependencyCollector; import jadx.core.dex.visitors.DotGraphVisitor; import jadx.core.dex.visitors.EnumVisitor; @@ -21,6 +20,8 @@ import jadx.core.dex.visitors.ExtractFieldInit; import jadx.core.dex.visitors.FallbackModeVisitor; import jadx.core.dex.visitors.FixAccessModifiers; import jadx.core.dex.visitors.IDexTreeVisitor; +import jadx.core.dex.visitors.InitCodeVariables; +import jadx.core.dex.visitors.MarkFinallyVisitor; import jadx.core.dex.visitors.MethodInlineVisitor; import jadx.core.dex.visitors.ModVisitor; import jadx.core.dex.visitors.PrepareForCodeGen; @@ -28,20 +29,21 @@ import jadx.core.dex.visitors.ReSugarCode; import jadx.core.dex.visitors.RenameVisitor; import jadx.core.dex.visitors.SimplifyVisitor; import jadx.core.dex.visitors.blocksmaker.BlockExceptionHandler; -import jadx.core.dex.visitors.blocksmaker.BlockFinallyExtract; import jadx.core.dex.visitors.blocksmaker.BlockFinish; import jadx.core.dex.visitors.blocksmaker.BlockProcessor; import jadx.core.dex.visitors.blocksmaker.BlockSplitter; +import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor; +import jadx.core.dex.visitors.debuginfo.DebugInfoParseVisitor; import jadx.core.dex.visitors.regions.CheckRegions; +import jadx.core.dex.visitors.regions.CleanRegions; import jadx.core.dex.visitors.regions.IfRegionVisitor; import jadx.core.dex.visitors.regions.LoopRegionVisitor; -import jadx.core.dex.visitors.regions.ProcessVariables; import jadx.core.dex.visitors.regions.RegionMakerVisitor; import jadx.core.dex.visitors.regions.ReturnVisitor; -import jadx.core.dex.visitors.ssa.EliminatePhiNodes; +import jadx.core.dex.visitors.regions.variables.ProcessVariables; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.dex.visitors.ssa.SSATransform; -import jadx.core.dex.visitors.typeinference.FinishTypeInference; -import jadx.core.dex.visitors.typeinference.TypeInference; +import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; public class Jadx { private static final Logger LOG = LoggerFactory.getLogger(Jadx.class); @@ -60,31 +62,32 @@ public class Jadx { if (args.isFallbackMode()) { passes.add(new FallbackModeVisitor()); } else { - passes.add(new BlockSplitter()); - passes.add(new BlockProcessor()); - passes.add(new BlockExceptionHandler()); - passes.add(new BlockFinallyExtract()); - passes.add(new BlockFinish()); - - passes.add(new SSATransform()); if (args.isDebugInfo()) { - passes.add(new DebugInfoVisitor()); + passes.add(new DebugInfoParseVisitor()); } - passes.add(new TypeInference()); + passes.add(new BlockSplitter()); if (args.isRawCFGOutput()) { passes.add(DotGraphVisitor.dumpRaw()); } + passes.add(new BlockProcessor()); + passes.add(new BlockExceptionHandler()); + passes.add(new BlockFinish()); + + passes.add(new SSATransform()); + passes.add(new ConstructorVisitor()); + passes.add(new InitCodeVariables()); + passes.add(new MarkFinallyVisitor()); passes.add(new ConstInlineVisitor()); - passes.add(new FinishTypeInference()); - passes.add(new EliminatePhiNodes()); + passes.add(new TypeInferenceVisitor()); + if (args.isDebugInfo()) { + passes.add(new DebugInfoApplyVisitor()); + } passes.add(new ModVisitor()); - - passes.add(new CodeShrinker()); + passes.add(new CodeShrinkVisitor()); passes.add(new ReSugarCode()); - if (args.isCfgOutput()) { passes.add(DotGraphVisitor.dump()); } @@ -92,8 +95,9 @@ public class Jadx { passes.add(new RegionMakerVisitor()); passes.add(new IfRegionVisitor()); passes.add(new ReturnVisitor()); + passes.add(new CleanRegions()); - passes.add(new CodeShrinker()); + passes.add(new CodeShrinkVisitor()); passes.add(new SimplifyVisitor()); passes.add(new CheckRegions()); @@ -102,16 +106,15 @@ public class Jadx { passes.add(new ClassModifier()); passes.add(new MethodInlineVisitor()); passes.add(new EnumVisitor()); - passes.add(new PrepareForCodeGen()); passes.add(new LoopRegionVisitor()); - passes.add(new ProcessVariables()); + passes.add(new ProcessVariables()); + passes.add(new PrepareForCodeGen()); if (args.isCfgOutput()) { passes.add(DotGraphVisitor.dumpRegions()); } passes.add(new DependencyCollector()); - passes.add(new RenameVisitor()); } return passes; diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java index 51a7b04c1..ff2abcf16 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClsSet.java @@ -45,6 +45,8 @@ public class ClsSet { private static final String STRING_CHARSET = "US-ASCII"; + private static final NClass[] EMPTY_NCLASS_ARRAY = new NClass[0]; + private NClass[] classes; public void load(RootNode root) { @@ -93,7 +95,11 @@ public class ClsSet { parents.add(c); } } - return parents.toArray(new NClass[parents.size()]); + int size = parents.size(); + if (size == 0) { + return EMPTY_NCLASS_ARRAY; + } + return parents.toArray(new NClass[size]); } private static NClass getCls(String fullName, Map names) { diff --git a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java index 6143e23c1..d26090c9b 100644 --- a/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java +++ b/jadx-core/src/main/java/jadx/core/clsp/ClspGraph.java @@ -23,7 +23,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; public class ClspGraph { private static final Logger LOG = LoggerFactory.getLogger(ClspGraph.class); - private final Map> ancestorCache = Collections.synchronizedMap(new WeakHashMap>()); + private final Map> ancestorCache = Collections.synchronizedMap(new WeakHashMap<>()); private Map nameMap; private final Set missingClasses = new HashSet<>(); @@ -58,6 +58,10 @@ public class ClspGraph { } } + public boolean isClsKnown(String fullName) { + return nameMap.containsKey(fullName); + } + private NClass addClass(ClassNode cls) { String rawName = cls.getRawName(); NClass nClass = new NClass(rawName, -1); @@ -65,11 +69,24 @@ public class ClspGraph { return nClass; } + /** + * @return {@code clsName} instanceof {@code implClsName} + */ public boolean isImplements(String clsName, String implClsName) { Set anc = getAncestors(clsName); return anc.contains(implClsName); } + public List getImplementations(String clsName) { + List list = new ArrayList<>(); + for (String cls : nameMap.keySet()) { + if (isImplements(cls, clsName)) { + list.add(cls); + } + } + return list; + } + public String getCommonAncestor(String clsName, String implClsName) { if (clsName.equals(implClsName)) { return clsName; @@ -100,7 +117,7 @@ public class ClspGraph { return null; } - private Set getAncestors(String clsName) { + public Set getAncestors(String clsName) { Set result = ancestorCache.get(clsName); if (result != null) { return result; 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 09066ce82..30d1616d0 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java @@ -18,7 +18,6 @@ import jadx.core.dex.attributes.AttrNode; 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.JadxWarn; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.attributes.nodes.SourceFileAttr; import jadx.core.dex.info.AccessInfo; @@ -263,10 +262,11 @@ public class ClassGen { addMethod(code, mth); } catch (Exception e) { code.newLine().add("/*"); - code.newLine().add(ErrorsCounter.methodError(mth, "Method generation error", e)); - code.newLine().add(Utils.getStackTrace(e)); + code.newLine().addMultiLine(ErrorsCounter.methodError(mth, "Method generation error", e)); + code.newLine().addMultiLine(Utils.getStackTrace(e)); code.newLine().add("*/"); code.setIndent(savedIndent); + mth.addError("Method generation error: " + e.getMessage(), e); } } } @@ -331,7 +331,6 @@ public class ClassGen { private void insertDecompilationProblems(CodeWriter code, AttrNode node) { List errors = node.getAll(AType.JADX_ERROR); - List warns = node.getAll(AType.JADX_WARN); if (!errors.isEmpty()) { errors.forEach(err -> { code.startLine("/* JADX ERROR: ").add(err.getError()); @@ -344,8 +343,10 @@ public class ClassGen { code.add("*/"); }); } + List warns = node.getAll(AType.JADX_WARN); if (!warns.isEmpty()) { - warns.forEach(warn -> code.startLine("/* JADX WARNING: ").addMultiLine(warn.getWarn()).add(" */")); + warns.stream().distinct() + .forEach(warn -> code.startLine("/* JADX WARNING: ").addMultiLine(warn).add(" */")); } } 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 ec9cc1ef7..561cfc0d4 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java @@ -1,7 +1,6 @@ package jadx.core.codegen; import java.util.ArrayList; -import java.util.Arrays; import java.util.EnumSet; import java.util.Iterator; import java.util.List; @@ -35,6 +34,7 @@ import jadx.core.dex.instructions.InvokeType; import jadx.core.dex.instructions.NewArrayNode; import jadx.core.dex.instructions.SwitchNode; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.FieldArg; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; @@ -123,12 +123,16 @@ public class InsnGen { } public void declareVar(CodeWriter code, RegisterArg arg) { - if (arg.getSVar().contains(AFlag.FINAL)) { + declareVar(code, arg.getSVar().getCodeVar()); + } + + public void declareVar(CodeWriter code, CodeVar codeVar) { + if (codeVar.isFinal()) { code.add("final "); } - useType(code, arg.getType()); + useType(code, codeVar.getType()); code.add(' '); - code.add(mgen.getNameGen().assignArg(arg)); + code.add(mgen.getNameGen().assignArg(codeVar)); } private String lit(LiteralArg arg) { @@ -201,11 +205,11 @@ public class InsnGen { mgen.getClassGen().useType(code, type); } - public boolean makeInsn(InsnNode insn, CodeWriter code) throws CodegenException { - return makeInsn(insn, code, null); + public void makeInsn(InsnNode insn, CodeWriter code) throws CodegenException { + makeInsn(insn, code, null); } - protected boolean makeInsn(InsnNode insn, CodeWriter code, Flags flag) throws CodegenException { + protected void makeInsn(InsnNode insn, CodeWriter code, Flags flag) throws CodegenException { try { Set state = EnumSet.noneOf(Flags.class); if (flag == Flags.BODY_ONLY || flag == Flags.BODY_ONLY_NOWRAP) { @@ -227,7 +231,6 @@ public class InsnGen { } catch (Exception th) { throw new CodegenException(mth, "Error generate insn: " + insn, th); } - return true; } private void makeInsnBody(CodeWriter code, InsnNode insn, Set state) throws CodegenException { @@ -485,19 +488,7 @@ public class InsnGen { case FILL_ARRAY: fallbackOnlyInsn(insn); FillArrayNode arrayNode = (FillArrayNode) insn; - Object data = arrayNode.getData(); - String arrStr; - if (data instanceof int[]) { - arrStr = Arrays.toString((int[]) data); - } else if (data instanceof short[]) { - arrStr = Arrays.toString((short[]) data); - } else if (data instanceof byte[]) { - arrStr = Arrays.toString((byte[]) data); - } else if (data instanceof long[]) { - arrStr = Arrays.toString((long[]) data); - } else { - arrStr = "?"; - } + String arrStr = arrayNode.dataToString(); code.add('{').add(arrStr.substring(1, arrStr.length() - 1)).add('}'); break; @@ -508,7 +499,6 @@ public class InsnGen { break; case PHI: - case MERGE: fallbackOnlyInsn(insn); code.add(insn.getType().toString()).add('('); for (InsnArg insnArg : insn.getArguments()) { @@ -610,11 +600,8 @@ public class InsnGen { // inline method MethodNode callMthNode = mth.root().deepResolveMethod(callMth); - if (callMthNode != null) { - if (inlineMethod(callMthNode, insn, code)) { - return; - } - callMth = callMthNode.getMethodInfo(); + if (callMthNode != null && inlineMethod(callMthNode, insn, code)) { + return; } int k = 0; @@ -653,8 +640,10 @@ public class InsnGen { } if (callMthNode != null) { code.attachAnnotation(callMthNode); + code.add(callMthNode.getAlias()); + } else { + code.add(callMth.getAlias()); } - code.add(callMth.getAlias()); generateMethodArguments(code, insn, k, callMthNode); } @@ -675,7 +664,7 @@ public class InsnGen { do { ClassInfo nextClsInfo = nextParent.getClassInfo(); if (nextClsInfo.equals(declClass) - || ArgType.isInstanceOf(mth.dex(), nextClsInfo.getType(), declClass.getType())) { + || ArgType.isInstanceOf(mth.root(), nextClsInfo.getType(), declClass.getType())) { if (nextParent == useCls) { return null; } @@ -737,7 +726,14 @@ public class InsnGen { * Add additional cast for overloaded method argument. */ private boolean processOverloadedArg(CodeWriter code, MethodNode callMth, InsnArg arg, int origPos) { - ArgType origType = callMth.getMethodInfo().getArgumentsTypes().get(origPos); + ArgType origType; + List arguments = callMth.getArguments(false); + if (arguments == null || arguments.isEmpty()) { + mth.addComment("JADX INFO: used method not loaded: " + callMth + ", types can be incorrect"); + origType = callMth.getMethodInfo().getArgumentsTypes().get(origPos); + } else { + origType = arguments.get(origPos).getInitType(); + } if (!arg.getType().equals(origType)) { code.add('('); useType(code, origType); 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 9aac0ccec..7e5a2887f 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/MethodGen.java @@ -11,10 +11,13 @@ import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.annotations.MethodParameters; +import jadx.core.dex.attributes.nodes.JumpInfo; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.instructions.IfNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.InsnNode; @@ -22,7 +25,6 @@ import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.CatchAttr; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.FallbackModeVisitor; -import jadx.core.utils.ErrorsCounter; import jadx.core.utils.InsnUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.CodegenException; @@ -112,11 +114,10 @@ public class MethodGen { } else if (args.size() > 2) { args = args.subList(2, args.size()); } else { - LOG.warn(ErrorsCounter.formatMsg(mth, - "Incorrect number of args for enum constructor: " + args.size() - + " (expected >= 2)" - )); + mth.addComment("JADX WARN: Incorrect number of args for enum constructor: " + args.size() + " (expected >= 2)"); } + } else if (mth.contains(AFlag.SKIP_FIRST_ARG)) { + args = args.subList(1, args.size()); } addMethodArguments(code, args); code.add(')'); @@ -125,40 +126,48 @@ public class MethodGen { return true; } - private void addMethodArguments(CodeWriter argsCode, List args) { + private void addMethodArguments(CodeWriter code, List args) { MethodParameters paramsAnnotation = mth.get(AType.ANNOTATION_MTH_PARAMETERS); int i = 0; - for (Iterator it = args.iterator(); it.hasNext(); ) { - RegisterArg arg = it.next(); + Iterator it = args.iterator(); + while (it.hasNext()) { + RegisterArg mthArg = it.next(); + SSAVar ssaVar = mthArg.getSVar(); + CodeVar var; + if (ssaVar == null) { + // null for abstract or interface methods + var = CodeVar.fromMthArg(mthArg); + } else { + var = ssaVar.getCodeVar(); + } // add argument annotation if (paramsAnnotation != null) { - annotationGen.addForParameter(argsCode, paramsAnnotation, i); + annotationGen.addForParameter(code, paramsAnnotation, i); } - SSAVar argSVar = arg.getSVar(); - if (argSVar != null && argSVar.contains(AFlag.FINAL)) { - argsCode.add("final "); + if (var.isFinal()) { + code.add("final "); } + ArgType argType = var.getType(); if (!it.hasNext() && mth.getAccessFlags().isVarArgs()) { // change last array argument to varargs - ArgType type = arg.getType(); - if (type.isArray()) { - ArgType elType = type.getArrayElement(); - classGen.useType(argsCode, elType); - argsCode.add("..."); + if (argType.isArray()) { + ArgType elType = argType.getArrayElement(); + classGen.useType(code, elType); + code.add("..."); } else { - LOG.warn(ErrorsCounter.formatMsg(mth, "Last argument in varargs method not array")); - classGen.useType(argsCode, arg.getType()); + mth.addComment("JADX INFO: Last argument in varargs method is not array: " + var); + classGen.useType(code, argType); } } else { - classGen.useType(argsCode, arg.getType()); + classGen.useType(code, argType); } - argsCode.add(' '); - argsCode.add(nameGen.assignArg(arg)); + code.add(' '); + code.add(nameGen.assignArg(var)); i++; if (it.hasNext()) { - argsCode.add(", "); + code.add(", "); } } } @@ -192,6 +201,7 @@ public class MethodGen { if (mth.getInstructions() == null) { // load original instructions try { + mth.unload(); mth.load(); DepthTraversal.visit(new FallbackModeVisitor(), mth); } catch (DecodeException e) { @@ -213,29 +223,59 @@ public class MethodGen { public static void addFallbackInsns(CodeWriter code, MethodNode mth, InsnNode[] insnArr, boolean addLabels) { InsnGen insnGen = new InsnGen(getFallbackMethodGen(mth), true); + InsnNode prevInsn = null; for (InsnNode insn : insnArr) { if (insn == null || insn.getType() == InsnType.NOP) { continue; } - if (addLabels && (insn.contains(AType.JUMP) || insn.contains(AType.EXC_HANDLER))) { + if (addLabels && needLabel(insn, prevInsn)) { code.decIndent(); code.startLine(getLabelName(insn.getOffset()) + ':'); code.incIndent(); } try { - if (insnGen.makeInsn(insn, code)) { - CatchAttr catchAttr = insn.get(AType.CATCH_BLOCK); - if (catchAttr != null) { - code.add("\t " + catchAttr); + code.startLine(); + RegisterArg resArg = insn.getResult(); + if (resArg != null) { + ArgType varType = resArg.getInitType(); + if (varType.isTypeKnown()) { + code.add(varType.toString()).add(' '); } } + insnGen.makeInsn(insn, code, InsnGen.Flags.INLINE); + CatchAttr catchAttr = insn.get(AType.CATCH_BLOCK); + if (catchAttr != null) { + code.add(" // " + catchAttr); + } } catch (CodegenException e) { LOG.debug("Error generate fallback instruction: ", e.getCause()); code.startLine("// error: " + insn); } + prevInsn = insn; } } + private static boolean needLabel(InsnNode insn, InsnNode prevInsn) { + if (insn.contains(AType.EXC_HANDLER)) { + return true; + } + if (insn.contains(AType.JUMP)) { + // don't add label for ifs else branch + if (prevInsn != null && prevInsn.getType() == InsnType.IF) { + List jumps = insn.getAll(AType.JUMP); + if (jumps.size() == 1) { + JumpInfo jump = jumps.get(0); + if (jump.getSrc() == prevInsn.getOffset() && jump.getDest() == insn.getOffset()) { + int target = ((IfNode) prevInsn).getTarget(); + return insn.getOffset() == target; + } + } + } + return true; + } + return false; + } + /** * Return fallback variant of method codegen */ diff --git a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java index b39d71436..b1588a935 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/NameGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/NameGen.java @@ -1,6 +1,7 @@ package jadx.core.codegen; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -11,12 +12,15 @@ import jadx.core.dex.info.ClassInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.instructions.mods.ConstructorInsn; +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.utils.StringUtils; @@ -44,22 +48,35 @@ public class NameGen { "java.lang.Byte", "b", "java.lang.Float", "f", "java.lang.Long", "l", - "java.lang.Double", "d" + "java.lang.Double", "d", + "java.lang.StringBuilder", "sb", + "java.lang.Exception", "exc" ); } public NameGen(MethodNode mth, boolean fallback) { this.mth = mth; this.fallback = fallback; + addNamesUsedInClass(); } - public String assignArg(RegisterArg arg) { - String name = makeArgName(arg); + private void addNamesUsedInClass() { + ClassNode parentClass = mth.getParentClass(); + for (FieldNode field : parentClass.getFields()) { + varNames.add(field.getAlias()); + } + for (ClassNode innerClass : parentClass.getInnerClasses()) { + varNames.add(innerClass.getAlias().getShortName()); + } + } + + public String assignArg(CodeVar var) { + String name = makeArgName(var); if (fallback) { return name; } name = getUniqueVarName(name); - arg.setName(name); + var.setName(name); return name; } @@ -99,39 +116,50 @@ public class NameGen { return r; } - private String makeArgName(RegisterArg arg) { + private String makeArgName(CodeVar var) { if (fallback) { - return getFallbackName(arg); + return getFallbackName(var); } - if (arg.isThis()) { + if (var.isThis()) { return RegisterArg.THIS_ARG_NAME; } - String name = arg.getName(); - String varName = name != null ? name : guessName(arg); + String name = var.getName(); + String varName = name != null ? name : guessName(var); if (NameMapper.isReserved(varName)) { - return varName + 'R'; + varName = varName + 'R'; + } + if (!NameMapper.isValidIdentifier(varName)) { + varName = getFallbackName(var); } return varName; } + private String getFallbackName(CodeVar var) { + return getFallbackName(var.getSsaVars().get(0).getAssign()); + } + private String getFallbackName(RegisterArg arg) { return "r" + arg.getRegNum(); } - private String guessName(RegisterArg arg) { - SSAVar sVar = arg.getSVar(); - if (sVar != null && sVar.getName() == null) { - RegisterArg assignArg = sVar.getAssign(); - InsnNode assignInsn = assignArg.getParentInsn(); - if (assignInsn != null) { - String name = makeNameFromInsn(assignInsn); - if (name != null && !NameMapper.isReserved(name)) { - assignArg.setName(name); - return name; + private String guessName(CodeVar var) { + List ssaVars = var.getSsaVars(); + if (ssaVars != null && !ssaVars.isEmpty()) { + // TODO: use all vars for better name generation + SSAVar ssaVar = ssaVars.get(0); + if (ssaVar != null && ssaVar.getName() == null) { + RegisterArg assignArg = ssaVar.getAssign(); + InsnNode assignInsn = assignArg.getParentInsn(); + if (assignInsn != null) { + String name = makeNameFromInsn(assignInsn); + if (name != null && !NameMapper.isReserved(name)) { + assignArg.setName(name); + return name; + } } } } - return makeNameForType(arg.getType()); + return makeNameForType(var.getType()); } private String makeNameForType(ArgType type) { @@ -237,6 +265,9 @@ public class NameGen { if ("forName".equals(name) && declType.equals(ArgType.CLASS)) { return OBJ_ALIAS.get(Consts.CLASS_CLASS); } + if (name.startsWith("to")) { + return fromName(name.substring(2)); + } return name; } } 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 e0cfbe431..ca797c01e 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -3,6 +3,7 @@ package jadx.core.codegen; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +15,7 @@ import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.SwitchNode; +import jadx.core.dex.instructions.args.CodeVar; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; @@ -35,6 +37,7 @@ import jadx.core.dex.regions.loops.ForLoop; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.utils.BlockUtils; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.RegionUtils; import jadx.core.utils.exceptions.CodegenException; @@ -75,7 +78,7 @@ public class RegionGen extends InsnGen { private void declareVars(CodeWriter code, IContainer cont) { DeclareVariablesAttr declVars = cont.get(AType.DECLARE_VARIABLES); if (declVars != null) { - for (RegisterArg v : declVars.getVars()) { + for (CodeVar v : declVars.getVars()) { code.startLine(); declareVar(code, v); code.add(';'); @@ -97,8 +100,12 @@ public class RegionGen extends InsnGen { } private void makeSimpleBlock(IBlock block, CodeWriter code) throws CodegenException { + if (block.contains(AFlag.DONT_GENERATE)) { + return; + } + for (InsnNode insn : block.getInstructions()) { - if (!insn.contains(AFlag.SKIP)) { + if (!insn.contains(AFlag.DONT_GENERATE)) { makeInsn(insn, code); } } @@ -232,7 +239,8 @@ public class RegionGen extends InsnGen { } private CodeWriter makeSwitch(SwitchRegion sw, CodeWriter code) throws CodegenException { - SwitchNode insn = (SwitchNode) sw.getHeader().getInstructions().get(0); + SwitchNode insn = (SwitchNode) BlockUtils.getLastInsn(sw.getHeader()); + Objects.requireNonNull(insn, "Switch insn not found in header"); InsnArg arg = insn.getArg(0); code.startLine("switch ("); addArg(code, arg, false); @@ -323,7 +331,8 @@ public class RegionGen extends InsnGen { code.add(' '); InsnArg arg = handler.getArg(); if (arg instanceof RegisterArg) { - code.add(mgen.getNameGen().assignArg((RegisterArg) arg)); + RegisterArg reg = (RegisterArg) arg; + code.add(mgen.getNameGen().assignArg(reg.getSVar().getCodeVar())); } else if (arg instanceof NamedArg) { code.add(mgen.getNameGen().assignNamedArg((NamedArg) arg)); } diff --git a/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java b/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java index 422d32dc5..400e06fbf 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java @@ -3,7 +3,6 @@ package jadx.core.codegen; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.JadxArgs; import jadx.core.deobf.NameMapper; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.PrimitiveType; @@ -38,18 +37,20 @@ public class TypeGen { return literalToString(lit, type, dexNode.root().getStringUtils()); } - @Deprecated - public static String literalToString(long lit, ArgType type) { - return literalToString(lit, type, new StringUtils(new JadxArgs())); - } - - private static String literalToString(long lit, ArgType type, StringUtils stringUtils) { + public static String literalToString(long lit, ArgType type, StringUtils stringUtils) { if (type == null || !type.isTypeKnown()) { String n = Long.toString(lit); if (Math.abs(lit) > 100) { - n += "; // 0x" + Long.toHexString(lit) - + " float:" + Float.intBitsToFloat((int) lit) - + " double:" + Double.longBitsToDouble(lit); + StringBuilder sb = new StringBuilder(); + sb.append(n).append("(0x").append(Long.toHexString(lit)); + if (type == null || type.contains(PrimitiveType.FLOAT)) { + sb.append(", float:").append(Float.intBitsToFloat((int) lit)); + } + if (type == null || type.contains(PrimitiveType.DOUBLE)) { + sb.append(", double:").append(Double.longBitsToDouble(lit)); + } + sb.append(')'); + return sb.toString(); } return n; } 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 d50cff37a..a82e9f5ac 100644 --- a/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java +++ b/jadx-core/src/main/java/jadx/core/deobf/Deobfuscator.java @@ -11,13 +11,13 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import jadx.core.dex.attributes.AFlag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.JadxArgs; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.SourceFileAttr; import jadx.core.dex.info.ClassInfo; @@ -226,8 +226,9 @@ public class Deobfuscator { clsInfo.rename(cls.dex().root(), fullName); } for (FieldNode field : cls.getFields()) { - if (field.contains(AFlag.DONT_RENAME)) - continue; + if (field.contains(AFlag.DONT_RENAME)) { + continue; + } renameField(field); } for (MethodNode mth : cls.getMethods()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java index 1a4e3c61c..27cec6bc1 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AFlag.java @@ -8,27 +8,39 @@ public enum AFlag { LOOP_END, SYNTHETIC, - FINAL, // SSAVar attribute for make var final RETURN, // block contains only return instruction ORIG_RETURN, - DECLARE_VAR, DONT_WRAP, - - DONT_SHRINK, DONT_INLINE, - DONT_GENERATE, + DONT_GENERATE, // process as usual, but don't output to generated code DONT_RENAME, // do not rename during deobfuscation - SKIP, - REMOVE, + REMOVE, // can be completely removed + ADDED_TO_REGION, + + FINALLY_INSNS, SKIP_FIRST_ARG, SKIP_ARG, // skip argument in invoke call ANONYMOUS_CONSTRUCTOR, ANONYMOUS_CLASS, + THIS, + /** + * RegisterArg attribute for method arguments + */ + METHOD_ARGUMENT, + + /** + * Type of RegisterArg or SSAVar can't be changed + */ + IMMUTABLE_TYPE, + + CUSTOM_DECLARE, // variable for this register don't need declaration + DECLARE_VAR, + ELSE_IF_CHAIN, WRAPPED, diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java index 35b7346cd..aaeccd901 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AType.java @@ -10,12 +10,13 @@ import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.IgnoreEdgeAttr; import jadx.core.dex.attributes.nodes.JadxError; -import jadx.core.dex.attributes.nodes.JadxWarn; import jadx.core.dex.attributes.nodes.JumpInfo; +import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.attributes.nodes.LoopLabelAttr; import jadx.core.dex.attributes.nodes.MethodInlineAttr; import jadx.core.dex.attributes.nodes.PhiListAttr; +import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.attributes.nodes.SourceFileAttr; import jadx.core.dex.nodes.parser.FieldInitAttr; import jadx.core.dex.trycatch.CatchAttr; @@ -34,9 +35,9 @@ public class AType { public static final AType> LOOP = new AType<>(); public static final AType> EDGE_INSN = new AType<>(); - public static final AType> JADX_ERROR = new AType<>(); - public static final AType> JADX_WARN = new AType<>(); - public static final AType> COMMENTS = new AType<>(); + public static final AType> JADX_ERROR = new AType<>(); // code failed to decompile completely + public static final AType> JADX_WARN = new AType<>(); // mark code as inconsistent (code can be viewed) + public static final AType> COMMENTS = new AType<>(); // any additional info about decompilation public static final AType EXC_HANDLER = new AType<>(); public static final AType CATCH_BLOCK = new AType<>(); @@ -54,4 +55,10 @@ public class AType { public static final AType DECLARE_VARIABLES = new AType<>(); public static final AType LOOP_LABEL = new AType<>(); public static final AType IGNORE_EDGE = new AType<>(); + + // method + public static final AType LOCAL_VARS_DEBUG_INFO = new AType<>(); + + // registers + public static final AType REG_DEBUG_INFO = new AType<>(); } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java index 00e37a572..d378b7e3e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrList.java @@ -25,6 +25,6 @@ public class AttrList implements IAttribute { @Override public String toString() { - return Utils.listToString(list); + return Utils.listToString(list, "\n"); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java index f0385b491..3d18035db 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttrNode.java @@ -42,6 +42,12 @@ public abstract class AttrNode implements IAttributeNode { return store; } + private void unloadIfEmpty() { + if (storage.isEmpty() && storage != EMPTY_ATTR_STORAGE) { + storage = EMPTY_ATTR_STORAGE; + } + } + @Override public boolean contains(AFlag flag) { return storage.contains(flag); @@ -70,21 +76,25 @@ public abstract class AttrNode implements IAttributeNode { @Override public void remove(AFlag flag) { storage.remove(flag); + unloadIfEmpty(); } @Override public void remove(AType type) { storage.remove(type); + unloadIfEmpty(); } @Override public void removeAttr(IAttribute attr) { storage.remove(attr); + unloadIfEmpty(); } @Override public void clearAttributes() { storage.clear(); + unloadIfEmpty(); } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java b/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java index 2e1c2053b..caefb44d6 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/AttributeStorage.java @@ -121,6 +121,6 @@ public class AttributeStorage { if (list.isEmpty()) { return ""; } - return "A:{" + Utils.listToString(list) + '}'; + return "A[" + Utils.listToString(list) + ']'; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java index d6933100a..cac361c26 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/IAttribute.java @@ -1,5 +1,5 @@ package jadx.core.dex.attributes; public interface IAttribute { - AType getType(); + AType getType(); } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/annotations/AnnotationsList.java b/jadx-core/src/main/java/jadx/core/dex/attributes/annotations/AnnotationsList.java index 72fc6d8e6..3278ca538 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/annotations/AnnotationsList.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/annotations/AnnotationsList.java @@ -12,7 +12,7 @@ import jadx.core.utils.Utils; public class AnnotationsList implements IAttribute { - public static final AnnotationsList EMPTY = new AnnotationsList(Collections.emptyList()); + public static final AnnotationsList EMPTY = new AnnotationsList(Collections.emptyList()); private final Map map; diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DeclareVariablesAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DeclareVariablesAttr.java index a552b60db..c0d988dda 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DeclareVariablesAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/DeclareVariablesAttr.java @@ -1,11 +1,11 @@ package jadx.core.dex.attributes.nodes; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttribute; -import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.CodeVar; import jadx.core.utils.Utils; /** @@ -13,13 +13,13 @@ import jadx.core.utils.Utils; */ public class DeclareVariablesAttr implements IAttribute { - private final List vars = new LinkedList<>(); + private final List vars = new ArrayList<>(); - public Iterable getVars() { + public Iterable getVars() { return vars; } - public void addVar(RegisterArg arg) { + public void addVar(CodeVar arg) { vars.add(arg); } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java index 6a545852b..913927eee 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxError.java @@ -36,7 +36,7 @@ public class JadxError { str.append(cause.getClass()); str.append(':'); str.append(cause.getMessage()); - str.append("\n"); + str.append('\n'); str.append(Utils.getStackTrace(cause)); } return str.toString(); diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxWarn.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxWarn.java deleted file mode 100644 index 9a6e258c5..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/JadxWarn.java +++ /dev/null @@ -1,21 +0,0 @@ -package jadx.core.dex.attributes.nodes; - -import java.util.Objects; - -public class JadxWarn { - - private final String warn; - - public JadxWarn(String warn) { - this.warn = Objects.requireNonNull(warn); - } - - public String getWarn() { - return warn; - } - - @Override - public String toString() { - return "JadxWarn: " + warn; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java new file mode 100644 index 000000000..c4efe9235 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/LocalVarsDebugInfoAttr.java @@ -0,0 +1,30 @@ +package jadx.core.dex.attributes.nodes; + +import java.util.List; + +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.IAttribute; +import jadx.core.dex.visitors.debuginfo.LocalVar; +import jadx.core.utils.Utils; + +public class LocalVarsDebugInfoAttr implements IAttribute { + private final List localVars; + + public LocalVarsDebugInfoAttr(List localVars) { + this.localVars = localVars; + } + + public List getLocalVars() { + return localVars; + } + + @Override + public AType getType() { + return AType.LOCAL_VARS_DEBUG_INFO; + } + + @Override + public String toString() { + return "Debug Info:\n " + Utils.listToString(localVars, "\n "); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java index f66542de9..cba4c9010 100644 --- a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/PhiListAttr.java @@ -27,6 +27,9 @@ public class PhiListAttr implements IAttribute { for (PhiInsn phiInsn : list) { sb.append('r').append(phiInsn.getResult().getRegNum()).append(' '); } + for (PhiInsn phiInsn : list) { + sb.append("\n ").append(phiInsn).append(' ').append(phiInsn.getAttributesString()); + } return sb.toString(); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RegDebugInfoAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RegDebugInfoAttr.java new file mode 100644 index 000000000..531794775 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/RegDebugInfoAttr.java @@ -0,0 +1,58 @@ +package jadx.core.dex.attributes.nodes; + +import java.util.Objects; + +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.IAttribute; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.visitors.debuginfo.LocalVar; + +public class RegDebugInfoAttr implements IAttribute { + + private final ArgType type; + private final String name; + + public RegDebugInfoAttr(LocalVar var) { + this(var.getType(), var.getName()); + } + + public RegDebugInfoAttr(ArgType type, String name) { + this.type = type; + this.name = name; + } + + public String getName() { + return name; + } + + public ArgType getRegType() { + return type; + } + + @Override + public AType getType() { + return AType.REG_DEBUG_INFO; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RegDebugInfoAttr that = (RegDebugInfoAttr) o; + return Objects.equals(type, that.type) && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + + @Override + public String toString() { + return "D('" + name + "' " + type + ')'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayNode.java index 4d30de8d1..422a64700 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/FillArrayNode.java @@ -1,6 +1,7 @@ package jadx.core.dex.instructions; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import com.android.dx.io.instructions.FillArrayDataPayloadDecodedInstruction; @@ -9,43 +10,45 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.PrimitiveType; -import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.exceptions.JadxRuntimeException; public final class FillArrayNode extends InsnNode { + private static final ArgType ONE_BYTE_TYPE = ArgType.unknown(PrimitiveType.BOOLEAN, PrimitiveType.BYTE); + private static final ArgType TWO_BYTES_TYPE = ArgType.unknown(PrimitiveType.SHORT, PrimitiveType.CHAR); + private static final ArgType FOUR_BYTES_TYPE = ArgType.unknown(PrimitiveType.INT, PrimitiveType.FLOAT); + private static final ArgType EIGHT_BYTES_TYPE = ArgType.unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); + private final Object data; private final int size; private ArgType elemType; public FillArrayNode(int resReg, FillArrayDataPayloadDecodedInstruction payload) { - super(InsnType.FILL_ARRAY, 0); - ArgType elType; - switch (payload.getElementWidthUnit()) { - case 1: - elType = ArgType.unknown(PrimitiveType.BOOLEAN, PrimitiveType.BYTE); - break; - case 2: - elType = ArgType.unknown(PrimitiveType.SHORT, PrimitiveType.CHAR); - break; - case 4: - elType = ArgType.unknown(PrimitiveType.INT, PrimitiveType.FLOAT); - break; - case 8: - elType = ArgType.unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); - break; - - default: - throw new JadxRuntimeException("Unknown array element width: " + payload.getElementWidthUnit()); - } - setResult(InsnArg.reg(resReg, ArgType.array(elType))); + super(InsnType.FILL_ARRAY, 1); + ArgType elType = getElementType(payload.getElementWidthUnit()); + addArg(InsnArg.reg(resReg, ArgType.array(elType))); this.data = payload.getData(); this.size = payload.getSize(); this.elemType = elType; } + private static ArgType getElementType(short elementWidthUnit) { + switch (elementWidthUnit) { + case 1: + return ONE_BYTE_TYPE; + case 2: + return TWO_BYTES_TYPE; + case 4: + return FOUR_BYTES_TYPE; + case 8: + return EIGHT_BYTES_TYPE; + default: + throw new JadxRuntimeException("Unknown array element width: " + elementWidthUnit); + } + } + public Object getData() { return data; } @@ -58,34 +61,27 @@ public final class FillArrayNode extends InsnNode { return elemType; } - public void mergeElementType(DexNode dex, ArgType foundElemType) { - ArgType r = ArgType.merge(dex, elemType, foundElemType); - if (r != null) { - elemType = r; - } - } - - public List getLiteralArgs() { + public List getLiteralArgs(ArgType type) { List list = new ArrayList<>(size); Object array = data; if (array instanceof int[]) { for (int b : (int[]) array) { - list.add(InsnArg.lit(b, elemType)); + list.add(InsnArg.lit(b, type)); } } else if (array instanceof byte[]) { for (byte b : (byte[]) array) { - list.add(InsnArg.lit(b, elemType)); + list.add(InsnArg.lit(b, type)); } } else if (array instanceof short[]) { for (short b : (short[]) array) { - list.add(InsnArg.lit(b, elemType)); + list.add(InsnArg.lit(b, type)); } } else if (array instanceof long[]) { for (long b : (long[]) array) { - list.add(InsnArg.lit(b, elemType)); + list.add(InsnArg.lit(b, type)); } } else { - throw new JadxRuntimeException("Unknown type: " + data.getClass() + ", expected: " + elemType); + throw new JadxRuntimeException("Unknown type: " + data.getClass() + ", expected: " + type); } return list; } @@ -101,4 +97,25 @@ public final class FillArrayNode extends InsnNode { FillArrayNode other = (FillArrayNode) obj; return elemType.equals(other.elemType) && data == other.data; } + + public String dataToString() { + if (data instanceof int[]) { + return Arrays.toString((int[]) data); + } + if (data instanceof short[]) { + return Arrays.toString((short[]) data); + } + if (data instanceof byte[]) { + return Arrays.toString((byte[]) data); + } + if (data instanceof long[]) { + return Arrays.toString((long[]) data); + } + return "?"; + } + + @Override + public String toString() { + return super.toString() + ", data: " + dataToString(); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/IfNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/IfNode.java index d0b74b1dd..98feb1019 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/IfNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/IfNode.java @@ -16,21 +16,21 @@ import static jadx.core.utils.BlockUtils.selectOther; public class IfNode extends GotoNode { - // change default types priority - private static final ArgType ARG_TYPE = ArgType.unknown( - PrimitiveType.INT, - PrimitiveType.OBJECT, PrimitiveType.ARRAY, - PrimitiveType.BOOLEAN, PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.CHAR); - protected IfOp op; private BlockNode thenBlock; private BlockNode elseBlock; public IfNode(DecodedInstruction insn, IfOp op) { - this(op, insn.getTarget(), - InsnArg.reg(insn, 0, ARG_TYPE), - insn.getRegisterCount() == 1 ? InsnArg.lit(0, ARG_TYPE) : InsnArg.reg(insn, 1, ARG_TYPE)); + super(InsnType.IF, insn.getTarget(), 2); + this.op = op; + ArgType argType = narrowTypeByOp(op); + addArg(InsnArg.reg(insn, 0, argType)); + if (insn.getRegisterCount() == 1) { + addArg(InsnArg.lit(0, argType)); + } else { + addArg(InsnArg.reg(insn, 1, argType)); + } } public IfNode(IfOp op, int targetOffset, InsnArg arg1, InsnArg arg2) { @@ -40,6 +40,22 @@ public class IfNode extends GotoNode { addArg(arg2); } + // change default types priority + private static final ArgType WIDE_TYPE = ArgType.unknown( + PrimitiveType.INT, PrimitiveType.BOOLEAN, + PrimitiveType.OBJECT, PrimitiveType.ARRAY, + PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.CHAR); + + private static final ArgType NUMBERS_TYPE = ArgType.unknown( + PrimitiveType.INT, PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.CHAR); + + private static ArgType narrowTypeByOp(IfOp op) { + if (op == IfOp.EQ || op == IfOp.NE) { + return WIDE_TYPE; + } + return NUMBERS_TYPE; + } + public IfOp getOp() { return op; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java b/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java index 8056e1214..66cea5297 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/IndexInsnNode.java @@ -1,7 +1,10 @@ package jadx.core.dex.instructions; +import java.util.Objects; + import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; +import jadx.core.utils.Utils; public class IndexInsnNode extends InsnNode { @@ -30,11 +33,21 @@ public class IndexInsnNode extends InsnNode { return false; } IndexInsnNode other = (IndexInsnNode) obj; - return index == null ? other.index == null : index.equals(other.index); + return Objects.equals(index, other.index); } @Override public String toString() { - return super.toString() + ' ' + InsnUtils.indexToString(index); + switch (insnType) { + case CAST: + case CHECK_CAST: + return InsnUtils.formatOffset(offset) + ": " + + InsnUtils.insnTypeToString(insnType) + + getResult() + " = (" + InsnUtils.indexToString(index) + ") " + + Utils.listToString(getArguments()); + + default: + return super.toString() + ' ' + InsnUtils.indexToString(index); + } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java index ab1acf30a..51d0b568e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnDecoder.java @@ -18,9 +18,10 @@ import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.DexNode; +import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.InsnUtils; @@ -101,15 +102,15 @@ public class InsnDecoder { case Opcodes.CONST_4: case Opcodes.CONST_16: case Opcodes.CONST_HIGH16: - return insn(InsnType.CONST, InsnArg.reg(insn, 0, ArgType.NARROW), - InsnArg.lit(insn, ArgType.NARROW)); + LiteralArg narrowLitArg = InsnArg.lit(insn, ArgType.NARROW); + return insn(InsnType.CONST, InsnArg.reg(insn, 0, narrowLitArg.getType()), narrowLitArg); case Opcodes.CONST_WIDE: case Opcodes.CONST_WIDE_16: case Opcodes.CONST_WIDE_32: case Opcodes.CONST_WIDE_HIGH16: - return insn(InsnType.CONST, InsnArg.reg(insn, 0, ArgType.WIDE), - InsnArg.lit(insn, ArgType.WIDE)); + LiteralArg wideLitArg = InsnArg.lit(insn, ArgType.WIDE); + return insn(InsnType.CONST, InsnArg.reg(insn, 0, wideLitArg.getType()), wideLitArg); case Opcodes.CONST_STRING: case Opcodes.CONST_STRING_JUMBO: @@ -403,12 +404,10 @@ public class InsnDecoder { return new GotoNode(insn.getTarget()); case Opcodes.THROW: - return insn(InsnType.THROW, null, - InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT))); + return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE)); case Opcodes.MOVE_EXCEPTION: - return insn(InsnType.MOVE_EXCEPTION, - InsnArg.reg(insn, 0, ArgType.unknown(PrimitiveType.OBJECT))); + return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY)); case Opcodes.RETURN_VOID: return new InsnNode(InsnType.RETURN, 0); @@ -442,7 +441,7 @@ public class InsnDecoder { case Opcodes.IGET_OBJECT: FieldInfo igetFld = FieldInfo.fromDex(dex, insn.getIndex()); InsnNode igetInsn = new IndexInsnNode(InsnType.IGET, igetFld, 1); - igetInsn.setResult(InsnArg.reg(insn, 0, igetFld.getType())); + igetInsn.setResult(InsnArg.reg(insn, 0, tryResolveFieldType(igetFld))); igetInsn.addArg(InsnArg.reg(insn, 1, igetFld.getDeclClass().getType())); return igetInsn; @@ -455,7 +454,7 @@ public class InsnDecoder { case Opcodes.IPUT_OBJECT: FieldInfo iputFld = FieldInfo.fromDex(dex, insn.getIndex()); InsnNode iputInsn = new IndexInsnNode(InsnType.IPUT, iputFld, 2); - iputInsn.addArg(InsnArg.reg(insn, 0, iputFld.getType())); + iputInsn.addArg(InsnArg.reg(insn, 0, tryResolveFieldType(iputFld))); iputInsn.addArg(InsnArg.reg(insn, 1, iputFld.getDeclClass().getType())); return iputInsn; @@ -468,7 +467,7 @@ public class InsnDecoder { case Opcodes.SGET_OBJECT: FieldInfo sgetFld = FieldInfo.fromDex(dex, insn.getIndex()); InsnNode sgetInsn = new IndexInsnNode(InsnType.SGET, sgetFld, 0); - sgetInsn.setResult(InsnArg.reg(insn, 0, sgetFld.getType())); + sgetInsn.setResult(InsnArg.reg(insn, 0, tryResolveFieldType(sgetFld))); return sgetInsn; case Opcodes.SPUT: @@ -480,7 +479,7 @@ public class InsnDecoder { case Opcodes.SPUT_OBJECT: FieldInfo sputFld = FieldInfo.fromDex(dex, insn.getIndex()); InsnNode sputInsn = new IndexInsnNode(InsnType.SPUT, sputFld, 1); - sputInsn.addArg(InsnArg.reg(insn, 0, sputFld.getType())); + sputInsn.addArg(InsnArg.reg(insn, 0, tryResolveFieldType(sputFld))); return sputInsn; case Opcodes.ARRAY_LENGTH: @@ -490,7 +489,7 @@ public class InsnDecoder { return arrLenInsn; case Opcodes.AGET: - return arrayGet(insn, ArgType.NARROW); + return arrayGet(insn, ArgType.INT_FLOAT); case Opcodes.AGET_BOOLEAN: return arrayGet(insn, ArgType.BOOLEAN); case Opcodes.AGET_BYTE: @@ -505,7 +504,7 @@ public class InsnDecoder { return arrayGet(insn, ArgType.UNKNOWN_OBJECT); case Opcodes.APUT: - return arrayPut(insn, ArgType.NARROW); + return arrayPut(insn, ArgType.INT_FLOAT); case Opcodes.APUT_BOOLEAN: return arrayPut(insn, ArgType.BOOLEAN); case Opcodes.APUT_BYTE: @@ -544,14 +543,16 @@ public class InsnDecoder { return invoke(insn, offset, InvokeType.VIRTUAL, true); case Opcodes.NEW_INSTANCE: - return insn(InsnType.NEW_INSTANCE, - InsnArg.reg(insn, 0, dex.getType(insn.getIndex()))); + ArgType clsType = dex.getType(insn.getIndex()); + IndexInsnNode newInstInsn = new IndexInsnNode(InsnType.NEW_INSTANCE, clsType, 0); + newInstInsn.setResult(InsnArg.reg(insn, 0, clsType)); + return newInstInsn; case Opcodes.NEW_ARRAY: ArgType arrType = dex.getType(insn.getIndex()); return new NewArrayNode(arrType, InsnArg.reg(insn, 0, arrType), - InsnArg.reg(insn, 1, ArgType.INT)); + InsnArg.typeImmutableReg(insn, 1, ArgType.INT)); case Opcodes.FILL_ARRAY_DATA: return fillArray(insn); @@ -582,6 +583,14 @@ public class InsnDecoder { } } + private ArgType tryResolveFieldType(FieldInfo igetFld) { + FieldNode fieldNode = dex.resolveField(igetFld); + if (fieldNode != null) { + return fieldNode.getType(); + } + return igetFld.getType(); + } + private InsnNode decodeSwitch(DecodedInstruction insn, int offset, boolean packed) { int payloadOffset = insn.getTarget(); DecodedInstruction payload = insnArr[payloadOffset]; @@ -666,26 +675,34 @@ public class InsnDecoder { private InsnNode arrayGet(DecodedInstruction insn, ArgType argType) { InsnNode inode = new InsnNode(InsnType.AGET, 2); - inode.setResult(InsnArg.reg(insn, 0, argType)); - inode.addArg(InsnArg.reg(insn, 1, ArgType.unknown(PrimitiveType.ARRAY))); - inode.addArg(InsnArg.reg(insn, 2, ArgType.INT)); + inode.setResult(InsnArg.typeImmutableIfKnownReg(insn, 0, argType)); + inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(argType))); + inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL)); return inode; } private InsnNode arrayPut(DecodedInstruction insn, ArgType argType) { InsnNode inode = new InsnNode(InsnType.APUT, 3); - inode.addArg(InsnArg.reg(insn, 1, ArgType.unknown(PrimitiveType.ARRAY))); - inode.addArg(InsnArg.reg(insn, 2, ArgType.INT)); - inode.addArg(InsnArg.reg(insn, 0, argType)); + inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 1, ArgType.array(argType))); + inode.addArg(InsnArg.reg(insn, 2, ArgType.NARROW_INTEGRAL)); + inode.addArg(InsnArg.typeImmutableIfKnownReg(insn, 0, argType)); return inode; } private InsnNode arith(DecodedInstruction insn, ArithOp op, ArgType type) { - return new ArithNode(insn, op, type, false); + return new ArithNode(insn, op, fixTypeForBitOps(op, type), false); } private InsnNode arithLit(DecodedInstruction insn, ArithOp op, ArgType type) { - return new ArithNode(insn, op, type, true); + return new ArithNode(insn, op, fixTypeForBitOps(op, type), true); + } + + private ArgType fixTypeForBitOps(ArithOp op, ArgType type) { + if (type == ArgType.INT + && (op == ArithOp.AND || op == ArithOp.OR || op == ArithOp.XOR)) { + return ArgType.NARROW_NUMBERS_NO_FLOAT; + } + return type; } private InsnNode neg(DecodedInstruction insn, ArgType type) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnType.java index 7162b61cd..9708f802f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/InsnType.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/InsnType.java @@ -66,9 +66,6 @@ public enum InsnType { ONE_ARG, PHI, - // merge all arguments in one - MERGE, - // TODO: now multidimensional arrays created using Array.newInstance function NEW_MULTIDIM_ARRAY } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java b/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java index 02d8134c6..fddd7d6cd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/PhiInsn.java @@ -24,10 +24,11 @@ public final class PhiInsn extends InsnNode { this.blockBinds = new LinkedHashMap<>(predecessors); setResult(InsnArg.reg(regNum, ArgType.UNKNOWN)); add(AFlag.DONT_INLINE); + add(AFlag.DONT_GENERATE); } public RegisterArg bindArg(BlockNode pred) { - RegisterArg arg = InsnArg.reg(getResult().getRegNum(), getResult().getType()); + RegisterArg arg = InsnArg.reg(getResult().getRegNum(), getResult().getInitType()); bindArg(arg, pred); return arg; } @@ -62,6 +63,7 @@ public final class PhiInsn extends InsnNode { RegisterArg reg = (RegisterArg) arg; if (super.removeArg(reg)) { blockBinds.remove(reg); + reg.getSVar().removeUse(reg); InstructionRemover.fixUsedInPhiFlag(reg); return true; } @@ -78,7 +80,9 @@ public final class PhiInsn extends InsnNode { throw new JadxRuntimeException("Unknown predecessor block by arg " + from + " in PHI: " + this); } if (removeArg(from)) { - bindArg((RegisterArg) to, pred); + RegisterArg reg = (RegisterArg) to; + bindArg(reg, pred); + reg.getSVar().setUsedInPhi(this); } return true; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java index 05c28bd32..6bbb103ea 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/ArgType.java @@ -1,18 +1,16 @@ package jadx.core.dex.instructions.args; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import org.jetbrains.annotations.Nullable; - import jadx.core.Consts; import jadx.core.dex.nodes.DexNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.utils.Utils; public abstract class ArgType { - public static final ArgType INT = primitive(PrimitiveType.INT); public static final ArgType BOOLEAN = primitive(PrimitiveType.BOOLEAN); public static final ArgType BYTE = primitive(PrimitiveType.BYTE); @@ -28,9 +26,12 @@ public abstract class ArgType { public static final ArgType STRING = object(Consts.CLASS_STRING); public static final ArgType ENUM = object(Consts.CLASS_ENUM); public static final ArgType THROWABLE = object(Consts.CLASS_THROWABLE); + public static final ArgType OBJECT_ARRAY = array(OBJECT); public static final ArgType UNKNOWN = unknown(PrimitiveType.values()); public static final ArgType UNKNOWN_OBJECT = unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY); + public static final ArgType UNKNOWN_OBJECT_NO_ARRAY = unknown(PrimitiveType.OBJECT); + public static final ArgType UNKNOWN_ARRAY = array(UNKNOWN); public static final ArgType NARROW = unknown( PrimitiveType.INT, PrimitiveType.FLOAT, @@ -38,11 +39,24 @@ public abstract class ArgType { PrimitiveType.OBJECT, PrimitiveType.ARRAY); public static final ArgType NARROW_NUMBERS = unknown( + PrimitiveType.BOOLEAN, PrimitiveType.INT, PrimitiveType.FLOAT, + PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + + public static final ArgType NARROW_INTEGRAL = unknown( + PrimitiveType.INT, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + + public static final ArgType NARROW_NUMBERS_NO_BOOL = unknown( PrimitiveType.INT, PrimitiveType.FLOAT, - PrimitiveType.BOOLEAN, PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); + + public static final ArgType NARROW_NUMBERS_NO_FLOAT = unknown( + PrimitiveType.INT, PrimitiveType.BOOLEAN, + PrimitiveType.SHORT, PrimitiveType.BYTE, PrimitiveType.CHAR); public static final ArgType WIDE = unknown(PrimitiveType.LONG, PrimitiveType.DOUBLE); + public static final ArgType INT_FLOAT = unknown(PrimitiveType.INT, PrimitiveType.FLOAT); + protected int hash; private static ArgType primitive(PrimitiveType stype) { @@ -174,6 +188,8 @@ public abstract class ArgType { } private static final class GenericType extends ObjectType { + private List extendTypes; + public GenericType(String obj) { super(obj); } @@ -182,6 +198,16 @@ public abstract class ArgType { public boolean isGenericType() { return true; } + + @Override + public List getExtendTypes() { + return extendTypes; + } + + @Override + public void setExtendTypes(List extendTypes) { + this.extendTypes = extendTypes; + } } private static final class WildcardType extends ObjectType { @@ -275,7 +301,7 @@ public abstract class ArgType { @Override public String toString() { - return super.toString() + '<' + Utils.arrayToString(generics) + '>'; + return super.toString() + '<' + Utils.arrayToStr(generics) + '>'; } } @@ -329,8 +355,9 @@ public abstract class ArgType { } @Override - boolean internalEquals(Object obj) { - return arrayElement.equals(((ArrayArg) obj).arrayElement); + boolean internalEquals(Object other) { + ArrayArg otherArr = (ArrayArg) other; + return this.arrayElement.equals(otherArr.getArrayElement()); } @Override @@ -369,14 +396,13 @@ public abstract class ArgType { @Override public ArgType selectFirst() { - PrimitiveType f = possibleTypes[0]; if (contains(PrimitiveType.OBJECT)) { return OBJECT; - } else if (contains(PrimitiveType.ARRAY)) { - return array(OBJECT); - } else { - return primitive(f); } + if (contains(PrimitiveType.ARRAY)) { + return array(OBJECT); + } + return primitive(possibleTypes[0]); } @Override @@ -389,7 +415,7 @@ public abstract class ArgType { if (possibleTypes.length == PrimitiveType.values().length) { return "?"; } else { - return '?' + Arrays.toString(possibleTypes); + return "?[" + Utils.arrayToStr(possibleTypes) + ']'; } } } @@ -426,6 +452,13 @@ public abstract class ArgType { return null; } + public List getExtendTypes() { + return Collections.emptyList(); + } + + public void setExtendTypes(List extendTypes) { + } + public ArgType getWildcardType() { return null; } @@ -463,110 +496,6 @@ public abstract class ArgType { public abstract PrimitiveType[] getPossibleTypes(); - @Nullable - public static ArgType merge(@Nullable DexNode dex, ArgType a, ArgType b) { - if (a == null || b == null) { - return null; - } - if (a.equals(b)) { - return a; - } - ArgType res = mergeInternal(dex, a, b); - if (res == null) { - res = mergeInternal(dex, b, a); // swap - } - return res; - } - - private static ArgType mergeInternal(@Nullable DexNode dex, ArgType a, ArgType b) { - if (a == UNKNOWN) { - return b; - } - if (a.isArray()) { - return mergeArrays(dex, (ArrayArg) a, b); - } else if (b.isArray()) { - return mergeArrays(dex, (ArrayArg) b, a); - } - if (!a.isTypeKnown()) { - if (b.isTypeKnown()) { - if (a.contains(b.getPrimitiveType())) { - return b; - } - return null; - } else { - // both types unknown - List types = new ArrayList<>(); - for (PrimitiveType type : a.getPossibleTypes()) { - if (b.contains(type)) { - types.add(type); - } - } - if (types.isEmpty()) { - return null; - } - if (types.size() == 1) { - PrimitiveType nt = types.get(0); - if (nt == PrimitiveType.OBJECT || nt == PrimitiveType.ARRAY) { - return unknown(nt); - } else { - return primitive(nt); - } - } else { - return unknown(types.toArray(new PrimitiveType[types.size()])); - } - } - } else { - if (a.isGenericType()) { - return a; - } - if (b.isGenericType()) { - return b; - } - - if (a.isObject() && b.isObject()) { - String aObj = a.getObject(); - String bObj = b.getObject(); - if (aObj.equals(bObj)) { - return a.getGenericTypes() != null ? a : b; - } - if (aObj.equals(Consts.CLASS_OBJECT)) { - return b; - } - if (bObj.equals(Consts.CLASS_OBJECT)) { - return a; - } - if (dex == null) { - return null; - } - String obj = dex.root().getClsp().getCommonAncestor(aObj, bObj); - return obj == null ? null : object(obj); - } - if (a.isPrimitive() && b.isPrimitive() && a.getRegCount() == b.getRegCount()) { - return primitive(PrimitiveType.getSmaller(a.getPrimitiveType(), b.getPrimitiveType())); - } - } - return null; - } - - private static ArgType mergeArrays(DexNode dex, ArrayArg array, ArgType b) { - if (b.isArray()) { - ArgType ea = array.getArrayElement(); - ArgType eb = b.getArrayElement(); - if (ea.isPrimitive() && eb.isPrimitive()) { - return OBJECT; - } - ArgType res = merge(dex, ea, eb); - return res == null ? null : array(res); - } - if (b.contains(PrimitiveType.ARRAY)) { - return array; - } - if (b.equals(OBJECT)) { - return OBJECT; - } - return null; - } - public static boolean isCastNeeded(DexNode dex, ArgType from, ArgType to) { if (from.equals(to)) { return false; @@ -578,14 +507,62 @@ public abstract class ArgType { return true; } - public static boolean isInstanceOf(DexNode dex, ArgType type, ArgType of) { + public static boolean isInstanceOf(RootNode root, ArgType type, ArgType of) { if (type.equals(of)) { return true; } if (!type.isObject() || !of.isObject()) { return false; } - return dex.root().getClsp().isImplements(type.getObject(), of.getObject()); + return root.getClsp().isImplements(type.getObject(), of.getObject()); + } + + public static boolean isClsKnown(RootNode root, ArgType cls) { + if (cls.isObject()) { + return root.getClsp().isClsKnown(cls.getObject()); + } + return false; + } + + public boolean canBeObject() { + return isObject() || (!isTypeKnown() && contains(PrimitiveType.OBJECT)); + } + + public boolean canBeArray() { + return isArray() || (!isTypeKnown() && contains(PrimitiveType.ARRAY)); + } + + public boolean canBePrimitive(PrimitiveType primitiveType) { + return (isPrimitive() && getPrimitiveType() == primitiveType) + || (!isTypeKnown() && contains(primitiveType)); + } + + public static ArgType convertFromPrimitiveType(PrimitiveType primitiveType) { + switch (primitiveType) { + case BOOLEAN: + return BOOLEAN; + case CHAR: + return CHAR; + case BYTE: + return BYTE; + case SHORT: + return SHORT; + case INT: + return INT; + case FLOAT: + return FLOAT; + case LONG: + return LONG; + case DOUBLE: + return DOUBLE; + case OBJECT: + return OBJECT; + case ARRAY: + return OBJECT_ARRAY; + case VOID: + return ArgType.VOID; + } + return OBJECT; } public static ArgType parse(String type) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java new file mode 100644 index 000000000..8685e4875 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/CodeVar.java @@ -0,0 +1,99 @@ +package jadx.core.dex.instructions.args; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CodeVar { + private String name; + private ArgType type; // before type inference can be null and set only for immutable types + private List ssaVars = new ArrayList<>(3); + + private boolean isFinal; + private boolean isThis; + private boolean isDeclared; + + public static CodeVar fromMthArg(RegisterArg mthArg) { + CodeVar var = new CodeVar(); + var.setType(mthArg.getInitType()); + var.setName(mthArg.getName()); + var.setDeclared(true); + var.setThis(mthArg.isThis()); + var.setSsaVars(Collections.singletonList(new SSAVar(mthArg.getRegNum(), 0, mthArg))); + return var; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ArgType getType() { + return type; + } + + public void setType(ArgType type) { + this.type = type; + } + + public List getSsaVars() { + return ssaVars; + } + + public void addSsaVar(SSAVar ssaVar) { + if (!ssaVars.contains(ssaVar)) { + ssaVars.add(ssaVar); + } + } + + public void setSsaVars(List ssaVars) { + this.ssaVars = ssaVars; + } + + public boolean isFinal() { + return isFinal; + } + + public void setFinal(boolean aFinal) { + isFinal = aFinal; + } + + public boolean isThis() { + return isThis; + } + + public void setThis(boolean aThis) { + isThis = aThis; + } + + public boolean isDeclared() { + return isDeclared; + } + + public void setDeclared(boolean declared) { + isDeclared = declared; + } + + /** + * Merge flags with OR operator + */ + public void mergeFlagsFrom(CodeVar other) { + if (other.isDeclared()) { + setDeclared(true); + } + if (other.isThis()) { + setThis(true); + } + if (other.isFinal()) { + setFinal(true); + } + } + + @Override + public String toString() { + return (isFinal ? "final " : "") + type + ' ' + name; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/FieldArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/FieldArg.java index a26113bdc..8d2767b6c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/FieldArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/FieldArg.java @@ -3,6 +3,7 @@ package jadx.core.dex.instructions.args; import org.jetbrains.annotations.Nullable; import jadx.core.dex.info.FieldInfo; +import jadx.core.utils.exceptions.JadxRuntimeException; // TODO: don't extend RegisterArg (now used as a result of instruction) public final class FieldArg extends RegisterArg { @@ -13,7 +14,7 @@ public final class FieldArg extends RegisterArg { private final InsnArg instArg; public FieldArg(FieldInfo field, @Nullable InsnArg reg) { - super(-1); + super(-1, field.getType()); this.instArg = reg; this.field = field; } @@ -41,8 +42,18 @@ public final class FieldArg extends RegisterArg { } @Override - public void setType(ArgType type) { - this.type = type; + public ArgType getType() { + return this.field.getType(); + } + + @Override + public ArgType getInitType() { + return this.field.getType(); + } + + @Override + public void setType(ArgType newType) { + throw new JadxRuntimeException("Can't set type for FieldArg"); } @Override diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java index f9ac7a500..7daa6926e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java @@ -31,12 +31,27 @@ public abstract class InsnArg extends Typed { return reg(InsnUtils.getArg(insn, argNum), type); } - public static TypeImmutableArg typeImmutableReg(int regNum, ArgType type) { - return new TypeImmutableArg(regNum, type); + public static RegisterArg typeImmutableIfKnownReg(DecodedInstruction insn, int argNum, ArgType type) { + if (type.isTypeKnown()) { + return typeImmutableReg(InsnUtils.getArg(insn, argNum), type); + } + return reg(InsnUtils.getArg(insn, argNum), type); + } + + public static RegisterArg typeImmutableReg(DecodedInstruction insn, int argNum, ArgType type) { + return typeImmutableReg(InsnUtils.getArg(insn, argNum), type); + } + + public static RegisterArg typeImmutableReg(int regNum, ArgType type) { + return reg(regNum, type, true); } public static RegisterArg reg(int regNum, ArgType type, boolean typeImmutable) { - return typeImmutable ? new TypeImmutableArg(regNum, type) : new RegisterArg(regNum, type); + RegisterArg reg = new RegisterArg(regNum, type); + if (typeImmutable) { + reg.add(AFlag.IMMUTABLE_TYPE); + } + return reg; } public static LiteralArg lit(long literal, ArgType type) { @@ -142,4 +157,8 @@ public abstract class InsnArg extends Typed { public boolean isThis() { return contains(AFlag.THIS); } + + public InsnArg duplicate() { + return this; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/LiteralArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/LiteralArg.java index e40376e66..4e5740a69 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/LiteralArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/LiteralArg.java @@ -1,6 +1,8 @@ package jadx.core.dex.instructions.args; +import jadx.api.JadxArgs; import jadx.core.codegen.TypeGen; +import jadx.core.utils.StringUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public final class LiteralArg extends InsnArg { @@ -17,9 +19,10 @@ public final class LiteralArg extends InsnArg { } else if (!type.isTypeKnown() && !type.contains(PrimitiveType.LONG) && !type.contains(PrimitiveType.DOUBLE)) { - ArgType m = ArgType.merge(null, type, ArgType.NARROW_NUMBERS); - if (m != null) { - type = m; + if (value != 1) { + type = ArgType.NARROW_NUMBERS_NO_BOOL; + } else { + type = ArgType.NARROW_NUMBERS; } } } @@ -62,10 +65,12 @@ public final class LiteralArg extends InsnArg { return literal == that.literal && getType().equals(that.getType()); } + private static final StringUtils DEF_STRING_UTILS = new StringUtils(new JadxArgs()); + @Override public String toString() { try { - String value = TypeGen.literalToString(literal, getType()); + String value = TypeGen.literalToString(literal, getType(), DEF_STRING_UTILS); if (getType().equals(ArgType.BOOLEAN) && (value.equals("true") || value.equals("false"))) { return value; } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java index 97d49ed96..a8ef88e14 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/RegisterArg.java @@ -3,27 +3,26 @@ package jadx.core.dex.instructions.args; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.PhiInsn; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; public class RegisterArg extends InsnArg implements Named { - + private static final Logger LOG = LoggerFactory.getLogger(RegisterArg.class); public static final String THIS_ARG_NAME = "this"; protected final int regNum; // not null after SSATransform pass private SSAVar sVar; - public RegisterArg(int rn) { - this.regNum = rn; - } - public RegisterArg(int rn, ArgType type) { - this.type = type; + this.type = type; // initial type, not changing, can be unknown this.regNum = rn; } @@ -36,12 +35,50 @@ public class RegisterArg extends InsnArg implements Named { return true; } + @Override + public void setType(ArgType newType) { + if (sVar == null) { + throw new JadxRuntimeException("Can't change type for register without SSA variable: " + this); + } + if (contains(AFlag.IMMUTABLE_TYPE)) { + if (!type.isTypeKnown()) { + throw new JadxRuntimeException("Unknown immutable type '" + type + "' in " + this); + } + if (!type.equals(newType)) { + LOG.warn("JADX WARNING: Can't change immutable type from '{}' to '{}' for {}", type, newType, this); + return; + } + } + sVar.setType(newType); + } + + @Override + public ArgType getType() { + if (sVar != null) { + return sVar.getTypeInfo().getType(); + } + LOG.warn("Register type unknown, SSA variable not initialized: r{}", regNum); + return type; + } + + public ArgType getInitType() { + return type; + } + + @Override + public boolean isTypeImmutable() { + return contains(AFlag.IMMUTABLE_TYPE) || (sVar != null && sVar.contains(AFlag.IMMUTABLE_TYPE)); + } + public SSAVar getSVar() { return sVar; } void setSVar(@NotNull SSAVar sVar) { this.sVar = sVar; + if (contains(AFlag.IMMUTABLE_TYPE)) { + sVar.add(AFlag.IMMUTABLE_TYPE); + } } public String getName() { @@ -68,39 +105,13 @@ public class RegisterArg extends InsnArg implements Named { return n.equals(((Named) arg).getName()); } - public void mergeName(InsnArg arg) { - if (arg instanceof Named) { - Named otherArg = (Named) arg; - String otherName = otherArg.getName(); - String name = getName(); - if (!Objects.equals(name, otherName)) { - if (name == null) { - setName(otherName); - } else if (otherName == null) { - otherArg.setName(name); - } - } - } - } - @Override - public void setType(ArgType type) { - if (sVar != null) { - sVar.setType(type); - } - } - - public void mergeDebugInfo(ArgType type, String name) { - setType(type); - setName(name); - } - public RegisterArg duplicate() { return duplicate(getRegNum(), sVar); } - public RegisterArg duplicate(int regNum, SSAVar sVar) { - RegisterArg dup = new RegisterArg(regNum, getType()); + public RegisterArg duplicate(int regNum, @Nullable SSAVar sVar) { + RegisterArg dup = new RegisterArg(regNum, getInitType()); if (sVar != null) { dup.setSVar(sVar); } @@ -110,8 +121,6 @@ public class RegisterArg extends InsnArg implements Named { /** * Return constant value from register assign or null if not constant - * - * @return LiteralArg, String or ArgType */ public Object getConstValue(DexNode dex) { InsnNode parInsn = getAssignInsn(); @@ -121,6 +130,7 @@ public class RegisterArg extends InsnArg implements Named { return InsnUtils.getConstValueByInsn(dex, parInsn); } + @Nullable public InsnNode getAssignInsn() { if (sVar == null) { return null; @@ -128,22 +138,23 @@ public class RegisterArg extends InsnArg implements Named { return sVar.getAssign().getParentInsn(); } - public InsnNode getPhiAssignInsn() { - PhiInsn usePhi = sVar.getUsedInPhi(); - if (usePhi != null) { - return usePhi; - } - InsnNode parent = sVar.getAssign().getParentInsn(); - if (parent != null && parent.getType() == InsnType.PHI) { - return parent; - } - return null; - } - public boolean equalRegisterAndType(RegisterArg arg) { return regNum == arg.regNum && type.equals(arg.type); } + public boolean sameRegAndSVar(InsnArg arg) { + if (!arg.isRegister()) { + return false; + } + RegisterArg reg = (RegisterArg) arg; + return regNum == reg.getRegNum() + && Objects.equals(sVar, reg.getSVar()); + } + + public boolean sameCodeVar(RegisterArg arg) { + return this.getSVar().getCodeVar() == arg.getSVar().getCodeVar(); + } + @Override public int hashCode() { return regNum; @@ -159,7 +170,6 @@ public class RegisterArg extends InsnArg implements Named { } RegisterArg other = (RegisterArg) obj; return regNum == other.regNum - && type.equals(other.type) && Objects.equals(sVar, other.getSVar()); } @@ -169,13 +179,19 @@ public class RegisterArg extends InsnArg implements Named { sb.append("(r"); sb.append(regNum); if (sVar != null) { - sb.append('_').append(sVar.getVersion()); + sb.append('v').append(sVar.getVersion()); } if (getName() != null) { sb.append(" '").append(getName()).append('\''); } - sb.append(' '); - sb.append(type); + ArgType type = sVar != null ? getType() : null; + if (type != null) { + sb.append(' ').append(type); + } + ArgType initType = getInitType(); + if (type == null || (!type.equals(initType) && !type.isTypeKnown())) { + sb.append(" I:").append(initType); + } if (!isAttrStorageEmpty()) { sb.append(' ').append(getAttributesString()); } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java index 21d1f2ebc..e8612e3d5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/SSAVar.java @@ -1,31 +1,36 @@ package jadx.core.dex.instructions.args; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; +import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.PhiInsn; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.typeinference.TypeInfo; +import jadx.core.utils.StringUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; public class SSAVar extends AttrNode { - private final int regNum; private final int version; - private VarName varName; - private int startUseAddr; - private int endUseAddr; - - @NotNull private RegisterArg assign; private final List useList = new ArrayList<>(2); @Nullable private PhiInsn usedInPhi; - private ArgType type; - private boolean typeImmutable; + private TypeInfo typeInfo = new TypeInfo(); + + @Nullable("Set in InitCodeVariables pass") + private CodeVar codeVar; public SSAVar(int regNum, int v, @NotNull RegisterArg assign) { this.regNum = regNum; @@ -33,48 +38,6 @@ public class SSAVar extends AttrNode { this.assign = assign; assign.setSVar(this); - startUseAddr = -1; - endUseAddr = -1; - } - - public int getStartAddr() { - if (startUseAddr == -1) { - calcUsageAddrRange(); - } - return startUseAddr; - } - - public int getEndAddr() { - if (endUseAddr == -1) { - calcUsageAddrRange(); - } - return endUseAddr; - } - - private void calcUsageAddrRange() { - int start = Integer.MAX_VALUE; - int end = Integer.MIN_VALUE; - - if (assign.getParentInsn() != null) { - int insnAddr = assign.getParentInsn().getOffset(); - if (insnAddr >= 0) { - start = Math.min(insnAddr, start); - end = Math.max(insnAddr, end); - } - } - for (RegisterArg arg : useList) { - if (arg.getParentInsn() != null) { - int insnAddr = arg.getParentInsn().getOffset(); - if (insnAddr >= 0) { - start = Math.min(insnAddr, start); - end = Math.max(insnAddr, end); - } - } - } - if (start != Integer.MAX_VALUE && end != Integer.MIN_VALUE) { - startUseAddr = start; - endUseAddr = end; - } } public int getRegNum() { @@ -102,6 +65,14 @@ public class SSAVar extends AttrNode { return useList.size(); } + // must be used only from RegisterArg#setType() + void setType(ArgType type) { + typeInfo.setType(type); + if (codeVar != null) { + codeVar.setType(type); + } + } + public void use(RegisterArg arg) { if (arg.getSVar() != null) { arg.getSVar().removeUse(arg); @@ -111,12 +82,7 @@ public class SSAVar extends AttrNode { } public void removeUse(RegisterArg arg) { - for (int i = 0, useListSize = useList.size(); i < useListSize; i++) { - if (useList.get(i) == arg) { - useList.remove(i); - break; - } - } + useList.removeIf(registerArg -> registerArg == arg); } public void setUsedInPhi(@Nullable PhiInsn usedInPhi) { @@ -139,52 +105,41 @@ public class SSAVar extends AttrNode { return useList.size() + usedInPhi.getResult().getSVar().getUseCount(); } - public void setType(ArgType type) { - ArgType acceptedType; - if (typeImmutable) { - // don't change type, just update types in useList - acceptedType = this.type; - } else { - acceptedType = type; - this.type = acceptedType; - } - assign.type = acceptedType; - for (int i = 0, useListSize = useList.size(); i < useListSize; i++) { - useList.get(i).type = acceptedType; - } - } - - public void setTypeImmutable(ArgType type) { - setType(type); - this.typeImmutable = true; - } - - public boolean isTypeImmutable() { - return typeImmutable; - } - public void setName(String name) { if (name != null) { - if (varName == null) { - varName = new VarName(); + if (codeVar == null) { + throw new JadxRuntimeException("CodeVar not initialized for name set in SSAVar: " + this); } - varName.setName(name); + codeVar.setName(name); } } public String getName() { - if (varName == null) { + if (codeVar == null) { return null; } - return varName.getName(); + return codeVar.getName(); } - public VarName getVarName() { - return varName; + public TypeInfo getTypeInfo() { + return typeInfo; } - public void setVarName(VarName varName) { - this.varName = varName; + @NotNull + public CodeVar getCodeVar() { + if (codeVar == null) { + throw new JadxRuntimeException("Code variable not set in " + this); + } + return codeVar; + } + + public void setCodeVar(@NotNull CodeVar codeVar) { + this.codeVar = codeVar; + codeVar.addSsaVar(this); + } + + public boolean isCodeVarSet() { + return codeVar != null; } @Override @@ -204,8 +159,54 @@ public class SSAVar extends AttrNode { return 31 * regNum + version; } + public String toShortString() { + return "r" + regNum + 'v' + version; + } + @Override public String toString() { - return "r" + regNum + '_' + version; + return toShortString() + + (StringUtils.notEmpty(getName()) ? " '" + getName() + "' " : "") + + ' ' + typeInfo.getType(); + } + + public String getDetailedVarInfo(MethodNode mth) { + Set types = new HashSet<>(); + Set names = Collections.emptySet(); + + List useArgs = new ArrayList<>(1 + useList.size()); + useArgs.add(assign); + useArgs.addAll(useList); + + if (mth.contains(AType.LOCAL_VARS_DEBUG_INFO)) { + names = new HashSet<>(); + for (RegisterArg arg : useArgs) { + RegDebugInfoAttr debugInfoAttr = arg.get(AType.REG_DEBUG_INFO); + if (debugInfoAttr != null) { + names.add(debugInfoAttr.getName()); + types.add(debugInfoAttr.getRegType()); + } + } + } + + for (RegisterArg arg : useArgs) { + ArgType initType = arg.getInitType(); + if (initType.isTypeKnown()) { + types.add(initType); + } + ArgType type = arg.getType(); + if (type.isTypeKnown()) { + types.add(type); + } + } + StringBuilder sb = new StringBuilder(); + sb.append('r').append(regNum).append('v').append(version); + if (!names.isEmpty()) { + sb.append(", names: ").append(names); + } + if (!types.isEmpty()) { + sb.append(", types: ").append(types); + } + return sb.toString(); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/TypeImmutableArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/TypeImmutableArg.java deleted file mode 100644 index a8f757db0..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/TypeImmutableArg.java +++ /dev/null @@ -1,26 +0,0 @@ -package jadx.core.dex.instructions.args; - -import org.jetbrains.annotations.NotNull; - -public class TypeImmutableArg extends RegisterArg { - - public TypeImmutableArg(int rn, ArgType type) { - super(rn, type); - } - - @Override - public boolean isTypeImmutable() { - return true; - } - - @Override - public void setType(ArgType type) { - // not allowed - } - - @Override - void setSVar(@NotNull SSAVar sVar) { - sVar.setTypeImmutable(type); - super.setSVar(sVar); - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/Typed.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/Typed.java index 125d9f540..46b4c007d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/Typed.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/Typed.java @@ -1,7 +1,6 @@ package jadx.core.dex.instructions.args; import jadx.core.dex.attributes.AttrNode; -import jadx.core.dex.nodes.DexNode; public abstract class Typed extends AttrNode { @@ -18,17 +17,4 @@ public abstract class Typed extends AttrNode { public boolean isTypeImmutable() { return false; } - - public boolean merge(DexNode dex, ArgType newType) { - ArgType m = ArgType.merge(dex, type, newType); - if (m != null && !m.equals(type)) { - setType(m); - return true; - } - return false; - } - - public boolean merge(DexNode dex, InsnArg arg) { - return merge(dex, arg.getType()); - } } diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java index 3ea92e013..21eb17b97 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/ConstructorInsn.java @@ -46,11 +46,10 @@ public class ConstructorInsn extends InsnNode implements CallMthInterface { instanceArg.getSVar().setAssign(instanceArg); } instanceArg.getSVar().removeUse(instanceArg); - for (int i = 1; i < invoke.getArgsCount(); i++) { + int argsCount = invoke.getArgsCount(); + for (int i = 1; i < argsCount; i++) { addArg(invoke.getArg(i)); } - offset = invoke.getOffset(); - setSourceLine(invoke.getSourceLine()); } public ConstructorInsn(MethodInfo callMth, CallType callType, RegisterArg instanceArg) { diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/TernaryInsn.java b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/TernaryInsn.java index bbc69443f..f1b98ecb8 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/mods/TernaryInsn.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/mods/TernaryInsn.java @@ -15,10 +15,6 @@ public final class TernaryInsn extends InsnNode { private IfCondition condition; - public TernaryInsn(IfCondition condition, RegisterArg result) { - this(condition, result, LiteralArg.TRUE, LiteralArg.FALSE); - } - public TernaryInsn(IfCondition condition, RegisterArg result, InsnArg th, InsnArg els) { super(InsnType.TERNARY, 2); setResult(result); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java index b791ad552..1a0b11714 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java @@ -231,7 +231,7 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode { return; } if (fileName.contains("$") - && fileName.endsWith("$" + name)) { + && fileName.endsWith('$' + name)) { return; } ClassInfo parentClass = clsInfo.getTopParentClass(); diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java index a93b68a6d..c5880b017 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/DexNode.java @@ -98,6 +98,10 @@ public class DexNode implements IDexNode { @Nullable public ClassNode resolveClass(ClassInfo clsInfo) { + ClassNode classNode = resolveClassLocal(clsInfo); + if (classNode != null) { + return classNode; + } return root.resolveClass(clsInfo); } @@ -109,7 +113,6 @@ public class DexNode implements IDexNode { return resolveClass(ClassInfo.fromType(root, type)); } - @Deprecated @Nullable public MethodNode resolveMethod(@NotNull MethodInfo mth) { ClassNode cls = resolveClass(mth.getDeclClass()); @@ -149,7 +152,6 @@ public class DexNode implements IDexNode { return null; } - @Deprecated @Nullable public FieldNode resolveField(FieldInfo field) { ClassNode cls = resolveClass(field.getDeclClass()); @@ -194,10 +196,16 @@ public class DexNode implements IDexNode { // DexBuffer wrappers public String getString(int index) { + if (index == DexNode.NO_INDEX) { + return null; + } return dexBuf.strings().get(index); } public ArgType getType(int index) { + if (index == DexNode.NO_INDEX) { + return null; + } return ArgType.parse(getString(dexBuf.typeIds().get(index))); } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java index a6d585caa..fa1d142b2 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/FieldNode.java @@ -14,7 +14,7 @@ public class FieldNode extends LineAttrNode implements ICodeNode { private final FieldInfo fieldInfo; private AccessInfo accFlags; - private ArgType type; // store signature + private ArgType type; public FieldNode(ClassNode cls, Field field) { this(cls, FieldInfo.fromDex(cls.dex(), field.getFieldIndex()), diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java index f04dd9d12..15a9b1c2e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/InsnNode.java @@ -8,6 +8,7 @@ import java.util.Objects; import com.android.dx.io.instructions.DecodedInstruction; import com.rits.cloning.Cloner; +import org.jetbrains.annotations.Nullable; import jadx.core.dex.attributes.nodes.LineAttrNode; import jadx.core.dex.instructions.InsnType; @@ -19,6 +20,7 @@ import jadx.core.dex.instructions.args.NamedArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.utils.InsnUtils; +import jadx.core.utils.InstructionRemover; import jadx.core.utils.Utils; public class InsnNode extends LineAttrNode { @@ -53,16 +55,27 @@ public class InsnNode extends LineAttrNode { return insn; } - public void setResult(RegisterArg res) { + public void setResult(@Nullable RegisterArg res) { if (res != null) { res.setParentInsn(this); + SSAVar ssaVar = res.getSVar(); + if (ssaVar != null) { + ssaVar.setAssign(res); + } } this.result = res; } public void addArg(InsnArg arg) { - arg.setParentInsn(this); arguments.add(arg); + arg.setParentInsn(this); + if (arg.isRegister()) { + RegisterArg reg = (RegisterArg) arg; + SSAVar ssaVar = reg.getSVar(); + if (ssaVar != null) { + ssaVar.use(reg); + } + } } public InsnType getType() { @@ -110,6 +123,7 @@ public class InsnNode extends LineAttrNode { for (int i = 0; i < count; i++) { InsnArg arg = arguments.get(i); if (arg == from) { + InstructionRemover.unbindArgUsage(null, arg); setArg(i, to); return true; } @@ -125,10 +139,7 @@ public class InsnNode extends LineAttrNode { for (int i = 0; i < count; i++) { if (arg == arguments.get(i)) { arguments.remove(i); - if (arg instanceof RegisterArg) { - RegisterArg reg = (RegisterArg) arg; - reg.getSVar().removeUse(reg); - } + InstructionRemover.unbindArgUsage(null, arg); return true; } } 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 0fe8a0b04..0d16099d5 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 @@ -33,7 +33,6 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; -import jadx.core.dex.instructions.args.TypeImmutableArg; import jadx.core.dex.nodes.parser.SignatureParser; import jadx.core.dex.regions.Region; import jadx.core.dex.trycatch.ExcHandlerAttr; @@ -54,17 +53,18 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { private AccessInfo accFlags; private final Method methodData; + private final boolean methodIsVirtual; + + private boolean noCode; private int regsCount; private InsnNode[] instructions; private int codeSize; private int debugInfoOffset; - private boolean noCode; - private boolean methodIsVirtual; private ArgType retType; private RegisterArg thisArg; private List argsList; - private List sVars = Collections.emptyList(); + private List sVars; private Map> genericMap; private List blocks; @@ -72,8 +72,8 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { private List exitBlocks; private Region region; - private List exceptionHandlers = Collections.emptyList(); - private List loops = Collections.emptyList(); + private List exceptionHandlers; + private List loops; public MethodNode(ClassNode classNode, Method mthData, boolean isVirtual) { this.mthInfo = MethodInfo.fromDex(classNode.dex(), mthData.getMethodIndex()); @@ -82,6 +82,26 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { this.noCode = mthData.getCodeOffset() == 0; this.methodData = noCode ? null : mthData; this.methodIsVirtual = isVirtual; + unload(); + } + + @Override + public void unload() { + if (noCode) { + return; + } + retType = null; + thisArg = null; + argsList = Collections.emptyList(); + sVars = Collections.emptyList(); + genericMap = null; + instructions = null; + blocks = null; + enterBlock = null; + exitBlocks = null; + region = null; + exceptionHandlers = Collections.emptyList(); + loops = Collections.emptyList(); } @Override @@ -148,21 +168,6 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { } } - @Override - public void unload() { - if (noCode) { - return; - } - instructions = null; - blocks = null; - enterBlock = null; - exitBlocks = null; - exceptionHandlers = Collections.emptyList(); - sVars.clear(); - region = null; - loops = Collections.emptyList(); - } - private boolean parseSignature() { SignatureParser sp = SignatureParser.fromNode(this); if (sp == null) { @@ -214,8 +219,9 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { if (accFlags.isStatic()) { thisArg = null; } else { - TypeImmutableArg arg = InsnArg.typeImmutableReg(pos - 1, parentClass.getClassInfo().getType()); + RegisterArg arg = InsnArg.reg(pos - 1, parentClass.getClassInfo().getType()); arg.add(AFlag.THIS); + arg.add(AFlag.IMMUTABLE_TYPE); thisArg = arg; } if (args.isEmpty()) { @@ -224,7 +230,10 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { } argsList = new ArrayList<>(args.size()); for (ArgType arg : args) { - argsList.add(InsnArg.typeImmutableReg(pos, arg)); + RegisterArg regArg = InsnArg.reg(pos, arg); + regArg.add(AFlag.METHOD_ARGUMENT); + regArg.add(AFlag.IMMUTABLE_TYPE); + argsList.add(regArg); pos += arg.getRegCount(); } } @@ -239,9 +248,8 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return argsList; } - public RegisterArg removeFirstArgument() { + public void skipFirstArgument() { this.add(AFlag.SKIP_FIRST_ARG); - return argsList.remove(0); } @Nullable @@ -554,7 +562,7 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { && !parentClass.getAccessFlags().isStatic()) { ClassNode outerCls = parentClass.getParentClass(); if (argsList != null && !argsList.isEmpty() - && argsList.get(0).getType().equals(outerCls.getClassInfo().getType())) { + && argsList.get(0).getInitType().equals(outerCls.getClassInfo().getType())) { defaultArgCount = 1; } } @@ -575,6 +583,10 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return debugInfoOffset; } + public SSAVar makeNewSVar(int regNum, @NotNull RegisterArg assignArg) { + return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg); + } + public SSAVar makeNewSVar(int regNum, int version, @NotNull RegisterArg assignArg) { SSAVar var = new SSAVar(regNum, version, assignArg); if (sVars.isEmpty()) { @@ -636,8 +648,13 @@ public class MethodNode extends LineAttrNode implements ILoadable, ICodeNode { return "method"; } - public void addWarn(String errStr) { - ErrorsCounter.methodWarn(this, errStr); + public void addWarn(String warnStr) { + ErrorsCounter.methodWarn(this, warnStr); + } + + public void addComment(String commentStr) { + addAttr(AType.COMMENTS, commentStr); + LOG.info("{} in {}", commentStr, this); } public void addError(String errStr, Exception e) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index e71cbab6b..067d4f42c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -18,6 +18,7 @@ import jadx.core.dex.info.ConstStorage; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.InfoStorage; import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.visitors.typeinference.TypeUpdate; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.StringUtils; import jadx.core.utils.android.AndroidResourcesUtils; @@ -35,6 +36,7 @@ public class RootNode { private final StringUtils stringUtils; private final ConstStorage constValues; private final InfoStorage infoStorage = new InfoStorage(); + private final TypeUpdate typeUpdate; private ClspGraph clsp; private List dexNodes; @@ -47,6 +49,7 @@ public class RootNode { this.args = args; this.stringUtils = new StringUtils(args); this.constValues = new ConstStorage(args); + this.typeUpdate = new TypeUpdate(this); } public void load(List inputFiles) { @@ -222,4 +225,8 @@ public class RootNode { public JadxArgs getArgs() { return args; } + + public TypeUpdate getTypeUpdate() { + return typeUpdate; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java index 81bf63daf..5d7b983e9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java @@ -211,7 +211,7 @@ public class SignatureParser { list.add(type); } } while (type != null && !lookAhead('>')); - return list.toArray(new ArgType[list.size()]); + return list.toArray(new ArgType[0]); } /** diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java index 4bfb532e6..2ac0a44df 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/SwitchRegion.java @@ -64,7 +64,9 @@ public final class SwitchRegion extends AbstractRegion implements IBranchRegion public List getBranches() { List branches = new ArrayList<>(cases.size() + 1); branches.addAll(cases); - branches.add(defCase); + if (defCase != null) { + branches.add(defCase); + } return Collections.unmodifiableList(branches); } diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java index 0cff99eb6..14644ae6e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfCondition.java @@ -15,6 +15,7 @@ import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; +import jadx.core.utils.BlockUtils; import jadx.core.utils.exceptions.JadxRuntimeException; public final class IfCondition { @@ -54,10 +55,11 @@ public final class IfCondition { } public static IfCondition fromIfBlock(BlockNode header) { - if (header == null) { + InsnNode lastInsn = BlockUtils.getLastInsn(header); + if (lastInsn == null) { return null; } - return fromIfNode((IfNode) header.getInstructions().get(0)); + return fromIfNode((IfNode) lastInsn); } public static IfCondition fromIfNode(IfNode insn) { diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java index 04f26e8a0..f1fcf74b5 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/conditions/IfRegion.java @@ -8,8 +8,9 @@ import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBranchRegion; import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.AbstractRegion; -import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.BlockUtils; public final class IfRegion extends AbstractRegion implements IBranchRegion { @@ -21,9 +22,6 @@ public final class IfRegion extends AbstractRegion implements IBranchRegion { public IfRegion(IRegion parent, BlockNode header) { super(parent); - if (header.getInstructions().size() != 1) { - throw new JadxRuntimeException("Expected only one instruction in 'if' header"); - } this.header = header; this.condition = IfCondition.fromIfBlock(header); } @@ -74,10 +72,8 @@ public final class IfRegion extends AbstractRegion implements IBranchRegion { } public int getSourceLine() { - if (header.getInstructions().isEmpty()) { - return 0; - } - return header.getInstructions().get(0).getSourceLine(); + InsnNode lastInsn = BlockUtils.getLastInsn(header); + return lastInsn == null ? 0 : lastInsn.getSourceLine(); } @Override @@ -130,6 +126,6 @@ public final class IfRegion extends AbstractRegion implements IBranchRegion { @Override public String toString() { - return "IF " + header + " then (" + thenRegion + ") else (" + elseRegion + ')'; + return "IF " + header + " THEN:" + thenRegion + " ELSE:" + elseRegion; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/loops/ForEachLoop.java b/jadx-core/src/main/java/jadx/core/dex/regions/loops/ForEachLoop.java index 2235e11a2..ad575f26c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/loops/ForEachLoop.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/loops/ForEachLoop.java @@ -10,6 +10,9 @@ public final class ForEachLoop extends LoopType { public ForEachLoop(RegisterArg varArg, InsnArg iterableArg) { this.varArg = varArg; this.iterableArg = iterableArg; + + // will be declared at codegen + varArg.getSVar().getCodeVar().setDeclared(true); } public RegisterArg getVarArg() { diff --git a/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java b/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java index a10483139..9b42f7bd3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java +++ b/jadx-core/src/main/java/jadx/core/dex/regions/loops/LoopRegion.java @@ -15,6 +15,7 @@ import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.regions.AbstractRegion; import jadx.core.dex.regions.conditions.IfCondition; +import jadx.core.utils.BlockUtils; public final class LoopRegion extends AbstractRegion { @@ -76,7 +77,7 @@ public final class LoopRegion extends AbstractRegion { } private IfNode getIfInsn() { - return (IfNode) conditionBlock.getInstructions().get(0); + return (IfNode) BlockUtils.getLastInsn(conditionBlock); } /** @@ -132,13 +133,8 @@ public final class LoopRegion extends AbstractRegion { } public int getConditionSourceLine() { - if (conditionBlock != null) { - List condInsns = conditionBlock.getInstructions(); - if (!condInsns.isEmpty()) { - return condInsns.get(0).getSourceLine(); - } - } - return 0; + InsnNode lastInsn = BlockUtils.getLastInsn(conditionBlock); + return lastInsn == null ? 0 : lastInsn.getSourceLine(); } public LoopType getType() { diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java index 191dc074e..aa8106a5e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/ExceptionHandler.java @@ -39,6 +39,7 @@ public class ExceptionHandler { /** * Add exception type to catch block + * * @param type - null for 'all' or 'Throwable' handler */ public void addCatchType(@Nullable ClassInfo type) { diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java index 20a0b8b5a..82405e7cc 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlock.java @@ -70,7 +70,7 @@ public class TryCatchBlock { for (BlockNode block : handler.getBlocks()) { // skip synthetic loop exit blocks BlockUtils.skipPredSyntheticPaths(block); - block.add(AFlag.SKIP); + block.add(AFlag.REMOVE); ExcHandlerAttr excHandlerAttr = block.get(AType.EXC_HANDLER); if (excHandlerAttr != null && excHandlerAttr.getHandler().equals(handler)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java index 963130f4d..af47e5973 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java @@ -47,10 +47,7 @@ public class ClassModifier extends AbstractVisitor { for (ClassNode inner : cls.getInnerClasses()) { visit(inner); } - if (cls.getAccessFlags().isSynthetic() - && cls.getFields().isEmpty() - && cls.getMethods().isEmpty() - && cls.getInnerClasses().isEmpty()) { + if (isEmptySyntheticClass(cls)) { cls.add(AFlag.DONT_GENERATE); return false; } @@ -61,6 +58,13 @@ public class ClassModifier extends AbstractVisitor { return false; } + private static boolean isEmptySyntheticClass(ClassNode cls) { + return cls.getAccessFlags().isSynthetic() + && cls.getFields().isEmpty() + && cls.getMethods().isEmpty() + && cls.getInnerClasses().isEmpty(); + } + private void markAnonymousClass(ClassNode cls) { if (cls.isAnonymous()) { cls.add(AFlag.ANONYMOUS_CLASS); @@ -126,7 +130,7 @@ public class ClassModifier extends AbstractVisitor { if (!fieldInfo.equals(field.getFieldInfo()) || !putInsn.getArg(0).equals(arg)) { return false; } - mth.removeFirstArgument(); + mth.skipFirstArgument(); InstructionRemover.remove(mth, block, insn); // other arg usage -> wrap with IGET insn if (arg.getSVar().getUseCount() != 0) { @@ -180,7 +184,7 @@ public class ClassModifier extends AbstractVisitor { return true; } } else { - if (argCls.contains(AFlag.DONT_GENERATE)) { + if (argCls.contains(AFlag.DONT_GENERATE) || isEmptySyntheticClass(argCls)) { return true; } } @@ -302,15 +306,34 @@ public class ClassModifier extends AbstractVisitor { private static void removeEmptyMethods(MethodNode mth) { AccessInfo af = mth.getAccessFlags(); - // remove public empty constructors + // remove public empty constructors (static or default) if (af.isConstructor() && (af.isPublic() || af.isStatic()) - && mth.getArguments(false).isEmpty() - && !mth.contains(AType.JADX_ERROR)) { + && mth.getArguments(false).isEmpty()) { List bb = mth.getBasicBlocks(); if (bb == null || bb.isEmpty() || BlockUtils.isAllBlocksEmpty(bb)) { - mth.add(AFlag.DONT_GENERATE); + if (af.isStatic() && mth.getMethodInfo().isClassInit()) { + mth.add(AFlag.DONT_GENERATE); + } else { + // don't remove default constructor if other constructors exists + if (mth.isDefaultConstructor() && !isNonDefaultConstructorExists(mth)) { + mth.add(AFlag.DONT_GENERATE); + } + } } } } + + private static boolean isNonDefaultConstructorExists(MethodNode defCtor) { + ClassNode parentClass = defCtor.getParentClass(); + for (MethodNode mth : parentClass.getMethods()) { + if (mth != defCtor + && mth.getAccessFlags().isConstructor() + && mth.getMethodInfo().isConstructor() + && !mth.isDefaultConstructor()) { + return true; + } + } + return false; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java index a82ba3909..905eeb933 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstInlineVisitor.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; import jadx.core.dex.attributes.AFlag; -import jadx.core.dex.info.FieldInfo; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.InvokeNode; @@ -16,14 +15,24 @@ import jadx.core.dex.instructions.args.PrimitiveType; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.visitors.typeinference.PostTypeInference; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; import jadx.core.utils.InstructionRemover; import jadx.core.utils.exceptions.JadxException; +import jadx.core.utils.exceptions.JadxOverflowException; +@JadxVisitor( + name = "Constants Inline", + desc = "Inline constant registers into instructions", + runAfter = { + SSATransform.class, + MarkFinallyVisitor.class + }, + runBefore = TypeInferenceVisitor.class +) public class ConstInlineVisitor extends AbstractVisitor { @Override @@ -35,21 +44,23 @@ public class ConstInlineVisitor extends AbstractVisitor { for (BlockNode block : mth.getBasicBlocks()) { toRemove.clear(); for (InsnNode insn : block.getInstructions()) { - if (checkInsn(mth, insn)) { - toRemove.add(insn); - } + checkInsn(mth, insn, toRemove); } InstructionRemover.removeAll(mth, block, toRemove); } } - private static boolean checkInsn(MethodNode mth, InsnNode insn) { - if (insn.getType() != InsnType.CONST || insn.contains(AFlag.DONT_INLINE)) { - return false; + private static void checkInsn(MethodNode mth, InsnNode insn, List toRemove) { + if (insn.contains(AFlag.DONT_INLINE) || insn.contains(AFlag.DONT_GENERATE)) { + return; + } + InsnType insnType = insn.getType(); + if (insnType != InsnType.CONST && insnType != InsnType.MOVE) { + return; } InsnArg arg = insn.getArg(0); if (!arg.isLiteral()) { - return false; + return; } long lit = ((LiteralArg) arg).getLiteral(); @@ -61,14 +72,36 @@ public class ConstInlineVisitor extends AbstractVisitor { assignInsn.add(AFlag.DONT_INLINE); } } + return; + } + + if (checkForFinallyBlock(sVar)) { + return; + } + + // all check passed, run replace + replaceConst(mth, insn, lit, toRemove); + } + + private static boolean checkForFinallyBlock(SSAVar sVar) { + List ssaVars = sVar.getCodeVar().getSsaVars(); + if (ssaVars.size() <= 1) { return false; } - ArgType resType = insn.getResult().getType(); - // make sure arg has correct type - if (!arg.getType().isTypeKnown()) { - arg.merge(mth.dex(), resType); + int countInsns = 0; + int countFinallyInsns = 0; + for (SSAVar ssaVar : ssaVars) { + for (RegisterArg reg : ssaVar.getUseList()) { + InsnNode parentInsn = reg.getParentInsn(); + if (parentInsn != null) { + countInsns++; + if (parentInsn.contains(AFlag.FINALLY_INSNS)) { + countFinallyInsns++; + } + } + } } - return replaceConst(mth, insn, lit); + return countFinallyInsns != 0 && countFinallyInsns != countInsns; } /** @@ -98,145 +131,61 @@ public class ConstInlineVisitor extends AbstractVisitor { return false; } - private static boolean replaceConst(MethodNode mth, InsnNode constInsn, long literal) { - SSAVar sVar = constInsn.getResult().getSVar(); - List use = new ArrayList<>(sVar.getUseList()); + private static int replaceConst(MethodNode mth, InsnNode constInsn, long literal, List toRemove) { + SSAVar ssaVar = constInsn.getResult().getSVar(); + List useList = new ArrayList<>(ssaVar.getUseList()); int replaceCount = 0; - for (RegisterArg arg : use) { - InsnNode useInsn = arg.getParentInsn(); - if (useInsn == null - || useInsn.getType() == InsnType.PHI - || useInsn.getType() == InsnType.MERGE) { - continue; - } - LiteralArg litArg; - ArgType argType = arg.getType(); - if (argType.isObject() && literal != 0) { - argType = ArgType.NARROW_NUMBERS; - } - if (use.size() == 1 || arg.isTypeImmutable()) { - // arg used only in one place - litArg = InsnArg.lit(literal, argType); - } else if (useInsn.getType() == InsnType.MOVE - && !useInsn.getResult().getType().isTypeKnown()) { - // save type for 'move' instructions (hard to find type in chains of 'move') - litArg = InsnArg.lit(literal, argType); - } else { - // in most cases type not equal arg.getType() - // just set unknown type and run type fixer - litArg = InsnArg.lit(literal, ArgType.UNKNOWN); - } - if (useInsn.replaceArg(arg, litArg)) { - fixTypes(mth, useInsn, litArg); + for (RegisterArg arg : useList) { + if (replaceArg(mth, arg, literal, constInsn, toRemove)) { replaceCount++; - if (useInsn.getType() == InsnType.RETURN) { - useInsn.setSourceLine(constInsn.getSourceLine()); - } - - FieldNode f = null; - ArgType litArgType = litArg.getType(); - if (litArgType.isTypeKnown()) { - f = mth.getParentClass().getConstFieldByLiteralArg(litArg); - } else if (litArgType.contains(PrimitiveType.INT)) { - f = mth.getParentClass().getConstField((int) literal, false); - } - if (f != null) { - litArg.wrapInstruction(new IndexInsnNode(InsnType.SGET, f.getFieldInfo(), 0)); - } } } - return replaceCount == use.size(); + if (replaceCount == useList.size()) { + toRemove.add(constInsn); + } + return replaceCount; } - /** - * This is method similar to PostTypeInference.process method, - * but contains some expensive operations needed only after constant inline - */ - private static void fixTypes(MethodNode mth, InsnNode insn, LiteralArg litArg) { - DexNode dex = mth.dex(); - PostTypeInference.process(mth, insn); - switch (insn.getType()) { - case CONST: - insn.getArg(0).merge(dex, insn.getResult()); - break; - - case MOVE: - insn.getResult().merge(dex, insn.getArg(0)); - insn.getArg(0).merge(dex, insn.getResult()); - break; - - case IPUT: - case SPUT: - IndexInsnNode node = (IndexInsnNode) insn; - insn.getArg(0).merge(dex, ((FieldInfo) node.getIndex()).getType()); - break; - - case IF: - InsnArg firstArg = insn.getArg(0); - InsnArg secondArg = insn.getArg(1); - if (firstArg == litArg) { - firstArg.merge(dex, secondArg); - } else { - secondArg.merge(dex, firstArg); - } - break; - - case CMP_G: - case CMP_L: - InsnArg arg0 = insn.getArg(0); - InsnArg arg1 = insn.getArg(1); - if (arg0 == litArg) { - arg0.merge(dex, arg1); - } else { - arg1.merge(dex, arg0); - } - break; - - case RETURN: - if (insn.getArgsCount() != 0) { - insn.getArg(0).merge(dex, mth.getReturnType()); - } - break; - - case INVOKE: - InvokeNode inv = (InvokeNode) insn; - List types = inv.getCallMth().getArgumentsTypes(); - int count = insn.getArgsCount(); - int k = types.size() == count ? 0 : -1; - for (int i = 0; i < count; i++) { - InsnArg arg = insn.getArg(i); - if (!arg.getType().isTypeKnown()) { - ArgType type; - if (k >= 0) { - type = types.get(k); - } else { - type = mth.getParentClass().getClassInfo().getType(); - } - arg.merge(dex, type); - } - k++; - } - break; - - case ARITH: - litArg.merge(dex, insn.getResult()); - break; - - case APUT: - case AGET: - if (litArg == insn.getArg(1)) { - litArg.merge(dex, ArgType.INT); - } - break; - - case NEW_ARRAY: - if (litArg == insn.getArg(0)) { - litArg.merge(dex, ArgType.INT); - } - break; - - default: - break; + private static boolean replaceArg(MethodNode mth, RegisterArg arg, long literal, InsnNode constInsn, List toRemove) { + InsnNode useInsn = arg.getParentInsn(); + if (useInsn == null) { + return false; } + InsnType insnType = useInsn.getType(); + if (insnType == InsnType.PHI) { + return false; + } + ArgType argType = arg.getInitType(); + if (argType.isObject() && literal != 0) { + argType = ArgType.NARROW_NUMBERS; + } + LiteralArg litArg = InsnArg.lit(literal, argType); + if (!useInsn.replaceArg(arg, litArg)) { + return false; + } + // arg replaced, made some optimizations + litArg.setType(arg.getInitType()); + + FieldNode fieldNode = null; + ArgType litArgType = litArg.getType(); + if (litArgType.isTypeKnown()) { + fieldNode = mth.getParentClass().getConstFieldByLiteralArg(litArg); + } else if (litArgType.contains(PrimitiveType.INT)) { + fieldNode = mth.getParentClass().getConstField((int) literal, false); + } + if (fieldNode != null) { + litArg.wrapInstruction(new IndexInsnNode(InsnType.SGET, fieldNode.getFieldInfo(), 0)); + } + + if (insnType == InsnType.RETURN) { + useInsn.setSourceLine(constInsn.getSourceLine()); + } else if (insnType == InsnType.MOVE) { + try { + replaceConst(mth, useInsn, literal, toRemove); + } catch (StackOverflowError e) { + throw new JadxOverflowException("Stack overflow at const inline visitor"); + } + } + return true; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java new file mode 100644 index 000000000..1d7fa21d9 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ConstructorVisitor.java @@ -0,0 +1,168 @@ +package jadx.core.dex.visitors; + +import java.util.ArrayList; + +import jadx.core.codegen.TypeGen; +import jadx.core.dex.info.MethodInfo; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.InvokeNode; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.LiteralArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.mods.ConstructorInsn; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.InstructionRemover; + +@JadxVisitor( + name = "ConstructorVisitor", + desc = "Replace invoke with constructor call", + runAfter = SSATransform.class, + runBefore = TypeInferenceVisitor.class +) +public class ConstructorVisitor extends AbstractVisitor { + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) { + return; + } + + replaceInvoke(mth); + } + + private static void replaceInvoke(MethodNode mth) { + InstructionRemover remover = new InstructionRemover(mth); + for (BlockNode block : mth.getBasicBlocks()) { + remover.setBlock(block); + int size = block.getInstructions().size(); + for (int i = 0; i < size; i++) { + InsnNode insn = block.getInstructions().get(i); + if (insn.getType() == InsnType.INVOKE) { + processInvoke(mth, block, i, remover); + } + } + remover.perform(); + } + } + + private static void processInvoke(MethodNode mth, BlockNode block, int indexInBlock, InstructionRemover remover) { + ClassNode parentClass = mth.getParentClass(); + InsnNode insn = block.getInstructions().get(indexInBlock); + InvokeNode inv = (InvokeNode) insn; + MethodInfo callMth = inv.getCallMth(); + if (!callMth.isConstructor()) { + return; + } + InsnNode instArgAssignInsn = ((RegisterArg) inv.getArg(0)).getAssignInsn(); + ConstructorInsn co = new ConstructorInsn(mth, inv); + boolean remove = false; + if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) { + remove = true; + } else if (co.isThis() && co.getArgsCount() == 0) { + MethodNode defCo = parentClass.searchMethodByShortId(callMth.getShortId()); + if (defCo == null || defCo.isNoCode()) { + // default constructor not implemented + remove = true; + } + } + // remove super() call in instance initializer + if (parentClass.isAnonymous() && mth.isDefaultConstructor() && co.isSuper()) { + remove = true; + } + if (remove) { + remover.add(insn); + return; + } + if (co.isNewInstance()) { + InsnNode newInstInsn = removeAssignChain(mth, instArgAssignInsn, remover, InsnType.NEW_INSTANCE); + if (newInstInsn != null) { + RegisterArg instArg = newInstInsn.getResult(); + RegisterArg resultArg = co.getResult(); + if (!resultArg.equals(instArg)) { + // replace all usages of 'instArg' with result of this constructor instruction + for (RegisterArg useArg : new ArrayList<>(instArg.getSVar().getUseList())) { + RegisterArg dup = resultArg.duplicate(); + InsnNode parentInsn = useArg.getParentInsn(); + parentInsn.replaceArg(useArg, dup); + dup.setParentInsn(parentInsn); + resultArg.getSVar().use(dup); + } + } + newInstInsn.setResult(null); // don't unbind result arg on remove + } + } + ConstructorInsn replace = processConstructor(mth, co); + if (replace != null) { + co = replace; + } + BlockUtils.replaceInsn(block, indexInBlock, co); + } + + /** + * Replace call of synthetic constructor + */ + private static ConstructorInsn processConstructor(MethodNode mth, ConstructorInsn co) { + MethodNode callMth = mth.dex().resolveMethod(co.getCallMth()); + if (callMth == null + || !callMth.getAccessFlags().isSynthetic() + || !allArgsNull(co)) { + return null; + } + ClassNode classNode = mth.dex().resolveClass(callMth.getParentClass().getClassInfo()); + if (classNode == null) { + return null; + } + RegisterArg instanceArg = co.getInstanceArg(); + boolean passThis = instanceArg.isThis(); + String ctrId = "(" + (passThis ? TypeGen.signature(instanceArg.getInitType()) : "") + ")V"; + MethodNode defCtr = classNode.searchMethodByShortId(ctrId); + if (defCtr == null) { + return null; + } + ConstructorInsn newInsn = new ConstructorInsn(defCtr.getMethodInfo(), co.getCallType(), instanceArg); + newInsn.setResult(co.getResult()); + return newInsn; + } + + private static boolean allArgsNull(ConstructorInsn insn) { + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg.isLiteral()) { + LiteralArg lit = (LiteralArg) insnArg; + if (lit.getLiteral() != 0) { + return false; + } + } else { + return false; + } + } + return true; + } + + /** + * Remove instructions on 'move' chain until instruction with type 'insnType' + */ + private static InsnNode removeAssignChain(MethodNode mth, InsnNode insn, InstructionRemover remover, InsnType insnType) { + if (insn == null) { + return null; + } + if (insn.isAttrStorageEmpty()) { + remover.add(insn); + } else { + BlockUtils.replaceInsn(mth, insn, new InsnNode(InsnType.NOP, 0)); + } + InsnType type = insn.getType(); + if (type == insnType) { + return insn; + } + if (type == InsnType.MOVE) { + RegisterArg arg = (RegisterArg) insn.getArg(0); + return removeAssignChain(mth, arg.getAssignInsn(), remover, insnType); + } + return null; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DebugInfoVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DebugInfoVisitor.java deleted file mode 100644 index bb8cdfe45..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DebugInfoVisitor.java +++ /dev/null @@ -1,80 +0,0 @@ -package jadx.core.dex.visitors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.nodes.parser.DebugInfoParser; -import jadx.core.utils.BlockUtils; -import jadx.core.utils.ErrorsCounter; -import jadx.core.utils.exceptions.DecodeException; -import jadx.core.utils.exceptions.JadxException; - -public class DebugInfoVisitor extends AbstractVisitor { - - private static final Logger LOG = LoggerFactory.getLogger(DebugInfoVisitor.class); - - @Override - public void visit(MethodNode mth) throws JadxException { - try { - int debugOffset = mth.getDebugInfoOffset(); - if (debugOffset > 0 && mth.dex().checkOffset(debugOffset)) { - processDebugInfo(mth, debugOffset); - } - } catch (Exception e) { - LOG.error("Error in debug info parser: {}", ErrorsCounter.formatMsg(mth, e.getMessage()), e); - } finally { - mth.unloadInsnArr(); - } - } - - private void processDebugInfo(MethodNode mth, int debugOffset) throws DecodeException { - InsnNode[] insnArr = mth.getInstructions(); - DebugInfoParser debugInfoParser = new DebugInfoParser(mth, debugOffset, insnArr); - debugInfoParser.process(); - - if (insnArr.length != 0) { - setMethodSourceLine(mth, insnArr); - } - if (!mth.getReturnType().equals(ArgType.VOID)) { - setLineForReturn(mth, insnArr); - } - } - - /** - * Fix debug info for splitter 'return' instructions - */ - private void setLineForReturn(MethodNode mth, InsnNode[] insnArr) { - for (BlockNode exit : mth.getExitBlocks()) { - InsnNode ret = BlockUtils.getLastInsn(exit); - if (ret != null) { - InsnNode oldRet = insnArr[ret.getOffset()]; - if (oldRet != ret) { - RegisterArg oldArg = (RegisterArg) oldRet.getArg(0); - RegisterArg newArg = (RegisterArg) ret.getArg(0); - newArg.mergeDebugInfo(oldArg.getType(), oldArg.getName()); - ret.setSourceLine(oldRet.getSourceLine()); - } - } - } - } - - /** - * Set method source line from first instruction - */ - private void setMethodSourceLine(MethodNode mth, InsnNode[] insnArr) { - for (InsnNode insn : insnArr) { - if (insn != null) { - int line = insn.getSourceLine(); - if (line != 0) { - mth.setSourceLine(line - 1); - } - return; - } - } - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java index 6eb97fa12..301062339 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/DotGraphVisitor.java @@ -96,8 +96,8 @@ public class DotGraphVisitor extends AbstractVisitor { dot.startLine("MethodNode[shape=record,label=\"{"); dot.add(escape(mth.getAccessFlags().makeString())); dot.add(escape(mth.getReturnType() + " " - + mth.getParentClass() + "." + mth.getName() - + "(" + Utils.listToString(mth.getArguments(true)) + ") ")); + + mth.getParentClass() + '.' + mth.getName() + + '(' + Utils.listToString(mth.getArguments(true)) + ") ")); String attrs = attributesString(mth); if (!attrs.isEmpty()) { @@ -185,9 +185,9 @@ public class DotGraphVisitor extends AbstractVisitor { dot.add("}\"];"); BlockNode falsePath = null; - List list = block.getInstructions(); - if (!list.isEmpty() && list.get(0).getType() == InsnType.IF) { - falsePath = ((IfNode) list.get(0)).getElseBlock(); + InsnNode lastInsn = BlockUtils.getLastInsn(block); + if (lastInsn != null && lastInsn.getType() == InsnType.IF) { + falsePath = ((IfNode) lastInsn).getElseBlock(); } for (BlockNode next : block.getSuccessors()) { String style = next == falsePath ? "[style=dashed]" : ""; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java index d00a114ea..b7a553560 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/EnumVisitor.java @@ -26,6 +26,7 @@ import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.InsnUtils; import jadx.core.utils.exceptions.JadxException; @@ -33,7 +34,7 @@ import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "EnumVisitor", desc = "Restore enum classes", - runAfter = {CodeShrinker.class, ModVisitor.class} + runAfter = {CodeShrinkVisitor.class, ModVisitor.class} ) public class EnumVisitor extends AbstractVisitor { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/InitCodeVariables.java b/jadx-core/src/main/java/jadx/core/dex/visitors/InitCodeVariables.java new file mode 100644 index 000000000..298a39fde --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/InitCodeVariables.java @@ -0,0 +1,110 @@ +package jadx.core.dex.visitors; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.instructions.PhiInsn; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.utils.exceptions.JadxException; +import jadx.core.utils.exceptions.JadxRuntimeException; + +@JadxVisitor( + name = "InitCodeVariables", + desc = "Initialize code variables", + runAfter = SSATransform.class +) +public class InitCodeVariables extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode()) { + return; + } + initCodeVars(mth); + } + + private static void initCodeVars(MethodNode mth) { + for (RegisterArg mthArg : mth.getArguments(true)) { + initCodeVar(mthArg.getSVar()); + } + for (SSAVar ssaVar : mth.getSVars()) { + initCodeVar(ssaVar); + } + } + + public static void initCodeVar(SSAVar ssaVar) { + if (ssaVar.isCodeVarSet()) { + return; + } + CodeVar codeVar = new CodeVar(); + RegisterArg assignArg = ssaVar.getAssign(); + if (assignArg.contains(AFlag.THIS)) { + codeVar.setName(RegisterArg.THIS_ARG_NAME); + codeVar.setThis(true); + } + if (assignArg.contains(AFlag.METHOD_ARGUMENT) || assignArg.contains(AFlag.CUSTOM_DECLARE)) { + codeVar.setDeclared(true); + } + + setCodeVar(ssaVar, codeVar); + } + + private static void setCodeVar(SSAVar ssaVar, CodeVar codeVar) { + PhiInsn usedInPhi = ssaVar.getUsedInPhi(); + if (usedInPhi != null) { + Set vars = new LinkedHashSet<>(); + vars.add(ssaVar); + collectConnectedVars(usedInPhi, vars); + setCodeVarType(codeVar, vars); + vars.forEach(var -> { + if (var.isCodeVarSet()) { + codeVar.mergeFlagsFrom(var.getCodeVar()); + } + var.setCodeVar(codeVar); + }); + } else { + ssaVar.setCodeVar(codeVar); + } + } + + private static void setCodeVarType(CodeVar codeVar, Set vars) { + if (vars.size() > 1) { + List imTypes = vars.stream() + .filter(var -> var.contains(AFlag.IMMUTABLE_TYPE)) + .map(var -> var.getTypeInfo().getType()) + .filter(ArgType::isTypeKnown) + .distinct() + .collect(Collectors.toList()); + int imCount = imTypes.size(); + if (imCount == 1) { + codeVar.setType(imTypes.get(0)); + } else if (imCount > 1) { + throw new JadxRuntimeException("Several immutable types in one variable: " + imTypes + ", vars: " + vars); + } + } + } + + private static void collectConnectedVars(PhiInsn phiInsn, Set vars) { + if (phiInsn == null) { + return; + } + SSAVar resultVar = phiInsn.getResult().getSVar(); + if (vars.add(resultVar)) { + collectConnectedVars(resultVar.getUsedInPhi(), vars); + } + phiInsn.getArguments().forEach(arg -> { + SSAVar sVar = ((RegisterArg) arg).getSVar(); + if (vars.add(sVar)) { + collectConnectedVars(sVar.getUsedInPhi(), vars); + } + }); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/MarkFinallyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/MarkFinallyVisitor.java new file mode 100644 index 000000000..87742c789 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/MarkFinallyVisitor.java @@ -0,0 +1,435 @@ +package jadx.core.dex.visitors; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Jadx; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.trycatch.SplitterBlockAttr; +import jadx.core.dex.trycatch.TryCatchBlock; +import jadx.core.dex.visitors.blocksmaker.helpers.FinallyExtractInfo; +import jadx.core.dex.visitors.blocksmaker.helpers.InsnsSlice; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.utils.BlockUtils; + +@JadxVisitor( + name = "MarkFinallyVisitor", + desc = "Search and mark duplicate code generated for finally block", + runAfter = SSATransform.class, + runBefore = ConstInlineVisitor.class +) +public class MarkFinallyVisitor extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class); + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode() || mth.isNoExceptionHandlers()) { + return; + } + try { + for (ExceptionHandler excHandler : mth.getExceptionHandlers()) { + processExceptionHandler(mth, excHandler); + } + } catch (Exception e) { + LOG.warn("Undo finally extract visitor, mth: {}", mth, e); + try { + // reload method without applying this visitor + // TODO: make more common and less hacky + mth.unload(); + mth.load(); + List passes = Jadx.getPassesList(mth.root().getArgs()); + for (IDexTreeVisitor visitor : passes) { + if (visitor instanceof MarkFinallyVisitor) { + break; + } + visitor.init(mth.root()); + DepthTraversal.visit(visitor, mth); + } + } catch (Exception ee) { + LOG.error("Undo finally extract failed, mth: {}", mth, e); + } + } + } + + private static boolean processExceptionHandler(MethodNode mth, ExceptionHandler excHandler) { + // check if handler has exit edge to block not from this handler + boolean noExitNode = true; + InsnNode reThrowInsn = null; + + for (BlockNode excBlock : excHandler.getBlocks()) { + if (noExitNode) { + noExitNode = excHandler.getBlocks().containsAll(excBlock.getCleanSuccessors()); + } + List insns = excBlock.getInstructions(); + int size = insns.size(); + if (excHandler.isCatchAll() + && size != 0 + && insns.get(size - 1).getType() == InsnType.THROW) { + reThrowInsn = insns.get(size - 1); + } + } + if (noExitNode && reThrowInsn != null) { + boolean extracted = extractFinally(mth, excHandler); + if (extracted) { + reThrowInsn.add(AFlag.DONT_GENERATE); + } + return extracted; + } + return false; + } + + /** + * Search and mark common code from 'try' block and 'handlers'. + */ + private static boolean extractFinally(MethodNode mth, ExceptionHandler allHandler) { + List handlerBlocks = new ArrayList<>(); + for (BlockNode block : allHandler.getBlocks()) { + InsnNode lastInsn = BlockUtils.getLastInsn(block); + if (lastInsn != null) { + InsnType insnType = lastInsn.getType(); + if (insnType != InsnType.MOVE_EXCEPTION && insnType != InsnType.THROW) { + handlerBlocks.add(block); + } + } + } + if (handlerBlocks.isEmpty()) { + // remove empty catch + allHandler.getTryBlock().removeHandler(mth, allHandler); + return true; + } + + BlockNode startBlock = handlerBlocks.get(0); + FinallyExtractInfo extractInfo = new FinallyExtractInfo(allHandler, startBlock, handlerBlocks); + + // remove 'finally' from 'catch' handlers + TryCatchBlock tryBlock = allHandler.getTryBlock(); + if (tryBlock.getHandlersCount() > 1) { + for (ExceptionHandler otherHandler : tryBlock.getHandlers()) { + if (otherHandler == allHandler) { + continue; + } + for (BlockNode checkBlock : otherHandler.getBlocks()) { + if (searchDuplicateInsns(checkBlock, extractInfo)) { + break; + } + } + } + if (extractInfo.getDuplicateSlices().size() != tryBlock.getHandlersCount() - 1) { + return false; + } + } + + Set splitters = new HashSet<>(); + for (ExceptionHandler otherHandler : tryBlock.getHandlers()) { + SplitterBlockAttr splitterAttr = otherHandler.getHandlerBlock().get(AType.SPLITTER_BLOCK); + if (splitterAttr != null) { + BlockNode splBlock = splitterAttr.getBlock(); + if (!splBlock.getCleanSuccessors().isEmpty()) { + splitters.add(splBlock); + } + } + } + + // remove 'finally' from 'try' blocks (dominated by splitter block) + boolean found = false; + for (BlockNode splitter : splitters) { + BlockNode start = splitter.getCleanSuccessors().get(0); + List list = BlockUtils.collectBlocksDominatedBy(splitter, start); + for (BlockNode block : list) { + if (searchDuplicateInsns(block, extractInfo)) { + found = true; + break; + } + } + } + if (!found) { + return false; + } + if (!checkSlices(extractInfo)) { + mth.addComment("JADX INFO: finally extract failed"); + return false; + } + + // 'finally' extract confirmed, apply + apply(extractInfo); + allHandler.setFinally(true); + return true; + } + + private static boolean checkSlices(FinallyExtractInfo extractInfo) { + InsnsSlice finallySlice = extractInfo.getFinallyInsnsSlice(); + List finallyInsnsList = finallySlice.getInsnsList(); + + for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) { + List dupInsnsList = dupSlice.getInsnsList(); + if (dupInsnsList.size() != finallyInsnsList.size()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Incorrect finally slice size: {}, expected: {}", dupSlice, finallySlice); + } + return false; + } + } + for (int i = 0; i < finallyInsnsList.size(); i++) { + InsnNode finallyInsn = finallyInsnsList.get(i); + for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) { + List insnsList = dupSlice.getInsnsList(); + InsnNode dupInsn = insnsList.get(i); + if (finallyInsn.getType() != dupInsn.getType()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Incorrect finally slice insn: {}, expected: {}", dupInsn, finallyInsn); + } + return false; + } + } + } + return true; + } + + private static void apply(FinallyExtractInfo extractInfo) { + markSlice(extractInfo.getFinallyInsnsSlice(), AFlag.FINALLY_INSNS); + for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) { + markSlice(dupSlice, AFlag.DONT_GENERATE); + } + InsnsSlice finallySlice = extractInfo.getFinallyInsnsSlice(); + List finallyInsnsList = finallySlice.getInsnsList(); + for (int i = 0; i < finallyInsnsList.size(); i++) { + InsnNode finallyInsn = finallyInsnsList.get(i); + for (InsnsSlice dupSlice : extractInfo.getDuplicateSlices()) { + InsnNode dupInsn = dupSlice.getInsnsList().get(i); + copyCodeVars(finallyInsn, dupInsn); + } + } + } + + private static void markSlice(InsnsSlice slice, AFlag flag) { + List insnsList = slice.getInsnsList(); + for (InsnNode insn : insnsList) { + insn.add(flag); + } + for (BlockNode block : slice.getBlocks()) { + boolean allInsnMarked = true; + for (InsnNode insn : block.getInstructions()) { + if (!insn.contains(flag)) { + allInsnMarked = false; + break; + } + } + if (allInsnMarked) { + block.add(flag); + } + } + } + + private static void copyCodeVars(InsnNode fromInsn, InsnNode toInsn) { + copyCodeVars(fromInsn.getResult(), toInsn.getResult()); + int argsCount = fromInsn.getArgsCount(); + for (int i = 0; i < argsCount; i++) { + copyCodeVars(fromInsn.getArg(i), toInsn.getArg(i)); + } + } + + private static void copyCodeVars(InsnArg fromArg, InsnArg toArg) { + if (fromArg == null || toArg == null + || !fromArg.isRegister() || !toArg.isRegister()) { + return; + } + SSAVar fromSsaVar = ((RegisterArg) fromArg).getSVar(); + SSAVar toSsaVar = ((RegisterArg) toArg).getSVar(); + toSsaVar.setCodeVar(fromSsaVar.getCodeVar()); + } + + private static boolean searchDuplicateInsns(BlockNode checkBlock, FinallyExtractInfo extractInfo) { + boolean isNew = extractInfo.getCheckedBlocks().add(checkBlock); + if (!isNew) { + return false; + } + BlockNode startBlock = extractInfo.getStartBlock(); + InsnsSlice dupSlice = searchFromFirstBlock(checkBlock, startBlock, extractInfo); + if (dupSlice == null) { + return false; + } + extractInfo.getDuplicateSlices().add(dupSlice); + return true; + } + + private static InsnsSlice searchFromFirstBlock(BlockNode dupBlock, BlockNode startBlock, FinallyExtractInfo extractInfo) { + InsnsSlice dupSlice = isStartBlock(dupBlock, startBlock, extractInfo); + if (dupSlice == null) { + return null; + } + if (dupSlice.isComplete()) { + return dupSlice; + } + if (!checkBlocksTree(dupBlock, startBlock, dupSlice, extractInfo)) { + return null; + } + return dupSlice; + } + + /** + * 'Finally' instructions can start in the middle of the first block. + */ + private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) { + List dupInsns = dupBlock.getInstructions(); + List finallyInsns = finallyBlock.getInstructions(); + if (dupInsns.size() < finallyInsns.size()) { + return null; + } + int startPos = dupInsns.size() - finallyInsns.size(); + int endPos = 0; + // fast check from end of block + if (!checkInsns(dupInsns, finallyInsns, startPos)) { + // check from block start + if (checkInsns(dupInsns, finallyInsns, 0)) { + startPos = 0; + endPos = finallyInsns.size(); + } else { + // search start insn + boolean found = false; + for (int i = 1; i < startPos; i++) { + if (checkInsns(dupInsns, finallyInsns, i)) { + startPos = i; + endPos = finallyInsns.size() + i; + found = true; + break; + } + } + if (!found) { + return null; + } + } + } + + // put instructions into slices + boolean complete; + InsnsSlice slice = new InsnsSlice(); + int endIndex; + if (endPos != 0) { + endIndex = endPos + 1; + // both slices completed + complete = true; + } else { + endIndex = dupInsns.size(); + complete = false; + } + + // fill dup insns slice + for (int i = startPos; i < endIndex; i++) { + slice.addInsn(dupInsns.get(i), dupBlock); + } + + // fill finally insns slice + InsnsSlice finallySlice = extractInfo.getFinallyInsnsSlice(); + if (finallySlice.isComplete()) { + // compare slices + if (finallySlice.getInsnsList().size() != slice.getInsnsList().size()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Another duplicated slice has different insns count: {}, finally: {}", slice, finallySlice); + } + return null; + } + // TODO: add additional slices checks + // and try to extract common part if found difference + } else { + for (InsnNode finallyInsn : finallyInsns) { + finallySlice.addInsn(finallyInsn, finallyBlock); + } + } + + if (complete) { + slice.setComplete(true); + finallySlice.setComplete(true); + } + return slice; + } + + private static boolean checkInsns(List remInsns, List finallyInsns, int delta) { + for (int i = finallyInsns.size() - 1; i >= 0; i--) { + InsnNode startInsn = finallyInsns.get(i); + InsnNode remInsn = remInsns.get(delta + i); + if (!sameInsns(remInsn, startInsn)) { + return false; + } + } + return true; + } + + private static boolean checkBlocksTree(BlockNode dupBlock, BlockNode finallyBlock, + InsnsSlice dupSlice, FinallyExtractInfo extractInfo) { + InsnsSlice finallySlice = extractInfo.getFinallyInsnsSlice(); + + List finallyCS = finallyBlock.getCleanSuccessors(); + List dupCS = dupBlock.getCleanSuccessors(); + if (finallyCS.size() == dupCS.size()) { + for (int i = 0; i < finallyCS.size(); i++) { + BlockNode finSBlock = finallyCS.get(i); + BlockNode dupSBlock = dupCS.get(i); + if (extractInfo.getAllHandlerBlocks().contains(finSBlock)) { + if (!compareBlocks(dupSBlock, finSBlock, dupSlice, extractInfo)) { + return false; + } + if (!checkBlocksTree(dupSBlock, finSBlock, dupSlice, extractInfo)) { + return false; + } + dupSlice.addBlock(dupSBlock); + finallySlice.addBlock(finSBlock); + } + } + } + dupSlice.setComplete(true); + finallySlice.setComplete(true); + return true; + } + + private static boolean compareBlocks(BlockNode dupBlock, BlockNode finallyBlock, InsnsSlice dupSlice, FinallyExtractInfo extractInfo) { + List dupInsns = dupBlock.getInstructions(); + List finallyInsns = finallyBlock.getInstructions(); + if (dupInsns.size() < finallyInsns.size()) { + return false; + } + int size = finallyInsns.size(); + for (int i = 0; i < size; i++) { + if (!sameInsns(dupInsns.get(i), finallyInsns.get(i))) { + return false; + } + } + if (dupInsns.size() > finallyInsns.size()) { + dupSlice.addInsns(dupBlock, 0, finallyInsns.size()); + dupSlice.setComplete(true); + InsnsSlice finallyInsnsSlice = extractInfo.getFinallyInsnsSlice(); + finallyInsnsSlice.addBlock(finallyBlock); + finallyInsnsSlice.setComplete(true); + } + return true; + } + + private static boolean sameInsns(InsnNode remInsn, InsnNode fInsn) { + if (!remInsn.isSame(fInsn)) { + return false; + } + // TODO: check instance arg in ConstructorInsn + // TODO: compare literals + for (int i = 0; i < remInsn.getArgsCount(); i++) { + InsnArg remArg = remInsn.getArg(i); + InsnArg fArg = fInsn.getArg(i); + if (remArg.isRegister() != fArg.isRegister()) { + return false; + } + } + return true; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java index 3798cd043..e3ba1e651 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/MethodInlineVisitor.java @@ -21,6 +21,7 @@ import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( @@ -77,7 +78,7 @@ public class MethodInlineVisitor extends AbstractVisitor { && get.getResult().equalRegisterAndType((RegisterArg) retArg)) { RegisterArg retReg = (RegisterArg) retArg; retReg.getSVar().removeUse(retReg); - CodeShrinker.shrinkMethod(mth); + CodeShrinkVisitor.shrinkMethod(mth); insnList = firstBlock.getInstructions(); if (insnList.size() == 1) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java index 77c1142de..f10552f15 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java @@ -1,6 +1,5 @@ package jadx.core.dex.visitors; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -9,8 +8,6 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.core.codegen.TypeGen; -import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; @@ -24,7 +21,6 @@ import jadx.core.dex.instructions.FillArrayNode; import jadx.core.dex.instructions.FilledNewArrayNode; import jadx.core.dex.instructions.IndexInsnNode; import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.InvokeNode; import jadx.core.dex.instructions.NewArrayNode; import jadx.core.dex.instructions.SwitchNode; import jadx.core.dex.instructions.args.ArgType; @@ -41,11 +37,14 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExcHandlerAttr; import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.ErrorsCounter; import jadx.core.utils.InsnUtils; import jadx.core.utils.InstructionRemover; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.utils.BlockUtils.replaceInsn; + /** * Visitor for modify method instructions * (remove, replace, process exception handlers) @@ -53,7 +52,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; @JadxVisitor( name = "ModVisitor", desc = "Modify method instructions", - runBefore = CodeShrinker.class + runBefore = CodeShrinkVisitor.class ) public class ModVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(ModVisitor.class); @@ -67,8 +66,6 @@ public class ModVisitor extends AbstractVisitor { InstructionRemover remover = new InstructionRemover(mth); replaceStep(mth, remover); removeStep(mth, remover); - - checkArgsNames(mth); } private static void replaceStep(MethodNode mth, InstructionRemover remover) { @@ -79,8 +76,8 @@ public class ModVisitor extends AbstractVisitor { for (int i = 0; i < size; i++) { InsnNode insn = block.getInstructions().get(i); switch (insn.getType()) { - case INVOKE: - processInvoke(mth, block, i, remover); + case CONSTRUCTOR: + processAnonymousConstructor(mth, ((ConstructorInsn) insn)); break; case CONST: @@ -115,24 +112,20 @@ public class ModVisitor extends AbstractVisitor { break; case NEW_ARRAY: - // create array in 'fill-array' instruction + // replace with filled array if 'fill-array' is next instruction int next = i + 1; if (next < size) { InsnNode ni = block.getInstructions().get(next); if (ni.getType() == InsnType.FILL_ARRAY) { - ni.getResult().merge(mth.dex(), insn.getResult()); - ArgType arrType = ((NewArrayNode) insn).getArrayType(); - ((FillArrayNode) ni).mergeElementType(mth.dex(), arrType.getArrayElement()); - remover.add(insn); + InsnNode filledArr = makeFilledArrayInsn(mth, (NewArrayNode) insn, (FillArrayNode) ni); + if (filledArr != null) { + replaceInsn(block, i, filledArr); + remover.add(ni); + } } } break; - case FILL_ARRAY: - InsnNode filledArr = makeFilledArrayInsn(mth, (FillArrayNode) insn); - replaceInsn(block, i, filledArr); - break; - case MOVE_EXCEPTION: processMoveException(block, insn, remover); break; @@ -151,6 +144,18 @@ public class ModVisitor extends AbstractVisitor { } break; + case CHECK_CAST: + InsnArg castArg = insn.getArg(0); + ArgType castType = (ArgType) ((IndexInsnNode) insn).getIndex(); + if (!ArgType.isCastNeeded(mth.dex(), castArg.getType(), castType) + || isCastDuplicate((IndexInsnNode) insn)) { + InsnNode insnNode = new InsnNode(InsnType.MOVE, 1); + insnNode.setResult(insn.getResult()); + insnNode.addArg(castArg); + replaceInsn(block, i, insnNode); + } + break; + default: break; } @@ -159,6 +164,21 @@ public class ModVisitor extends AbstractVisitor { } } + private static boolean isCastDuplicate(IndexInsnNode castInsn) { + InsnArg arg = castInsn.getArg(0); + if (arg.isRegister()) { + SSAVar sVar = ((RegisterArg) arg).getSVar(); + if (sVar != null && sVar.getUseCount() == 1 && !sVar.isUsedInPhi()) { + InsnNode assignInsn = sVar.getAssign().getParentInsn(); + if (assignInsn != null && assignInsn.getType() == InsnType.CHECK_CAST) { + ArgType assignCastType = (ArgType) ((IndexInsnNode) assignInsn).getIndex(); + return assignCastType.equals(castInsn.getIndex()); + } + } + } + return false; + } + /** * Remove unnecessary instructions */ @@ -181,60 +201,6 @@ public class ModVisitor extends AbstractVisitor { } } - private static void processInvoke(MethodNode mth, BlockNode block, int insnNumber, InstructionRemover remover) { - ClassNode parentClass = mth.getParentClass(); - InsnNode insn = block.getInstructions().get(insnNumber); - InvokeNode inv = (InvokeNode) insn; - MethodInfo callMth = inv.getCallMth(); - if (!callMth.isConstructor()) { - return; - } - InsnNode instArgAssignInsn = ((RegisterArg) inv.getArg(0)).getAssignInsn(); - ConstructorInsn co = new ConstructorInsn(mth, inv); - boolean remove = false; - if (co.isSuper() && (co.getArgsCount() == 0 || parentClass.isEnum())) { - remove = true; - } else if (co.isThis() && co.getArgsCount() == 0) { - MethodNode defCo = parentClass.searchMethodByShortId(callMth.getShortId()); - if (defCo == null || defCo.isNoCode()) { - // default constructor not implemented - remove = true; - } - } - // remove super() call in instance initializer - if (parentClass.isAnonymous() && mth.isDefaultConstructor() && co.isSuper()) { - remove = true; - } - if (remove) { - remover.add(insn); - return; - } - if (co.isNewInstance()) { - InsnNode newInstInsn = removeAssignChain(instArgAssignInsn, remover, InsnType.NEW_INSTANCE); - if (newInstInsn != null) { - RegisterArg instArg = newInstInsn.getResult(); - RegisterArg resultArg = co.getResult(); - if (!resultArg.equals(instArg)) { - // replace all usages of 'instArg' with result of this constructor instruction - for (RegisterArg useArg : new ArrayList<>(instArg.getSVar().getUseList())) { - RegisterArg dup = resultArg.duplicate(); - InsnNode parentInsn = useArg.getParentInsn(); - parentInsn.replaceArg(useArg, dup); - dup.setParentInsn(parentInsn); - resultArg.getSVar().use(dup); - } - } - } - } - ConstructorInsn replace = processConstructor(mth, co); - if (replace != null) { - co = replace; - } - replaceInsn(block, insnNumber, co); - - processAnonymousConstructor(mth, co); - } - private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn co) { MethodInfo callMth = co.getCallMth(); MethodNode callMthNode = mth.dex().resolveMethod(callMth); @@ -270,7 +236,7 @@ public class ModVisitor extends AbstractVisitor { RegisterArg reg = (RegisterArg) arg; SSAVar sVar = reg.getSVar(); if (sVar != null) { - sVar.add(AFlag.FINAL); + sVar.getCodeVar().setFinal(true); sVar.add(AFlag.DONT_INLINE); } reg.add(AFlag.SKIP_ARG); @@ -331,33 +297,8 @@ public class ModVisitor extends AbstractVisitor { return parentInsn; } - /** - * Replace call of synthetic constructor - */ - private static ConstructorInsn processConstructor(MethodNode mth, ConstructorInsn co) { - MethodNode callMth = mth.dex().resolveMethod(co.getCallMth()); - if (callMth == null - || !callMth.getAccessFlags().isSynthetic() - || !allArgsNull(co)) { - return null; - } - ClassNode classNode = mth.dex().resolveClass(callMth.getParentClass().getClassInfo()); - if (classNode == null) { - return null; - } - boolean passThis = co.getArgsCount() >= 1 && co.getArg(0).isThis(); - String ctrId = "(" + (passThis ? TypeGen.signature(co.getArg(0).getType()) : "") + ")V"; - MethodNode defCtr = classNode.searchMethodByShortId(ctrId); - if (defCtr == null) { - return null; - } - ConstructorInsn newInsn = new ConstructorInsn(defCtr.getMethodInfo(), co.getCallType(), co.getInstanceArg()); - newInsn.setResult(co.getResult()); - return newInsn; - } - - private static InsnNode makeFilledArrayInsn(MethodNode mth, FillArrayNode insn) { - ArgType insnArrayType = insn.getResult().getType(); + private static InsnNode makeFilledArrayInsn(MethodNode mth, NewArrayNode newArrayNode, FillArrayNode insn) { + ArgType insnArrayType = newArrayNode.getArrayType(); ArgType insnElementType = insnArrayType.getArrayElement(); ArgType elType = insn.getElementType(); if (!elType.isTypeKnown() @@ -378,12 +319,10 @@ public class ModVisitor extends AbstractVisitor { throw new JadxRuntimeException("Null array element type"); } } - insn.mergeElementType(mth.dex(), elType); - elType = insn.getElementType(); - List list = insn.getLiteralArgs(); + List list = insn.getLiteralArgs(elType); InsnNode filledArr = new FilledNewArrayNode(elType, list.size()); - filledArr.setResult(insn.getResult()); + filledArr.setResult(newArrayNode.getResult()); for (LiteralArg arg : list) { FieldNode f = mth.getParentClass().getConstFieldByLiteralArg(arg); if (f != null) { @@ -396,39 +335,6 @@ public class ModVisitor extends AbstractVisitor { return filledArr; } - private static boolean allArgsNull(ConstructorInsn insn) { - for (InsnArg insnArg : insn.getArguments()) { - if (insnArg.isLiteral()) { - LiteralArg lit = (LiteralArg) insnArg; - if (lit.getLiteral() != 0) { - return false; - } - } else { - return false; - } - } - return true; - } - - /** - * Remove instructions on 'move' chain until instruction with type 'insnType' - */ - private static InsnNode removeAssignChain(InsnNode insn, InstructionRemover remover, InsnType insnType) { - if (insn == null) { - return null; - } - remover.add(insn); - InsnType type = insn.getType(); - if (type == insnType) { - return insn; - } - if (type == InsnType.MOVE) { - RegisterArg arg = (RegisterArg) insn.getArg(0); - return removeAssignChain(arg.getAssignInsn(), remover, insnType); - } - return null; - } - private static void processMoveException(BlockNode block, InsnNode insn, InstructionRemover remover) { ExcHandlerAttr excHandlerAttr = block.get(AType.EXC_HANDLER); if (excHandlerAttr == null) { @@ -457,25 +363,4 @@ public class ModVisitor extends AbstractVisitor { replaceInsn(block, 0, moveInsn); } } - - /** - * Replace insn by index i in block, - * for proper copy attributes, assume attributes are not overlap - */ - private static void replaceInsn(BlockNode block, int i, InsnNode insn) { - InsnNode prevInsn = block.getInstructions().get(i); - insn.copyAttributesFrom(prevInsn); - insn.setSourceLine(prevInsn.getSourceLine()); - block.getInstructions().set(i, insn); - } - - private static void checkArgsNames(MethodNode mth) { - for (RegisterArg arg : mth.getArguments(false)) { - String name = arg.getName(); - if (name != null && NameMapper.isReserved(name)) { - name = name + '_'; - arg.getSVar().setName(name); - } - } - } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java b/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java index 668057b1f..6223429c8 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/PrepareForCodeGen.java @@ -14,6 +14,8 @@ import jadx.core.dex.instructions.mods.ConstructorInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.regions.variables.ProcessVariables; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.exceptions.JadxException; /** @@ -24,7 +26,7 @@ import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "PrepareForCodeGen", desc = "Prepare instructions for code generation pass", - runAfter = {CodeShrinker.class, ClassModifier.class} + runAfter = {CodeShrinkVisitor.class, ClassModifier.class, ProcessVariables.class} ) public class PrepareForCodeGen extends AbstractVisitor { @@ -35,6 +37,9 @@ public class PrepareForCodeGen extends AbstractVisitor { return; } for (BlockNode block : blocks) { + if (block.contains(AFlag.DONT_GENERATE)) { + continue; + } removeInstructions(block); checkInline(block); // removeParenthesis(block); @@ -141,11 +146,11 @@ public class PrepareForCodeGen extends AbstractVisitor { replace = true; } else if (arg.isRegister()) { RegisterArg regArg = (RegisterArg) arg; - replace = res.equalRegisterAndType(regArg); + replace = res.sameCodeVar(regArg); } if (replace) { insn.add(AFlag.ARITH_ONEARG); - insn.getResult().mergeName(arg); +// insn.getResult().mergeName(arg); } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ReSugarCode.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ReSugarCode.java index 134375919..89ef2b85c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ReSugarCode.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ReSugarCode.java @@ -1,6 +1,7 @@ package jadx.core.dex.visitors; import java.util.List; +import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -21,19 +22,25 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.LiteralArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; +import jadx.core.utils.InsnList; +import jadx.core.utils.InsnUtils; import jadx.core.utils.InstructionRemover; +import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxException; @JadxVisitor( name = "ReSugarCode", desc = "Simplify synthetic or verbose code", - runAfter = CodeShrinker.class + runAfter = CodeShrinkVisitor.class ) public class ReSugarCode extends AbstractVisitor { @@ -56,60 +63,119 @@ public class ReSugarCode extends AbstractVisitor { List instructions = block.getInstructions(); int size = instructions.size(); for (int i = 0; i < size; i++) { - InsnNode replacedInsn = process(mth, instructions, i, remover); - if (replacedInsn != null) { - instructions.set(i, replacedInsn); - } + process(mth, instructions, i, remover); } remover.perform(); } } - private static InsnNode process(MethodNode mth, List instructions, int i, InstructionRemover remover) { + private static void process(MethodNode mth, List instructions, int i, InstructionRemover remover) { InsnNode insn = instructions.get(i); + if (insn.contains(AFlag.REMOVE)) { + return; + } switch (insn.getType()) { case NEW_ARRAY: - return processNewArray(mth, instructions, i, remover); + processNewArray(mth, instructions, i, remover); + break; case SWITCH: processEnumSwitch(mth, (SwitchNode) insn); - return null; + break; default: - return null; + break; } } /** * Replace new array and sequence of array-put to new filled-array instruction. */ - private static InsnNode processNewArray(MethodNode mth, List instructions, int i, - InstructionRemover remover) { + private static void processNewArray(MethodNode mth, List instructions, int i, + InstructionRemover remover) { NewArrayNode newArrayInsn = (NewArrayNode) instructions.get(i); - InsnArg arg = newArrayInsn.getArg(0); - if (!arg.isLiteral()) { - return null; + InsnArg arrLenArg = newArrayInsn.getArg(0); + if (!arrLenArg.isLiteral()) { + return; } - int len = (int) ((LiteralArg) arg).getLiteral(); - int size = instructions.size(); - if (len <= 0 - || i + len >= size - || instructions.get(i + len).getType() != InsnType.APUT) { - return null; + int len = (int) ((LiteralArg) arrLenArg).getLiteral(); + if (len == 0) { + return; } + RegisterArg arrArg = newArrayInsn.getResult(); + SSAVar ssaVar = arrArg.getSVar(); + List useList = ssaVar.getUseList(); + if (useList.size() < len) { + return; + } + // check sequential array put with increasing index + int putIndex = 0; + for (RegisterArg useArg : useList) { + InsnNode insn = useArg.getParentInsn(); + if (checkPutInsn(mth, insn, arrArg, putIndex)) { + putIndex++; + } else { + break; + } + } + if (putIndex != len) { + return; + } + List arrPuts = useList.subList(0, len).stream().map(InsnArg::getParentInsn).collect(Collectors.toList()); + // check that all puts in current block + for (InsnNode arrPut : arrPuts) { + int index = InsnList.getIndex(instructions, arrPut); + if (index == -1) { + if (LOG.isDebugEnabled()) { + LOG.debug("TODO: APUT found in different block: {}, mth: {}", arrPut, mth); + } + return; + } + } + + // checks complete, apply ArgType arrType = newArrayInsn.getArrayType(); InsnNode filledArr = new FilledNewArrayNode(arrType.getArrayElement(), len); - filledArr.setResult(newArrayInsn.getResult()); - for (int j = 0; j < len; j++) { - InsnNode put = instructions.get(i + 1 + j); - if (put.getType() != InsnType.APUT) { - LOG.debug("Not a APUT in expected new filled array: {}, method: {}", put, mth); - return null; - } - filledArr.addArg(put.getArg(2)); - remover.add(put); + filledArr.setResult(arrArg.duplicate()); + for (InsnNode put : arrPuts) { + filledArr.addArg(put.getArg(2).duplicate()); + remover.addAndUnbind(mth, put); } - return filledArr; + remover.addAndUnbind(mth, newArrayInsn); + + int replaceIndex = InsnList.getIndex(instructions, Utils.last(arrPuts)); + instructions.set(replaceIndex, filledArr); + } + + private static boolean checkPutInsn(MethodNode mth, InsnNode insn, RegisterArg arrArg, int putIndex) { + if (insn == null || insn.getType() != InsnType.APUT) { + return false; + } + if (!arrArg.sameRegAndSVar(insn.getArg(0))) { + return false; + } + InsnArg indexArg = insn.getArg(1); + int index = -1; + if (indexArg.isLiteral()) { + index = (int) ((LiteralArg) indexArg).getLiteral(); + } else if (indexArg.isRegister()) { + RegisterArg reg = (RegisterArg) indexArg; + index = getIntConst(reg.getConstValue(mth.dex())); + } else if (indexArg.isInsnWrap()) { + InsnNode constInsn = ((InsnWrapArg) indexArg).getWrapInsn(); + index = getIntConst(InsnUtils.getConstValueByInsn(mth.dex(), constInsn)); + } + return index == putIndex; + } + + private static int getIntConst(Object value) { + if (value instanceof Integer) { + return (Integer) value; + } + if (value instanceof Long) { + return ((Long) value).intValue(); + } + return -1; } private static void processEnumSwitch(MethodNode mth, SwitchNode insn) { 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 1b9476b98..9595d72fa 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 @@ -76,6 +76,7 @@ public class RenameVisitor extends AbstractVisitor { String newShortName = fixClsShortName(clsName); if (!newShortName.equals(clsName)) { classInfo.rename(cls.root(), alias.makeFullClsName(newShortName, true)); + alias = classInfo.getAlias(); } if (alias.getPackage().isEmpty()) { String fullName = alias.makeFullClsName(alias.getShortName(), true); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java index ed3aedf26..ecaf534bd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.FieldInfo; import jadx.core.dex.info.MethodInfo; import jadx.core.dex.instructions.ArithNode; @@ -52,6 +53,9 @@ public class SimplifyVisitor extends AbstractVisitor { } private static InsnNode simplifyInsn(MethodNode mth, InsnNode insn) { + if (insn.contains(AFlag.DONT_GENERATE)) { + return null; + } for (InsnArg arg : insn.getArguments()) { if (arg.isInsnWrap()) { InsnNode ni = simplifyInsn(mth, ((InsnWrapArg) arg).getWrapInsn()); @@ -187,7 +191,8 @@ public class SimplifyVisitor extends AbstractVisitor { if (constrIndex != -1) { // If we found a CONSTRUCTOR, is it a StringBuilder? ConstructorInsn constr = (ConstructorInsn) chain.get(constrIndex); if (constr.getClassType().getFullName().equals(Consts.CLASS_STRING_BUILDER)) { - int len = chain.size(), argInd = 1; + int len = chain.size(); + int argInd = 1; InsnNode concatInsn = new InsnNode(InsnType.STR_CONCAT, len - 1); InsnNode argInsn; if (constrIndex > 0) { // There was an arg to the StringBuilder constr @@ -302,20 +307,16 @@ public class SimplifyVisitor extends AbstractVisitor { } } FieldArg fArg = new FieldArg(field, reg); - if (reg != null) { - fArg.setType(get.getArg(0).getType()); - } if (wrapType == InsnType.ARITH) { ArithNode ar = (ArithNode) wrap; return new ArithNode(ar.getOp(), fArg, ar.getArg(1)); - } else { - int argsCount = wrap.getArgsCount(); - InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1); - for (int i = 1; i < argsCount; i++) { - concat.addArg(wrap.getArg(i)); - } - return new ArithNode(ArithOp.ADD, fArg, InsnArg.wrapArg(concat)); } + int argsCount = wrap.getArgsCount(); + InsnNode concat = new InsnNode(InsnType.STR_CONCAT, argsCount - 1); + for (int i = 1; i < argsCount; i++) { + concat.addArg(wrap.getArg(i)); + } + return new ArithNode(ArithOp.ADD, fArg, InsnArg.wrapArg(concat)); } catch (Exception e) { LOG.debug("Can't convert field arith insn: {}, mth: {}", insn, mth, e); } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java index 1fabd9828..65355a01f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockExceptionHandler.java @@ -49,17 +49,17 @@ public class BlockExceptionHandler extends AbstractVisitor { } ExceptionHandler excHandler = handlerAttr.getHandler(); ArgType argType = excHandler.getArgType(); - if (!block.getInstructions().isEmpty()) { - InsnNode me = block.getInstructions().get(0); - if (me.getType() == InsnType.MOVE_EXCEPTION) { - // set correct type for 'move-exception' operation - RegisterArg resArg = InsnArg.reg(me.getResult().getRegNum(), argType); - resArg.copyAttributesFrom(me); - me.setResult(resArg); - me.add(AFlag.DONT_INLINE); - excHandler.setArg(resArg); - return; - } + InsnNode me = BlockUtils.getLastInsn(block); + if (me != null && me.getType() == InsnType.MOVE_EXCEPTION) { + // set correct type for 'move-exception' operation + RegisterArg resArg = InsnArg.reg(me.getResult().getRegNum(), argType); + resArg.copyAttributesFrom(me); + me.setResult(resArg); + me.add(AFlag.DONT_INLINE); + resArg.add(AFlag.CUSTOM_DECLARE); + excHandler.setArg(resArg); + me.addAttr(handlerAttr); + return; } // handler arguments not used excHandler.setArg(new NamedArg("unused", argType)); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockFinallyExtract.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockFinallyExtract.java deleted file mode 100644 index 0bf879e04..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockFinallyExtract.java +++ /dev/null @@ -1,821 +0,0 @@ -package jadx.core.dex.visitors.blocksmaker; - -import java.util.ArrayList; -import java.util.BitSet; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.core.Jadx; -import jadx.core.dex.attributes.AFlag; -import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.IgnoreEdgeAttr; -import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.trycatch.CatchAttr; -import jadx.core.dex.trycatch.ExceptionHandler; -import jadx.core.dex.trycatch.SplitterBlockAttr; -import jadx.core.dex.trycatch.TryCatchBlock; -import jadx.core.dex.visitors.AbstractVisitor; -import jadx.core.dex.visitors.DepthTraversal; -import jadx.core.dex.visitors.IDexTreeVisitor; -import jadx.core.dex.visitors.blocksmaker.helpers.BlocksPair; -import jadx.core.dex.visitors.blocksmaker.helpers.BlocksRemoveInfo; -import jadx.core.dex.visitors.ssa.LiveVarAnalysis; -import jadx.core.utils.BlockUtils; -import jadx.core.utils.ErrorsCounter; -import jadx.core.utils.exceptions.JadxRuntimeException; - -import static jadx.core.dex.visitors.blocksmaker.BlockSplitter.connect; -import static jadx.core.dex.visitors.blocksmaker.BlockSplitter.insertBlockBetween; -import static jadx.core.dex.visitors.blocksmaker.BlockSplitter.removeConnection; - -public class BlockFinallyExtract extends AbstractVisitor { - private static final Logger LOG = LoggerFactory.getLogger(BlockFinallyExtract.class); - - @Override - public void visit(MethodNode mth) { - if (mth.isNoCode() || mth.isNoExceptionHandlers()) { - return; - } - try { - boolean reloadBlocks = false; - for (ExceptionHandler excHandler : mth.getExceptionHandlers()) { - if (processExceptionHandler(mth, excHandler)) { - reloadBlocks = true; - } - } - if (reloadBlocks) { - mergeReturnBlocks(mth); - BlockProcessor.rerun(mth); - } - } catch (Exception e) { - LOG.warn("Undo finally extract visitor, mth: {}", mth, e); - try { - // reload method without applying this visitor - // TODO: make more common and less hacky - mth.unload(); - mth.load(); - List passes = Jadx.getPassesList(mth.root().getArgs()); - for (IDexTreeVisitor visitor : passes) { - if (visitor instanceof BlockFinallyExtract) { - break; - } - DepthTraversal.visit(visitor, mth); - } - } catch (Exception ee) { - LOG.error("Undo finally extract failed, mth: {}", mth, e); - } - } - } - - private static boolean processExceptionHandler(MethodNode mth, ExceptionHandler excHandler) { - // check if handler has exit edge to block not from this handler - boolean noExitNode = true; - boolean reThrowRemoved = false; - - for (BlockNode excBlock : excHandler.getBlocks()) { - if (noExitNode) { - noExitNode = excHandler.getBlocks().containsAll(excBlock.getCleanSuccessors()); - } - List insns = excBlock.getInstructions(); - int size = insns.size(); - if (excHandler.isCatchAll() - && size != 0 - && insns.get(size - 1).getType() == InsnType.THROW) { - reThrowRemoved = true; - insns.remove(size - 1); - } - } - if (reThrowRemoved && noExitNode - && extractFinally(mth, excHandler)) { - return true; - } - int totalSize = countInstructions(excHandler); - if (totalSize == 0 && reThrowRemoved && noExitNode) { - excHandler.getTryBlock().removeHandler(mth, excHandler); - } - return false; - } - - /** - * Search and remove common code from 'catch' and 'handlers'. - */ - private static boolean extractFinally(MethodNode mth, ExceptionHandler handler) { - int count = handler.getBlocks().size(); - BitSet bs = new BitSet(count); - List blocks = new ArrayList<>(count); - for (BlockNode block : handler.getBlocks()) { - List insns = block.getInstructions(); - if (!insns.isEmpty()) { - if (insns.get(0).getType() != InsnType.MOVE_EXCEPTION) { - blocks.add(block); - } - bs.set(block.getId()); - } - } - if (blocks.isEmpty()) { - // nothing to do - return false; - } - - List removes = new LinkedList<>(); - Set splitters = new HashSet<>(); - - // remove 'finally' from handlers - TryCatchBlock tryBlock = handler.getTryBlock(); - if (tryBlock.getHandlersCount() > 1) { - for (ExceptionHandler otherHandler : tryBlock.getHandlers()) { - if (otherHandler == handler) { - continue; - } - for (BlockNode hb : otherHandler.getBlocks()) { - BlocksRemoveInfo removeInfo = removeInsns(mth, hb, blocks, bs); - if (removeInfo != null) { - removes.add(removeInfo); - break; - } - } - } - if (removes.size() != tryBlock.getHandlersCount() - 1) { - return false; - } - } - - for (ExceptionHandler otherHandler : tryBlock.getHandlers()) { - SplitterBlockAttr splitterAttr = otherHandler.getHandlerBlock().get(AType.SPLITTER_BLOCK); - if (splitterAttr != null) { - BlockNode splBlock = splitterAttr.getBlock(); - if (!splBlock.getCleanSuccessors().isEmpty()) { - splitters.add(splBlock); - } - } - } - - // remove 'finally' from 'try' blocks (dominated by splitter block) - boolean removed = false; - for (BlockNode splitter : splitters) { - BlockNode start = splitter.getCleanSuccessors().get(0); - List list = BlockUtils.collectBlocksDominatedBy(splitter, start); - for (BlockNode block : list) { - if (bs.get(block.getId())) { - continue; - } - BlocksRemoveInfo removeInfo = removeInsns(mth, block, blocks, bs); - if (removeInfo != null) { - removes.add(removeInfo); - removed = true; - break; - } - } - } - if (!removed) { - return false; - } - - // 'finally' extract confirmed, run remove steps - - LiveVarAnalysis laBefore = null; - boolean runReMap = isReMapNeeded(removes); - if (runReMap) { - laBefore = new LiveVarAnalysis(mth); - laBefore.runAnalysis(); - } - - int removeApplied = 0; - for (BlocksRemoveInfo removeInfo : removes) { - if (applyRemove(mth, removeInfo)) { - removeApplied++; - removeInfo.setApplied(true); - } - } - if (removeApplied == 0) { - return false; - } - if (removeApplied != removes.size()) { - throw new JadxRuntimeException("Some finally instructions failed to remove: " - + removes.stream().filter(n -> !n.isApplied()).map(BlocksRemoveInfo::toString).collect(Collectors.joining(",")) - ); - } - - LiveVarAnalysis laAfter = null; - - // remove 'move-exception' instruction - BlockNode handlerBlock = handler.getHandlerBlock(); - InsnNode me = BlockUtils.getLastInsn(handlerBlock); - if (me != null && me.getType() == InsnType.MOVE_EXCEPTION) { - boolean replaced = false; - List insnsList = handlerBlock.getInstructions(); - if (!handlerBlock.getCleanSuccessors().isEmpty()) { - laAfter = new LiveVarAnalysis(mth); - laAfter.runAnalysis(); - - RegisterArg resArg = me.getResult(); - BlockNode succ = handlerBlock.getCleanSuccessors().get(0); - if (laAfter.isLive(succ, resArg.getRegNum())) { - // kill variable - InsnNode kill = new InsnNode(InsnType.NOP, 0); - kill.setResult(resArg); - kill.add(AFlag.REMOVE); - insnsList.set(insnsList.size() - 1, kill); - replaced = true; - } - } - if (!replaced) { - insnsList.remove(insnsList.size() - 1); - handlerBlock.add(AFlag.SKIP); - } - } - - // generate 'move' instruction for mapped register pairs - if (runReMap) { - if (laAfter == null) { - laAfter = new LiveVarAnalysis(mth); - laAfter.runAnalysis(); - } - performVariablesReMap(mth, removes, laBefore, laAfter); - } - - handler.setFinally(true); - return true; - } - - private static void performVariablesReMap(MethodNode mth, List removes, - LiveVarAnalysis laBefore, LiveVarAnalysis laAfter) { - BitSet processed = new BitSet(mth.getRegsCount()); - for (BlocksRemoveInfo removeInfo : removes) { - processed.clear(); - BlocksPair start = removeInfo.getStart(); - BlockNode insertBlockBefore = start.getFirst(); - BlockNode insertBlock = start.getSecond(); - if (removeInfo.getRegMap().isEmpty() || insertBlock == null) { - continue; - } - for (Map.Entry entry : removeInfo.getRegMap().entrySet()) { - RegisterArg fromReg = entry.getKey(); - RegisterArg toReg = entry.getValue(); - int fromRegNum = fromReg.getRegNum(); - int toRegNum = toReg.getRegNum(); - if (!processed.get(fromRegNum)) { - boolean liveFromBefore = laBefore.isLive(insertBlockBefore, fromRegNum); - boolean liveFromAfter = laAfter.isLive(insertBlock, fromRegNum); - boolean liveToAfter = laAfter.isLive(insertBlock, toRegNum); - if (liveToAfter && liveFromBefore) { - // merge 'to' and 'from' registers - InsnNode merge = new InsnNode(InsnType.MERGE, 2); - merge.setResult(toReg.duplicate()); - merge.addArg(toReg.duplicate()); - merge.addArg(fromReg.duplicate()); - injectInsn(mth, insertBlock, merge); - } else if (liveFromBefore) { - // remap variable - InsnNode move = new InsnNode(InsnType.MOVE, 1); - move.setResult(toReg.duplicate()); - move.addArg(fromReg.duplicate()); - injectInsn(mth, insertBlock, move); - } else if (liveFromAfter) { - // kill variable - InsnNode kill = new InsnNode(InsnType.NOP, 0); - kill.setResult(fromReg.duplicate()); - kill.add(AFlag.REMOVE); - injectInsn(mth, insertBlock, kill); - } - processed.set(fromRegNum); - } - } - } - } - - private static void injectInsn(MethodNode mth, BlockNode insertBlock, InsnNode insn) { - insn.add(AFlag.SYNTHETIC); - if (insertBlock.getInstructions().isEmpty()) { - insertBlock.getInstructions().add(insn); - } else { - BlockNode succBlock = splitBlock(mth, insertBlock, 0); - BlockNode predBlock = succBlock.getPredecessors().get(0); - predBlock.getInstructions().add(insn); - } - } - - private static boolean isReMapNeeded(List removes) { - for (BlocksRemoveInfo removeInfo : removes) { - if (!removeInfo.getRegMap().isEmpty()) { - return true; - } - } - return false; - } - - private static BlocksRemoveInfo removeInsns(MethodNode mth, BlockNode remBlock, List blocks, BitSet bs) { - if (blocks.isEmpty()) { - return null; - } - BlockNode startBlock = blocks.get(0); - BlocksRemoveInfo removeInfo = checkFromFirstBlock(remBlock, startBlock, bs); - if (removeInfo == null) { - return null; - } - Set outs = removeInfo.getOuts(); - if (outs.size() == 1) { - return removeInfo; - } - // check if several 'return' blocks maps to one out - if (mergeReturns(mth, outs)) { - return removeInfo; - } - LOG.debug("Unexpected finally block outs count: {}", outs); - return null; - } - - private static boolean mergeReturns(MethodNode mth, Set outs) { - Set rightOuts = new HashSet<>(); - boolean allReturns = true; - for (BlocksPair outPair : outs) { - BlockNode first = outPair.getFirst(); - if (!first.isReturnBlock()) { - allReturns = false; - } - rightOuts.add(outPair.getSecond()); - } - if (!allReturns || rightOuts.size() != 1) { - return false; - } - Iterator it = outs.iterator(); - while (it.hasNext()) { - BlocksPair out = it.next(); - BlockNode returnBlock = out.getFirst(); - if (!returnBlock.contains(AFlag.ORIG_RETURN)) { - markForRemove(mth, returnBlock); - it.remove(); - } - } - return true; - } - - private static BlocksRemoveInfo checkFromFirstBlock(BlockNode remBlock, BlockNode startBlock, BitSet bs) { - BlocksRemoveInfo removeInfo = isStartBlock(remBlock, startBlock); - if (removeInfo == null) { - return null; - } - if (!checkBlocksTree(remBlock, startBlock, removeInfo, bs)) { - return null; - } - return removeInfo; - } - - /** - * 'Finally' instructions can start in the middle of the first block. - */ - private static @Nullable - BlocksRemoveInfo isStartBlock(BlockNode remBlock, BlockNode startBlock) { - List remInsns = remBlock.getInstructions(); - List startInsns = startBlock.getInstructions(); - if (remInsns.size() < startInsns.size()) { - return null; - } - // first - fast check - int startPos = remInsns.size() - startInsns.size(); - int endPos = 0; - if (!checkInsns(remInsns, startInsns, startPos, null)) { - if (checkInsns(remInsns, startInsns, 0, null)) { - startPos = 0; - endPos = startInsns.size(); - } else { - boolean found = false; - for (int i = 1; i < startPos; i++) { - if (checkInsns(remInsns, startInsns, i, null)) { - startPos = i; - endPos = startInsns.size() + i; - found = true; - break; - } - } - if (!found) { - return null; - } - } - } - BlocksPair startPair = new BlocksPair(remBlock, startBlock); - BlocksRemoveInfo removeInfo = new BlocksRemoveInfo(startPair); - removeInfo.setStartSplitIndex(startPos); - removeInfo.setEndSplitIndex(endPos); - if (endPos != 0) { - removeInfo.setEnd(startPair); - } - // second - run checks again for collect registers mapping - if (!checkInsns(remInsns, startInsns, startPos, removeInfo)) { - return null; - } - return removeInfo; - } - - private static boolean checkInsns(List remInsns, List startInsns, int delta, - @Nullable BlocksRemoveInfo removeInfo) { - for (int i = startInsns.size() - 1; i >= 0; i--) { - InsnNode startInsn = startInsns.get(i); - InsnNode remInsn = remInsns.get(delta + i); - if (!sameInsns(remInsn, startInsn, removeInfo)) { - return false; - } - } - return true; - } - - private static boolean checkBlocksTree(BlockNode remBlock, BlockNode startBlock, - @NotNull BlocksRemoveInfo removeInfo, BitSet bs) { - // skip check on start block - if (!removeInfo.getProcessed().isEmpty() - && !sameBlocks(remBlock, startBlock, removeInfo)) { - return false; - } - BlocksPair currentPair = new BlocksPair(remBlock, startBlock); - removeInfo.getProcessed().add(currentPair); - - List baseCS = startBlock.getCleanSuccessors(); - List remCS = remBlock.getCleanSuccessors(); - if (baseCS.size() != remCS.size()) { - removeInfo.getOuts().add(currentPair); - return true; - } - for (int i = 0; i < baseCS.size(); i++) { - BlockNode sBlock = baseCS.get(i); - BlockNode rBlock = remCS.get(i); - if (bs.get(sBlock.getId())) { - if (removeInfo.getEndSplitIndex() != 0) { - // end block is not correct - return false; - } - if (!checkBlocksTree(rBlock, sBlock, removeInfo, bs)) { - return false; - } - } else { - removeInfo.getOuts().add(new BlocksPair(rBlock, sBlock)); - } - } - return true; - } - - private static boolean sameBlocks(BlockNode remBlock, BlockNode finallyBlock, - @NotNull BlocksRemoveInfo removeInfo) { - List first = remBlock.getInstructions(); - List second = finallyBlock.getInstructions(); - if (first.size() < second.size()) { - return false; - } - int size = second.size(); - for (int i = 0; i < size; i++) { - if (!sameInsns(first.get(i), second.get(i), removeInfo)) { - return false; - } - } - if (first.size() > second.size()) { - removeInfo.setEndSplitIndex(second.size()); - removeInfo.setEnd(new BlocksPair(remBlock, finallyBlock)); - } - return true; - } - - private static boolean sameInsns(InsnNode remInsn, InsnNode fInsn, @Nullable BlocksRemoveInfo removeInfo) { - if (!remInsn.isSame(fInsn)) { - return false; - } - // TODO: check instance arg in ConstructorInsn - // TODO: compare literals - for (int i = 0; i < remInsn.getArgsCount(); i++) { - InsnArg remArg = remInsn.getArg(i); - InsnArg fArg = fInsn.getArg(i); - if (remArg.isRegister() != fArg.isRegister()) { - return false; - } - if (removeInfo != null && fArg.isRegister()) { - RegisterArg remReg = (RegisterArg) remArg; - RegisterArg fReg = (RegisterArg) fArg; - if (remReg.getRegNum() != fReg.getRegNum()) { - RegisterArg mapReg = removeInfo.getRegMap().get(remArg); - if (mapReg == null) { - removeInfo.getRegMap().put(remReg, fReg); - } else if (!mapReg.equalRegisterAndType(fReg)) { - return false; - } - } - } - } - return true; - } - - private static boolean applyRemove(MethodNode mth, BlocksRemoveInfo removeInfo) { - BlockNode remBlock = removeInfo.getStart().getFirst(); - BlockNode startBlock = removeInfo.getStart().getSecond(); - - if (remBlock.contains(AFlag.REMOVE)) { - // already processed - return true; - } - if (remBlock.getPredecessors().size() != 1) { - LOG.warn("Finally extract failed: remBlock pred: {}, {}, method: {}", remBlock, remBlock.getPredecessors(), mth); - return false; - } - if (removeInfo.getOuts().isEmpty()) { - ErrorsCounter.methodWarn(mth, "Failed to extract finally block: empty outs"); - return false; - } - // safe checks finished, altering blocks tree - // all error must throw exception to undo changes - - BlockNode remBlockPred = remBlock.getPredecessors().get(0); - removeInfo.setStartPredecessor(remBlockPred); - - int startSplitIndex = removeInfo.getStartSplitIndex(); - int endSplitIndex = removeInfo.getEndSplitIndex(); - if (removeInfo.getStart().equals(removeInfo.getEnd())) { - removeInfo.setEndSplitIndex(endSplitIndex - startSplitIndex); - } - // split start block (remBlock) - if (startSplitIndex > 0) { - remBlock = splitBlock(mth, remBlock, startSplitIndex); - // change start block in removeInfo - removeInfo.getProcessed().remove(removeInfo.getStart()); - BlocksPair newStart = new BlocksPair(remBlock, startBlock); -// removeInfo.setStart(newStart); - removeInfo.getProcessed().add(newStart); - } - // split end block - if (endSplitIndex > 0) { - BlocksPair end = removeInfo.getEnd(); - BlockNode newOut = splitBlock(mth, end.getFirst(), endSplitIndex); - for (BlockNode s : newOut.getSuccessors()) { - BlocksPair replaceOut = null; - Iterator it = removeInfo.getOuts().iterator(); - while (it.hasNext()) { - BlocksPair outPair = it.next(); - if (outPair.getFirst().equals(s)) { - it.remove(); - replaceOut = new BlocksPair(newOut, outPair.getSecond()); - break; - } - } - if (replaceOut != null) { - removeInfo.getOuts().add(replaceOut); - } - } - } - - Set outs = removeInfo.getOuts(); - if (outs.isEmpty()) { - throw new JadxRuntimeException("Failed to extract finally block: all outs is deleted"); - } - BlocksPair out = outs.iterator().next(); - BlockNode rOut = out.getFirst(); - BlockNode sOut = out.getSecond(); - - // redirect out edges - List filtPreds = BlockUtils.filterPredecessors(sOut); - if (filtPreds.size() > 1) { - BlockNode pred = sOut.getPredecessors().get(0); - BlockNode newPred = BlockSplitter.insertBlockBetween(mth, pred, sOut); - for (BlockNode predBlock : new ArrayList<>(sOut.getPredecessors())) { - if (predBlock != newPred) { - BlockSplitter.replaceConnection(predBlock, sOut, newPred); - } - } - rOut.getPredecessors().clear(); - addIgnoredEdge(newPred, rOut); - connect(newPred, rOut); - } else if (filtPreds.size() == 1) { - BlockNode pred = filtPreds.get(0); - BlockNode repl = removeInfo.getBySecond(pred); - if (repl == null) { - throw new JadxRuntimeException("Block not found by " + pred + ", in " + removeInfo); - } - removeConnection(pred, rOut); - addIgnoredEdge(repl, rOut); - connect(repl, rOut); - } else { - throw new JadxRuntimeException("Finally extract failed, unexpected preds: " + filtPreds - + " for " + sOut + ", method: " + mth); - } - - // redirect input edges - for (BlockNode pred : new ArrayList<>(remBlock.getPredecessors())) { - BlockNode middle = insertBlockBetween(mth, pred, remBlock); - removeConnection(middle, remBlock); - connect(middle, startBlock); - addIgnoredEdge(middle, startBlock); - connect(middle, rOut); - BlockSplitter.replaceTarget(middle, remBlock, rOut); - } - - // mark blocks for remove - markForRemove(mth, remBlock); - for (BlocksPair pair : removeInfo.getProcessed()) { - markForRemove(mth, pair.getFirst()); - BlockNode second = pair.getSecond(); - second.updateCleanSuccessors(); - } - return true; - } - - /** - * Split one block into connected 2 blocks with same connections. - * - * @return new successor block - */ - private static BlockNode splitBlock(MethodNode mth, BlockNode block, int splitIndex) { - BlockNode newBlock = BlockSplitter.startNewBlock(mth, -1); - - newBlock.getSuccessors().addAll(block.getSuccessors()); - for (BlockNode s : new ArrayList<>(block.getSuccessors())) { - removeConnection(block, s); - connect(newBlock, s); - } - block.getSuccessors().clear(); - connect(block, newBlock); - block.updateCleanSuccessors(); - newBlock.updateCleanSuccessors(); - - List insns = block.getInstructions(); - int size = insns.size(); - for (int i = splitIndex; i < size; i++) { - InsnNode insnNode = insns.get(i); - insnNode.add(AFlag.SKIP); - newBlock.getInstructions().add(insnNode); - } - Iterator it = insns.iterator(); - while (it.hasNext()) { - InsnNode insnNode = it.next(); - if (insnNode.contains(AFlag.SKIP)) { - it.remove(); - } - } - for (InsnNode insnNode : newBlock.getInstructions()) { - insnNode.remove(AFlag.SKIP); - } - return newBlock; - } - - /** - * Unbind block for removing. - */ - private static void markForRemove(MethodNode mth, BlockNode block) { - for (BlockNode p : block.getPredecessors()) { - p.getSuccessors().remove(block); - p.updateCleanSuccessors(); - } - for (BlockNode s : block.getSuccessors()) { - s.getPredecessors().remove(block); - } - block.getPredecessors().clear(); - block.getSuccessors().clear(); - block.add(AFlag.REMOVE); - block.remove(AFlag.SKIP); - - CatchAttr catchAttr = block.get(AType.CATCH_BLOCK); - if (catchAttr != null) { - catchAttr.getTryBlock().removeBlock(mth, block); - for (BlockNode skipBlock : mth.getBasicBlocks()) { - if (skipBlock.contains(AFlag.SKIP)) { - markForRemove(mth, skipBlock); - } - } - } - } - - private static void addIgnoredEdge(BlockNode from, BlockNode toBlock) { - IgnoreEdgeAttr edgeAttr = from.get(AType.IGNORE_EDGE); - if (edgeAttr == null) { - edgeAttr = new IgnoreEdgeAttr(); - from.addAttr(edgeAttr); - } - edgeAttr.getBlocks().add(toBlock); - } - - private static int countInstructions(ExceptionHandler excHandler) { - int totalSize = 0; - for (BlockNode excBlock : excHandler.getBlocks()) { - List list = excBlock.getInstructions(); - if (!list.isEmpty() && list.get(0).getType() == InsnType.MOVE_EXCEPTION) { - // don't count 'move-exception' it will be removed later - totalSize--; - } - totalSize += list.size(); - } - return totalSize; - } - - /** - * Merge return block with same predecessor. - */ - private static void mergeReturnBlocks(MethodNode mth) { - List exitBlocks = mth.getExitBlocks(); - BlockNode pred = getFinallyOutBlock(exitBlocks); - if (pred == null) { - return; - } - IgnoreEdgeAttr edgeAttr = pred.get(AType.IGNORE_EDGE); - if (edgeAttr == null) { - return; - } - List merge = new LinkedList<>(); - for (BlockNode blockNode : pred.getSuccessors()) { - if (blockNode.contains(AFlag.RETURN)) { - merge.add(blockNode); - } - } - if (merge.size() < 2) { - return; - } - // select 'original' return block - BlockNode origReturnBlock = null; - for (BlockNode ret : merge) { - if (ret.contains(AFlag.ORIG_RETURN)) { - origReturnBlock = ret; - break; - } - } - if (origReturnBlock == null) { - return; - } - for (BlockNode mb : merge) { - if (mb == origReturnBlock) { - continue; - } - for (BlockNode remPred : mb.getPredecessors()) { - connect(remPred, origReturnBlock); - } - markForRemove(mth, mb); - edgeAttr.getBlocks().remove(mb); - } - mergeSyntheticPredecessors(mth, origReturnBlock); - } - - private static void mergeSyntheticPredecessors(MethodNode mth, BlockNode block) { - List preds = new ArrayList<>(block.getPredecessors()); - Iterator it = preds.iterator(); - while (it.hasNext()) { - BlockNode predBlock = it.next(); - if (!predBlock.isSynthetic()) { - it.remove(); - } - } - if (preds.size() < 2) { - return; - } - BlockNode commonBlock = null; - for (BlockNode predBlock : preds) { - List successors = predBlock.getSuccessors(); - if (successors.size() != 2) { - return; - } - BlockNode cmnBlk = BlockUtils.selectOtherSafe(block, successors); - if (commonBlock == null) { - commonBlock = cmnBlk; - } else if (cmnBlk != commonBlock) { - return; - } - if (!predBlock.contains(AType.IGNORE_EDGE)) { - return; - } - } - if (commonBlock == null) { - return; - } - - // merge confirmed - BlockNode mergeBlock = null; - for (BlockNode predBlock : preds) { - if (mergeBlock == null) { - mergeBlock = predBlock; - continue; - } - for (BlockNode remPred : predBlock.getPredecessors()) { - connect(remPred, mergeBlock); - } - markForRemove(mth, predBlock); - } - } - - private static BlockNode getFinallyOutBlock(List exitBlocks) { - for (BlockNode exitBlock : exitBlocks) { - for (BlockNode exitPred : exitBlock.getPredecessors()) { - IgnoreEdgeAttr edgeAttr = exitPred.get(AType.IGNORE_EDGE); - if (edgeAttr != null && edgeAttr.contains(exitBlock)) { - return exitPred; - } - } - } - return null; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java index 13152cacc..3cfee8bec 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockProcessor.java @@ -8,6 +8,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,6 +17,7 @@ import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.LiteralArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.Edge; @@ -66,17 +68,27 @@ public class BlockProcessor extends AbstractVisitor { markReturnBlocks(mth); if (i++ > 100) { - throw new AssertionError("Can't fix method cfg: " + mth); + mth.addWarn("CFG modification limit reached, blocks count: " + mth.getBasicBlocks().size()); + break; } } + checkForUnreachableBlocks(mth); + computeDominanceFrontier(mth); registerLoops(mth); processNestedLoops(mth); } + private static void checkForUnreachableBlocks(MethodNode mth) { + mth.getBasicBlocks().forEach(block -> { + if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { + throw new JadxRuntimeException("Unreachable block: " + block); + } + }); + } + private static boolean canRemoveBlock(BlockNode block) { return block.getInstructions().isEmpty() - && !block.isSynthetic() && block.isAttrStorageEmpty() && block.getSuccessors().size() <= 1 && !block.getPredecessors().isEmpty(); @@ -117,6 +129,7 @@ public class BlockProcessor extends AbstractVisitor { if (lastInsn != null && lastInsn.getType() == InsnType.IF) { return false; } + // TODO: implement insn extraction into separate block for partial predecessors int sameInsnCount = getSameLastInsnCount(predecessors); if (sameInsnCount > 0) { List insns = getLastInsns(predecessors.get(0), sameInsnCount); @@ -169,7 +182,43 @@ public class BlockProcessor extends AbstractVisitor { } private static boolean isSame(InsnNode insn, InsnNode curInsn) { - return /*insn.getType() == InsnType.MOVE &&*/ insn.isDeepEquals(curInsn) && insn.canReorder(); + return isInsnsEquals(insn, curInsn) && insn.canReorder(); + } + + private static boolean isInsnsEquals(InsnNode insn, InsnNode otherInsn) { + if (insn == otherInsn) { + return true; + } + if (insn.isSame(otherInsn) + && sameArgs(insn.getResult(), otherInsn.getResult())) { + int argsCount = insn.getArgsCount(); + for (int i = 0; i < argsCount; i++) { + if (!sameArgs(insn.getArg(i), otherInsn.getArg(i))) { + return false; + } + } + return true; + } + return false; + } + + private static boolean sameArgs(@Nullable InsnArg arg, @Nullable InsnArg otherArg) { + if (arg == otherArg) { + return true; + } + if (arg == null || otherArg == null) { + return false; + } + if (arg.getClass().equals(otherArg.getClass())) { + if (arg.isRegister()) { + return ((RegisterArg) arg).getRegNum() == ((RegisterArg) otherArg).getRegNum(); + } + if (arg.isLiteral()) { + return ((LiteralArg) arg).getLiteral() == ((LiteralArg) otherArg).getLiteral(); + } + throw new JadxRuntimeException("Unexpected InsnArg types: " + arg + " and " + otherArg); + } + return false; } private static InsnNode getInsnsFromEnd(BlockNode block, int number) { @@ -192,32 +241,7 @@ public class BlockProcessor extends AbstractVisitor { } BlockNode entryBlock = mth.getEnterBlock(); - entryBlock.getDoms().clear(); - entryBlock.getDoms().set(entryBlock.getId()); - - BitSet dset = new BitSet(nBlocks); - boolean changed; - do { - changed = false; - for (BlockNode block : basicBlocks) { - if (block == entryBlock) { - continue; - } - BitSet d = block.getDoms(); - if (!changed) { - dset.clear(); - dset.or(d); - } - for (BlockNode pred : block.getPredecessors()) { - d.and(pred.getDoms()); - } - d.set(block.getId()); - if (!changed && !d.equals(dset)) { - changed = true; - } - } - } while (changed); - + calcDominators(basicBlocks, entryBlock); markLoops(mth); // clear self dominance @@ -228,7 +252,38 @@ public class BlockProcessor extends AbstractVisitor { } }); - // calculate immediate dominators + calcImmediateDominators(basicBlocks, entryBlock); + } + + private static void calcDominators(List basicBlocks, BlockNode entryBlock) { + entryBlock.getDoms().clear(); + entryBlock.getDoms().set(entryBlock.getId()); + + BitSet domSet = new BitSet(basicBlocks.size()); + boolean changed; + do { + changed = false; + for (BlockNode block : basicBlocks) { + if (block == entryBlock) { + continue; + } + BitSet d = block.getDoms(); + if (!changed) { + domSet.clear(); + domSet.or(d); + } + for (BlockNode pred : block.getPredecessors()) { + d.and(pred.getDoms()); + } + d.set(block.getId()); + if (!changed && !d.equals(domSet)) { + changed = true; + } + } + } while (changed); + } + + private static void calcImmediateDominators(List basicBlocks, BlockNode entryBlock) { for (BlockNode block : basicBlocks) { if (block == entryBlock) { continue; @@ -326,7 +381,7 @@ public class BlockProcessor extends AbstractVisitor { private static void markLoops(MethodNode mth) { mth.getBasicBlocks().forEach(block -> { // Every successor that dominates its predecessor is a header of a loop, - // block -> succ is a back edge. + // block -> successor is a back edge. block.getSuccessors().forEach(successor -> { if (block.getDoms().get(successor.getId())) { successor.add(AFlag.LOOP_START); @@ -413,59 +468,95 @@ public class BlockProcessor extends AbstractVisitor { } private static boolean checkLoops(MethodNode mth, BlockNode block) { - // check loops List loops = block.getAll(AType.LOOP); - if (loops.size() > 1) { - boolean oneHeader = true; - for (LoopInfo loop : loops) { - if (loop.getStart() != block) { - oneHeader = false; - break; + int loopsCount = loops.size(); + if (loopsCount == 0) { + return false; + } + if (loopsCount > 1 && splitLoops(mth, block, loops)) { + return true; + } + if (loopsCount == 1) { + LoopInfo loop = loops.get(0); + return insertBlocksForBreak(mth, loop) + || insertBlocksForContinue(mth, loop) + || insertBlockForProdecessors(mth, loop); + } + return false; + } + + /** + * Insert additional blocks for possible 'break' insertion + */ + private static boolean insertBlocksForBreak(MethodNode mth, LoopInfo loop) { + boolean change = false; + List edges = loop.getExitEdges(); + if (!edges.isEmpty()) { + for (Edge edge : edges) { + BlockNode target = edge.getTarget(); + BlockNode source = edge.getSource(); + if (!target.contains(AFlag.SYNTHETIC) && !source.contains(AFlag.SYNTHETIC)) { + BlockSplitter.insertBlockBetween(mth, source, target); + change = true; } } - if (oneHeader) { - // several back edges connected to one loop header => make additional block - BlockNode newLoopEnd = BlockSplitter.startNewBlock(mth, block.getStartOffset()); - newLoopEnd.add(AFlag.SYNTHETIC); - connect(newLoopEnd, block); - for (LoopInfo la : loops) { - BlockSplitter.replaceConnection(la.getEnd(), block, newLoopEnd); - } - return true; - } } - if (loops.size() == 1) { - LoopInfo loop = loops.get(0); - // insert additional blocks for possible 'break' insertion - List edges = loop.getExitEdges(); - if (!edges.isEmpty()) { - boolean change = false; - for (Edge edge : edges) { - BlockNode target = edge.getTarget(); - BlockNode source = edge.getSource(); - if (!target.contains(AFlag.SYNTHETIC) && !source.contains(AFlag.SYNTHETIC)) { - BlockSplitter.insertBlockBetween(mth, source, target); - change = true; - } - } - if (change) { - return true; + return change; + } + + /** + * Insert additional blocks for possible 'continue' insertion + */ + private static boolean insertBlocksForContinue(MethodNode mth, LoopInfo loop) { + BlockNode loopEnd = loop.getEnd(); + boolean change = false; + List preds = loopEnd.getPredecessors(); + if (preds.size() > 1) { + for (BlockNode pred : new ArrayList<>(preds)) { + if (!pred.contains(AFlag.SYNTHETIC)) { + BlockSplitter.insertBlockBetween(mth, pred, loopEnd); + change = true; } } - // insert additional blocks for possible 'continue' insertion - BlockNode loopEnd = loop.getEnd(); - if (loopEnd.getPredecessors().size() > 1) { - boolean change = false; - List nodes = new ArrayList<>(loopEnd.getPredecessors()); - for (BlockNode pred : nodes) { - if (!pred.contains(AFlag.SYNTHETIC)) { - BlockSplitter.insertBlockBetween(mth, pred, loopEnd); - change = true; - } - } - return change; + } + return change; + } + + /** + * Insert additional block if loop header has several predecessors (exclude back edges) + */ + private static boolean insertBlockForProdecessors(MethodNode mth, LoopInfo loop) { + BlockNode loopHeader = loop.getStart(); + List preds = loopHeader.getPredecessors(); + if (preds.size() > 2) { + List blocks = new LinkedList<>(preds); + blocks.removeIf(block -> block.contains(AFlag.LOOP_END)); + BlockNode first = blocks.remove(0); + BlockNode preHeader = BlockSplitter.insertBlockBetween(mth, first, loopHeader); + blocks.forEach(block -> BlockSplitter.replaceConnection(block, loopHeader, preHeader)); + return true; + } + return false; + } + + private static boolean splitLoops(MethodNode mth, BlockNode block, List loops) { + boolean oneHeader = true; + for (LoopInfo loop : loops) { + if (loop.getStart() != block) { + oneHeader = false; + break; } } + if (oneHeader) { + // several back edges connected to one loop header => make additional block + BlockNode newLoopEnd = BlockSplitter.startNewBlock(mth, block.getStartOffset()); + newLoopEnd.add(AFlag.SYNTHETIC); + connect(newLoopEnd, block); + for (LoopInfo la : loops) { + BlockSplitter.replaceConnection(la.getEnd(), block, newLoopEnd); + } + return true; + } return false; } @@ -523,11 +614,8 @@ public class BlockProcessor extends AbstractVisitor { } private static RegisterArg getMoveExceptionRegister(BlockNode block) { - if (block.getInstructions().isEmpty()) { - return null; - } - InsnNode insn = block.getInstructions().get(0); - if (insn.getType() != InsnType.MOVE_EXCEPTION) { + InsnNode insn = BlockUtils.getLastInsn(block); + if (insn == null || insn.getType() != InsnType.MOVE_EXCEPTION) { return null; } return insn.getResult(); @@ -621,7 +709,7 @@ public class BlockProcessor extends AbstractVisitor { InsnNode insn = new InsnNode(returnInsn.getType(), returnInsn.getArgsCount()); if (returnInsn.getArgsCount() == 1) { RegisterArg arg = (RegisterArg) returnInsn.getArg(0); - insn.addArg(InsnArg.reg(arg.getRegNum(), arg.getType())); + insn.addArg(InsnArg.reg(arg.getRegNum(), arg.getInitType())); } insn.copyAttributesFrom(returnInsn); insn.setOffset(returnInsn.getOffset()); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java index 94732c48f..a08e090cf 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/BlockSplitter.java @@ -29,7 +29,7 @@ import jadx.core.utils.exceptions.JadxRuntimeException; public class BlockSplitter extends AbstractVisitor { // leave these instructions alone in block node - private static final Set SEPARATE_INSNS = EnumSet.of( + public static final Set SEPARATE_INSNS = EnumSet.of( InsnType.RETURN, InsnType.IF, InsnType.SWITCH, @@ -52,6 +52,9 @@ public class BlockSplitter extends AbstractVisitor { removeEmptyDetachedBlocks(mth); removeUnreachableBlocks(mth); initBlocksInTargetNodes(mth); + + removeJumpAttributes(mth.getInstructions()); + mth.unloadInsnArr(); } /** @@ -329,6 +332,14 @@ public class BlockSplitter extends AbstractVisitor { ); } + private void removeJumpAttributes(InsnNode[] insnArr) { + for (InsnNode insn : insnArr) { + if (insn != null && insn.contains(AType.JUMP)) { + insn.remove(AType.JUMP); + } + } + } + private void removeUnreachableBlocks(MethodNode mth) { Set toRemove = new LinkedHashSet<>(); for (BlockNode block : mth.getBasicBlocks()) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksPair.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksPair.java deleted file mode 100644 index 8c0b88e6b..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksPair.java +++ /dev/null @@ -1,43 +0,0 @@ -package jadx.core.dex.visitors.blocksmaker.helpers; - -import jadx.core.dex.nodes.BlockNode; - -public final class BlocksPair { - private final BlockNode first; - private final BlockNode second; - - public BlocksPair(BlockNode first, BlockNode second) { - this.first = first; - this.second = second; - } - - public BlockNode getFirst() { - return first; - } - - public BlockNode getSecond() { - return second; - } - - @Override - public int hashCode() { - return 31 * first.hashCode() + second.hashCode(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof BlocksPair)) { - return false; - } - BlocksPair other = (BlocksPair) o; - return first.equals(other.first) && second.equals(other.second); - } - - @Override - public String toString() { - return "(" + first + ", " + second + ')'; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksRemoveInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksRemoveInfo.java deleted file mode 100644 index f37fb29a0..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/BlocksRemoveInfo.java +++ /dev/null @@ -1,122 +0,0 @@ -package jadx.core.dex.visitors.blocksmaker.helpers; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import org.jetbrains.annotations.Nullable; - -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.nodes.BlockNode; - -public final class BlocksRemoveInfo { - private final Set processed = new HashSet<>(); - private final Set outs = new HashSet<>(); - private final Map regMap = new HashMap<>(); - - private BlocksPair start; - private BlocksPair end; - - private int startSplitIndex; - private int endSplitIndex; - - private BlockNode startPredecessor; - - private boolean applied; - - public BlocksRemoveInfo(BlocksPair start) { - this.start = start; - } - - public Set getProcessed() { - return processed; - } - - public Set getOuts() { - return outs; - } - - public BlocksPair getStart() { - return start; - } - - public void setStart(BlocksPair start) { - this.start = start; - } - - public BlocksPair getEnd() { - return end; - } - - public void setEnd(BlocksPair end) { - this.end = end; - } - - public int getStartSplitIndex() { - return startSplitIndex; - } - - public void setStartSplitIndex(int startSplitIndex) { - this.startSplitIndex = startSplitIndex; - } - - public int getEndSplitIndex() { - return endSplitIndex; - } - - public void setEndSplitIndex(int endSplitIndex) { - this.endSplitIndex = endSplitIndex; - } - - public void setStartPredecessor(BlockNode startPredecessor) { - this.startPredecessor = startPredecessor; - } - - public BlockNode getStartPredecessor() { - return startPredecessor; - } - - public Map getRegMap() { - return regMap; - } - - @Nullable - public BlockNode getByFirst(BlockNode first) { - for (BlocksPair blocksPair : processed) { - if (blocksPair.getFirst() == first) { - return blocksPair.getSecond(); - } - } - return null; - } - - @Nullable - public BlockNode getBySecond(BlockNode second) { - for (BlocksPair blocksPair : processed) { - if (blocksPair.getSecond() == second) { - return blocksPair.getSecond(); - } - } - return null; - } - - public boolean isApplied() { - return applied; - } - - public void setApplied(boolean applied) { - this.applied = applied; - } - - @Override - public String toString() { - return "BRI{start: " + start - + ", end: " + end - + ", processed: " + processed - + ", outs: " + outs - + ", regMap: " + regMap - + ", split: " + startSplitIndex + '-' + endSplitIndex - + '}'; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/FinallyExtractInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/FinallyExtractInfo.java new file mode 100644 index 000000000..84015459e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/FinallyExtractInfo.java @@ -0,0 +1,48 @@ +package jadx.core.dex.visitors.blocksmaker.helpers; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.trycatch.ExceptionHandler; + +public class FinallyExtractInfo { + private final ExceptionHandler finallyHandler; + private final List allHandlerBlocks; + private final List duplicateSlices = new ArrayList<>(); + private final Set checkedBlocks = new HashSet<>(); + private final InsnsSlice finallyInsnsSlice = new InsnsSlice(); + private final BlockNode startBlock; + + public FinallyExtractInfo(ExceptionHandler finallyHandler, BlockNode startBlock, List allHandlerBlocks) { + this.finallyHandler = finallyHandler; + this.startBlock = startBlock; + this.allHandlerBlocks = allHandlerBlocks; + } + + public ExceptionHandler getFinallyHandler() { + return finallyHandler; + } + + public List getAllHandlerBlocks() { + return allHandlerBlocks; + } + + public InsnsSlice getFinallyInsnsSlice() { + return finallyInsnsSlice; + } + + public List getDuplicateSlices() { + return duplicateSlices; + } + + public Set getCheckedBlocks() { + return checkedBlocks; + } + + public BlockNode getStartBlock() { + return startBlock; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/InsnsSlice.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/InsnsSlice.java new file mode 100644 index 000000000..f8eccab4d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocksmaker/helpers/InsnsSlice.java @@ -0,0 +1,72 @@ +package jadx.core.dex.visitors.blocksmaker.helpers; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; + +public class InsnsSlice { + private final List insnsList = new ArrayList<>(); + private final Map insnMap = new IdentityHashMap<>(); + private boolean complete; + + public void addInsn(InsnNode insn, BlockNode block) { + insnsList.add(insn); + insnMap.put(insn, block); + } + + public void addBlock(BlockNode block) { + for (InsnNode insn : block.getInstructions()) { + addInsn(insn, block); + } + } + + public void addInsns(BlockNode block, int startIndex, int endIndex) { + List insns = block.getInstructions(); + for (int i = startIndex; i < endIndex; i++) { + addInsn(insns.get(i), block); + } + } + + @Nullable + public BlockNode getBlock(InsnNode insn) { + return insnMap.get(insn); + } + + public List getInsnsList() { + return insnsList; + } + + public Set getBlocks() { + Set set = new LinkedHashSet<>(); + for (InsnNode insn : insnsList) { + set.add(insnMap.get(insn)); + } + return set; + } + + public boolean isComplete() { + return complete; + } + + public void setComplete(boolean complete) { + this.complete = complete; + } + + @Override + public String toString() { + return "{[" + + insnsList.stream().map(insn -> insn.getType().toString()).collect(Collectors.joining(", ")) + + ']' + + (complete ? " complete" : "") + + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java new file mode 100644 index 000000000..a59487f73 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoApplyVisitor.java @@ -0,0 +1,238 @@ +package jadx.core.dex.visitors.debuginfo; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.deobf.NameMapper; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; +import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; +import jadx.core.dex.instructions.PhiInsn; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.Named; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.dex.visitors.typeinference.TypeInferenceVisitor; +import jadx.core.dex.visitors.typeinference.TypeUpdateResult; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.ErrorsCounter; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "Debug Info Apply", + desc = "Apply debug info to registers (type and names)", + runAfter = { + SSATransform.class, + TypeInferenceVisitor.class + } +) +public class DebugInfoApplyVisitor extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(DebugInfoApplyVisitor.class); + + @Override + public void visit(MethodNode mth) throws JadxException { + try { + if (mth.contains(AType.LOCAL_VARS_DEBUG_INFO)) { + applyDebugInfo(mth); + mth.remove(AType.LOCAL_VARS_DEBUG_INFO); + } + checkTypes(mth); + } catch (Exception e) { + LOG.error("Error to apply debug info: {}", ErrorsCounter.formatMsg(mth, e.getMessage()), e); + } + } + + private static void checkTypes(MethodNode mth) { + if (mth.isNoCode() || mth.getSVars().isEmpty()) { + return; + } + mth.getSVars().forEach(var -> { + ArgType type = var.getTypeInfo().getType(); + if (!type.isTypeKnown()) { + mth.addComment("JADX WARNING: type inference failed for: " + var.getDetailedVarInfo(mth)); + } + }); + } + + private static void applyDebugInfo(MethodNode mth) { + mth.getSVars().forEach(ssaVar -> collectVarDebugInfo(mth, ssaVar)); + + fixLinesForReturn(mth); + fixNamesForPhiInsns(mth); + } + + private static void collectVarDebugInfo(MethodNode mth, SSAVar ssaVar) { + Set debugInfoSet = new HashSet<>(ssaVar.getUseCount() + 1); + addRegDbdInfo(debugInfoSet, ssaVar.getAssign()); + ssaVar.getUseList().forEach(registerArg -> addRegDbdInfo(debugInfoSet, registerArg)); + + int dbgCount = debugInfoSet.size(); + if (dbgCount == 0) { + searchDebugInfoByOffset(mth, ssaVar); + return; + } + if (dbgCount == 1) { + RegDebugInfoAttr debugInfo = debugInfoSet.iterator().next(); + applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); + } else { + LOG.warn("Multiple debug info for {}: {}", ssaVar, debugInfoSet); + for (RegDebugInfoAttr debugInfo : debugInfoSet) { + applyDebugInfo(mth, ssaVar, debugInfo.getRegType(), debugInfo.getName()); + } + } + } + + private static void searchDebugInfoByOffset(MethodNode mth, SSAVar ssaVar) { + LocalVarsDebugInfoAttr debugInfoAttr = mth.get(AType.LOCAL_VARS_DEBUG_INFO); + if (debugInfoAttr == null) { + return; + } + Optional max = ssaVar.getUseList().stream() + .map(DebugInfoApplyVisitor::getInsnOffsetByArg) + .max(Integer::compareTo); + if (!max.isPresent()) { + return; + } + int startOffset = getInsnOffsetByArg(ssaVar.getAssign()); + int endOffset = max.get(); + int regNum = ssaVar.getRegNum(); + for (LocalVar localVar : debugInfoAttr.getLocalVars()) { + if (localVar.getRegNum() == regNum) { + int startAddr = localVar.getStartAddr(); + int endAddr = localVar.getEndAddr(); + if (isInside(startOffset, startAddr, endAddr) || isInside(endOffset, startAddr, endAddr)) { + if (Consts.DEBUG) { + LOG.debug("Apply debug info by offset for: {} to {}", ssaVar, localVar); + } + applyDebugInfo(mth, ssaVar, localVar.getType(), localVar.getName()); + break; + } + } + } + } + + private static boolean isInside(int var, int start, int end) { + return start <= var && var <= end; + } + + private static int getInsnOffsetByArg(InsnArg arg) { + if (arg != null) { + InsnNode insn = arg.getParentInsn(); + if (insn != null) { + return insn.getOffset(); + } + } + return -1; + } + + public static void applyDebugInfo(MethodNode mth, SSAVar ssaVar, ArgType type, String varName) { + TypeUpdateResult result = mth.root().getTypeUpdate().applyWithWiderAllow(ssaVar, type); + if (result == TypeUpdateResult.REJECT) { + if (LOG.isDebugEnabled()) { + LOG.debug("Reject debug info of type: {} and name: '{}' for {}, mth: {}", type, varName, ssaVar, mth); + } + } else { + if (NameMapper.isValidIdentifier(varName)) { + ssaVar.setName(varName); + } + detachDebugInfo(ssaVar.getAssign()); + ssaVar.getUseList().forEach(DebugInfoApplyVisitor::detachDebugInfo); + } + } + + private static void detachDebugInfo(RegisterArg reg) { + if (reg != null) { + reg.remove(AType.REG_DEBUG_INFO); + } + } + + private static void addRegDbdInfo(Set debugInfo, RegisterArg reg) { + RegDebugInfoAttr debugInfoAttr = reg.get(AType.REG_DEBUG_INFO); + if (debugInfoAttr != null) { + debugInfo.add(debugInfoAttr); + } + } + + /** + * Fix debug info for splitter 'return' instructions + */ + private static void fixLinesForReturn(MethodNode mth) { + if (mth.getReturnType().equals(ArgType.VOID)) { + return; + } + InsnNode origReturn = null; + List newReturns = new ArrayList<>(mth.getExitBlocks().size()); + for (BlockNode exit : mth.getExitBlocks()) { + InsnNode ret = BlockUtils.getLastInsn(exit); + if (ret != null) { + if (ret.contains(AFlag.ORIG_RETURN)) { + origReturn = ret; + } else { + newReturns.add(ret); + } + } + } + if (origReturn != null) { + for (InsnNode ret : newReturns) { + InsnArg oldArg = origReturn.getArg(0); + InsnArg newArg = ret.getArg(0); + if (oldArg.isRegister() && newArg.isRegister()) { + RegisterArg oldArgReg = (RegisterArg) oldArg; + RegisterArg newArgReg = (RegisterArg) newArg; + applyDebugInfo(mth, newArgReg.getSVar(), oldArgReg.getType(), oldArgReg.getName()); + } + ret.setSourceLine(origReturn.getSourceLine()); + } + } + } + + private static void fixNamesForPhiInsns(MethodNode mth) { + mth.getSVars().forEach(ssaVar -> { + PhiInsn phiInsn = ssaVar.getUsedInPhi(); + if (phiInsn != null) { + Set names = new HashSet<>(1 + phiInsn.getArgsCount()); + addArgName(phiInsn.getResult(), names); + phiInsn.getArguments().forEach(arg -> addArgName(arg, names)); + if (names.size() == 1) { + setNameForInsn(phiInsn, names.iterator().next()); + } else if (names.size() > 1) { + LOG.warn("Different names in phi insn: {}, use first", names); + setNameForInsn(phiInsn, names.iterator().next()); + } + } + }); + } + + private static void addArgName(InsnArg arg, Set names) { + if (arg instanceof Named) { + String name = ((Named) arg).getName(); + if (name != null) { + names.add(name); + } + } + } + + private static void setNameForInsn(PhiInsn phiInsn, String name) { + phiInsn.getResult().setName(name); + phiInsn.getArguments().forEach(arg -> { + if (arg instanceof Named) { + ((Named) arg).setName(name); + } + }); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java new file mode 100644 index 000000000..15cfb31bc --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParseVisitor.java @@ -0,0 +1,111 @@ +package jadx.core.dex.visitors.debuginfo; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.dex.attributes.nodes.LocalVarsDebugInfoAttr; +import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.blocksmaker.BlockSplitter; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.utils.ErrorsCounter; +import jadx.core.utils.exceptions.DecodeException; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "Debug Info Parser", + desc = "Parse debug information (variable names and types, instruction lines)", + runBefore = { + BlockSplitter.class, + SSATransform.class + } +) +public class DebugInfoParseVisitor extends AbstractVisitor { + + private static final Logger LOG = LoggerFactory.getLogger(DebugInfoParseVisitor.class); + + @Override + public void visit(MethodNode mth) throws JadxException { + try { + int debugOffset = mth.getDebugInfoOffset(); + if (debugOffset > 0 && mth.dex().checkOffset(debugOffset)) { + processDebugInfo(mth, debugOffset); + } + } catch (Exception e) { + LOG.error("Error to parse debug info: {}", ErrorsCounter.formatMsg(mth, e.getMessage()), e); + } + } + + private void processDebugInfo(MethodNode mth, int debugOffset) throws DecodeException { + InsnNode[] insnArr = mth.getInstructions(); + DebugInfoParser debugInfoParser = new DebugInfoParser(mth, debugOffset, insnArr); + List localVars = debugInfoParser.process(); + attachDebugInfo(mth, localVars, insnArr); + setMethodSourceLine(mth, insnArr); + } + + private void attachDebugInfo(MethodNode mth, List localVars, InsnNode[] insnArr) { + if (localVars.isEmpty()) { + return; + } + if (Consts.DEBUG) { + LOG.debug("Parsed debug info for {}: ", mth); + localVars.forEach(v -> LOG.debug(" {}", v)); + } + localVars.forEach(var -> { + int start = var.getStartAddr(); + int end = var.getEndAddr(); + RegDebugInfoAttr debugInfoAttr = new RegDebugInfoAttr(var); + if (start < 0) { + // attach to method arguments + for (RegisterArg arg : mth.getArguments(true)) { + attachDebugInfo(arg, var, debugInfoAttr); + } + start = 0; + } + for (int i = start; i <= end; i++) { + InsnNode insn = insnArr[i]; + if (insn != null) { + attachDebugInfo(insn.getResult(), var, debugInfoAttr); + for (InsnArg arg : insn.getArguments()) { + attachDebugInfo(arg, var, debugInfoAttr); + } + } + } + }); + + mth.addAttr(new LocalVarsDebugInfoAttr(localVars)); + } + + private void attachDebugInfo(InsnArg arg, LocalVar var, RegDebugInfoAttr debugInfoAttr) { + if (arg instanceof RegisterArg) { + RegisterArg reg = (RegisterArg) arg; + if (var.getRegNum() == reg.getRegNum()) { + reg.addAttr(debugInfoAttr); + } + } + } + + /** + * Set method source line from first instruction + */ + private void setMethodSourceLine(MethodNode mth, InsnNode[] insnArr) { + for (InsnNode insn : insnArr) { + if (insn != null) { + int line = insn.getSourceLine(); + if (line != 0) { + mth.setSourceLine(line - 1); + } + return; + } + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/DebugInfoParser.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParser.java similarity index 57% rename from jadx-core/src/main/java/jadx/core/dex/nodes/parser/DebugInfoParser.java rename to jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParser.java index 932e96e77..0b79fe710 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/DebugInfoParser.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/DebugInfoParser.java @@ -1,21 +1,21 @@ -package jadx.core.dex.nodes.parser; +package jadx.core.dex.visitors.debuginfo; +import java.util.ArrayList; import java.util.List; import com.android.dex.Dex.Section; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.nodes.SourceFileAttr; -import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.utils.exceptions.DecodeException; public class DebugInfoParser { - + private static final Logger LOG = LoggerFactory.getLogger(DebugInfoParser.class); private static final int DBG_END_SEQUENCE = 0x00; private static final int DBG_ADVANCE_PC = 0x01; private static final int DBG_ADVANCE_LINE = 0x02; @@ -39,9 +39,10 @@ public class DebugInfoParser { private final DexNode dex; private final LocalVar[] locals; - private final InsnArg[] activeRegisters; private final InsnNode[] insnByOffset; + private List resultList; + public DebugInfoParser(MethodNode mth, int debugOffset, InsnNode[] insnByOffset) { this.mth = mth; this.dex = mth.dex(); @@ -49,38 +50,37 @@ public class DebugInfoParser { int regsCount = mth.getRegsCount(); this.locals = new LocalVar[regsCount]; - this.activeRegisters = new InsnArg[regsCount]; this.insnByOffset = insnByOffset; } - public void process() throws DecodeException { + public List process() throws DecodeException { + boolean varsInfoFound = false; + resultList = new ArrayList<>(); + int addr = 0; int line = section.readUleb128(); int paramsCount = section.readUleb128(); List mthArgs = mth.getArguments(false); + for (int i = 0; i < paramsCount; i++) { - int id = section.readUleb128() - 1; - if (id != DexNode.NO_INDEX) { - String name = dex.getString(id); - if (i < mthArgs.size()) { - mthArgs.get(i).setName(name); + int nameId = section.readUleb128() - 1; + if (nameId != DexNode.NO_INDEX) { + String name = dex.getString(nameId); + if (i < mthArgs.size() && name != null) { + RegisterArg arg = mthArgs.get(i); + int regNum = arg.getRegNum(); + LocalVar lVar = new LocalVar(regNum, name, arg.getInitType()); + startVar(lVar, -1); + varsInfoFound = true; } } } - for (RegisterArg arg : mthArgs) { - int rn = arg.getRegNum(); - locals[rn] = new LocalVar(arg); - activeRegisters[rn] = arg; - } - // process '0' instruction addrChange(-1, 1, line); setLine(addr, line); - boolean varsInfoFound = false; - int c = section.readByte() & 0xFF; while (c != DBG_END_SEQUENCE) { switch (c) { @@ -100,7 +100,7 @@ public class DebugInfoParser { int nameId = section.readUleb128() - 1; int type = section.readUleb128() - 1; LocalVar var = new LocalVar(dex, regNum, nameId, type, DexNode.NO_INDEX); - startVar(var, addr, line); + startVar(var, addr); varsInfoFound = true; break; } @@ -110,19 +110,13 @@ public class DebugInfoParser { int type = section.readUleb128() - 1; int sign = section.readUleb128() - 1; LocalVar var = new LocalVar(dex, regNum, nameId, type, sign); - startVar(var, addr, line); + startVar(var, addr); varsInfoFound = true; break; } case DBG_RESTART_LOCAL: { int regNum = section.readUleb128(); - LocalVar var = locals[regNum]; - if (var != null) { - if (var.end(addr, line)) { - setVar(var); - } - var.start(addr, line); - } + restartVar(regNum, addr); varsInfoFound = true; break; } @@ -130,8 +124,7 @@ public class DebugInfoParser { int regNum = section.readUleb128(); LocalVar var = locals[regNum]; if (var != null) { - var.end(addr, line); - setVar(var); + endVar(var, addr); } varsInfoFound = true; break; @@ -153,10 +146,10 @@ public class DebugInfoParser { default: { if (c >= DBG_FIRST_SPECIAL) { - int adjustedOpcode = c - DBG_FIRST_SPECIAL; - int addrInc = adjustedOpcode / DBG_LINE_RANGE; + int adjustedOpCode = c - DBG_FIRST_SPECIAL; + int addrInc = adjustedOpCode / DBG_LINE_RANGE; addr = addrChange(addr, addrInc, line); - line += DBG_LINE_BASE + adjustedOpcode % DBG_LINE_RANGE; + line += DBG_LINE_BASE + adjustedOpCode % DBG_LINE_RANGE; setLine(addr, line); } else { throw new DecodeException("Unknown debug insn code: " + c); @@ -170,33 +163,19 @@ public class DebugInfoParser { if (varsInfoFound) { for (LocalVar var : locals) { if (var != null && !var.isEnd()) { - var.end(mth.getCodeSize() - 1, line); - setVar(var); + endVar(var, mth.getCodeSize() - 1); } } } setSourceLines(addr, insnByOffset.length, line); + + return resultList; } private int addrChange(int addr, int addrInc, int line) { int newAddr = addr + addrInc; int maxAddr = insnByOffset.length - 1; newAddr = Math.min(newAddr, maxAddr); - for (int i = addr + 1; i <= newAddr; i++) { - InsnNode insn = insnByOffset[i]; - if (insn == null) { - continue; - } - for (InsnArg arg : insn.getArguments()) { - if (arg.isRegister()) { - activeRegisters[((RegisterArg) arg).getRegNum()] = arg; - } - } - RegisterArg res = insn.getResult(); - if (res != null) { - activeRegisters[res.getRegNum()] = res; - } - } setSourceLines(addr, newAddr, line); return newAddr; } @@ -214,81 +193,30 @@ public class DebugInfoParser { } } - private void startVar(LocalVar var, int addr, int line) { - int regNum = var.getRegNum(); + private void restartVar(int regNum, int addr) { LocalVar prev = locals[regNum]; - if (prev != null && !prev.isEnd()) { - prev.end(addr, line); - setVar(prev); - } - InsnArg activeReg = activeRegisters[var.getRegNum()]; - if (activeReg instanceof RegisterArg) { - SSAVar ssaVar = ((RegisterArg) activeReg).getSVar(); - if (ssaVar != null && ssaVar.getStartAddr() != -1) { - InsnNode parentInsn = ssaVar.getAssign().getParentInsn(); - if (parentInsn != null && parentInsn.getOffset() >= 0) { - addr = parentInsn.getOffset(); - } - } - } - var.start(addr, line); - locals[regNum] = var; - } - - private void setVar(LocalVar var) { - int start = var.getStartAddr(); - int end = var.getEndAddr(); - - for (int i = start; i <= end; i++) { - InsnNode insn = insnByOffset[i]; - if (insn != null) { - fillLocals(insn, var); - } - } - merge(activeRegisters[var.getRegNum()], var); - } - - private static void fillLocals(InsnNode insn, LocalVar var) { - merge(insn.getResult(), var); - for (InsnArg arg : insn.getArguments()) { - merge(arg, var); - } - } - - private static void merge(InsnArg arg, LocalVar var) { - if (arg == null || !arg.isRegister()) { - return; - } - RegisterArg reg = (RegisterArg) arg; - if (var.getRegNum() != reg.getRegNum()) { - return; - } - boolean mergeRequired = false; - - SSAVar ssaVar = reg.getSVar(); - if (ssaVar != null) { - int ssaEnd = ssaVar.getEndAddr(); - int ssaStart = ssaVar.getStartAddr(); - int localStart = var.getStartAddr(); - int localEnd = var.getEndAddr(); - - boolean isIntersected = !(localEnd < ssaStart || ssaEnd < localStart); - if (isIntersected && ssaEnd <= localEnd) { - mergeRequired = true; - } + if (prev != null) { + endVar(prev, addr); + LocalVar newVar = new LocalVar(regNum, prev.getName(), prev.getType()); + startVar(newVar, addr); } else { - mergeRequired = true; - } - - if (mergeRequired) { - applyDebugInfo(reg, var); + mth.addComment("Debug info: failed to restart local var, previous not found, register: " + regNum); } } - private static void applyDebugInfo(RegisterArg reg, LocalVar var) { - String varName = var.getName(); - if (NameMapper.isValidIdentifier(varName)) { - reg.mergeDebugInfo(var.getType(), varName); + private void startVar(LocalVar newVar, int addr) { + int regNum = newVar.getRegNum(); + LocalVar prev = locals[regNum]; + if (prev != null) { + endVar(prev, addr); + } + newVar.start(addr); + locals[regNum] = newVar; + } + + private void endVar(LocalVar var, int addr) { + if (var.end(addr)) { + resultList.add(var); } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/LocalVar.java b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java similarity index 60% rename from jadx-core/src/main/java/jadx/core/dex/nodes/parser/LocalVar.java rename to jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java index 99e6a7e97..f20044a60 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/LocalVar.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/debuginfo/LocalVar.java @@ -1,54 +1,48 @@ -package jadx.core.dex.nodes.parser; +package jadx.core.dex.visitors.debuginfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.DexNode; import jadx.core.utils.InsnUtils; -final class LocalVar { +public final class LocalVar { private static final Logger LOG = LoggerFactory.getLogger(LocalVar.class); private final int regNum; - private String name; - private ArgType type; + private final String name; + private final ArgType type; private boolean isEnd; private int startAddr; private int endAddr; public LocalVar(DexNode dex, int rn, int nameId, int typeId, int signId) { - this.regNum = rn; - String name = nameId == DexNode.NO_INDEX ? null : dex.getString(nameId); - ArgType type = typeId == DexNode.NO_INDEX ? null : dex.getType(typeId); - String sign = signId == DexNode.NO_INDEX ? null : dex.getString(signId); - - init(name, type, sign); + this(rn, dex.getString(nameId), dex.getType(typeId), dex.getString(signId)); } - public LocalVar(RegisterArg arg) { - this.regNum = arg.getRegNum(); - init(arg.getName(), arg.getType(), null); + public LocalVar(int regNum, String name, ArgType type) { + this(regNum, name, type, null); } - private void init(String name, ArgType type, String sign) { + public LocalVar(int regNum, String name, ArgType type, String sign) { + this.regNum = regNum; + this.name = name; if (sign != null) { try { ArgType gType = ArgType.generic(sign); - if (checkSignature(type, sign, gType)) { + if (checkSignature(type, gType)) { type = gType; } } catch (Exception e) { LOG.error("Can't parse signature for local variable: {}", sign, e); } } - this.name = name; this.type = type; } - private boolean checkSignature(ArgType type, String sign, ArgType gType) { + private boolean checkSignature(ArgType type, ArgType gType) { boolean apply; ArgType el = gType.getArrayRootElement(); if (el.isGeneric()) { @@ -62,7 +56,7 @@ final class LocalVar { return apply; } - public void start(int addr, int line) { + public void start(int addr) { this.isEnd = false; this.startAddr = addr; } @@ -71,16 +65,15 @@ final class LocalVar { * Sets end address of local variable * * @param addr address - * @param line source line * @return true if local variable was active, else false */ - public boolean end(int addr, int line) { - if (!isEnd) { - this.isEnd = true; - this.endAddr = addr; - return true; + public boolean end(int addr) { + if (isEnd) { + return false; } - return false; + this.isEnd = true; + this.endAddr = addr; + return true; } public int getRegNum() { @@ -119,8 +112,8 @@ final class LocalVar { @Override public String toString() { - return super.toString() + ' ' + (isEnd - ? "end: " + InsnUtils.formatOffset(startAddr) + '-' + InsnUtils.formatOffset(endAddr) - : "active: " + InsnUtils.formatOffset(startAddr)); + return InsnUtils.formatOffset(startAddr) + + '-' + (isEnd ? InsnUtils.formatOffset(endAddr) : " ") + + ": r" + regNum + " '" + name + "' " + type; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CheckRegions.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CheckRegions.java index 49389ea7b..5ed55f609 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CheckRegions.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CheckRegions.java @@ -47,7 +47,7 @@ public class CheckRegions extends AbstractVisitor { } if (LOG.isDebugEnabled() && !block.contains(AFlag.RETURN) - && !block.contains(AFlag.SKIP) + && !block.contains(AFlag.REMOVE) && !block.contains(AFlag.SYNTHETIC) && !block.getInstructions().isEmpty()) { LOG.debug("Duplicated block: {} - {}", mth, block); @@ -58,18 +58,20 @@ public class CheckRegions extends AbstractVisitor { for (BlockNode block : mth.getBasicBlocks()) { if (!blocksInRegions.contains(block) && !block.getInstructions().isEmpty() - && !block.contains(AFlag.SKIP)) { + && !block.contains(AFlag.ADDED_TO_REGION) + && !block.contains(AFlag.DONT_GENERATE) + && !block.contains(AFlag.REMOVE)) { String blockCode = getBlockInsnStr(mth, block); - mth.addWarn("Missing block: " + block + ", code skipped:" + CodeWriter.NL + blockCode); + mth.addWarn("Code restructure failed: missing block: " + block + ", code lost:" + blockCode); } } } - // check loop conditions DepthRegionTraversal.traverse(mth, new AbstractRegionVisitor() { @Override public boolean enterRegion(MethodNode mth, IRegion region) { if (region instanceof LoopRegion) { + // check loop conditions BlockNode loopHeader = ((LoopRegion) region).getHeader(); if (loopHeader != null && loopHeader.getInstructions().size() != 1) { mth.addWarn("Incorrect condition in loop: " + loopHeader); @@ -80,8 +82,9 @@ public class CheckRegions extends AbstractVisitor { }); } - private static String getBlockInsnStr(MethodNode mth, BlockNode block) { + private static String getBlockInsnStr(MethodNode mth, IBlock block) { CodeWriter code = new CodeWriter(); + code.newLine(); code.setIndent(3); MethodGen mg = MethodGen.getFallbackMethodGen(mth); InsnGen ig = new InsnGen(mg, true); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CleanRegions.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CleanRegions.java index cd3b72e7e..b47a3881a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CleanRegions.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/CleanRegions.java @@ -1,34 +1,57 @@ package jadx.core.dex.visitors.regions; +import java.util.List; + +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; +import jadx.core.dex.visitors.AbstractVisitor; -public class CleanRegions { +public class CleanRegions extends AbstractVisitor { + private static final IRegionVisitor REMOVE_REGION_VISITOR = new RemoveRegionVisitor(); + + @Override + public void visit(MethodNode mth) { + process(mth); + } public static void process(MethodNode mth) { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } - IRegionVisitor removeEmptyBlocks = new AbstractRegionVisitor() { - @Override - public boolean enterRegion(MethodNode mth, IRegion region) { - if (region instanceof Region) { - region.getSubBlocks().removeIf(container -> { - if (container instanceof BlockNode) { - BlockNode block = (BlockNode) container; - return block.getInstructions().isEmpty(); - } + DepthRegionTraversal.traverse(mth, REMOVE_REGION_VISITOR); + } + + private static class RemoveRegionVisitor extends AbstractRegionVisitor { + @Override + public boolean enterRegion(MethodNode mth, IRegion region) { + if (region instanceof Region) { + region.getSubBlocks().removeIf(RemoveRegionVisitor::canRemoveRegion); + } + return true; + } + + private static boolean canRemoveRegion(IContainer container) { + if (container.contains(AFlag.DONT_GENERATE)) { + return true; + } + if (container instanceof BlockNode) { + BlockNode block = (BlockNode) container; + return block.getInstructions().isEmpty(); + } + if (container instanceof IRegion) { + List subBlocks = ((IRegion) container).getSubBlocks(); + for (IContainer subBlock : subBlocks) { + if (!canRemoveRegion(subBlock)) { return false; - }); + } } return true; } - }; - DepthRegionTraversal.traverse(mth, removeEmptyBlocks); - } - - private CleanRegions() { + return false; + } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DepthRegionTraversal.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DepthRegionTraversal.java index 351f741f9..e25174a4c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DepthRegionTraversal.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DepthRegionTraversal.java @@ -60,16 +60,19 @@ public class DepthRegionTraversal { } } - private static boolean traverseIterativeStepInternal(MethodNode mth, IRegionIterativeVisitor visitor, - IContainer container) { + private static boolean traverseIterativeStepInternal(MethodNode mth, IRegionIterativeVisitor visitor, IContainer container) { if (container instanceof IRegion) { IRegion region = (IRegion) container; if (visitor.visitRegion(mth, region)) { return true; } for (IContainer subCont : region.getSubBlocks()) { - if (traverseIterativeStepInternal(mth, visitor, subCont)) { - return true; + try { + if (traverseIterativeStepInternal(mth, visitor, subCont)) { + return true; + } + } catch (StackOverflowError overflow) { + throw new JadxOverflowException("Region traversal failed: Recursive call in traverseIterativeStepInternal method"); } } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfMakerHelper.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfMakerHelper.java index d2f56d9b2..948861926 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfMakerHelper.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/IfMakerHelper.java @@ -36,7 +36,10 @@ public class IfMakerHelper { } static IfInfo makeIfInfo(BlockNode ifBlock) { - IfNode ifNode = (IfNode) ifBlock.getInstructions().get(0); + IfNode ifNode = (IfNode) BlockUtils.getLastInsn(ifBlock); + if (ifNode == null) { + throw new JadxRuntimeException("Empty IF block: " + ifBlock); + } IfCondition condition = IfCondition.fromIfNode(ifNode); IfInfo info = new IfInfo(condition, ifNode.getThenBlock(), ifNode.getElseBlock()); info.setIfBlock(ifBlock); @@ -302,13 +305,13 @@ public class IfMakerHelper { if (info.getMergedBlocks().size() > 1) { for (BlockNode block : info.getMergedBlocks()) { if (block != info.getIfBlock()) { - block.add(AFlag.SKIP); + block.add(AFlag.ADDED_TO_REGION); } } } if (!info.getSkipBlocks().isEmpty()) { for (BlockNode block : info.getSkipBlocks()) { - block.add(AFlag.SKIP); + block.add(AFlag.ADDED_TO_REGION); } info.getSkipBlocks().clear(); } @@ -336,11 +339,11 @@ public class IfMakerHelper { } private static BlockNode getNextIfNode(BlockNode block) { - if (block == null || block.contains(AType.LOOP) || block.contains(AFlag.SKIP)) { + if (block == null || block.contains(AType.LOOP) || block.contains(AFlag.ADDED_TO_REGION)) { return null; } - List insns = block.getInstructions(); - if (insns.size() == 1 && insns.get(0).getType() == InsnType.IF) { + InsnNode lastInsn = BlockUtils.getLastInsn(block); + if (lastInsn != null && lastInsn.getType() == InsnType.IF) { return block; } // skip this block and search in successors chain @@ -353,6 +356,7 @@ public class IfMakerHelper { if (next.getPredecessors().size() != 1) { return null; } + List insns = block.getInstructions(); boolean pass = true; if (!insns.isEmpty()) { // check that all instructions can be inlined diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java index 660133bb7..fb653959f 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/LoopRegionVisitor.java @@ -33,11 +33,18 @@ import jadx.core.dex.regions.loops.ForLoop; import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.visitors.AbstractVisitor; -import jadx.core.dex.visitors.CodeShrinker; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.regions.variables.ProcessVariables; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.BlockUtils; -import jadx.core.utils.InstructionRemover; import jadx.core.utils.RegionUtils; +import jadx.core.utils.exceptions.JadxOverflowException; +@JadxVisitor( + name = "LoopRegionVisitor", + desc = "Convert 'while' loops to 'for' loops (indexed or for-each)", + runBefore = {ProcessVariables.class} +) public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor { private static final Logger LOG = LoggerFactory.getLogger(LoopRegionVisitor.class); @@ -65,9 +72,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor if (checkForIndexedLoop(mth, loopRegion, condition)) { return; } - if (checkIterableForEach(mth, loopRegion, condition)) { - return; - } + checkIterableForEach(mth, loopRegion, condition); } /** @@ -87,7 +92,7 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor PhiInsn phiInsn = incrArg.getSVar().getUsedInPhi(); if (phiInsn == null || phiInsn.getArgsCount() != 2 - || !phiInsn.getArg(1).equals(incrArg) + || !phiInsn.containsArg(incrArg) || incrArg.getSVar().getUseCount() != 1) { return false; } @@ -108,20 +113,24 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor List args = new LinkedList<>(); incrInsn.getRegisterArgs(args); for (RegisterArg iArg : args) { - if (assignOnlyInLoop(mth, loopRegion, iArg)) { - return false; + try { + if (assignOnlyInLoop(mth, loopRegion, iArg)) { + return false; + } + } catch (StackOverflowError error) { + throw new JadxOverflowException("LoopRegionVisitor.assignOnlyInLoop endless recursion"); } } // all checks passed - initInsn.add(AFlag.SKIP); - incrInsn.add(AFlag.SKIP); + initInsn.add(AFlag.DONT_GENERATE); + incrInsn.add(AFlag.DONT_GENERATE); LoopType arrForEach = checkArrayForEach(mth, loopRegion, initInsn, incrInsn, condition); if (arrForEach != null) { loopRegion.setType(arrForEach); - return true; + } else { + loopRegion.setType(new ForLoop(initInsn, incrInsn)); } - loopRegion.setType(new ForLoop(initInsn, incrInsn)); return true; } @@ -191,12 +200,18 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor } // array for each loop confirmed - len.add(AFlag.SKIP); - arrGetInsn.add(AFlag.SKIP); - InstructionRemover.unbindInsn(mth, len); + incrInsn.getResult().add(AFlag.DONT_GENERATE); + condArg.add(AFlag.DONT_GENERATE); + bCondArg.add(AFlag.DONT_GENERATE); + arrGetInsn.add(AFlag.DONT_GENERATE); // inline array variable - CodeShrinker.shrinkMethod(mth); + if (arrayArg.isRegister()) { + ((RegisterArg) arrayArg).getSVar().removeUse((RegisterArg) arrGetInsn.getArg(0)); + } + CodeShrinkVisitor.shrinkMethod(mth); + len.add(AFlag.DONT_GENERATE); + if (arrGetInsn.contains(AFlag.WRAPPED)) { InsnArg wrapArg = BlockUtils.searchWrappedInsnParent(mth, arrGetInsn); if (wrapArg != null && wrapArg.getParentInsn() != null) { @@ -218,18 +233,15 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor if (sVar == null || sVar.isUsedInPhi()) { return false; } - List useList = sVar.getUseList(); + List itUseList = sVar.getUseList(); InsnNode assignInsn = iteratorArg.getAssignInsn(); - if (useList.size() != 2 - || assignInsn == null - || !checkInvoke(assignInsn, null, "iterator()Ljava/util/Iterator;", 0)) { + if (itUseList.size() != 2 || !checkInvoke(assignInsn, null, "iterator()Ljava/util/Iterator;", 0)) { return false; } InsnArg iterableArg = assignInsn.getArg(0); - InsnNode hasNextCall = useList.get(0).getParentInsn(); - InsnNode nextCall = useList.get(1).getParentInsn(); - if (hasNextCall == null || nextCall == null - || !checkInvoke(hasNextCall, "java.util.Iterator", "hasNext()Z", 0) + InsnNode hasNextCall = itUseList.get(0).getParentInsn(); + InsnNode nextCall = itUseList.get(1).getParentInsn(); + if (!checkInvoke(hasNextCall, "java.util.Iterator", "hasNext()Z", 0) || !checkInvoke(nextCall, "java.util.Iterator", "next()Ljava/lang/Object;", 0)) { return false; } @@ -268,9 +280,12 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor toSkip.add(nextCall); } - assignInsn.add(AFlag.SKIP); + assignInsn.add(AFlag.DONT_GENERATE); for (InsnNode insnNode : toSkip) { - insnNode.add(AFlag.SKIP); + insnNode.add(AFlag.DONT_GENERATE); + } + for (RegisterArg itArg : itUseList) { + itArg.add(AFlag.DONT_GENERATE); } loopRegion.setType(new ForEachLoop(iterVar, iterableArg)); return true; @@ -292,13 +307,13 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor iterVar.setType(gType); return true; } - if (ArgType.isInstanceOf(mth.dex(), gType, varType)) { + if (ArgType.isInstanceOf(mth.root(), gType, varType)) { return true; } ArgType wildcardType = gType.getWildcardType(); if (wildcardType != null && gType.getWildcardBounds() == 1 - && ArgType.isInstanceOf(mth.dex(), wildcardType, varType)) { + && ArgType.isInstanceOf(mth.root(), wildcardType, varType)) { return true; } LOG.warn("Generic type differs: '{}' and '{}' in {}", gType, varType, mth); @@ -317,6 +332,9 @@ public class LoopRegionVisitor extends AbstractVisitor implements IRegionVisitor * Check if instruction is a interface invoke with corresponding parameters. */ private static boolean checkInvoke(InsnNode insn, String declClsFullName, String mthId, int argsCount) { + if (insn == null) { + return false; + } if (insn.getType() == InsnType.INVOKE) { InvokeNode inv = (InvokeNode) insn; MethodInfo callMth = inv.getCallMth(); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessTryCatchRegions.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessTryCatchRegions.java index bb2d543e8..ea04884f3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessTryCatchRegions.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessTryCatchRegions.java @@ -7,9 +7,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import jadx.core.dex.attributes.AType; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.IBranchRegion; @@ -33,8 +30,6 @@ import jadx.core.utils.RegionUtils; */ public class ProcessTryCatchRegions extends AbstractRegionVisitor { - private static final Logger LOG = LoggerFactory.getLogger(ProcessTryCatchRegions.class); - public static void process(MethodNode mth) { if (mth.isNoCode() || mth.isNoExceptionHandlers()) { return; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessVariables.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessVariables.java deleted file mode 100644 index 3207ea56e..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/ProcessVariables.java +++ /dev/null @@ -1,331 +0,0 @@ -package jadx.core.dex.visitors.regions; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import jadx.core.dex.attributes.AFlag; -import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; -import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.instructions.args.VarName; -import jadx.core.dex.nodes.IBlock; -import jadx.core.dex.nodes.IContainer; -import jadx.core.dex.nodes.IRegion; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.regions.loops.ForLoop; -import jadx.core.dex.regions.loops.LoopRegion; -import jadx.core.dex.regions.loops.LoopType; -import jadx.core.dex.visitors.AbstractVisitor; -import jadx.core.utils.RegionUtils; -import jadx.core.utils.exceptions.JadxException; - -public class ProcessVariables extends AbstractVisitor { - - private static class Variable { - private final int regNum; - private final ArgType type; - - public Variable(RegisterArg arg) { - this.regNum = arg.getRegNum(); - this.type = arg.getType(); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Variable variable = (Variable) o; - return regNum == variable.regNum && type.equals(variable.type); - } - - @Override - public int hashCode() { - return 31 * regNum + type.hashCode(); - } - - @Override - public String toString() { - return "r" + regNum + ':' + type; - } - } - - private static class Usage { - private RegisterArg arg; - private VarName varName; - private IRegion argRegion; - private final Set uses = new LinkedHashSet<>(2); - private final Set assigns = new LinkedHashSet<>(2); - - public void setArg(RegisterArg arg) { - this.arg = arg; - } - - public RegisterArg getArg() { - return arg; - } - - public VarName getVarName() { - return varName; - } - - public void setVarName(VarName varName) { - this.varName = varName; - } - - public void setArgRegion(IRegion argRegion) { - this.argRegion = argRegion; - } - - public IRegion getArgRegion() { - return argRegion; - } - - public Set getAssigns() { - return assigns; - } - - public Set getUseRegions() { - return uses; - } - - @Override - public String toString() { - return arg + ", a:" + assigns + ", u:" + uses; - } - } - - private static class CollectUsageRegionVisitor extends TracedRegionVisitor { - private final List args; - private final Map usageMap; - - public CollectUsageRegionVisitor(Map usageMap) { - this.usageMap = usageMap; - this.args = new ArrayList<>(); - } - - @Override - public void processBlockTraced(MethodNode mth, IBlock container, IRegion curRegion) { - regionProcess(curRegion); - int len = container.getInstructions().size(); - for (int i = 0; i < len; i++) { - InsnNode insn = container.getInstructions().get(i); - if (insn.contains(AFlag.SKIP)) { - continue; - } - args.clear(); - processInsn(insn, curRegion); - } - } - - private void regionProcess(IRegion region) { - if (region instanceof LoopRegion) { - LoopRegion loopRegion = (LoopRegion) region; - LoopType loopType = loopRegion.getType(); - if (loopType instanceof ForLoop) { - ForLoop forLoop = (ForLoop) loopType; - processInsn(forLoop.getInitInsn(), region); - processInsn(forLoop.getIncrInsn(), region); - } - } - } - - void processInsn(InsnNode insn, IRegion curRegion) { - if (insn == null) { - return; - } - // result - RegisterArg result = insn.getResult(); - if (result != null && result.isRegister()) { - Usage u = addToUsageMap(result, usageMap); - if (u.getArg() == null) { - u.setArg(result); - u.setArgRegion(curRegion); - } - u.getAssigns().add(curRegion); - } - // args - args.clear(); - insn.getRegisterArgs(args); - for (RegisterArg arg : args) { - Usage u = addToUsageMap(arg, usageMap); - u.getUseRegions().add(curRegion); - } - } - } - - @Override - public void visit(MethodNode mth) throws JadxException { - if (mth.isNoCode()) { - return; - } - List mthArguments = mth.getArguments(true); - - Map usageMap = new LinkedHashMap<>(); - for (RegisterArg arg : mthArguments) { - addToUsageMap(arg, usageMap); - } - - // collect all variables usage - IRegionVisitor collect = new CollectUsageRegionVisitor(usageMap); - DepthRegionTraversal.traverse(mth, collect); - - // reduce assigns map - for (RegisterArg arg : mthArguments) { - usageMap.remove(new Variable(arg)); - } - - Iterator> umIt = usageMap.entrySet().iterator(); - while (umIt.hasNext()) { - Entry entry = umIt.next(); - Usage u = entry.getValue(); - // if no assigns => remove - if (u.getAssigns().isEmpty()) { - umIt.remove(); - continue; - } - - // variable declared at 'catch' clause - InsnNode parentInsn = u.getArg().getParentInsn(); - if (parentInsn == null || parentInsn.getType() == InsnType.MOVE_EXCEPTION) { - umIt.remove(); - } - } - if (usageMap.isEmpty()) { - return; - } - - for (Iterator> it = usageMap.entrySet().iterator(); it.hasNext(); ) { - Entry entry = it.next(); - Usage u = entry.getValue(); - // check if variable can be declared at current assigns - for (IRegion assignRegion : u.getAssigns()) { - if (u.getArgRegion() == assignRegion - && canDeclareInRegion(u, assignRegion) - && declareAtAssign(u)) { - it.remove(); - break; - } - } - } - if (usageMap.isEmpty()) { - return; - } - - // apply - for (Entry entry : usageMap.entrySet()) { - Usage u = entry.getValue(); - - // find region which contain all usage regions - Set set = u.getUseRegions(); - for (Iterator it = set.iterator(); it.hasNext(); ) { - IRegion r = it.next(); - IRegion parent = r.getParent(); - if (parent != null && set.contains(parent)) { - it.remove(); - } - } - IRegion region = null; - if (!set.isEmpty()) { - region = set.iterator().next(); - } else if (!u.getAssigns().isEmpty()) { - region = u.getAssigns().iterator().next(); - } - if (region == null) { - continue; - } - IRegion parent = region; - boolean declared = false; - while (parent != null) { - if (canDeclareInRegion(u, region)) { - declareVar(region, u.getArg()); - declared = true; - break; - } - region = parent; - parent = region.getParent(); - } - if (!declared) { - declareVar(mth.getRegion(), u.getArg()); - } - } - } - - private static Usage addToUsageMap(RegisterArg arg, Map usageMap) { - Variable varId = new Variable(arg); - Usage usage = usageMap.computeIfAbsent(varId, v -> new Usage()); - // merge variables names - if (usage.getVarName() == null) { - VarName argVN = arg.getSVar().getVarName(); - if (argVN == null) { - argVN = new VarName(); - arg.getSVar().setVarName(argVN); - } - usage.setVarName(argVN); - } else { - arg.getSVar().setVarName(usage.getVarName()); - } - return usage; - } - - private static boolean declareAtAssign(Usage u) { - RegisterArg arg = u.getArg(); - InsnNode parentInsn = arg.getParentInsn(); - if (parentInsn == null) { - return false; - } - if (!arg.equals(parentInsn.getResult())) { - return false; - } - parentInsn.add(AFlag.DECLARE_VAR); - return true; - } - - private static void declareVar(IContainer region, RegisterArg arg) { - DeclareVariablesAttr dv = region.get(AType.DECLARE_VARIABLES); - if (dv == null) { - dv = new DeclareVariablesAttr(); - region.addAttr(dv); - } - dv.addVar(arg); - } - - private static boolean canDeclareInRegion(Usage u, IRegion region) { - // workaround for declare variables used in several loops - if (region instanceof LoopRegion) { - for (IRegion r : u.getAssigns()) { - if (!RegionUtils.isRegionContainsRegion(region, r)) { - return false; - } - } - } - // can't declare in else-if chain between 'else' and next 'if' - if (region.contains(AFlag.ELSE_IF_CHAIN)) { - return false; - } - // TODO: make index for faster search - return isAllRegionsAfter(region, u.getAssigns()) - && isAllRegionsAfter(region, u.getUseRegions()); - } - - private static boolean isAllRegionsAfter(IRegion region, Set others) { - for (IRegion r : others) { - if (!RegionUtils.isRegionContainsRegion(region, r)) { - return false; - } - } - return true; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java index 5940f78fa..fc4da506d 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMaker.java @@ -116,8 +116,8 @@ public class RegionMaker { } } - if (!processed && block.getInstructions().size() == 1) { - InsnNode insn = block.getInstructions().get(0); + InsnNode insn = BlockUtils.getLastInsn(block); + if (!processed && insn != null) { switch (insn.getType()) { case IF: next = processIf(r, block, (IfNode) insn, stack); @@ -204,13 +204,13 @@ public class RegionMaker { BlockNode thenBlock = condInfo.getThenBlock(); out = thenBlock == loopStart ? condInfo.getElseBlock() : thenBlock; loopStart.remove(AType.LOOP); - loop.getEnd().add(AFlag.SKIP); + loop.getEnd().add(AFlag.ADDED_TO_REGION); stack.addExit(loop.getEnd()); processedBlocks.clear(loopStart.getId()); Region body = makeRegion(loopStart, stack); loopRegion.setBody(body); loopStart.addAttr(AType.LOOP, loop); - loop.getEnd().remove(AFlag.SKIP); + loop.getEnd().remove(AFlag.ADDED_TO_REGION); } else { out = condInfo.getElseBlock(); if (outerRegion != null @@ -230,7 +230,7 @@ public class RegionMaker { blocks.remove(conditionBlock); for (BlockNode block : blocks) { if (block.getInstructions().isEmpty() - && !block.contains(AFlag.SKIP) + && !block.contains(AFlag.ADDED_TO_REGION) && !RegionUtils.isRegionContainsBlock(body, block)) { body.add(block); } @@ -248,9 +248,11 @@ public class RegionMaker { */ private LoopRegion makeLoopRegion(IRegion curRegion, LoopInfo loop, List exitBlocks) { for (BlockNode block : exitBlocks) { - if (block.contains(AType.EXC_HANDLER) - || block.getInstructions().size() != 1 - || block.getInstructions().get(0).getType() != InsnType.IF) { + if (block.contains(AType.EXC_HANDLER)) { + continue; + } + InsnNode lastInsn = BlockUtils.getLastInsn(block); + if (lastInsn == null || lastInsn.getType() != InsnType.IF) { continue; } List loops = block.getAll(AType.LOOP); @@ -520,7 +522,7 @@ public class RegionMaker { return false; } BlockNode codePred = preds.get(0); - if (codePred.contains(AFlag.SKIP)) { + if (codePred.contains(AFlag.ADDED_TO_REGION)) { return false; } if (loopEnd.isDominator(codePred) @@ -561,9 +563,10 @@ public class RegionMaker { for (InsnNode exitInsn : synchRegion.getExitInsns()) { BlockNode insnBlock = BlockUtils.getBlockByInsn(mth, exitInsn); if (insnBlock != null) { - insnBlock.add(AFlag.SKIP); + insnBlock.add(AFlag.DONT_GENERATE); } - exitInsn.add(AFlag.SKIP); + exitInsn.add(AFlag.DONT_GENERATE); + exitInsn.add(AFlag.REMOVE); InstructionRemover.unbindInsn(mth, exitInsn); } @@ -646,7 +649,7 @@ public class RegionMaker { } private BlockNode processIf(IRegion currentRegion, BlockNode block, IfNode ifnode, RegionStack stack) { - if (block.contains(AFlag.SKIP)) { + if (block.contains(AFlag.ADDED_TO_REGION)) { // block already included in other 'if' region return ifnode.getThenBlock(); } @@ -712,7 +715,7 @@ public class RegionMaker { private void addEdgeInsn(IfInfo ifInfo, Region region, EdgeInsnAttr edgeInsnAttr) { BlockNode start = edgeInsnAttr.getStart(); - if (start.contains(AFlag.SKIP)) { + if (start.contains(AFlag.ADDED_TO_REGION)) { return; } boolean fromThisIf = false; @@ -849,7 +852,10 @@ public class RegionMaker { } if (!stack.containsExit(defCase)) { - sw.setDefaultCase(makeRegion(defCase, stack)); + Region defRegion = makeRegion(defCase, stack); + if (RegionUtils.notEmpty(defRegion)) { + sw.setDefaultCase(defRegion); + } } for (Entry> entry : blocksMap.entrySet()) { BlockNode caseBlock = entry.getKey(); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java index ffea7cd43..14057fcf3 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/RegionMakerVisitor.java @@ -35,7 +35,7 @@ import jadx.core.utils.exceptions.JadxException; public class RegionMakerVisitor extends AbstractVisitor { private static final Logger LOG = LoggerFactory.getLogger(RegionMakerVisitor.class); - private static final PostRegionVisitor POST_REGION_VISITOR = new PostRegionVisitor(); + private static final IRegionVisitor POST_REGION_VISITOR = new PostRegionVisitor(); @Override public void visit(MethodNode mth) throws JadxException { @@ -54,7 +54,6 @@ public class RegionMakerVisitor extends AbstractVisitor { mth.getRegion().add(expOutBlock); } } - postProcessRegions(mth); } @@ -85,82 +84,83 @@ public class RegionMakerVisitor extends AbstractVisitor { insertEdgeInsn((Region) region); } } - } - /** - * Insert insn block from edge insn attribute. - */ - private static void insertEdgeInsn(Region region) { - List subBlocks = region.getSubBlocks(); - if (subBlocks.isEmpty()) { - return; - } - IContainer last = subBlocks.get(subBlocks.size() - 1); - List edgeInsnAttrs = last.getAll(AType.EDGE_INSN); - if (edgeInsnAttrs.isEmpty()) { - return; - } - EdgeInsnAttr insnAttr = edgeInsnAttrs.get(0); - if (!insnAttr.getStart().equals(last)) { - return; - } - if (last instanceof BlockNode) { - BlockNode block = (BlockNode) last; - if (block.getInstructions().isEmpty()) { - block.getInstructions().add(insnAttr.getInsn()); + /** + * Insert insn block from edge insn attribute. + */ + private static void insertEdgeInsn(Region region) { + List subBlocks = region.getSubBlocks(); + if (subBlocks.isEmpty()) { return; } - } - List insns = Collections.singletonList(insnAttr.getInsn()); - region.add(new InsnContainer(insns)); - } - - private static void processSwitch(MethodNode mth, SwitchRegion sw) { - for (IContainer c : sw.getBranches()) { - if (!(c instanceof Region)) { - continue; + IContainer last = subBlocks.get(subBlocks.size() - 1); + List edgeInsnAttrs = last.getAll(AType.EDGE_INSN); + if (edgeInsnAttrs.isEmpty()) { + return; } - Set blocks = new HashSet<>(); - RegionUtils.getAllRegionBlocks(c, blocks); - if (blocks.isEmpty()) { - addBreakToContainer((Region) c); - continue; + EdgeInsnAttr insnAttr = edgeInsnAttrs.get(0); + if (!insnAttr.getStart().equals(last)) { + return; } - for (IBlock block : blocks) { - if (!(block instanceof BlockNode)) { - continue; + if (last instanceof BlockNode) { + BlockNode block = (BlockNode) last; + if (block.getInstructions().isEmpty()) { + block.getInstructions().add(insnAttr.getInsn()); + return; } - BlockNode bn = (BlockNode) block; - for (BlockNode s : bn.getCleanSuccessors()) { - if (!blocks.contains(s) - && !bn.contains(AFlag.SKIP) - && !s.contains(AFlag.FALL_THROUGH)) { - addBreak(mth, c, bn); - break; + } + List insns = Collections.singletonList(insnAttr.getInsn()); + region.add(new InsnContainer(insns)); + } + + private static void processSwitch(MethodNode mth, SwitchRegion sw) { + for (IContainer c : sw.getBranches()) { + if (c instanceof Region) { + Set blocks = new HashSet<>(); + RegionUtils.getAllRegionBlocks(c, blocks); + if (blocks.isEmpty()) { + addBreakToContainer((Region) c); + } else { + for (IBlock block : blocks) { + if (block instanceof BlockNode) { + addBreakForBlock(mth, c, blocks, (BlockNode) block); + } + } } } } } - } - private static void addBreak(MethodNode mth, IContainer c, BlockNode bn) { - IContainer blockContainer = RegionUtils.getBlockContainer(c, bn); - if (blockContainer instanceof Region) { - addBreakToContainer((Region) blockContainer); - } else if (c instanceof Region) { - addBreakToContainer((Region) c); - } else { - LOG.warn("Can't insert break, container: {}, block: {}, mth: {}", blockContainer, bn, mth); + private static void addBreakToContainer(Region c) { + if (RegionUtils.hasExitEdge(c)) { + return; + } + List insns = new ArrayList<>(1); + insns.add(new InsnNode(InsnType.BREAK, 0)); + c.add(new InsnContainer(insns)); } - } - private static void addBreakToContainer(Region c) { - if (RegionUtils.hasExitEdge(c)) { - return; + private static void addBreakForBlock(MethodNode mth, IContainer c, Set blocks, BlockNode bn) { + for (BlockNode s : bn.getCleanSuccessors()) { + if (!blocks.contains(s) + && !bn.contains(AFlag.ADDED_TO_REGION) + && !s.contains(AFlag.FALL_THROUGH)) { + addBreak(mth, c, bn); + return; + } + } + } + + private static void addBreak(MethodNode mth, IContainer c, BlockNode bn) { + IContainer blockContainer = RegionUtils.getBlockContainer(c, bn); + if (blockContainer instanceof Region) { + addBreakToContainer((Region) blockContainer); + } else if (c instanceof Region) { + addBreakToContainer((Region) c); + } else { + LOG.warn("Can't insert break, container: {}, block: {}, mth: {}", blockContainer, bn, mth); + } } - List insns = new ArrayList<>(1); - insns.add(new InsnNode(InsnType.BREAK, 0)); - c.add(new InsnContainer(insns)); } private static void removeSynchronized(MethodNode mth) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java index cd176a877..f373f4fe9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/TernaryMod.java @@ -17,7 +17,7 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.conditions.IfRegion; -import jadx.core.dex.visitors.CodeShrinker; +import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.InsnList; public class TernaryMod { @@ -40,18 +40,18 @@ public class TernaryMod { return false; } BlockNode header = ifRegion.getHeader(); - InsnNode t = tb.getInstructions().get(0); - InsnNode e = eb.getInstructions().get(0); + InsnNode thenInsn = tb.getInstructions().get(0); + InsnNode elseInsn = eb.getInstructions().get(0); - if (t.getSourceLine() != e.getSourceLine()) { - if (t.getSourceLine() != 0 && e.getSourceLine() != 0) { + if (thenInsn.getSourceLine() != elseInsn.getSourceLine()) { + if (thenInsn.getSourceLine() != 0 && elseInsn.getSourceLine() != 0) { // sometimes source lines incorrect - if (!checkLineStats(t, e)) { + if (!checkLineStats(thenInsn, elseInsn)) { return false; } } else { // no debug info - if (containsTernary(t) || containsTernary(e)) { + if (containsTernary(thenInsn) || containsTernary(elseInsn)) { // don't make nested ternary by default // TODO: add addition checks return false; @@ -59,50 +59,60 @@ public class TernaryMod { } } - if (t.getResult() != null && e.getResult() != null) { - PhiInsn phi = t.getResult().getSVar().getUsedInPhi(); - if (phi == null || !t.getResult().equalRegisterAndType(e.getResult())) { + RegisterArg thenResArg = thenInsn.getResult(); + RegisterArg elseResArg = elseInsn.getResult(); + if (thenResArg != null && elseResArg != null) { + PhiInsn thenPhi = thenResArg.getSVar().getUsedInPhi(); + PhiInsn elsePhi = elseResArg.getSVar().getUsedInPhi(); + if (thenPhi == null || thenPhi != elsePhi) { return false; } if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) { return false; } - InsnList.remove(tb, t); - InsnList.remove(eb, e); + InsnList.remove(tb, thenInsn); + InsnList.remove(eb, elseInsn); RegisterArg resArg; - if (phi.getArgsCount() == 2) { - resArg = phi.getResult(); + if (thenPhi.getArgsCount() == 2) { + resArg = thenPhi.getResult(); } else { - resArg = t.getResult(); - phi.removeArg(e.getResult()); + resArg = thenResArg; + thenPhi.removeArg(elseResArg); } TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), - resArg, InsnArg.wrapArg(t), InsnArg.wrapArg(e)); - ternInsn.setSourceLine(t.getSourceLine()); + resArg, InsnArg.wrapArg(thenInsn), InsnArg.wrapArg(elseInsn)); + ternInsn.setSourceLine(thenInsn.getSourceLine()); // remove 'if' instruction header.getInstructions().clear(); header.getInstructions().add(ternInsn); // shrink method again - CodeShrinker.shrinkMethod(mth); + CodeShrinkVisitor.shrinkMethod(mth); return true; } if (!mth.getReturnType().equals(ArgType.VOID) - && t.getType() == InsnType.RETURN && e.getType() == InsnType.RETURN) { + && thenInsn.getType() == InsnType.RETURN + && elseInsn.getType() == InsnType.RETURN) { + InsnArg thenArg = thenInsn.getArg(0); + InsnArg elseArg = elseInsn.getArg(0); + if (thenArg.isLiteral() != elseArg.isLiteral()) { + // one arg is literal + return false; + } if (!ifRegion.getParent().replaceSubBlock(ifRegion, header)) { return false; } - InsnList.remove(tb, t); - InsnList.remove(eb, e); + InsnList.remove(tb, thenInsn); + InsnList.remove(eb, elseInsn); tb.remove(AFlag.RETURN); eb.remove(AFlag.RETURN); - TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), null, t.getArg(0), e.getArg(0)); - ternInsn.setSourceLine(t.getSourceLine()); + TernaryInsn ternInsn = new TernaryInsn(ifRegion.getCondition(), null, thenArg, elseArg); + ternInsn.setSourceLine(thenInsn.getSourceLine()); InsnNode retInsn = new InsnNode(InsnType.RETURN, 1); retInsn.addArg(InsnArg.wrapArg(ternInsn)); @@ -110,7 +120,7 @@ public class TernaryMod { header.getInstructions().add(retInsn); header.add(AFlag.RETURN); - CodeShrinker.shrinkMethod(mth); + CodeShrinkVisitor.shrinkMethod(mth); return true; } return false; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/CollectUsageRegionVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/CollectUsageRegionVisitor.java new file mode 100644 index 000000000..bc35718c2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/CollectUsageRegionVisitor.java @@ -0,0 +1,87 @@ +package jadx.core.dex.visitors.regions.variables; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.IBlock; +import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.regions.loops.ForLoop; +import jadx.core.dex.regions.loops.LoopRegion; +import jadx.core.dex.regions.loops.LoopType; +import jadx.core.dex.visitors.regions.TracedRegionVisitor; + +class CollectUsageRegionVisitor extends TracedRegionVisitor { + private final List args; + private final Map usageMap; + + public CollectUsageRegionVisitor() { + this.usageMap = new LinkedHashMap<>(); + this.args = new ArrayList<>(); + } + + public Map getUsageMap() { + return usageMap; + } + + @Override + public void processBlockTraced(MethodNode mth, IBlock block, IRegion curRegion) { + UsePlace usePlace = new UsePlace(curRegion, block); + regionProcess(usePlace); + int len = block.getInstructions().size(); + for (int i = 0; i < len; i++) { + InsnNode insn = block.getInstructions().get(i); + if (insn.contains(AFlag.DONT_GENERATE)) { + continue; + } + processInsn(insn, usePlace); + } + } + + private void regionProcess(UsePlace usePlace) { + IRegion region = usePlace.getRegion(); + if (region instanceof LoopRegion) { + LoopRegion loopRegion = (LoopRegion) region; + LoopType loopType = loopRegion.getType(); + if (loopType instanceof ForLoop) { + ForLoop forLoop = (ForLoop) loopType; + processInsn(forLoop.getInitInsn(), usePlace); + processInsn(forLoop.getIncrInsn(), usePlace); + } + } + } + + void processInsn(InsnNode insn, UsePlace usePlace) { + if (insn == null) { + return; + } + // result + RegisterArg result = insn.getResult(); + if (result != null && result.isRegister()) { + if (!result.contains(AFlag.DONT_GENERATE)) { + VarUsage usage = getUsage(result.getSVar()); + usage.getAssigns().add(usePlace); + } + } + // args + args.clear(); + insn.getRegisterArgs(args); + for (RegisterArg arg : args) { + if (arg.contains(AFlag.DONT_GENERATE)) { + continue; + } + VarUsage usage = getUsage(arg.getSVar()); + usage.getUses().add(usePlace); + } + } + + private VarUsage getUsage(SSAVar ssaVar) { + return usageMap.computeIfAbsent(ssaVar, VarUsage::new); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java new file mode 100644 index 000000000..8d427c707 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/ProcessVariables.java @@ -0,0 +1,308 @@ +package jadx.core.dex.visitors.regions.variables; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.DeclareVariablesAttr; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.CodeVar; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.IBlock; +import jadx.core.dex.nodes.IContainer; +import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.regions.loops.LoopRegion; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.regions.DepthRegionTraversal; +import jadx.core.utils.RegionUtils; +import jadx.core.utils.exceptions.JadxException; + +public class ProcessVariables extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(ProcessVariables.class); + + @Override + public void visit(MethodNode mth) throws JadxException { + if (mth.isNoCode() || mth.getSVars().isEmpty()) { + return; + } + + List codeVars = collectCodeVars(mth); + if (codeVars.isEmpty()) { + return; + } + checkCodeVars(mth, codeVars); + // TODO: reduce code vars by name if debug info applied. Need checks for variable scopes before reduce + + // collect all variables usage + CollectUsageRegionVisitor usageCollector = new CollectUsageRegionVisitor(); + DepthRegionTraversal.traverse(mth, usageCollector); + Map ssaUsageMap = usageCollector.getUsageMap(); + if (ssaUsageMap.isEmpty()) { + return; + } + + Map> codeVarUsage = mergeUsageMaps(codeVars, ssaUsageMap); + + for (Entry> entry : codeVarUsage.entrySet()) { + declareVar(mth, entry.getKey(), entry.getValue()); + } + } + + private void checkCodeVars(MethodNode mth, List codeVars) { + int unknownTypesCount = 0; + for (CodeVar codeVar : codeVars) { + codeVar.getSsaVars().stream() + .filter(ssaVar -> ssaVar.contains(AFlag.IMMUTABLE_TYPE)) + .forEach(ssaVar -> { + ArgType ssaType = ssaVar.getAssign().getInitType(); + if (ssaType.isTypeKnown() && !ssaType.equals(codeVar.getType())) { + mth.addWarn("Incorrect type for immutable var: ssa=" + ssaType + + ", code=" + codeVar.getType() + + ", for " + ssaVar.getDetailedVarInfo(mth)); + } + }); + if (codeVar.getType() == null) { + codeVar.setType(ArgType.UNKNOWN); + unknownTypesCount++; + } + } + if (unknownTypesCount != 0) { + mth.addWarn("Unknown variable types count: " + unknownTypesCount); + } + } + + private void declareVar(MethodNode mth, CodeVar codeVar, List usageList) { + if (codeVar.isDeclared()) { + return; + } + + VarUsage mergedUsage = new VarUsage(null); + for (VarUsage varUsage : usageList) { + mergedUsage.getAssigns().addAll(varUsage.getAssigns()); + mergedUsage.getUses().addAll(varUsage.getUses()); + } + if (mergedUsage.getAssigns().isEmpty() && mergedUsage.getUses().isEmpty()) { + return; + } + + // check if variable can be declared at one of assigns + if (checkDeclareAtAssign(usageList, mergedUsage)) { + return; + } + // search closest region for declare + if (searchDeclareRegion(mergedUsage, codeVar)) { + return; + } + // region not found, declare at method start + declareVarInRegion(mth.getRegion(), codeVar); + } + + private List collectCodeVars(MethodNode mth) { + Map> codeVars = new LinkedHashMap<>(); + for (SSAVar ssaVar : mth.getSVars()) { + if (ssaVar.getCodeVar().isThis()) { + continue; + } + CodeVar codeVar = ssaVar.getCodeVar(); + List list = codeVars.computeIfAbsent(codeVar, k -> new ArrayList<>()); + list.add(ssaVar); + } + + for (Entry> entry : codeVars.entrySet()) { + CodeVar codeVar = entry.getKey(); + List list = entry.getValue(); + for (SSAVar ssaVar : list) { + CodeVar localCodeVar = ssaVar.getCodeVar(); + codeVar.mergeFlagsFrom(localCodeVar); + } + if (list.size() > 1) { + for (SSAVar ssaVar : list) { + ssaVar.setCodeVar(codeVar); + } + } + codeVar.setSsaVars(list); + } + return new ArrayList<>(codeVars.keySet()); + } + + private Map> mergeUsageMaps(List codeVars, Map ssaUsageMap) { + Map> codeVarUsage = new LinkedHashMap<>(codeVars.size()); + for (CodeVar codeVar : codeVars) { + List list = new ArrayList<>(); + for (SSAVar ssaVar : codeVar.getSsaVars()) { + VarUsage usage = ssaUsageMap.get(ssaVar); + if (usage != null) { + list.add(usage); + } + } + if (!list.isEmpty()) { + codeVarUsage.put(codeVar, list); + } + } + return codeVarUsage; + } + + private boolean checkDeclareAtAssign(List list, VarUsage mergedUsage) { + if (mergedUsage.getAssigns().isEmpty()) { + return false; + } + for (VarUsage u : list) { + for (UsePlace assign : u.getAssigns()) { + if (canDeclareAt(mergedUsage, assign)) { + return checkDeclareAtAssign(u.getVar()); + } + } + } + return false; + } + + private static boolean canDeclareAt(VarUsage usage, UsePlace usePlace) { + IRegion region = usePlace.getRegion(); + // workaround for declare variables used in several loops + if (region instanceof LoopRegion) { + for (UsePlace use : usage.getAssigns()) { + if (!RegionUtils.isRegionContainsRegion(region, use.getRegion())) { + return false; + } + } + } + // can't declare in else-if chain between 'else' and next 'if' + if (region.contains(AFlag.ELSE_IF_CHAIN)) { + return false; + } + return isAllUseAfter(usePlace, usage.getAssigns()) + && isAllUseAfter(usePlace, usage.getUses()); + } + + /** + * Check if all {@code usePlaces} are after {@code checkPlace} + */ + private static boolean isAllUseAfter(UsePlace checkPlace, List usePlaces) { + + IRegion region = checkPlace.getRegion(); + IBlock block = checkPlace.getBlock(); + Set toCheck = new HashSet<>(usePlaces); + boolean blockFound = false; + for (IContainer subBlock : region.getSubBlocks()) { + if (!blockFound && subBlock == block) { + blockFound = true; + } + if (blockFound) { + toCheck.removeIf(usePlace -> isContainerContainsUsePlace(subBlock, usePlace)); + if (toCheck.isEmpty()) { + return true; + } + } + } + return false; + } + + private static boolean isContainerContainsUsePlace(IContainer subBlock, UsePlace usePlace) { + if (subBlock == usePlace.getBlock()) { + return true; + } + if (subBlock instanceof IRegion) { + // TODO: make index for faster check + return RegionUtils.isRegionContainsRegion(subBlock, usePlace.getRegion()); + } + return false; + } + + private static boolean checkDeclareAtAssign(SSAVar var) { + RegisterArg arg = var.getAssign(); + InsnNode parentInsn = arg.getParentInsn(); + if (parentInsn == null) { + return false; + } + if (!arg.equals(parentInsn.getResult())) { + return false; + } + parentInsn.add(AFlag.DECLARE_VAR); + return true; + } + + private boolean searchDeclareRegion(VarUsage u, CodeVar codeVar) { + /* + Set set = u.getUseRegions(); + for (Iterator it = set.iterator(); it.hasNext(); ) { + IRegion r = it.next(); + IRegion parent = r.getParent(); + if (parent != null && set.contains(parent)) { + it.remove(); + } + } + IRegion region = null; + if (!set.isEmpty()) { + region = set.iterator().next(); + } else if (!u.getAssigns().isEmpty()) { + region = u.getAssigns().iterator().next(); + } + if (region == null) { + return false; + } + IRegion parent = region; + while (parent != null) { + if (canDeclareAt(u, region)) { + declareVarInRegion(region, codeVar); + return true; + } + region = parent; + parent = region.getParent(); + } + */ + return false; + } + + private static void declareVarInRegion(IContainer region, CodeVar var) { + if (var.isDeclared()) { + LOG.warn("Try to declare already declared variable: {}", var); + return; + } + DeclareVariablesAttr dv = region.get(AType.DECLARE_VARIABLES); + if (dv == null) { + dv = new DeclareVariablesAttr(); + region.addAttr(dv); + } + dv.addVar(var); + var.setDeclared(true); + } + + private static boolean isAllRegionsAfter(IRegion region, Set others) { + IRegion parent = region.getParent(); + if (parent == null) { + return true; + } + // lazy init for + int regionIndex = -2; + List subBlocks = Collections.emptyList(); + for (IRegion r : others) { + if (parent == r.getParent()) { + // on same level, check order by index + if (regionIndex == -2) { + subBlocks = parent.getSubBlocks(); + regionIndex = subBlocks.indexOf(region); + } + int rIndex = subBlocks.indexOf(r); + if (regionIndex > rIndex) { + return false; + } + } else if (!RegionUtils.isRegionContainsRegion(region, r)) { + return false; + } + } + return true; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/UsePlace.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/UsePlace.java new file mode 100644 index 000000000..d76daf960 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/UsePlace.java @@ -0,0 +1,50 @@ +package jadx.core.dex.visitors.regions.variables; + +import java.util.Objects; + +import jadx.core.dex.nodes.IBlock; +import jadx.core.dex.nodes.IRegion; + +public class UsePlace { + public final IRegion region; + public final IBlock block; + + public UsePlace(IRegion region, IBlock block) { + this.region = region; + this.block = block; + } + + public IRegion getRegion() { + return region; + } + + public IBlock getBlock() { + return block; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UsePlace usePlace = (UsePlace) o; + return Objects.equals(region, usePlace.region) + && Objects.equals(block, usePlace.block); + } + + @Override + public int hashCode() { + return Objects.hash(region, block); + } + + @Override + public String toString() { + return "UsePlace{" + + "region=" + region + + ", block=" + block + + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/VarUsage.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/VarUsage.java new file mode 100644 index 000000000..706818303 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/variables/VarUsage.java @@ -0,0 +1,33 @@ +package jadx.core.dex.visitors.regions.variables; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.instructions.args.SSAVar; + +class VarUsage { + private final SSAVar var; + private final List assigns = new ArrayList<>(3); + private final List uses = new ArrayList<>(3); + + VarUsage(SSAVar var) { + this.var = var; + } + + public SSAVar getVar() { + return var; + } + + public List getAssigns() { + return assigns; + } + + public List getUses() { + return uses; + } + + @Override + public String toString() { + return '{' + (var == null ? "-" : var.toShortString()) + ", a:" + assigns + ", u:" + uses + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/ArgsInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/ArgsInfo.java new file mode 100644 index 000000000..e96e09532 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/ArgsInfo.java @@ -0,0 +1,142 @@ +package jadx.core.dex.visitors.shrink; + +import java.util.BitSet; +import java.util.LinkedList; +import java.util.List; + +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.InsnWrapArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.mods.ConstructorInsn; +import jadx.core.dex.instructions.mods.TernaryInsn; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.utils.EmptyBitSet; +import jadx.core.utils.exceptions.JadxRuntimeException; + +final class ArgsInfo { + private final InsnNode insn; + private final List argsList; + private final List args; + private final int pos; + private int inlineBorder; + private ArgsInfo inlinedInsn; + + public ArgsInfo(InsnNode insn, List argsList, int pos) { + this.insn = insn; + this.argsList = argsList; + this.pos = pos; + this.inlineBorder = pos; + this.args = getArgs(insn); + } + + public static List getArgs(InsnNode insn) { + List args = new LinkedList<>(); + addArgs(insn, args); + return args; + } + + private static void addArgs(InsnNode insn, List args) { + if (insn.getType() == InsnType.CONSTRUCTOR) { + args.add(((ConstructorInsn) insn).getInstanceArg()); + } else if (insn.getType() == InsnType.TERNARY) { + args.addAll(((TernaryInsn) insn).getCondition().getRegisterArgs()); + } + for (InsnArg arg : insn.getArguments()) { + if (arg.isRegister()) { + args.add((RegisterArg) arg); + } + } + for (InsnArg arg : insn.getArguments()) { + if (arg.isInsnWrap()) { + addArgs(((InsnWrapArg) arg).getWrapInsn(), args); + } + } + } + + public InsnNode getInsn() { + return insn; + } + + List getArgs() { + return args; + } + + public WrapInfo checkInline(int assignPos, RegisterArg arg) { + if (assignPos >= inlineBorder || !canMove(assignPos, inlineBorder)) { + return null; + } + inlineBorder = assignPos; + return inline(assignPos, arg); + } + + private boolean canMove(int from, int to) { + ArgsInfo startInfo = argsList.get(from); + List movedArgs = startInfo.getArgs(); + int start = from + 1; + if (start == to) { + // previous instruction or on edge of inline border + return true; + } + if (start > to) { + throw new JadxRuntimeException("Invalid inline insn positions: " + start + " - " + to); + } + BitSet movedSet; + if (movedArgs.isEmpty()) { + if (startInfo.insn.isConstInsn()) { + return true; + } + movedSet = EmptyBitSet.EMPTY; + } else { + movedSet = new BitSet(); + for (RegisterArg arg : movedArgs) { + movedSet.set(arg.getRegNum()); + } + } + for (int i = start; i < to; i++) { + ArgsInfo argsInfo = argsList.get(i); + if (argsInfo.getInlinedInsn() == this) { + continue; + } + InsnNode curInsn = argsInfo.insn; + if (!curInsn.canReorder() || usedArgAssign(curInsn, movedSet)) { + return false; + } + } + return true; + } + + static boolean usedArgAssign(InsnNode insn, BitSet args) { + if (args.isEmpty()) { + return false; + } + RegisterArg result = insn.getResult(); + if (result == null || result.isField()) { + return false; + } + return args.get(result.getRegNum()); + } + + WrapInfo inline(int assignInsnPos, RegisterArg arg) { + ArgsInfo argsInfo = argsList.get(assignInsnPos); + argsInfo.inlinedInsn = this; + return new WrapInfo(argsInfo.insn, arg); + } + + ArgsInfo getInlinedInsn() { + if (inlinedInsn != null) { + ArgsInfo parent = inlinedInsn.getInlinedInsn(); + if (parent != null) { + inlinedInsn = parent; + } + } + return inlinedInsn; + } + + @Override + public String toString() { + return "ArgsInfo: |" + inlineBorder + + " ->" + (inlinedInsn == null ? "-" : inlinedInsn.pos) + + ' ' + args + " : " + insn; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/CodeShrinker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java similarity index 55% rename from jadx-core/src/main/java/jadx/core/dex/visitors/CodeShrinker.java rename to jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java index b464eab5f..2c80b1b9c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/CodeShrinker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/CodeShrinkVisitor.java @@ -1,8 +1,7 @@ -package jadx.core.dex.visitors; +package jadx.core.dex.visitors.shrink; import java.util.ArrayList; import java.util.BitSet; -import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Set; @@ -13,17 +12,22 @@ import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; -import jadx.core.dex.instructions.mods.ConstructorInsn; -import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.ModVisitor; import jadx.core.utils.BlockUtils; -import jadx.core.utils.EmptyBitSet; import jadx.core.utils.InsnList; import jadx.core.utils.exceptions.JadxRuntimeException; -public class CodeShrinker extends AbstractVisitor { +@JadxVisitor( + name = "CodeShrinkVisitor", + desc = "Inline variables for make code smaller", + runAfter = {ModVisitor.class} +) +public class CodeShrinkVisitor extends AbstractVisitor { @Override public void visit(MethodNode mth) { @@ -31,7 +35,7 @@ public class CodeShrinker extends AbstractVisitor { } public static void shrinkMethod(MethodNode mth) { - if (mth.isNoCode() || mth.contains(AFlag.DONT_SHRINK)) { + if (mth.isNoCode()) { return; } for (BlockNode block : mth.getBasicBlocks()) { @@ -40,156 +44,6 @@ public class CodeShrinker extends AbstractVisitor { } } - private static final class ArgsInfo { - private final InsnNode insn; - private final List argsList; - private final List args; - private final int pos; - private int inlineBorder; - private ArgsInfo inlinedInsn; - - public ArgsInfo(InsnNode insn, List argsList, int pos) { - this.insn = insn; - this.argsList = argsList; - this.pos = pos; - this.inlineBorder = pos; - this.args = getArgs(insn); - } - - public static List getArgs(InsnNode insn) { - List args = new LinkedList<>(); - addArgs(insn, args); - return args; - } - - private static void addArgs(InsnNode insn, List args) { - if (insn.getType() == InsnType.CONSTRUCTOR) { - args.add(((ConstructorInsn) insn).getInstanceArg()); - } else if (insn.getType() == InsnType.TERNARY) { - args.addAll(((TernaryInsn) insn).getCondition().getRegisterArgs()); - } - for (InsnArg arg : insn.getArguments()) { - if (arg.isRegister()) { - args.add((RegisterArg) arg); - } - } - for (InsnArg arg : insn.getArguments()) { - if (arg.isInsnWrap()) { - addArgs(((InsnWrapArg) arg).getWrapInsn(), args); - } - } - } - - public InsnNode getInsn() { - return insn; - } - - private List getArgs() { - return args; - } - - public WrapInfo checkInline(int assignPos, RegisterArg arg) { - if (assignPos >= inlineBorder || !canMove(assignPos, inlineBorder)) { - return null; - } - inlineBorder = assignPos; - return inline(assignPos, arg); - } - - private boolean canMove(int from, int to) { - ArgsInfo startInfo = argsList.get(from); - List movedArgs = startInfo.getArgs(); - int start = from + 1; - if (start == to) { - // previous instruction or on edge of inline border - return true; - } - if (start > to) { - throw new JadxRuntimeException("Invalid inline insn positions: " + start + " - " + to); - } - BitSet movedSet; - if (movedArgs.isEmpty()) { - if (startInfo.insn.isConstInsn()) { - return true; - } - movedSet = EmptyBitSet.EMPTY; - } else { - movedSet = new BitSet(); - for (RegisterArg arg : movedArgs) { - movedSet.set(arg.getRegNum()); - } - } - for (int i = start; i < to; i++) { - ArgsInfo argsInfo = argsList.get(i); - if (argsInfo.getInlinedInsn() == this) { - continue; - } - InsnNode curInsn = argsInfo.insn; - if (!curInsn.canReorder() || usedArgAssign(curInsn, movedSet)) { - return false; - } - } - return true; - } - - private static boolean usedArgAssign(InsnNode insn, BitSet args) { - if (args.isEmpty()) { - return false; - } - RegisterArg result = insn.getResult(); - if (result == null || result.isField()) { - return false; - } - return args.get(result.getRegNum()); - } - - public WrapInfo inline(int assignInsnPos, RegisterArg arg) { - ArgsInfo argsInfo = argsList.get(assignInsnPos); - argsInfo.inlinedInsn = this; - return new WrapInfo(argsInfo.insn, arg); - } - - public ArgsInfo getInlinedInsn() { - if (inlinedInsn != null) { - ArgsInfo parent = inlinedInsn.getInlinedInsn(); - if (parent != null) { - inlinedInsn = parent; - } - } - return inlinedInsn; - } - - @Override - public String toString() { - return "ArgsInfo: |" + inlineBorder - + " ->" + (inlinedInsn == null ? '-' : inlinedInsn.pos) - + ' ' + args + " : " + insn; - } - } - - private static final class WrapInfo { - private final InsnNode insn; - private final RegisterArg arg; - - public WrapInfo(InsnNode assignInsn, RegisterArg arg) { - this.insn = assignInsn; - this.arg = arg; - } - - private InsnNode getInsn() { - return insn; - } - - private RegisterArg getArg() { - return arg; - } - - @Override - public String toString() { - return "WrapInfo: " + arg + " -> " + insn; - } - } - private static void shrinkBlock(MethodNode mth, BlockNode block) { if (block.getInstructions().isEmpty()) { return; @@ -223,6 +77,14 @@ public class CodeShrinker extends AbstractVisitor { if (assignInsn == null || assignInsn.contains(AFlag.DONT_INLINE)) { continue; } + List useList = sVar.getUseList(); + if (!useList.isEmpty()) { + InsnNode parentInsn = useList.get(0).getParentInsn(); + if (parentInsn != null && parentInsn.contains(AFlag.DONT_GENERATE)) { + continue; + } + } + int assignPos = insnList.getIndex(assignInsn); if (assignPos != -1) { WrapInfo wrapInfo = argsInfo.checkInline(assignPos, arg); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/WrapInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/WrapInfo.java new file mode 100644 index 000000000..3169345e2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/shrink/WrapInfo.java @@ -0,0 +1,27 @@ +package jadx.core.dex.visitors.shrink; + +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.InsnNode; + +final class WrapInfo { + private final InsnNode insn; + private final RegisterArg arg; + + WrapInfo(InsnNode assignInsn, RegisterArg arg) { + this.insn = assignInsn; + this.arg = arg; + } + + InsnNode getInsn() { + return insn; + } + + RegisterArg getArg() { + return arg; + } + + @Override + public String toString() { + return "WrapInfo: " + arg + " -> " + insn; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java deleted file mode 100644 index a3819836c..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/EliminatePhiNodes.java +++ /dev/null @@ -1,131 +0,0 @@ -package jadx.core.dex.visitors.ssa; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.PhiListAttr; -import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.instructions.PhiInsn; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.instructions.args.SSAVar; -import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.visitors.AbstractVisitor; -import jadx.core.utils.BlockUtils; -import jadx.core.utils.exceptions.JadxException; -import jadx.core.utils.exceptions.JadxRuntimeException; - -public class EliminatePhiNodes extends AbstractVisitor { - private static final Logger LOG = LoggerFactory.getLogger(EliminatePhiNodes.class); - - @Override - public void visit(MethodNode mth) throws JadxException { - if (mth.isNoCode()) { - return; - } - replaceMergeInstructions(mth); - removePhiInstructions(mth); - } - - private static void removePhiInstructions(MethodNode mth) { - for (BlockNode block : mth.getBasicBlocks()) { - PhiListAttr phiList = block.get(AType.PHI_LIST); - if (phiList == null) { - continue; - } - List list = phiList.getList(); - for (PhiInsn phiInsn : list) { - removeInsn(mth, block, phiInsn); - } - } - } - - private static void removeInsn(MethodNode mth, BlockNode block, PhiInsn phiInsn) { - Iterator it = block.getInstructions().iterator(); - while (it.hasNext()) { - InsnNode insn = it.next(); - if (insn == phiInsn) { - it.remove(); - return; - } - } - LOG.warn("Phi node not removed: {}, mth: {}", phiInsn, mth); - } - - private void replaceMergeInstructions(MethodNode mth) { - for (BlockNode block : mth.getBasicBlocks()) { - List insns = block.getInstructions(); - if (insns.isEmpty()) { - continue; - } - InsnNode insn = insns.get(0); - if (insn.getType() == InsnType.MERGE) { - replaceMerge(mth, block, insn); - } - } - } - - /** - * Replace 'MERGE' with 'PHI' insn. - */ - private void replaceMerge(MethodNode mth, BlockNode block, InsnNode insn) { - if (insn.getArgsCount() != 2) { - throw new JadxRuntimeException("Unexpected count of arguments in merge insn: " + insn); - } - RegisterArg oldArg = (RegisterArg) insn.getArg(1); - RegisterArg newArg = (RegisterArg) insn.getArg(0); - int newRegNum = newArg.getRegNum(); - if (oldArg.getRegNum() == newRegNum) { - throw new JadxRuntimeException("Unexpected register number in merge insn: " + insn); - } - SSAVar oldSVar = oldArg.getSVar(); - RegisterArg assignArg = oldSVar.getAssign(); - - InsnNode assignParentInsn = assignArg.getParentInsn(); - BlockNode assignBlock = BlockUtils.getBlockByInsn(mth, assignParentInsn); - if (assignBlock == null) { - throw new JadxRuntimeException("Unknown assign block for " + assignParentInsn); - } - BlockNode assignPred = null; - for (BlockNode pred : block.getPredecessors()) { - if (BlockUtils.isPathExists(assignBlock, pred)) { - assignPred = pred; - break; - } - } - if (assignPred == null) { - throw new JadxRuntimeException("Assign predecessor not found for " + assignBlock + " from " + block); - } - - // all checks passed - RegisterArg newAssignArg = oldArg.duplicate(newRegNum, null); - SSAVar newSVar = mth.makeNewSVar(newRegNum, mth.getNextSVarVersion(newRegNum), newAssignArg); - newSVar.setName(oldSVar.getName()); - newSVar.setType(assignArg.getType()); - - if (assignParentInsn != null) { - assignParentInsn.setResult(newAssignArg); - } - for (RegisterArg useArg : new ArrayList<>(oldSVar.getUseList())) { - RegisterArg newUseArg = useArg.duplicate(newRegNum, newSVar); - InsnNode parentInsn = useArg.getParentInsn(); - if (parentInsn != null) { - newSVar.use(newUseArg); - parentInsn.replaceArg(useArg, newUseArg); - } - } - block.getInstructions().remove(0); - PhiInsn phiInsn = SSATransform.addPhi(mth, block, newRegNum); - phiInsn.setResult(insn.getResult()); - - phiInsn.bindArg(newAssignArg.duplicate(), assignPred); - phiInsn.bindArg(newArg.duplicate(), - BlockUtils.selectOtherSafe(assignPred, block.getPredecessors())); - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/RenameState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/RenameState.java index e5773d3bf..3dce9569a 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/RenameState.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/RenameState.java @@ -2,6 +2,7 @@ package jadx.core.dex.visitors.ssa; import java.util.Arrays; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.args.SSAVar; import jadx.core.dex.nodes.BlockNode; @@ -22,7 +23,8 @@ final class RenameState { new int[regsCount] ); for (RegisterArg arg : mth.getArguments(true)) { - state.startVar(arg); + SSAVar ssaVar = state.startVar(arg); + ssaVar.add(AFlag.METHOD_ARGUMENT); } return state; } @@ -51,9 +53,11 @@ final class RenameState { return vars[regNum]; } - public void startVar(RegisterArg regArg) { + public SSAVar startVar(RegisterArg regArg) { int regNum = regArg.getRegNum(); int version = versions[regNum]++; - vars[regNum] = mth.makeNewSVar(regNum, version, regArg); + SSAVar ssaVar = mth.makeNewSVar(regNum, version, regArg); + vars[regNum] = ssaVar; + return ssaVar; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java index 914df9b1c..b4545b4ac 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ssa/SSATransform.java @@ -42,6 +42,10 @@ public class SSATransform extends AbstractVisitor { } private static void process(MethodNode mth) { + if (!mth.getSVars().isEmpty()) { + return; + } + LiveVarAnalysis la = new LiveVarAnalysis(mth); la.runAnalysis(); int regsCount = mth.getRegsCount(); @@ -62,6 +66,8 @@ public class SSATransform extends AbstractVisitor { throw new JadxRuntimeException("Phi nodes fix limit reached!"); } } while (repeatFix); + + hidePhiInsns(mth); } private static void placePhi(MethodNode mth, int regNum, LiveVarAnalysis la) { @@ -116,9 +122,6 @@ public class SSATransform extends AbstractVisitor { } private static void renameVariables(MethodNode mth) { - if (!mth.getSVars().isEmpty()) { - throw new JadxRuntimeException("SSA rename variables already executed"); - } RenameState initState = RenameState.init(mth); initPhiInEnterBlock(initState); @@ -210,7 +213,7 @@ public class SSATransform extends AbstractVisitor { if (parentInsn != null && parentInsn.getResult() != null && parentInsn.contains(AFlag.TRY_LEAVE) - && phi.removeArg(arg)) { + && phi.removeArg(arg) /* TODO: fix registers removing*/) { argsCount--; continue; } @@ -409,7 +412,6 @@ public class SSATransform extends AbstractVisitor { return; } arg.add(AFlag.THIS); - arg.setName(RegisterArg.THIS_ARG_NAME); // mark all moved 'this' InsnNode parentInsn = arg.getParentInsn(); if (parentInsn != null @@ -419,8 +421,14 @@ public class SSATransform extends AbstractVisitor { if (resArg.getRegNum() != arg.getRegNum() && !resArg.getSVar().isUsedInPhi()) { markThisArgs(resArg); - parentInsn.add(AFlag.SKIP); + parentInsn.add(AFlag.DONT_GENERATE); } } } + + private static void hidePhiInsns(MethodNode mth) { + for (BlockNode block : mth.getBasicBlocks()) { + block.getInstructions().removeIf(insn -> insn.getType() == InsnType.PHI); + } + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java new file mode 100644 index 000000000..307dfc54f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/AbstractTypeConstraint.java @@ -0,0 +1,50 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.utils.Utils; + +public abstract class AbstractTypeConstraint implements ITypeConstraint { + + protected InsnNode insn; + protected List relatedVars; + + public AbstractTypeConstraint(InsnNode insn, InsnArg arg) { + this.insn = insn; + this.relatedVars = collectRelatedVars(insn, arg); + } + + private List collectRelatedVars(InsnNode insn, InsnArg arg) { + List list = new ArrayList<>(insn.getArgsCount()); + if (insn.getResult() == arg) { + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg.isRegister()) { + list.add(((RegisterArg) insnArg).getSVar()); + } + } + } else { + list.add(insn.getResult().getSVar()); + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg != arg && insnArg.isRegister()) { + list.add(((RegisterArg) insnArg).getSVar()); + } + } + } + return list; + } + + @Override + public List getRelatedVars() { + return relatedVars; + } + + @Override + public String toString() { + return "(" + insn.getType() + ':' + Utils.listToString(relatedVars, SSAVar::toShortString) + ')'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/BoundEnum.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/BoundEnum.java new file mode 100644 index 000000000..b66d46013 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/BoundEnum.java @@ -0,0 +1,6 @@ +package jadx.core.dex.visitors.typeinference; + +public enum BoundEnum { + ASSIGN, + USE +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/CheckTypeVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/CheckTypeVisitor.java deleted file mode 100644 index 5e6c96667..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/CheckTypeVisitor.java +++ /dev/null @@ -1,28 +0,0 @@ -package jadx.core.dex.visitors.typeinference; - -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.utils.ErrorsCounter; - -public class CheckTypeVisitor { - - public static void visit(MethodNode mth, InsnNode insn) { - if (insn.getResult() != null - && !insn.getResult().getType().isTypeKnown()) { - error("Wrong return type", mth, insn); - return; - } - - for (InsnArg arg : insn.getArguments()) { - if (!arg.getType().isTypeKnown()) { - error("Wrong type", mth, insn); - return; - } - } - } - - private static void error(String msg, MethodNode mth, InsnNode insn) { - ErrorsCounter.methodWarn(mth, msg + ": " + insn); - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FinishTypeInference.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FinishTypeInference.java deleted file mode 100644 index 981ad8d20..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FinishTypeInference.java +++ /dev/null @@ -1,49 +0,0 @@ -package jadx.core.dex.visitors.typeinference; - -import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.DexNode; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.visitors.AbstractVisitor; - -public class FinishTypeInference extends AbstractVisitor { - - @Override - public void visit(MethodNode mth) { - if (mth.isNoCode()) { - return; - } - - boolean change; - int i = 0; - do { - change = false; - for (BlockNode block : mth.getBasicBlocks()) { - for (InsnNode insn : block.getInstructions()) { - if (PostTypeInference.process(mth, insn)) { - change = true; - } - } - } - i++; - if (i > 1000) { - break; - } - } while (change); - - // last chance to set correct value (just use first type from 'possible' list) - DexNode dex = mth.dex(); - for (BlockNode block : mth.getBasicBlocks()) { - for (InsnNode insn : block.getInstructions()) { - SelectTypeVisitor.visit(dex, insn); - } - } - - // check - for (BlockNode block : mth.getBasicBlocks()) { - for (InsnNode insn : block.getInstructions()) { - CheckTypeVisitor.visit(mth, insn); - } - } - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java new file mode 100644 index 000000000..099ff773c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeBound.java @@ -0,0 +1,9 @@ +package jadx.core.dex.visitors.typeinference; + +import jadx.core.dex.instructions.args.ArgType; + +public interface ITypeBound { + BoundEnum getBound(); + + ArgType getType(); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java new file mode 100644 index 000000000..1757c48a6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeConstraint.java @@ -0,0 +1,12 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.List; + +import jadx.core.dex.instructions.args.SSAVar; + +public interface ITypeConstraint { + + List getRelatedVars(); + + boolean check(TypeSearchState state); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeListener.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeListener.java new file mode 100644 index 000000000..5a72e0bd1 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/ITypeListener.java @@ -0,0 +1,20 @@ +package jadx.core.dex.visitors.typeinference; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.nodes.InsnNode; + +@FunctionalInterface +public interface ITypeListener { + + /** + * Listener function - triggered on type update + * + * @param updateInfo store all allowed type updates + * @param arg apply suggested type for this arg + * @param candidateType suggest new type + */ + TypeUpdateResult update(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, @NotNull ArgType candidateType); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/PostTypeInference.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/PostTypeInference.java deleted file mode 100644 index 5b81ce8a3..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/PostTypeInference.java +++ /dev/null @@ -1,152 +0,0 @@ -package jadx.core.dex.visitors.typeinference; - -import java.util.List; - -import jadx.core.dex.info.MethodInfo; -import jadx.core.dex.instructions.IndexInsnNode; -import jadx.core.dex.instructions.InvokeNode; -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.instructions.args.LiteralArg; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.nodes.DexNode; -import jadx.core.dex.nodes.InsnNode; -import jadx.core.dex.nodes.MethodNode; - -public class PostTypeInference { - - private PostTypeInference() { - } - - public static boolean process(MethodNode mth, InsnNode insn) { - DexNode dex = mth.dex(); - switch (insn.getType()) { - case CONST: - RegisterArg res = insn.getResult(); - LiteralArg litArg = (LiteralArg) insn.getArg(0); - if (res.getType().isObject()) { - long lit = litArg.getLiteral(); - if (lit != 0) { - // incorrect literal value for object - ArgType type = lit == 1 ? ArgType.BOOLEAN : ArgType.INT; - // can't merge with object -> force it - litArg.setType(type); - res.getSVar().setType(type); - return true; - } - } - return litArg.merge(dex, res); - - case MOVE: { - boolean change = false; - if (insn.getResult().merge(dex, insn.getArg(0))) { - change = true; - } - if (insn.getArg(0).merge(dex, insn.getResult())) { - change = true; - } - return change; - } - - case AGET: - return fixArrayTypes(dex, insn.getArg(0), insn.getResult()); - - case APUT: - return fixArrayTypes(dex, insn.getArg(0), insn.getArg(2)); - - case IF: { - boolean change = false; - if (insn.getArg(1).merge(dex, insn.getArg(0))) { - change = true; - } - if (insn.getArg(0).merge(dex, insn.getArg(1))) { - change = true; - } - return change; - } - - // check argument types for overloaded methods - case INVOKE: { - boolean change = false; - InvokeNode inv = (InvokeNode) insn; - MethodInfo callMth = inv.getCallMth(); - MethodNode node = mth.dex().resolveMethod(callMth); - if (node != null && node.isArgsOverload()) { - List args = callMth.getArgumentsTypes(); - int j = inv.getArgsCount() - 1; - for (int i = args.size() - 1; i >= 0; i--) { - ArgType argType = args.get(i); - InsnArg insnArg = inv.getArg(j--); - if (insnArg.isRegister() && !argType.equals(insnArg.getType())) { - insnArg.setType(argType); - change = true; - } - } - } - return change; - } - - case CHECK_CAST: { - ArgType castType = (ArgType) ((IndexInsnNode) insn).getIndex(); - RegisterArg result = insn.getResult(); - ArgType resultType = result.getType(); - // don't override generic types of same base class - boolean skip = castType.isObject() && resultType.isObject() - && castType.getObject().equals(resultType.getObject()); - if (!skip) { - // workaround for compiler bug (see TestDuplicateCast) - result.getSVar().setType(castType); - } - return true; - } - - case PHI: - case MERGE: { - ArgType type = insn.getResult().getType(); - if (!type.isTypeKnown()) { - for (InsnArg arg : insn.getArguments()) { - if (arg.getType().isTypeKnown()) { - type = arg.getType(); - break; - } - } - } - boolean changed = false; - if (updateType(insn.getResult(), type)) { - changed = true; - } - for (int i = 0; i < insn.getArgsCount(); i++) { - RegisterArg arg = (RegisterArg) insn.getArg(i); - if (updateType(arg, type)) { - changed = true; - } - } - return changed; - } - - default: - break; - } - return false; - } - - private static boolean updateType(RegisterArg arg, ArgType type) { - ArgType prevType = arg.getType(); - if (prevType == null || !prevType.equals(type)) { - arg.setType(type); - return true; - } - return false; - } - - private static boolean fixArrayTypes(DexNode dex, InsnArg array, InsnArg elem) { - boolean change = false; - if (!elem.getType().isTypeKnown() && elem.merge(dex, array.getType().getArrayElement())) { - change = true; - } - if (!array.getType().isTypeKnown() && array.merge(dex, ArgType.array(elem.getType()))) { - change = true; - } - return change; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/SelectTypeVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/SelectTypeVisitor.java deleted file mode 100644 index 0553059e2..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/SelectTypeVisitor.java +++ /dev/null @@ -1,30 +0,0 @@ -package jadx.core.dex.visitors.typeinference; - -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.nodes.DexNode; -import jadx.core.dex.nodes.InsnNode; - -public class SelectTypeVisitor { - - private SelectTypeVisitor() { - } - - public static void visit(DexNode dex, InsnNode insn) { - InsnArg res = insn.getResult(); - if (res != null && !res.getType().isTypeKnown()) { - selectType(dex, res); - } - for (InsnArg arg : insn.getArguments()) { - if (!arg.getType().isTypeKnown()) { - selectType(dex, arg); - } - } - } - - private static void selectType(DexNode dex, InsnArg arg) { - ArgType t = arg.getType(); - ArgType newType = ArgType.merge(dex, t, t.selectFirst()); - arg.setType(newType); - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundConst.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundConst.java new file mode 100644 index 000000000..344374378 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeBoundConst.java @@ -0,0 +1,48 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.Objects; + +import jadx.core.dex.instructions.args.ArgType; + +public final class TypeBoundConst implements ITypeBound { + private final BoundEnum bound; + private final ArgType type; + + public TypeBoundConst(BoundEnum bound, ArgType type) { + this.bound = bound; + this.type = type; + } + + @Override + public BoundEnum getBound() { + return bound; + } + + @Override + public ArgType getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TypeBoundConst that = (TypeBoundConst) o; + return bound == that.bound && + Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(bound, type); + } + + @Override + public String toString() { + return "{" + bound + ": " + type + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java new file mode 100644 index 000000000..228ef2d2a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompare.java @@ -0,0 +1,221 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.CONFLICT; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.NARROW_BY_GENERIC; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.UNKNOWN; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.WIDER; +import static jadx.core.dex.visitors.typeinference.TypeCompareEnum.WIDER_BY_GENERIC; + +public class TypeCompare { + private static final Logger LOG = LoggerFactory.getLogger(TypeCompare.class); + + private final RootNode root; + private final ArgTypeComparator comparator; + + public TypeCompare(RootNode root) { + this.root = root; + this.comparator = new ArgTypeComparator(); + } + + /** + * Compare two type and return result for first argument (narrow, wider or conflict) + */ + public TypeCompareEnum compareTypes(ArgType first, ArgType second) { + if (first == second || Objects.equals(first, second)) { + return TypeCompareEnum.EQUAL; + } + boolean firstKnown = first.isTypeKnown(); + boolean secondKnown = second.isTypeKnown(); + if (firstKnown != secondKnown) { + if (firstKnown) { + return compareWithUnknown(first, second); + } else { + return compareWithUnknown(second, first).invert(); + } + } + boolean firstArray = first.isArray(); + boolean secondArray = second.isArray(); + if (firstArray != secondArray) { + if (firstArray) { + return compareArrayWithOtherType(first, second); + } else { + return compareArrayWithOtherType(second, first).invert(); + } + } + if (firstArray /* && secondArray */) { + // both arrays + return compareTypes(first.getArrayElement(), second.getArrayElement()); + } + if (!firstKnown /*&& !secondKnown*/) { + int variantLen = Integer.compare(first.getPossibleTypes().length, second.getPossibleTypes().length); + return variantLen > 0 ? WIDER : NARROW; + } + boolean firstPrimitive = first.isPrimitive(); + boolean secondPrimitive = second.isPrimitive(); + + boolean firstObj = first.isObject(); + boolean secondObj = second.isObject(); + if (firstObj && secondObj) { + return compareObjects(first, second); + } else { + // primitive types conflicts with objects + if (firstObj && secondPrimitive) { + return CONFLICT; + } + if (firstPrimitive && secondObj) { + return CONFLICT; + } + } + if (firstPrimitive && secondPrimitive) { + int comparePrimitives = first.getPrimitiveType().compareTo(second.getPrimitiveType()); + return comparePrimitives > 0 ? WIDER : NARROW; + } + + LOG.warn("Type compare function not complete, can't compare {} and {}", first, second); + return TypeCompareEnum.CONFLICT; + } + + private TypeCompareEnum compareArrayWithOtherType(ArgType array, ArgType other) { + if (!other.isTypeKnown()) { + if (other.contains(PrimitiveType.ARRAY)) { + return NARROW; + } + return CONFLICT; + } + if (other.isObject()) { + if (other.equals(ArgType.OBJECT)) { + return NARROW; + } + return CONFLICT; + } + if (other.isPrimitive()) { + return CONFLICT; + } + throw new JadxRuntimeException("Unprocessed type: " + other + " in array compare"); + } + + private TypeCompareEnum compareWithUnknown(ArgType known, ArgType unknown) { + if (unknown == ArgType.UNKNOWN) { + return NARROW; + } + if (unknown == ArgType.UNKNOWN_OBJECT && (known.isObject() || known.isArray())) { + return NARROW; + } + if (known.equals(ArgType.OBJECT) && unknown.isArray()) { + return WIDER; + } + PrimitiveType knownPrimitive; + if (known.isPrimitive()) { + knownPrimitive = known.getPrimitiveType(); + } else if (known.isArray()) { + knownPrimitive = PrimitiveType.ARRAY; + } else { + knownPrimitive = PrimitiveType.OBJECT; + } + PrimitiveType[] possibleTypes = unknown.getPossibleTypes(); + for (PrimitiveType possibleType : possibleTypes) { + if (possibleType == knownPrimitive) { + return NARROW; + } + } + return CONFLICT; + } + + private TypeCompareEnum compareObjects(ArgType first, ArgType second) { + boolean objectsEquals = first.getObject().equals(second.getObject()); + boolean firstGenericType = first.isGenericType(); + boolean secondGenericType = second.isGenericType(); + if (firstGenericType && secondGenericType && !objectsEquals) { + return CONFLICT; + } + if (firstGenericType || secondGenericType) { + if (firstGenericType) { + return compareGenericTypeWithObject(first, second); + } else { + return compareGenericTypeWithObject(second, first).invert(); + } + } + boolean firstGeneric = first.isGeneric(); + boolean secondGeneric = second.isGeneric(); + if (firstGeneric != secondGeneric && objectsEquals) { + // don't check generics for now + return firstGeneric ? NARROW_BY_GENERIC : WIDER_BY_GENERIC; + } + boolean firstIsObjCls = first.equals(ArgType.OBJECT); + if (firstIsObjCls || second.equals(ArgType.OBJECT)) { + return firstIsObjCls ? WIDER : NARROW; + } + if (ArgType.isInstanceOf(root, first, second)) { + return NARROW; + } + if (ArgType.isInstanceOf(root, second, first)) { + return WIDER; + } + if (!ArgType.isClsKnown(root, first) || !ArgType.isClsKnown(root, second)) { + return UNKNOWN; + } + return TypeCompareEnum.CONFLICT; + } + + private TypeCompareEnum compareGenericTypeWithObject(ArgType genericType, ArgType objType) { + List extendTypes = genericType.getExtendTypes(); + if (extendTypes == null || extendTypes.isEmpty()) { + if (objType.equals(ArgType.OBJECT)) { + return NARROW_BY_GENERIC; + } + } else { + if (extendTypes.contains(objType) || objType.equals(ArgType.OBJECT)) { + return NARROW_BY_GENERIC; + } + for (ArgType extendType : extendTypes) { + if (!ArgType.isInstanceOf(root, extendType, objType)) { + return CONFLICT; + } + } + return NARROW_BY_GENERIC; + } + // TODO: fill extendTypes +// return CONFLICT; + return NARROW_BY_GENERIC; + } + + public ArgTypeComparator getComparator() { + return comparator; + } + + private final class ArgTypeComparator implements Comparator { + @Override + public int compare(ArgType a, ArgType b) { + TypeCompareEnum result = compareTypes(a, b); + switch (result) { + case CONFLICT: + return -2; + + case WIDER: + case WIDER_BY_GENERIC: + return -1; + + case NARROW: + case NARROW_BY_GENERIC: + return 1; + + case EQUAL: + default: + return 0; + } + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java new file mode 100644 index 000000000..0bb40333f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeCompareEnum.java @@ -0,0 +1,41 @@ +package jadx.core.dex.visitors.typeinference; + +public enum TypeCompareEnum { + EQUAL, + NARROW, + NARROW_BY_GENERIC, // same basic type with generic + WIDER, + WIDER_BY_GENERIC, // same basic type without generic + CONFLICT, + UNKNOWN; + + public TypeCompareEnum invert() { + switch (this) { + case NARROW: + return WIDER; + + case NARROW_BY_GENERIC: + return WIDER_BY_GENERIC; + + case WIDER: + return NARROW; + + case WIDER_BY_GENERIC: + return NARROW_BY_GENERIC; + + case CONFLICT: + case EQUAL: + case UNKNOWN: + default: + return this; + } + } + + public boolean isWider() { + return this == WIDER || this == WIDER_BY_GENERIC; + } + + public boolean isNarrow() { + return this == NARROW || this == NARROW_BY_GENERIC; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInference.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInference.java deleted file mode 100644 index e8eb941c6..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInference.java +++ /dev/null @@ -1,130 +0,0 @@ -package jadx.core.dex.visitors.typeinference; - -import java.util.List; - -import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.PhiListAttr; -import jadx.core.dex.instructions.PhiInsn; -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.InsnArg; -import jadx.core.dex.instructions.args.RegisterArg; -import jadx.core.dex.instructions.args.SSAVar; -import jadx.core.dex.instructions.args.VarName; -import jadx.core.dex.nodes.BlockNode; -import jadx.core.dex.nodes.DexNode; -import jadx.core.dex.nodes.MethodNode; -import jadx.core.dex.visitors.AbstractVisitor; -import jadx.core.utils.exceptions.JadxException; - -public class TypeInference extends AbstractVisitor { - - @Override - public void visit(MethodNode mth) throws JadxException { - if (mth.isNoCode()) { - return; - } - fixPhiVarNames(mth); - - DexNode dex = mth.dex(); - for (SSAVar var : mth.getSVars()) { - // inference variable type - ArgType type = processType(dex, var); - if (type == null) { - type = ArgType.UNKNOWN; - } - var.setType(type); - - // search variable name - String name = processVarName(var); - var.setName(name); - } - - // fix type for vars used only in Phi nodes - for (SSAVar sVar : mth.getSVars()) { - PhiInsn phi = sVar.getUsedInPhi(); - if (phi != null) { - processPhiNode(phi); - } - } - } - - private static ArgType processType(DexNode dex, SSAVar var) { - RegisterArg assign = var.getAssign(); - List useList = var.getUseList(); - if (useList.isEmpty() || var.isTypeImmutable()) { - return assign.getType(); - } - ArgType type = assign.getType(); - for (RegisterArg arg : useList) { - ArgType useType = arg.getType(); - if (!type.isTypeKnown() || !useType.isTypeKnown()) { - ArgType newType = ArgType.merge(dex, type, useType); - if (newType != null) { - type = newType; - } - } - } - return type; - } - - private static void processPhiNode(PhiInsn phi) { - ArgType type = phi.getResult().getType(); - if (!type.isTypeKnown()) { - for (InsnArg arg : phi.getArguments()) { - if (arg.getType().isTypeKnown()) { - type = arg.getType(); - break; - } - } - } - phi.getResult().setType(type); - for (int i = 0; i < phi.getArgsCount(); i++) { - RegisterArg arg = phi.getArg(i); - arg.setType(type); - SSAVar sVar = arg.getSVar(); - if (sVar != null) { - sVar.setName(phi.getResult().getName()); - } - } - } - - private static void fixPhiVarNames(MethodNode mth) { - for (BlockNode block : mth.getBasicBlocks()) { - PhiListAttr phiList = block.get(AType.PHI_LIST); - if (phiList == null) { - continue; - } - for (PhiInsn phiInsn : phiList.getList()) { - RegisterArg resArg = phiInsn.getResult(); - int argsCount = phiInsn.getArgsCount(); - for (int i = 0; i < argsCount; i++) { - RegisterArg arg = phiInsn.getArg(i); - arg.mergeName(resArg); - } - VarName varName = resArg.getSVar().getVarName(); - if (varName == null) { - varName = new VarName(); - resArg.getSVar().setVarName(varName); - } - for (int i = 0; i < argsCount; i++) { - RegisterArg arg = phiInsn.getArg(i); - arg.getSVar().setVarName(varName); - } - } - } - } - - private static String processVarName(SSAVar var) { - String name = var.getAssign().getName(); - if (name != null) { - return name; - } - for (RegisterArg arg : var.getUseList()) { - String vName = arg.getName(); - if (vName != null) { - return vName; - } - } - return null; - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java new file mode 100644 index 000000000..925ab616b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInferenceVisitor.java @@ -0,0 +1,381 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.clsp.ClspGraph; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.instructions.IndexInsnNode; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.PhiInsn; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.LiteralArg; +import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.trycatch.ExcHandlerAttr; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.ConstInlineVisitor; +import jadx.core.dex.visitors.InitCodeVariables; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.blocksmaker.BlockSplitter; +import jadx.core.dex.visitors.ssa.SSATransform; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.Utils; + +@JadxVisitor( + name = "Type Inference", + desc = "Calculate best types for SSA variables", + runAfter = { + SSATransform.class, + ConstInlineVisitor.class + } +) +public final class TypeInferenceVisitor extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(TypeInferenceVisitor.class); + + private TypeUpdate typeUpdate; + + @Override + public void init(RootNode root) { + typeUpdate = root.getTypeUpdate(); + } + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode()) { + return; + } + // collect initial type bounds from assign and usages + mth.getSVars().forEach(this::attachBounds); + mth.getSVars().forEach(this::mergePhiBounds); + + // start initial type propagation + mth.getSVars().forEach(this::setImmutableType); + mth.getSVars().forEach(this::setBestType); + + // try other types if type is still unknown + boolean resolved = true; + for (SSAVar var : mth.getSVars()) { + ArgType type = var.getTypeInfo().getType(); + if (!type.isTypeKnown() + && !var.getAssign().isTypeImmutable() + && !tryDeduceType(mth, var, type)) { + resolved = false; + } + } + if (!resolved) { + for (SSAVar var : new ArrayList<>(mth.getSVars())) { + tryInsertAdditionalInsn(mth, var); + } + runMultiVariableSearch(mth); + } + } + + private void runMultiVariableSearch(MethodNode mth) { + long startTime = System.currentTimeMillis(); + TypeSearch typeSearch = new TypeSearch(mth); + boolean success; + try { + success = typeSearch.run(); + } catch (Exception e) { + success = false; + mth.addWarn("Multi-variable type inference failed. Error: " + Utils.getStackTrace(e)); + } + long time = System.currentTimeMillis() - startTime; + mth.addComment("JADX DEBUG: Multi-variable type inference result: " + (success ? "success" : "failure") + + ", time: " + time + " ms"); + } + + private boolean setImmutableType(SSAVar ssaVar) { + try { + ArgType codeVarType = ssaVar.getCodeVar().getType(); + if (codeVarType != null) { + return applyImmutableType(ssaVar, codeVarType); + } + RegisterArg assignArg = ssaVar.getAssign(); + if (assignArg.isTypeImmutable()) { + return applyImmutableType(ssaVar, assignArg.getInitType()); + } + if (ssaVar.contains(AFlag.IMMUTABLE_TYPE)) { + for (RegisterArg arg : ssaVar.getUseList()) { + if (arg.isTypeImmutable()) { + return applyImmutableType(ssaVar, arg.getInitType()); + } + } + } + return false; + } catch (Exception e) { + LOG.error("Failed to set immutable type for var: {}", ssaVar, e); + return false; + } + } + + private boolean setBestType(SSAVar ssaVar) { + try { + return calculateFromBounds(ssaVar); + } catch (Exception e) { + LOG.error("Failed to calculate best type for var: {}", ssaVar, e); + return false; + } + } + + private boolean applyImmutableType(SSAVar ssaVar, ArgType initType) { + TypeUpdateResult result = typeUpdate.apply(ssaVar, initType); + if (result == TypeUpdateResult.REJECT) { + if (Consts.DEBUG) { + LOG.warn("Initial immutable type set rejected: {} -> {}", ssaVar, initType); + } + return false; + } + return result == TypeUpdateResult.CHANGED; + } + + private boolean calculateFromBounds(SSAVar ssaVar) { + TypeInfo typeInfo = ssaVar.getTypeInfo(); + Set bounds = typeInfo.getBounds(); + Optional bestTypeOpt = selectBestTypeFromBounds(bounds); + if (!bestTypeOpt.isPresent()) { + if (Consts.DEBUG) { + LOG.warn("Failed to select best type from bounds, count={} : ", bounds.size()); + for (ITypeBound bound : bounds) { + LOG.warn(" {}", bound); + } + } + return false; + } + ArgType candidateType = bestTypeOpt.get(); + TypeUpdateResult result = typeUpdate.apply(ssaVar, candidateType); + if (result == TypeUpdateResult.REJECT) { + if (Consts.DEBUG) { + if (ssaVar.getTypeInfo().getType().equals(candidateType)) { + LOG.info("Same type rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); + } else if (candidateType.isTypeKnown()) { + LOG.debug("Type set rejected: {} -> {}, bounds: {}", ssaVar, candidateType, bounds); + } + } + return false; + } + return result == TypeUpdateResult.CHANGED; + } + + private Optional selectBestTypeFromBounds(Set bounds) { + return bounds.stream() + .map(ITypeBound::getType) + .filter(Objects::nonNull) + .max(typeUpdate.getArgTypeComparator()); + } + + private void attachBounds(SSAVar var) { + TypeInfo typeInfo = var.getTypeInfo(); + typeInfo.getBounds().clear(); + RegisterArg assign = var.getAssign(); + addAssignBound(typeInfo, assign); + + for (RegisterArg regArg : var.getUseList()) { + addBound(typeInfo, makeUseBound(regArg)); + } + } + + private void mergePhiBounds(SSAVar ssaVar) { + PhiInsn usedInPhi = ssaVar.getUsedInPhi(); + if (usedInPhi != null) { + Set bounds = ssaVar.getTypeInfo().getBounds(); + bounds.addAll(usedInPhi.getResult().getSVar().getTypeInfo().getBounds()); + for (InsnArg arg : usedInPhi.getArguments()) { + bounds.addAll(((RegisterArg) arg).getSVar().getTypeInfo().getBounds()); + } + } + } + + private void addBound(TypeInfo typeInfo, ITypeBound bound) { + if (bound != null && bound.getType() != ArgType.UNKNOWN) { + typeInfo.getBounds().add(bound); + } + } + + private void addAssignBound(TypeInfo typeInfo, RegisterArg assign) { + InsnNode insn = assign.getParentInsn(); + if (insn == null || assign.isTypeImmutable()) { + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, assign.getInitType())); + return; + } + switch (insn.getType()) { + case NEW_INSTANCE: + ArgType clsType = (ArgType) ((IndexInsnNode) insn).getIndex(); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, clsType)); + break; + + case CONST: + LiteralArg constLit = (LiteralArg) insn.getArg(0); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, constLit.getType())); + break; + + case MOVE_EXCEPTION: + ExcHandlerAttr excHandlerAttr = insn.get(AType.EXC_HANDLER); + if (excHandlerAttr != null) { + for (ClassInfo catchType : excHandlerAttr.getHandler().getCatchTypes()) { + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, catchType.getType())); + } + } else { + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, insn.getResult().getInitType())); + } + break; + + default: + ArgType type = insn.getResult().getInitType(); + addBound(typeInfo, new TypeBoundConst(BoundEnum.ASSIGN, type)); + break; + } + } + + @Nullable + private ITypeBound makeUseBound(RegisterArg regArg) { + InsnNode insn = regArg.getParentInsn(); + if (insn == null) { + return null; + } + return new TypeBoundConst(BoundEnum.USE, regArg.getInitType()); + } + + private boolean tryPossibleTypes(SSAVar var, ArgType type) { + List types = makePossibleTypesList(type); + for (ArgType candidateType : types) { + TypeUpdateResult result = typeUpdate.apply(var, candidateType); + if (result == TypeUpdateResult.CHANGED) { + return true; + } + } + return false; + } + + private List makePossibleTypesList(ArgType type) { + List list = new ArrayList<>(); + if (type.isArray()) { + for (ArgType arrElemType : makePossibleTypesList(type.getArrayElement())) { + list.add(ArgType.array(arrElemType)); + } + } + for (PrimitiveType possibleType : type.getPossibleTypes()) { + if (possibleType == PrimitiveType.VOID) { + continue; + } + list.add(ArgType.convertFromPrimitiveType(possibleType)); + } + return list; + } + + private boolean tryDeduceType(MethodNode mth, SSAVar var, @Nullable ArgType type) { + // try best type from bounds again + if (setBestType(var)) { + return true; + } + // try all possible types (useful for primitives) + if (type != null && tryPossibleTypes(var, type)) { + return true; + } + // for objects try super types + if (tryWiderObjects(mth, var)) { + return true; + } + return false; + } + + /** + * Add MOVE instruction before PHI in bound blocks to make 'soft' type link. + * This allows to use different types in blocks merged by PHI. + */ + private boolean tryInsertAdditionalInsn(MethodNode mth, SSAVar var) { + if (var.getTypeInfo().getType().isTypeKnown()) { + return false; + } + PhiInsn phiInsn = var.getUsedInPhi(); + if (phiInsn == null) { + return false; + } + if (var.getUseCount() == 1) { + InsnNode assignInsn = var.getAssign().getAssignInsn(); + if (assignInsn != null && assignInsn.getType() == InsnType.MOVE) { + return false; + } + } + for (Map.Entry entry : phiInsn.getBlockBinds().entrySet()) { + RegisterArg reg = entry.getKey(); + if (reg.getSVar() == var) { + BlockNode blockNode = entry.getValue(); + InsnNode lastInsn = BlockUtils.getLastInsn(blockNode); + if (lastInsn != null && BlockSplitter.SEPARATE_INSNS.contains(lastInsn.getType())) { + if (Consts.DEBUG) { + LOG.warn("Can't insert move for PHI in block with separate insn: {}", lastInsn); + } + return false; + } + + int regNum = reg.getRegNum(); + RegisterArg resultArg = reg.duplicate(regNum, null); + SSAVar newSsaVar = mth.makeNewSVar(regNum, resultArg); + RegisterArg arg = reg.duplicate(regNum, var); + + InsnNode moveInsn = new InsnNode(InsnType.MOVE, 1); + moveInsn.setResult(resultArg); + moveInsn.addArg(arg); + moveInsn.add(AFlag.SYNTHETIC); + blockNode.getInstructions().add(moveInsn); + + phiInsn.replaceArg(reg, reg.duplicate(regNum, newSsaVar)); + + attachBounds(var); + for (InsnArg phiArg : phiInsn.getArguments()) { + attachBounds(((RegisterArg) phiArg).getSVar()); + } + for (InsnArg phiArg : phiInsn.getArguments()) { + mergePhiBounds(((RegisterArg) phiArg).getSVar()); + } + InitCodeVariables.initCodeVar(newSsaVar); + return true; + } + } + return false; + } + + private boolean tryWiderObjects(MethodNode mth, SSAVar var) { + Set objTypes = new LinkedHashSet<>(); + for (ITypeBound bound : var.getTypeInfo().getBounds()) { + ArgType boundType = bound.getType(); + if (boundType.isTypeKnown() && boundType.isObject()) { + objTypes.add(boundType); + } + } + if (objTypes.isEmpty()) { + return false; + } + ClspGraph clsp = mth.root().getClsp(); + for (ArgType objType : objTypes) { + for (String ancestor : clsp.getAncestors(objType.getObject())) { + ArgType ancestorType = ArgType.object(ancestor); + TypeUpdateResult result = typeUpdate.applyWithWiderAllow(var, ancestorType); + if (result == TypeUpdateResult.CHANGED) { + return true; + } + } + } + return false; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java new file mode 100644 index 000000000..ec108c91a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeInfo.java @@ -0,0 +1,35 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.instructions.args.ArgType; + +public class TypeInfo { + private ArgType type = ArgType.UNKNOWN; + + private final Set bounds = new LinkedHashSet<>(); + + @NotNull + public ArgType getType() { + return type; + } + + public void setType(ArgType type) { + this.type = type; + } + + public Set getBounds() { + return bounds; + } + + @Override + public String toString() { + return "TypeInfo{" + + "type=" + type + + ", bounds=" + bounds + + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java new file mode 100644 index 000000000..88a70f24d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearch.java @@ -0,0 +1,370 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * Slow and memory consuming multi-variable type search algorithm. + * Used only if fast type propagation is failed for some variables. + *

+ * Stages description: + * - find all possible candidate types within bounds + * - build dynamic constraint list for every variable + * - run search by checking all candidates + */ +public class TypeSearch { + private static final Logger LOG = LoggerFactory.getLogger(TypeSearch.class); + + private static final int CANDIDATES_COUNT_LIMIT = 10; + private static final int SEARCH_ITERATION_LIMIT = 1_000_000; + + private final MethodNode mth; + private final TypeSearchState state; + private final TypeCompare typeCompare; + private final TypeUpdate typeUpdate; + + public TypeSearch(MethodNode mth) { + this.mth = mth; + this.state = new TypeSearchState(mth); + this.typeUpdate = mth.root().getTypeUpdate(); + this.typeCompare = typeUpdate.getComparator(); + } + + public boolean run() { + mth.getSVars().forEach(this::fillTypeCandidates); + mth.getSVars().forEach(this::collectConstraints); + + // quick search for variables without dependencies + state.getUnresolvedVars().forEach(this::resolveIndependentVariables); + + boolean searchSuccess; + List vars = state.getUnresolvedVars(); + if (vars.isEmpty()) { + searchSuccess = true; + } else { + search(vars); + searchSuccess = fullCheck(vars); + if (Consts.DEBUG && !searchSuccess) { + LOG.warn("Multi-variable search failed in {}", mth); + } + } + + boolean applySuccess = applyResolvedVars(); + return searchSuccess && applySuccess; + } + + private boolean applyResolvedVars() { + List resolvedVars = state.getResolvedVars(); + for (TypeSearchVarInfo var : resolvedVars) { + SSAVar ssaVar = var.getVar(); + ArgType resolvedType = var.getCurrentType(); + ssaVar.getAssign().setType(resolvedType); + for (RegisterArg arg : ssaVar.getUseList()) { + arg.setType(resolvedType); + } + } + boolean applySuccess = true; + for (TypeSearchVarInfo var : resolvedVars) { + TypeUpdateResult res = typeUpdate.applyWithWiderAllow(var.getVar(), var.getCurrentType()); + if (res == TypeUpdateResult.REJECT) { + applySuccess = false; + } + } + return applySuccess; + } + + private boolean search(List vars) { + int len = vars.size(); + if (Consts.DEBUG) { + LOG.debug("Run search for {} vars: ", len); + StringBuilder sb = new StringBuilder(); + long count = 1; + for (TypeSearchVarInfo var : vars) { + LOG.debug(" {}", var); + int size = var.getCandidateTypes().size(); + sb.append(" * ").append(size); + count *= size; + } + sb.append(" = ").append(count); + LOG.debug("--- count = {}, {}", count, sb); + } + + // prepare vars + for (TypeSearchVarInfo var : vars) { + var.reset(); + } + // check all types combinations + int n = 0; + int i = 0; + while (!fullCheck(vars)) { + TypeSearchVarInfo first = vars.get(i); + if (first.nextType()) { + int k = i + 1; + if (k >= len) { + return false; + } + TypeSearchVarInfo next = vars.get(k); + while (true) { + if (next.nextType()) { + k++; + if (k >= len) { + return false; + } + next = vars.get(k); + } else { + break; + } + } + } + n++; + if (n > SEARCH_ITERATION_LIMIT) { + return false; + } + } + // mark all vars as resolved + for (TypeSearchVarInfo var : vars) { + var.setTypeResolved(true); + } + return true; + } + + private boolean resolveIndependentVariables(TypeSearchVarInfo varInfo) { + boolean allRelatedVarsResolved = varInfo.getConstraints().stream() + .flatMap(c -> c.getRelatedVars().stream()) + .allMatch(v -> state.getVarInfo(v).isTypeResolved()); + if (!allRelatedVarsResolved) { + return false; + } + // variable is independent, run single search + varInfo.reset(); + do { + if (singleCheck(varInfo)) { + varInfo.setTypeResolved(true); + return true; + } + } while (!varInfo.nextType()); + + return false; + } + + private boolean fullCheck(List vars) { + for (TypeSearchVarInfo var : vars) { + if (!singleCheck(var)) { + return false; + } + } + return true; + } + + private boolean singleCheck(TypeSearchVarInfo var) { + if (var.isTypeResolved()) { + return true; + } + for (ITypeConstraint constraint : var.getConstraints()) { + if (!constraint.check(state)) { + return false; + } + } + return true; + } + + private void fillTypeCandidates(SSAVar ssaVar) { + TypeSearchVarInfo varInfo = state.getVarInfo(ssaVar); + ArgType currentType = ssaVar.getTypeInfo().getType(); + if (currentType.isTypeKnown()) { + varInfo.setTypeResolved(true); + varInfo.setCurrentType(currentType); + varInfo.setCandidateTypes(Collections.emptyList()); + return; + } + if (ssaVar.getAssign().isTypeImmutable()) { + ArgType initType = ssaVar.getAssign().getInitType(); + varInfo.setTypeResolved(true); + varInfo.setCurrentType(initType); + varInfo.setCandidateTypes(Collections.emptyList()); + return; + } + + Set assigns = new LinkedHashSet<>(); + Set uses = new LinkedHashSet<>(); + Set bounds = ssaVar.getTypeInfo().getBounds(); + for (ITypeBound bound : bounds) { + if (bound.getBound() == BoundEnum.ASSIGN) { + assigns.add(bound.getType()); + } else { + uses.add(bound.getType()); + } + } + + Set candidateTypes = new LinkedHashSet<>(); + addCandidateTypes(bounds, candidateTypes, assigns); + addCandidateTypes(bounds, candidateTypes, uses); + + for (ArgType assignType : assigns) { + addCandidateTypes(bounds, candidateTypes, getWiderTypes(assignType)); + } + for (ArgType useType : uses) { + addCandidateTypes(bounds, candidateTypes, getNarrowTypes(useType)); + } + + int size = candidateTypes.size(); + if (size == 0) { + throw new JadxRuntimeException("No candidate types for var: " + ssaVar.getDetailedVarInfo(mth) + + "\n assigns: " + assigns + "\n uses: " + uses); + } + if (size == 1) { + varInfo.setTypeResolved(true); + varInfo.setCurrentType(candidateTypes.iterator().next()); + varInfo.setCandidateTypes(Collections.emptyList()); + } else { + varInfo.setTypeResolved(false); + varInfo.setCurrentType(ArgType.UNKNOWN); + ArrayList types = new ArrayList<>(candidateTypes); + types.sort(typeCompare.getComparator()); + varInfo.setCandidateTypes(Collections.unmodifiableList(types)); + } + } + + private void addCandidateTypes(Set bounds, Set collectedTypes, Collection candidateTypes) { + for (ArgType candidateType : candidateTypes) { + if (candidateType.isTypeKnown() && typeUpdate.inBounds(bounds, candidateType)) { + collectedTypes.add(candidateType); + if (collectedTypes.size() > CANDIDATES_COUNT_LIMIT) { + return; + } + } + } + } + + private List getWiderTypes(ArgType type) { + if (type.isTypeKnown()) { + if (type.isObject()) { + Set ancestors = mth.root().getClsp().getAncestors(type.getObject()); + return ancestors.stream().map(ArgType::object).collect(Collectors.toList()); + } + } else { + return expandUnknownType(type); + } + return Collections.emptyList(); + } + + private List getNarrowTypes(ArgType type) { + if (type.isTypeKnown()) { + if (type.isObject()) { + if (type.equals(ArgType.OBJECT)) { + // a lot of objects to return + return Collections.singletonList(ArgType.OBJECT); + } + List impList = mth.root().getClsp().getImplementations(type.getObject()); + return impList.stream().map(ArgType::object).collect(Collectors.toList()); + } + } else { + return expandUnknownType(type); + } + return Collections.emptyList(); + } + + private List expandUnknownType(ArgType type) { + List list = new ArrayList<>(); + for (PrimitiveType possibleType : type.getPossibleTypes()) { + list.add(ArgType.convertFromPrimitiveType(possibleType)); + } + return list; + } + + private void collectConstraints(SSAVar var) { + TypeSearchVarInfo varInfo = state.getVarInfo(var); + if (varInfo.isTypeResolved()) { + varInfo.setConstraints(Collections.emptyList()); + return; + } + varInfo.setConstraints(new ArrayList<>()); + addConstraint(varInfo, makeConstraint(var.getAssign())); + for (RegisterArg regArg : var.getUseList()) { + addConstraint(varInfo, makeConstraint(regArg)); + } + } + + public static ArgType getArgType(TypeSearchState state, InsnArg arg) { + if (arg.isRegister()) { + RegisterArg reg = (RegisterArg) arg; + return state.getVarInfo(reg.getSVar()).getCurrentType(); + } + return arg.getType(); + } + + private void addConstraint(TypeSearchVarInfo varInfo, ITypeConstraint constraint) { + if (constraint != null) { + varInfo.getConstraints().add(constraint); + } + } + + @Nullable + private ITypeConstraint makeConstraint(RegisterArg arg) { + InsnNode insn = arg.getParentInsn(); + if (insn == null || arg.isTypeImmutable()) { + return null; + } + switch (insn.getType()) { + case MOVE: + return makeMoveConstraint(insn, arg); + + case PHI: + return makePhiConstraint(insn, arg); + + default: + return null; + } + } + + @Nullable + private ITypeConstraint makeMoveConstraint(InsnNode insn, RegisterArg arg) { + if (!insn.getArg(0).isRegister()) { + return null; + } + return new AbstractTypeConstraint(insn, arg) { + @Override + public boolean check(TypeSearchState state) { + ArgType resType = getArgType(state, insn.getResult()); + ArgType argType = getArgType(state, insn.getArg(0)); + TypeCompareEnum res = typeCompare.compareTypes(resType, argType); + return res == TypeCompareEnum.EQUAL || res.isWider(); + } + }; + } + + private ITypeConstraint makePhiConstraint(InsnNode insn, RegisterArg arg) { + return new AbstractTypeConstraint(insn, arg) { + @Override + public boolean check(TypeSearchState state) { + ArgType resType = getArgType(state, insn.getResult()); + for (InsnArg insnArg : insn.getArguments()) { + ArgType argType = getArgType(state, insnArg); + if (!argType.equals(resType)) { + return false; + } + } + return true; + } + }; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java new file mode 100644 index 000000000..b37a73f84 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchState.java @@ -0,0 +1,51 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class TypeSearchState { + + private Map varInfoMap; + + public TypeSearchState(MethodNode mth) { + List vars = mth.getSVars(); + this.varInfoMap = new LinkedHashMap<>(vars.size()); + for (SSAVar var : vars) { + varInfoMap.put(var, new TypeSearchVarInfo(var)); + } + } + + @NotNull + public TypeSearchVarInfo getVarInfo(SSAVar var) { + TypeSearchVarInfo varInfo = this.varInfoMap.get(var); + if (varInfo == null) { + throw new JadxRuntimeException("TypeSearchVarInfo not found in map for var: " + var); + } + return varInfo; + } + + public List getAllVars() { + return new ArrayList<>(varInfoMap.values()); + } + + public List getUnresolvedVars() { + return varInfoMap.values().stream() + .filter(varInfo -> !varInfo.isTypeResolved()) + .collect(Collectors.toList()); + } + + public List getResolvedVars() { + return varInfoMap.values().stream() + .filter(TypeSearchVarInfo::isTypeResolved) + .collect(Collectors.toList()); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java new file mode 100644 index 000000000..3a0cc2407 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeSearchVarInfo.java @@ -0,0 +1,94 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.List; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.SSAVar; + +public class TypeSearchVarInfo { + private final SSAVar var; + private boolean typeResolved; + private ArgType currentType; + private List candidateTypes; + private int currentIndex = -1; + private List constraints; + + public TypeSearchVarInfo(SSAVar var) { + this.var = var; + } + + public void reset() { + if (typeResolved) { + return; + } + currentIndex = 0; + currentType = candidateTypes.get(0); + } + + /** + * Switch {@code currentType} to next candidate + * + * @return true - if this is the first candidate + */ + public boolean nextType() { + if (typeResolved) { + return false; + } + int len = candidateTypes.size(); + currentIndex = (currentIndex + 1) % len; + currentType = candidateTypes.get(currentIndex); + return currentIndex == 0; + } + + public SSAVar getVar() { + return var; + } + + public boolean isTypeResolved() { + return typeResolved; + } + + public void setTypeResolved(boolean typeResolved) { + this.typeResolved = typeResolved; + } + + public ArgType getCurrentType() { + return currentType; + } + + public void setCurrentType(ArgType currentType) { + this.currentType = currentType; + } + + public List getCandidateTypes() { + return candidateTypes; + } + + public void setCandidateTypes(List candidateTypes) { + this.candidateTypes = candidateTypes; + } + + public List getConstraints() { + return constraints; + } + + public void setConstraints(List constraints) { + this.constraints = constraints; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("TypeSearchVarInfo{"); + sb.append(var.toShortString()); + if (typeResolved) { + sb.append(", resolved type: ").append(currentType); + } else { + sb.append(", currentType=").append(currentType); + sb.append(", candidateTypes=").append(candidateTypes); + sb.append(", constraints=").append(constraints); + } + sb.append('}'); + return sb.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java new file mode 100644 index 000000000..f9738b40f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdate.java @@ -0,0 +1,425 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.Consts; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.PrimitiveType; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.instructions.args.SSAVar; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.exceptions.JadxOverflowException; +import jadx.core.utils.exceptions.JadxRuntimeException; + +import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.CHANGED; +import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.REJECT; +import static jadx.core.dex.visitors.typeinference.TypeUpdateResult.SAME; + +public final class TypeUpdate { + private static final Logger LOG = LoggerFactory.getLogger(TypeUpdate.class); + + private final Map listenerRegistry; + private final TypeCompare comparator; + + private ThreadLocal allowWider = new ThreadLocal<>(); + + public TypeUpdate(RootNode root) { + this.listenerRegistry = initListenerRegistry(); + this.comparator = new TypeCompare(root); + } + + /** + * Perform recursive type checking and type propagation for all related variables + */ + public TypeUpdateResult apply(SSAVar ssaVar, ArgType candidateType) { + if (candidateType == null) { + return REJECT; + } + if (!candidateType.isTypeKnown()/* && ssaVar.getTypeInfo().getType().isTypeKnown()*/) { + return REJECT; + } + + TypeUpdateInfo updateInfo = new TypeUpdateInfo(); + TypeUpdateResult result = updateTypeChecked(updateInfo, ssaVar.getAssign(), candidateType); + if (result == REJECT) { + return result; + } + List updates = updateInfo.getUpdates(); + if (updates.isEmpty()) { + return SAME; + } + if (Consts.DEBUG && LOG.isDebugEnabled()) { + LOG.debug("Applying types, init for {} -> {}", ssaVar, candidateType); + updates.forEach(updateEntry -> LOG.debug(" {} -> {}", updateEntry.getType(), updateEntry.getArg())); + } + updates.forEach(TypeUpdateEntry::apply); + return CHANGED; + } + + /** + * Allow wider types for apply from debug info and some special cases + */ + public TypeUpdateResult applyWithWiderAllow(SSAVar ssaVar, ArgType candidateType) { + try { + allowWider.set(true); + return apply(ssaVar, candidateType); + } finally { + allowWider.set(false); + } + } + + private TypeUpdateResult updateTypeChecked(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { + if (candidateType == null) { + throw new JadxRuntimeException("Null type update for arg: " + arg); + } + ArgType currentType = arg.getType(); + if (Objects.equals(currentType, candidateType)) { + return SAME; + } + TypeCompareEnum compareResult = comparator.compareTypes(candidateType, currentType); + if (arg.isTypeImmutable() && currentType != ArgType.UNKNOWN) { + // don't changed type + if (compareResult == TypeCompareEnum.CONFLICT) { + if (Consts.DEBUG) { + LOG.debug("Type rejected for {} due to conflict: candidate={}, current={}", arg, candidateType, currentType); + } + return REJECT; + } + return SAME; + } + if (compareResult.isWider()) { + if (allowWider.get() != Boolean.TRUE) { + if (Consts.DEBUG) { + LOG.debug("Type rejected for {}: candidate={} is wider than current={}", arg, candidateType, currentType); + } + return REJECT; + } + } + if (arg instanceof RegisterArg) { + RegisterArg reg = (RegisterArg) arg; + return updateTypeForSsaVar(updateInfo, reg.getSVar(), candidateType); + } + return requestUpdate(updateInfo, arg, candidateType); + } + + private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { + TypeInfo typeInfo = ssaVar.getTypeInfo(); + if (!inBounds(typeInfo.getBounds(), candidateType)) { + if (Consts.DEBUG) { + LOG.debug("Reject type '{}' for {} by bounds: {}", candidateType, ssaVar, typeInfo.getBounds()); + } + return REJECT; + } + return requestUpdateForSsaVar(updateInfo, ssaVar, candidateType); + } + + @NotNull + private TypeUpdateResult requestUpdateForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { + boolean allSame = true; + TypeUpdateResult result = requestUpdate(updateInfo, ssaVar.getAssign(), candidateType); + if (result == REJECT) { + return result; + } + List useList = ssaVar.getUseList(); + for (RegisterArg arg : useList) { + TypeUpdateResult useResult = requestUpdate(updateInfo, arg, candidateType); + if (useResult == REJECT) { + return REJECT; + } + if (useResult != SAME) { + allSame = false; + } + } + return allSame ? SAME : CHANGED; + } + + private TypeUpdateResult requestUpdate(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { + if (updateInfo.isProcessed(arg)) { + return CHANGED; + } + updateInfo.requestUpdate(arg, candidateType); + try { + TypeUpdateResult result = runListeners(updateInfo, arg, candidateType); + if (result == REJECT) { + updateInfo.rollbackUpdate(arg); + } + return result; + } catch (StackOverflowError overflow) { + throw new JadxOverflowException("Type update terminated with stack overflow, arg: " + arg); + } + } + + private TypeUpdateResult runListeners(TypeUpdateInfo updateInfo, InsnArg arg, ArgType candidateType) { + InsnNode insn = arg.getParentInsn(); + if (insn == null) { + return SAME; + } + ITypeListener listener = listenerRegistry.get(insn.getType()); + if (listener == null) { + return CHANGED; + } + return listener.update(updateInfo, insn, arg, candidateType); + } + + boolean inBounds(Set bounds, ArgType candidateType) { + for (ITypeBound bound : bounds) { + ArgType boundType = bound.getType(); + if (boundType != null && !checkBound(candidateType, bound, boundType)) { + return false; + } + } + return true; + } + + private boolean inBounds(InsnArg arg, ArgType candidateType) { + if (arg.isRegister()) { + TypeInfo typeInfo = ((RegisterArg) arg).getSVar().getTypeInfo(); + return inBounds(typeInfo.getBounds(), candidateType); + } + return arg.getType().equals(candidateType); + } + + private boolean checkBound(ArgType candidateType, ITypeBound bound, ArgType boundType) { + TypeCompareEnum compareResult = comparator.compareTypes(candidateType, boundType); + switch (compareResult) { + case EQUAL: + return true; + + case WIDER: + return bound.getBound() != BoundEnum.USE; + + case NARROW: + if (bound.getBound() == BoundEnum.ASSIGN) { + return !boundType.isTypeKnown() && checkAssignForUnknown(boundType, candidateType); + } + return true; + + case WIDER_BY_GENERIC: + case NARROW_BY_GENERIC: + // allow replace object to same object with known generic type + // due to incomplete information about external methods and fields + return true; + + case CONFLICT: + return false; + + case UNKNOWN: + LOG.warn("Can't compare types, unknown hierarchy: {} and {}", candidateType, boundType); + return true; + + default: + throw new JadxRuntimeException("Not processed type compare enum: " + compareResult); + } + } + + private boolean checkAssignForUnknown(ArgType boundType, ArgType candidateType) { + if (boundType == ArgType.UNKNOWN) { + return true; + } + boolean candidateArray = candidateType.isArray(); + if (boundType.isArray() && candidateArray) { + return checkAssignForUnknown(boundType.getArrayElement(), candidateType.getArrayElement()); + } + if (candidateArray && boundType.contains(PrimitiveType.ARRAY)) { + return true; + } + if (candidateType.isObject() && boundType.contains(PrimitiveType.OBJECT)) { + return true; + } + if (candidateType.isPrimitive() && boundType.contains(candidateType.getPrimitiveType())) { + return true; + } + return false; + } + + private Map initListenerRegistry() { + Map registry = new EnumMap<>(InsnType.class); + registry.put(InsnType.CONST, this::sameFirstArgListener); + registry.put(InsnType.MOVE, this::moveListener); + registry.put(InsnType.PHI, this::allSameListener); + registry.put(InsnType.AGET, this::arrayGetListener); + registry.put(InsnType.APUT, this::arrayPutListener); + registry.put(InsnType.IF, this::ifListener); + registry.put(InsnType.ARITH, this::suggestAllSameListener); + registry.put(InsnType.NEG, this::suggestAllSameListener); + registry.put(InsnType.NOT, this::suggestAllSameListener); + registry.put(InsnType.CHECK_CAST, this::checkCastListener); + return registry; + } + + private TypeUpdateResult sameFirstArgListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + InsnArg changeArg = isAssign(insn, arg) ? insn.getArg(0) : insn.getResult(); + return updateTypeChecked(updateInfo, changeArg, candidateType); + } + + private TypeUpdateResult moveListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + boolean assignChanged = isAssign(insn, arg); + InsnArg changeArg = assignChanged ? insn.getArg(0) : insn.getResult(); + boolean allowReject; + if (changeArg.getType().isTypeKnown()) { + // allow result to be wider + TypeCompareEnum compareTypes = comparator.compareTypes(candidateType, changeArg.getType()); + boolean correctType = assignChanged ? compareTypes.isWider() : compareTypes.isNarrow(); + if (correctType && inBounds(changeArg, candidateType)) { + allowReject = true; + } else { + return REJECT; + } + } else { + allowReject = arg.isThis() || arg.contains(AFlag.IMMUTABLE_TYPE); + } + + TypeUpdateResult result = updateTypeChecked(updateInfo, changeArg, candidateType); + if (result == REJECT && allowReject) { + return CHANGED; + } + return result; + } + + /** + * All args must have same types + */ + private TypeUpdateResult allSameListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + if (!isAssign(insn, arg)) { + return updateTypeChecked(updateInfo, insn.getResult(), candidateType); + } + boolean allSame = true; + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg != arg) { + TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); + if (result == REJECT) { + return result; + } + if (result != SAME) { + allSame = false; + } + } + } + return allSame ? SAME : CHANGED; + } + + /** + * Try to set candidate type to all args, don't fail on reject + */ + private TypeUpdateResult suggestAllSameListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + if (!isAssign(insn, arg)) { + updateTypeChecked(updateInfo, insn.getResult(), candidateType); + } + boolean allSame = true; + for (InsnArg insnArg : insn.getArguments()) { + if (insnArg != arg) { + TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); + if (result == REJECT) { + // ignore + } else if (result != SAME) { + allSame = false; + } + } + } + return allSame ? SAME : CHANGED; + } + + private TypeUpdateResult checkCastListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + if (!isAssign(insn, arg)) { + return SAME; + } + InsnArg insnArg = insn.getArg(0); + TypeUpdateResult result = updateTypeChecked(updateInfo, insnArg, candidateType); + return result == REJECT ? SAME : result; + } + + private TypeUpdateResult arrayGetListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + if (isAssign(insn, arg)) { + return updateTypeChecked(updateInfo, insn.getArg(0), ArgType.array(candidateType)); + } + InsnArg arrArg = insn.getArg(0); + if (arrArg == arg) { + ArgType arrayElement = candidateType.getArrayElement(); + if (arrayElement == null) { + return REJECT; + } + return updateTypeChecked(updateInfo, insn.getResult(), arrayElement); + } + // index argument + return SAME; + } + + private TypeUpdateResult arrayPutListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + InsnArg arrArg = insn.getArg(0); + InsnArg putArg = insn.getArg(2); + if (arrArg == arg) { + ArgType arrayElement = candidateType.getArrayElement(); + if (arrayElement == null) { + return REJECT; + } + TypeUpdateResult result = updateTypeChecked(updateInfo, putArg, arrayElement); + if (result == REJECT) { + ArgType putType = putArg.getType(); + if (putType.isTypeKnown() && putType.isObject()) { + TypeCompareEnum compResult = comparator.compareTypes(arrayElement, putType); + if (compResult == TypeCompareEnum.WIDER || compResult == TypeCompareEnum.WIDER_BY_GENERIC) { + // allow wider result (i.e allow put in Object[] any objects) + return CHANGED; + } + } + } + return result; + } + if (arrArg == putArg) { + return updateTypeChecked(updateInfo, arrArg, ArgType.array(candidateType)); + } + // index + return SAME; + } + + private TypeUpdateResult ifListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { + InsnArg firstArg = insn.getArg(0); + InsnArg secondArg = insn.getArg(1); + InsnArg updateArg = firstArg == arg ? secondArg : firstArg; + TypeUpdateResult result = updateTypeChecked(updateInfo, updateArg, candidateType); + if (result == REJECT) { + // soft checks for objects and array - exact type not compared + ArgType updateArgType = updateArg.getType(); + if (candidateType.isObject() && updateArgType.canBeObject()) { + return SAME; + } + if (candidateType.isArray() && updateArgType.canBeArray()) { + return SAME; + } + if (candidateType.isPrimitive()) { + if (updateArgType.canBePrimitive(candidateType.getPrimitiveType())) { + return SAME; + } + if (updateArgType.isTypeKnown() && candidateType.getRegCount() == updateArgType.getRegCount()) { + return SAME; + } + } + } + return result; + } + + private static boolean isAssign(InsnNode insn, InsnArg arg) { + return insn.getResult() == arg; + } + + public TypeCompare getComparator() { + return comparator; + } + + public Comparator getArgTypeComparator() { + return comparator.getComparator(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java new file mode 100644 index 000000000..0923ea0a2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateEntry.java @@ -0,0 +1,31 @@ +package jadx.core.dex.visitors.typeinference; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; + +public final class TypeUpdateEntry { + private final InsnArg arg; + private final ArgType type; + + public TypeUpdateEntry(InsnArg arg, ArgType type) { + this.arg = arg; + this.type = type; + } + + public void apply() { + arg.setType(type); + } + + public InsnArg getArg() { + return arg; + } + + public ArgType getType() { + return type; + } + + @Override + public String toString() { + return "TypeUpdateEntry{" + arg + " -> " + type + '}'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java new file mode 100644 index 000000000..3496669ab --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateInfo.java @@ -0,0 +1,35 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.instructions.args.InsnArg; + +public class TypeUpdateInfo { + private final List updates = new ArrayList<>(); + + public void requestUpdate(InsnArg arg, ArgType changeType) { + updates.add(new TypeUpdateEntry(arg, changeType)); + } + + public boolean isProcessed(InsnArg arg) { + if (updates.isEmpty()) { + return false; + } + for (TypeUpdateEntry entry : updates) { + if (entry.getArg() == arg) { + return true; + } + } + return false; + } + + public void rollbackUpdate(InsnArg arg) { + updates.removeIf(updateEntry -> updateEntry.getArg() == arg); + } + + public List getUpdates() { + return updates; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateRegistry.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateRegistry.java new file mode 100644 index 000000000..b66fea233 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateRegistry.java @@ -0,0 +1,29 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.instructions.InsnType; + +public class TypeUpdateRegistry { + + private final Map> listenersMap = new EnumMap<>(InsnType.class); + + public void add(InsnType insnType, ITypeListener listener) { + listenersMap.computeIfAbsent(insnType, k -> new ArrayList<>(3)).add(listener); + } + + @NotNull + public List getListenersForInsn(InsnType insnType) { + List list = listenersMap.get(insnType); + if (list == null) { + return Collections.emptyList(); + } + return list; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateResult.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateResult.java new file mode 100644 index 000000000..fbb0a56e0 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/TypeUpdateResult.java @@ -0,0 +1,7 @@ +package jadx.core.dex.visitors.typeinference; + +public enum TypeUpdateResult { + REJECT, + SAME, + CHANGED +} diff --git a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java index 8d6edc031..64064cc05 100644 --- a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java @@ -74,7 +74,7 @@ public class BlockUtils { } public static boolean isBlockMustBeCleared(BlockNode b) { - if (b.contains(AType.EXC_HANDLER) || b.contains(AFlag.SKIP)) { + if (b.contains(AType.EXC_HANDLER) || b.contains(AFlag.REMOVE)) { return true; } if (b.contains(AFlag.SYNTHETIC)) { @@ -495,7 +495,7 @@ public class BlockUtils { if (pred.contains(AFlag.SYNTHETIC) && !pred.contains(AType.SPLITTER_BLOCK) && pred.getInstructions().isEmpty()) { - pred.add(AFlag.SKIP); + pred.add(AFlag.DONT_GENERATE); skipPredSyntheticPaths(pred); } } @@ -556,4 +556,38 @@ public class BlockUtils { blocks.forEach(block -> insns.addAll(block.getInstructions())); return insns; } + + /** + * Replace insn by index i in block, + * for proper copy attributes, assume attributes are not overlap + */ + public static void replaceInsn(BlockNode block, int i, InsnNode insn) { + InsnNode prevInsn = block.getInstructions().get(i); + insn.copyAttributesFrom(prevInsn); + insn.setSourceLine(prevInsn.getSourceLine()); + insn.setOffset(prevInsn.getOffset()); + block.getInstructions().set(i, insn); + } + + public static boolean replaceInsn(BlockNode block, InsnNode oldInsn, InsnNode newInsn) { + List instructions = block.getInstructions(); + int size = instructions.size(); + for (int i = 0; i < size; i++) { + InsnNode instruction = instructions.get(i); + if (instruction == oldInsn) { + replaceInsn(block, i, newInsn); + return true; + } + } + return false; + } + + public static boolean replaceInsn(MethodNode mth, InsnNode oldInsn, InsnNode newInsn) { + for (BlockNode block : mth.getBasicBlocks()) { + if (replaceInsn(block, oldInsn, newInsn)) { + return true; + } + } + return false; + } } 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 68647637c..22af53fd9 100644 --- a/jadx-core/src/main/java/jadx/core/utils/CodegenUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/CodegenUtils.java @@ -1,5 +1,7 @@ package jadx.core.utils; +import java.util.List; + import jadx.core.codegen.CodeWriter; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.AttrNode; @@ -7,8 +9,10 @@ import jadx.core.dex.attributes.AttrNode; public class CodegenUtils { public static void addComments(CodeWriter code, AttrNode node) { - for (String comment : node.getAll(AType.COMMENTS)) { - code.startLine("/* ").add(comment).add(" */"); + List comments = node.getAll(AType.COMMENTS); + if (!comments.isEmpty()) { + comments.stream().distinct() + .forEach(comment -> code.startLine("/* ").addMultiLine(comment).add(" */")); } } } diff --git a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java index 03f0e9f0f..fa7423427 100644 --- a/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/DebugUtils.java @@ -4,6 +4,7 @@ import java.io.File; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.jetbrains.annotations.TestOnly; @@ -26,10 +27,13 @@ import jadx.core.dex.nodes.IContainer; import jadx.core.dex.nodes.IRegion; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.DotGraphVisitor; +import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.regions.TracedRegionVisitor; import jadx.core.utils.exceptions.CodegenException; +import jadx.core.utils.exceptions.JadxException; import jadx.core.utils.exceptions.JadxRuntimeException; @Deprecated @@ -44,6 +48,11 @@ public class DebugUtils { dump(mth, ""); } + public static void dumpRaw(MethodNode mth, String desc) { + File out = new File("test-graph-" + desc + "-tmp"); + DotGraphVisitor.dumpRaw().save(out, mth); + } + public static void dump(MethodNode mth, String desc) { File out = new File("test-graph-" + desc + "-tmp"); DotGraphVisitor.dump().save(out, mth); @@ -64,6 +73,15 @@ public class DebugUtils { LOG.debug(" Found block: {} in regions: {}", block, regions); } + public static IDexTreeVisitor printRegionsVisitor() { + return new AbstractVisitor() { + @Override + public void visit(MethodNode mth) throws JadxException { + printRegions(mth, true); + } + }; + } + public static void printRegions(MethodNode mth) { printRegions(mth, false); } @@ -101,9 +119,9 @@ public class DebugUtils { CodeWriter code = new CodeWriter(); ig.makeInsn(insn, code); String insnStr = code.toString().substring(CodeWriter.NL.length()); - LOG.debug("{}> {}\t{}", indent, insnStr, insn.getAttributesString()); + LOG.debug("{}|> {}\t{}", indent, insnStr, insn.getAttributesString()); } catch (CodegenException e) { - LOG.debug("{}>!! {}", indent, insn); + LOG.debug("{}|>!! {}", indent, insn); } } } @@ -193,4 +211,11 @@ public class DebugUtils { } } } + + public static void printMap(String desc, Map map) { + LOG.debug("Map of {}, size: {}", desc, map.size()); + for (Map.Entry entry : map.entrySet()) { + LOG.debug(" {} : {}", entry.getKey(), entry.getValue()); + } + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/ErrorsCounter.java b/jadx-core/src/main/java/jadx/core/utils/ErrorsCounter.java index 2328479fb..fc88a662e 100644 --- a/jadx-core/src/main/java/jadx/core/utils/ErrorsCounter.java +++ b/jadx-core/src/main/java/jadx/core/utils/ErrorsCounter.java @@ -14,7 +14,6 @@ import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.attributes.nodes.JadxError; -import jadx.core.dex.attributes.nodes.JadxWarn; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.IDexNode; import jadx.core.dex.nodes.MethodNode; @@ -60,7 +59,7 @@ public class ErrorsCounter { warnNodes.add(node); warnsCount++; - node.addAttr(AType.JADX_WARN, new JadxWarn(warn)); + node.addAttr(AType.JADX_WARN, warn); if (!node.contains(AType.JADX_ERROR)) { node.add(AFlag.INCONSISTENT_CODE); } diff --git a/jadx-core/src/main/java/jadx/core/utils/InstructionRemover.java b/jadx-core/src/main/java/jadx/core/utils/InstructionRemover.java index 7097bec7b..3af7386a1 100644 --- a/jadx-core/src/main/java/jadx/core/utils/InstructionRemover.java +++ b/jadx-core/src/main/java/jadx/core/utils/InstructionRemover.java @@ -45,6 +45,11 @@ public class InstructionRemover { toRemove.add(insn); } + public void addAndUnbind(MethodNode mth, InsnNode insn) { + toRemove.add(insn); + unbindInsn(mth, insn); + } + public void perform() { if (toRemove.isEmpty()) { return; @@ -53,14 +58,7 @@ public class InstructionRemover { toRemove.clear(); } - public static void unbindInsnList(MethodNode mth, List unbind) { - for (InsnNode rem : unbind) { - unbindInsn(mth, rem); - } - } - public static void unbindInsn(MethodNode mth, InsnNode insn) { - unbindResult(mth, insn); for (InsnArg arg : insn.getArguments()) { unbindArgUsage(mth, arg); } @@ -71,7 +69,8 @@ public class InstructionRemover { } } } - insn.add(AFlag.INCONSISTENT_CODE); + unbindResult(mth, insn); + insn.add(AFlag.REMOVE); } public static void fixUsedInPhiFlag(RegisterArg useReg) { @@ -89,8 +88,11 @@ public class InstructionRemover { public static void unbindResult(MethodNode mth, InsnNode insn) { RegisterArg r = insn.getResult(); - if (r != null && r.getSVar() != null) { - mth.removeSVar(r.getSVar()); + if (r != null && r.getSVar() != null && mth != null) { + SSAVar ssaVar = r.getSVar(); + if (ssaVar.getUseCount() == 0) { + mth.removeSVar(ssaVar); + } } } @@ -110,12 +112,15 @@ public class InstructionRemover { // Don't use 'instrList.removeAll(toRemove)' because it will remove instructions by content // and here can be several instructions with same content private static void removeAll(MethodNode mth, List insns, List toRemove) { + if (toRemove == null || toRemove.isEmpty()) { + return; + } for (InsnNode rem : toRemove) { - unbindInsn(mth, rem); int insnsCount = insns.size(); for (int i = 0; i < insnsCount; i++) { if (insns.get(i) == rem) { insns.remove(i); + unbindInsn(mth, rem); break; } } @@ -143,9 +148,6 @@ public class InstructionRemover { } public static void removeAll(MethodNode mth, BlockNode block, List insns) { - if (insns.isEmpty()) { - return; - } removeAll(mth, block.getInstructions(), insns); } diff --git a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java index 9357807d4..82c00695d 100644 --- a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java @@ -203,6 +203,10 @@ public class StringUtils { return str != null && !str.isEmpty(); } + public static boolean isEmpty(String str) { + return str == null || str.isEmpty(); + } + public static int countMatches(String str, String subStr) { if (str == null || str.isEmpty() || subStr == null || subStr.isEmpty()) { return 0; 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 07267bd97..91eccabb4 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import org.jetbrains.annotations.Nullable; + import jadx.api.JadxDecompiler; import jadx.core.codegen.CodeWriter; @@ -67,16 +69,15 @@ public class Utils { } } - public static String arrayToString(Object[] array) { - if (array == null) { + public static String arrayToStr(T[] arr) { + int len = arr == null ? 0 : arr.length; + if (len == 0) { return ""; } StringBuilder sb = new StringBuilder(); - for (int i = 0; i < array.length; i++) { - if (i != 0) { - sb.append(", "); - } - sb.append(array[i]); + sb.append(arr[0]); + for (int i = 1; i < len; i++) { + sb.append(", ").append(arr[i]); } return sb.toString(); } @@ -175,4 +176,12 @@ public class Utils { } return Collections.unmodifiableMap(result); } + + @Nullable + public static T last(List list) { + if (list.isEmpty()) { + return null; + } + return list.get(list.size() - 1); + } } diff --git a/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java b/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java index 1464354cc..cbc66ec3c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/android/AndroidResourcesUtils.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.TreeMap; import com.android.dx.rop.code.AccessFlags; -import jadx.core.dex.attributes.AFlag; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -15,6 +14,7 @@ import org.slf4j.LoggerFactory; import jadx.core.codegen.ClassGen; import jadx.core.codegen.CodeWriter; import jadx.core.deobf.NameMapper; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.info.ClassInfo; diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java b/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java index 10e37eb36..a50d45d7a 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ParserStream.java @@ -150,13 +150,15 @@ public class ParserStream { public void readFully(byte[] b, int off, int len) throws IOException { readPos += len; - if (len < 0) + if (len < 0) { throw new IndexOutOfBoundsException(); + } int n = 0; while (n < len) { int count = input.read(b, off + n, len - n); - if (count < 0) + if (count < 0) { throw new EOFException(); + } n += count; } } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java index a13af2bc4..58f721ceb 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java @@ -151,12 +151,14 @@ public class ResXmlGen { } switch (typeName) { case "attr": - if (nameStr != null) + if (nameStr != null) { addSimpleValue(cw, typeName, itemTag, nameStr, valueStr, ""); + } break; case "style": - if (nameStr != null) + if (nameStr != null) { addSimpleValue(cw, typeName, itemTag, nameStr, "", valueStr); + } break; case "plurals": final String quantity = PLURALS_MAP.get(value.getNameRef()); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java index db7806778..d1d0fdfc4 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/entry/EntryConfig.java @@ -20,630 +20,630 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Original source code can be found here + * Original source code can be found + * here */ public class EntryConfig { - public final short mcc; - public final short mnc; + public final short mcc; + public final short mnc; - public final char[] language; - public final char[] region; + public final char[] language; + public final char[] region; - public final byte orientation; - public final byte touchscreen; - public final int density; + public final byte orientation; + public final byte touchscreen; + public final int density; - public final byte keyboard; - public final byte navigation; - public final byte inputFlags; + public final byte keyboard; + public final byte navigation; + public final byte inputFlags; - public final short screenWidth; - public final short screenHeight; + public final short screenWidth; + public final short screenHeight; - public final short sdkVersion; + public final short sdkVersion; - public final byte screenLayout; - public final byte uiMode; - public final short smallestScreenWidthDp; + public final byte screenLayout; + public final byte uiMode; + public final short smallestScreenWidthDp; - public final short screenWidthDp; - public final short screenHeightDp; + public final short screenWidthDp; + public final short screenHeightDp; - private final char[] localeScript; - private final char[] localeVariant; + private final char[] localeScript; + private final char[] localeVariant; - private final byte screenLayout2; - private final byte colorMode; + private final byte screenLayout2; + private final byte colorMode; - public final boolean isInvalid; + public final boolean isInvalid; - private final String mQualifiers; + private final String mQualifiers; - private final int size; + private final int size; - public EntryConfig() { - mcc = 0; - mnc = 0; - language = new char[]{'\00', '\00'}; - region = new char[]{'\00', '\00'}; - orientation = ORIENTATION_ANY; - touchscreen = TOUCHSCREEN_ANY; - density = DENSITY_DEFAULT; - keyboard = KEYBOARD_ANY; - navigation = NAVIGATION_ANY; - inputFlags = KEYSHIDDEN_ANY | NAVHIDDEN_ANY; - screenWidth = 0; - screenHeight = 0; - sdkVersion = 0; - screenLayout = SCREENLONG_ANY | SCREENSIZE_ANY; - uiMode = UI_MODE_TYPE_ANY | UI_MODE_NIGHT_ANY; - smallestScreenWidthDp = 0; - screenWidthDp = 0; - screenHeightDp = 0; - localeScript = null; - localeVariant = null; - screenLayout2 = 0; - colorMode = COLOR_WIDE_UNDEFINED; - isInvalid = false; - mQualifiers = ""; - size = 0; - } + public EntryConfig() { + mcc = 0; + mnc = 0; + language = new char[]{'\00', '\00'}; + region = new char[]{'\00', '\00'}; + orientation = ORIENTATION_ANY; + touchscreen = TOUCHSCREEN_ANY; + density = DENSITY_DEFAULT; + keyboard = KEYBOARD_ANY; + navigation = NAVIGATION_ANY; + inputFlags = KEYSHIDDEN_ANY | NAVHIDDEN_ANY; + screenWidth = 0; + screenHeight = 0; + sdkVersion = 0; + screenLayout = SCREENLONG_ANY | SCREENSIZE_ANY; + uiMode = UI_MODE_TYPE_ANY | UI_MODE_NIGHT_ANY; + smallestScreenWidthDp = 0; + screenWidthDp = 0; + screenHeightDp = 0; + localeScript = null; + localeVariant = null; + screenLayout2 = 0; + colorMode = COLOR_WIDE_UNDEFINED; + isInvalid = false; + mQualifiers = ""; + size = 0; + } - public EntryConfig(short mcc, short mnc, char[] language, - char[] region, byte orientation, - byte touchscreen, int density, byte keyboard, byte navigation, - byte inputFlags, short screenWidth, short screenHeight, - short sdkVersion, byte screenLayout, byte uiMode, - short smallestScreenWidthDp, short screenWidthDp, - short screenHeightDp, char[] localeScript, char[] localeVariant, - byte screenLayout2, byte colorMode, boolean isInvalid, int size) { - if (orientation < 0 || orientation > 3) { - LOG.warn("Invalid orientation value: " + orientation); - orientation = 0; - isInvalid = true; - } - if (touchscreen < 0 || touchscreen > 3) { - LOG.warn("Invalid touchscreen value: " + touchscreen); - touchscreen = 0; - isInvalid = true; - } - if (density < -1) { - LOG.warn("Invalid density value: " + density); - density = 0; - isInvalid = true; - } - if (keyboard < 0 || keyboard > 3) { - LOG.warn("Invalid keyboard value: " + keyboard); - keyboard = 0; - isInvalid = true; - } - if (navigation < 0 || navigation > 4) { - LOG.warn("Invalid navigation value: " + navigation); - navigation = 0; - isInvalid = true; - } + public EntryConfig(short mcc, short mnc, char[] language, + char[] region, byte orientation, + byte touchscreen, int density, byte keyboard, byte navigation, + byte inputFlags, short screenWidth, short screenHeight, + short sdkVersion, byte screenLayout, byte uiMode, + short smallestScreenWidthDp, short screenWidthDp, + short screenHeightDp, char[] localeScript, char[] localeVariant, + byte screenLayout2, byte colorMode, boolean isInvalid, int size) { + if (orientation < 0 || orientation > 3) { + LOG.warn("Invalid orientation value: " + orientation); + orientation = 0; + isInvalid = true; + } + if (touchscreen < 0 || touchscreen > 3) { + LOG.warn("Invalid touchscreen value: " + touchscreen); + touchscreen = 0; + isInvalid = true; + } + if (density < -1) { + LOG.warn("Invalid density value: " + density); + density = 0; + isInvalid = true; + } + if (keyboard < 0 || keyboard > 3) { + LOG.warn("Invalid keyboard value: " + keyboard); + keyboard = 0; + isInvalid = true; + } + if (navigation < 0 || navigation > 4) { + LOG.warn("Invalid navigation value: " + navigation); + navigation = 0; + isInvalid = true; + } - if (localeScript != null && localeScript.length != 0) { - if (localeScript[0] == '\00') { - localeScript = null; - } - } else { - localeScript = null; - } + if (localeScript != null && localeScript.length != 0) { + if (localeScript[0] == '\00') { + localeScript = null; + } + } else { + localeScript = null; + } - if (localeVariant != null && localeVariant.length != 0) { - if (localeVariant[0] == '\00') { - localeVariant = null; - } - } else { - localeVariant = null; - } + if (localeVariant != null && localeVariant.length != 0) { + if (localeVariant[0] == '\00') { + localeVariant = null; + } + } else { + localeVariant = null; + } - this.mcc = mcc; - this.mnc = mnc; - this.language = language; - this.region = region; - this.orientation = orientation; - this.touchscreen = touchscreen; - this.density = density; - this.keyboard = keyboard; - this.navigation = navigation; - this.inputFlags = inputFlags; - this.screenWidth = screenWidth; - this.screenHeight = screenHeight; - this.sdkVersion = sdkVersion; - this.screenLayout = screenLayout; - this.uiMode = uiMode; - this.smallestScreenWidthDp = smallestScreenWidthDp; - this.screenWidthDp = screenWidthDp; - this.screenHeightDp = screenHeightDp; - this.localeScript = localeScript; - this.localeVariant = localeVariant; - this.screenLayout2 = screenLayout2; - this.colorMode = colorMode; - this.isInvalid = isInvalid; - this.size = size; - mQualifiers = generateQualifiers(); - } + this.mcc = mcc; + this.mnc = mnc; + this.language = language; + this.region = region; + this.orientation = orientation; + this.touchscreen = touchscreen; + this.density = density; + this.keyboard = keyboard; + this.navigation = navigation; + this.inputFlags = inputFlags; + this.screenWidth = screenWidth; + this.screenHeight = screenHeight; + this.sdkVersion = sdkVersion; + this.screenLayout = screenLayout; + this.uiMode = uiMode; + this.smallestScreenWidthDp = smallestScreenWidthDp; + this.screenWidthDp = screenWidthDp; + this.screenHeightDp = screenHeightDp; + this.localeScript = localeScript; + this.localeVariant = localeVariant; + this.screenLayout2 = screenLayout2; + this.colorMode = colorMode; + this.isInvalid = isInvalid; + this.size = size; + mQualifiers = generateQualifiers(); + } - public String getQualifiers() { - return mQualifiers; - } + public String getQualifiers() { + return mQualifiers; + } - private String generateQualifiers() { - StringBuilder ret = new StringBuilder(); - if (mcc != 0) { - ret.append("-mcc").append(String.format("%03d", mcc)); - if (mnc != MNC_ZERO) { - if (mnc != 0) { - ret.append("-mnc"); - if (size <= 32) { - if (mnc > 0 && mnc < 10) { - ret.append(String.format("%02d", mnc)); - } else { - ret.append(String.format("%03d", mnc)); - } - } else { - ret.append(mnc); - } - } - } else { - ret.append("-mnc00"); - } - } else { - if (mnc != 0) { - ret.append("-mnc").append(mnc); - } - } - ret.append(getLocaleString()); + private String generateQualifiers() { + StringBuilder ret = new StringBuilder(); + if (mcc != 0) { + ret.append("-mcc").append(String.format("%03d", mcc)); + if (mnc != MNC_ZERO) { + if (mnc != 0) { + ret.append("-mnc"); + if (size <= 32) { + if (mnc > 0 && mnc < 10) { + ret.append(String.format("%02d", mnc)); + } else { + ret.append(String.format("%03d", mnc)); + } + } else { + ret.append(mnc); + } + } + } else { + ret.append("-mnc00"); + } + } else { + if (mnc != 0) { + ret.append("-mnc").append(mnc); + } + } + ret.append(getLocaleString()); - switch (screenLayout & MASK_LAYOUTDIR) { - case SCREENLAYOUT_LAYOUTDIR_RTL: - ret.append("-ldrtl"); - break; - case SCREENLAYOUT_LAYOUTDIR_LTR: - ret.append("-ldltr"); - break; - } - if (smallestScreenWidthDp != 0) { - ret.append("-sw").append(smallestScreenWidthDp).append("dp"); - } - if (screenWidthDp != 0) { - ret.append("-w").append(screenWidthDp).append("dp"); - } - if (screenHeightDp != 0) { - ret.append("-h").append(screenHeightDp).append("dp"); - } - switch (screenLayout & MASK_SCREENSIZE) { - case SCREENSIZE_SMALL: - ret.append("-small"); - break; - case SCREENSIZE_NORMAL: - ret.append("-normal"); - break; - case SCREENSIZE_LARGE: - ret.append("-large"); - break; - case SCREENSIZE_XLARGE: - ret.append("-xlarge"); - break; - } - switch (screenLayout & MASK_SCREENLONG) { - case SCREENLONG_YES: - ret.append("-long"); - break; - case SCREENLONG_NO: - ret.append("-notlong"); - break; - } - switch (screenLayout2 & MASK_SCREENROUND) { - case SCREENLAYOUT_ROUND_NO: - ret.append("-notround"); - break; - case SCREENLAYOUT_ROUND_YES: - ret.append("-round"); - break; - } - switch (colorMode & COLOR_HDR_MASK) { - case COLOR_HDR_YES: - ret.append("-highdr"); - break; - case COLOR_HDR_NO: - ret.append("-lowdr"); - break; - } - switch (colorMode & COLOR_WIDE_MASK) { - case COLOR_WIDE_YES: - ret.append("-widecg"); - break; - case COLOR_WIDE_NO: - ret.append("-nowidecg"); - break; - } - switch (orientation) { - case ORIENTATION_PORT: - ret.append("-port"); - break; - case ORIENTATION_LAND: - ret.append("-land"); - break; - case ORIENTATION_SQUARE: - ret.append("-square"); - break; - } - switch (uiMode & MASK_UI_MODE_TYPE) { - case UI_MODE_TYPE_CAR: - ret.append("-car"); - break; - case UI_MODE_TYPE_DESK: - ret.append("-desk"); - break; - case UI_MODE_TYPE_TELEVISION: - ret.append("-television"); - break; - case UI_MODE_TYPE_SMALLUI: - ret.append("-smallui"); - break; - case UI_MODE_TYPE_MEDIUMUI: - ret.append("-mediumui"); - break; - case UI_MODE_TYPE_LARGEUI: - ret.append("-largeui"); - break; - case UI_MODE_TYPE_GODZILLAUI: - ret.append("-godzillaui"); - break; - case UI_MODE_TYPE_HUGEUI: - ret.append("-hugeui"); - break; - case UI_MODE_TYPE_APPLIANCE: - ret.append("-appliance"); - break; - case UI_MODE_TYPE_WATCH: - ret.append("-watch"); - break; - case UI_MODE_TYPE_VR_HEADSET: - ret.append("-vrheadset"); - break; - } - switch (uiMode & MASK_UI_MODE_NIGHT) { - case UI_MODE_NIGHT_YES: - ret.append("-night"); - break; - case UI_MODE_NIGHT_NO: - ret.append("-notnight"); - break; - } - switch (density) { - case DENSITY_DEFAULT: - break; - case DENSITY_LOW: - ret.append("-ldpi"); - break; - case DENSITY_MEDIUM: - ret.append("-mdpi"); - break; - case DENSITY_HIGH: - ret.append("-hdpi"); - break; - case DENSITY_TV: - ret.append("-tvdpi"); - break; - case DENSITY_XHIGH: - ret.append("-xhdpi"); - break; - case DENSITY_XXHIGH: - ret.append("-xxhdpi"); - break; - case DENSITY_XXXHIGH: - ret.append("-xxxhdpi"); - break; - case DENSITY_ANY: - ret.append("-anydpi"); - break; - case DENSITY_NONE: - ret.append("-nodpi"); - break; - default: - ret.append('-').append(density).append("dpi"); - } - switch (touchscreen) { - case TOUCHSCREEN_NOTOUCH: - ret.append("-notouch"); - break; - case TOUCHSCREEN_STYLUS: - ret.append("-stylus"); - break; - case TOUCHSCREEN_FINGER: - ret.append("-finger"); - break; - } - switch (inputFlags & MASK_KEYSHIDDEN) { - case KEYSHIDDEN_NO: - ret.append("-keysexposed"); - break; - case KEYSHIDDEN_YES: - ret.append("-keyshidden"); - break; - case KEYSHIDDEN_SOFT: - ret.append("-keyssoft"); - break; - } - switch (keyboard) { - case KEYBOARD_NOKEYS: - ret.append("-nokeys"); - break; - case KEYBOARD_QWERTY: - ret.append("-qwerty"); - break; - case KEYBOARD_12KEY: - ret.append("-12key"); - break; - } - switch (inputFlags & MASK_NAVHIDDEN) { - case NAVHIDDEN_NO: - ret.append("-navexposed"); - break; - case NAVHIDDEN_YES: - ret.append("-navhidden"); - break; - } - switch (navigation) { - case NAVIGATION_NONAV: - ret.append("-nonav"); - break; - case NAVIGATION_DPAD: - ret.append("-dpad"); - break; - case NAVIGATION_TRACKBALL: - ret.append("-trackball"); - break; - case NAVIGATION_WHEEL: - ret.append("-wheel"); - break; - } - if (screenWidth != 0 && screenHeight != 0) { - if (screenWidth > screenHeight) { - ret.append(String.format("-%dx%d", screenWidth, screenHeight)); - } else { - ret.append(String.format("-%dx%d", screenHeight, screenWidth)); - } - } - if (sdkVersion > 0 && sdkVersion >= getNaturalSdkVersionRequirement()) { - ret.append("-v").append(sdkVersion); - } - if (isInvalid) { - ret.append("-ERR").append(sErrCounter++); - } + switch (screenLayout & MASK_LAYOUTDIR) { + case SCREENLAYOUT_LAYOUTDIR_RTL: + ret.append("-ldrtl"); + break; + case SCREENLAYOUT_LAYOUTDIR_LTR: + ret.append("-ldltr"); + break; + } + if (smallestScreenWidthDp != 0) { + ret.append("-sw").append(smallestScreenWidthDp).append("dp"); + } + if (screenWidthDp != 0) { + ret.append("-w").append(screenWidthDp).append("dp"); + } + if (screenHeightDp != 0) { + ret.append("-h").append(screenHeightDp).append("dp"); + } + switch (screenLayout & MASK_SCREENSIZE) { + case SCREENSIZE_SMALL: + ret.append("-small"); + break; + case SCREENSIZE_NORMAL: + ret.append("-normal"); + break; + case SCREENSIZE_LARGE: + ret.append("-large"); + break; + case SCREENSIZE_XLARGE: + ret.append("-xlarge"); + break; + } + switch (screenLayout & MASK_SCREENLONG) { + case SCREENLONG_YES: + ret.append("-long"); + break; + case SCREENLONG_NO: + ret.append("-notlong"); + break; + } + switch (screenLayout2 & MASK_SCREENROUND) { + case SCREENLAYOUT_ROUND_NO: + ret.append("-notround"); + break; + case SCREENLAYOUT_ROUND_YES: + ret.append("-round"); + break; + } + switch (colorMode & COLOR_HDR_MASK) { + case COLOR_HDR_YES: + ret.append("-highdr"); + break; + case COLOR_HDR_NO: + ret.append("-lowdr"); + break; + } + switch (colorMode & COLOR_WIDE_MASK) { + case COLOR_WIDE_YES: + ret.append("-widecg"); + break; + case COLOR_WIDE_NO: + ret.append("-nowidecg"); + break; + } + switch (orientation) { + case ORIENTATION_PORT: + ret.append("-port"); + break; + case ORIENTATION_LAND: + ret.append("-land"); + break; + case ORIENTATION_SQUARE: + ret.append("-square"); + break; + } + switch (uiMode & MASK_UI_MODE_TYPE) { + case UI_MODE_TYPE_CAR: + ret.append("-car"); + break; + case UI_MODE_TYPE_DESK: + ret.append("-desk"); + break; + case UI_MODE_TYPE_TELEVISION: + ret.append("-television"); + break; + case UI_MODE_TYPE_SMALLUI: + ret.append("-smallui"); + break; + case UI_MODE_TYPE_MEDIUMUI: + ret.append("-mediumui"); + break; + case UI_MODE_TYPE_LARGEUI: + ret.append("-largeui"); + break; + case UI_MODE_TYPE_GODZILLAUI: + ret.append("-godzillaui"); + break; + case UI_MODE_TYPE_HUGEUI: + ret.append("-hugeui"); + break; + case UI_MODE_TYPE_APPLIANCE: + ret.append("-appliance"); + break; + case UI_MODE_TYPE_WATCH: + ret.append("-watch"); + break; + case UI_MODE_TYPE_VR_HEADSET: + ret.append("-vrheadset"); + break; + } + switch (uiMode & MASK_UI_MODE_NIGHT) { + case UI_MODE_NIGHT_YES: + ret.append("-night"); + break; + case UI_MODE_NIGHT_NO: + ret.append("-notnight"); + break; + } + switch (density) { + case DENSITY_DEFAULT: + break; + case DENSITY_LOW: + ret.append("-ldpi"); + break; + case DENSITY_MEDIUM: + ret.append("-mdpi"); + break; + case DENSITY_HIGH: + ret.append("-hdpi"); + break; + case DENSITY_TV: + ret.append("-tvdpi"); + break; + case DENSITY_XHIGH: + ret.append("-xhdpi"); + break; + case DENSITY_XXHIGH: + ret.append("-xxhdpi"); + break; + case DENSITY_XXXHIGH: + ret.append("-xxxhdpi"); + break; + case DENSITY_ANY: + ret.append("-anydpi"); + break; + case DENSITY_NONE: + ret.append("-nodpi"); + break; + default: + ret.append('-').append(density).append("dpi"); + } + switch (touchscreen) { + case TOUCHSCREEN_NOTOUCH: + ret.append("-notouch"); + break; + case TOUCHSCREEN_STYLUS: + ret.append("-stylus"); + break; + case TOUCHSCREEN_FINGER: + ret.append("-finger"); + break; + } + switch (inputFlags & MASK_KEYSHIDDEN) { + case KEYSHIDDEN_NO: + ret.append("-keysexposed"); + break; + case KEYSHIDDEN_YES: + ret.append("-keyshidden"); + break; + case KEYSHIDDEN_SOFT: + ret.append("-keyssoft"); + break; + } + switch (keyboard) { + case KEYBOARD_NOKEYS: + ret.append("-nokeys"); + break; + case KEYBOARD_QWERTY: + ret.append("-qwerty"); + break; + case KEYBOARD_12KEY: + ret.append("-12key"); + break; + } + switch (inputFlags & MASK_NAVHIDDEN) { + case NAVHIDDEN_NO: + ret.append("-navexposed"); + break; + case NAVHIDDEN_YES: + ret.append("-navhidden"); + break; + } + switch (navigation) { + case NAVIGATION_NONAV: + ret.append("-nonav"); + break; + case NAVIGATION_DPAD: + ret.append("-dpad"); + break; + case NAVIGATION_TRACKBALL: + ret.append("-trackball"); + break; + case NAVIGATION_WHEEL: + ret.append("-wheel"); + break; + } + if (screenWidth != 0 && screenHeight != 0) { + if (screenWidth > screenHeight) { + ret.append(String.format("-%dx%d", screenWidth, screenHeight)); + } else { + ret.append(String.format("-%dx%d", screenHeight, screenWidth)); + } + } + if (sdkVersion > 0 && sdkVersion >= getNaturalSdkVersionRequirement()) { + ret.append("-v").append(sdkVersion); + } + if (isInvalid) { + ret.append("-ERR").append(sErrCounter++); + } - return ret.toString(); - } + return ret.toString(); + } - private short getNaturalSdkVersionRequirement() { - if ((uiMode & MASK_UI_MODE_TYPE) == UI_MODE_TYPE_VR_HEADSET || (colorMode & COLOR_WIDE_MASK) != 0 || ((colorMode & COLOR_HDR_MASK) != 0)) { - return SDK_OREO; - } - if ((screenLayout2 & MASK_SCREENROUND) != 0) { - return SDK_MNC; - } - if (density == DENSITY_ANY) { - return SDK_LOLLIPOP; - } - if (smallestScreenWidthDp != 0 || screenWidthDp != 0 || screenHeightDp != 0) { - return SDK_HONEYCOMB_MR2; - } - if ((uiMode & (MASK_UI_MODE_TYPE | MASK_UI_MODE_NIGHT)) != UI_MODE_NIGHT_ANY) { - return SDK_FROYO; - } - if ((screenLayout & (MASK_SCREENSIZE | MASK_SCREENLONG)) != SCREENSIZE_ANY || density != DENSITY_DEFAULT) { - return SDK_DONUT; - } - return 0; - } + private short getNaturalSdkVersionRequirement() { + if ((uiMode & MASK_UI_MODE_TYPE) == UI_MODE_TYPE_VR_HEADSET || (colorMode & COLOR_WIDE_MASK) != 0 || ((colorMode & COLOR_HDR_MASK) != 0)) { + return SDK_OREO; + } + if ((screenLayout2 & MASK_SCREENROUND) != 0) { + return SDK_MNC; + } + if (density == DENSITY_ANY) { + return SDK_LOLLIPOP; + } + if (smallestScreenWidthDp != 0 || screenWidthDp != 0 || screenHeightDp != 0) { + return SDK_HONEYCOMB_MR2; + } + if ((uiMode & (MASK_UI_MODE_TYPE | MASK_UI_MODE_NIGHT)) != UI_MODE_NIGHT_ANY) { + return SDK_FROYO; + } + if ((screenLayout & (MASK_SCREENSIZE | MASK_SCREENLONG)) != SCREENSIZE_ANY || density != DENSITY_DEFAULT) { + return SDK_DONUT; + } + return 0; + } - private String getLocaleString() { - StringBuilder sb = new StringBuilder(); + private String getLocaleString() { + StringBuilder sb = new StringBuilder(); - // check for old style non BCP47 tags - // allows values-xx-rXX, values-xx, values-xxx-rXX - // denies values-xxx, anything else - if (localeVariant == null && localeScript == null && (region[0] != '\00' || language[0] != '\00') && - region.length != 3) { - sb.append('-').append(language); - if (region[0] != '\00') { - sb.append("-r").append(region); - } - } else { // BCP47 - if (language[0] == '\00' && region[0] == '\00') { - return sb.toString(); // early return, no language or region - } - sb.append("-b+"); - if (language[0] != '\00') { - sb.append(language); - } - if (localeScript != null && localeScript.length == 4) { - sb.append('+').append(localeScript); - } - if ((region.length == 2 || region.length == 3) && region[0] != '\00') { - sb.append('+').append(region); - } - if (localeVariant != null && localeVariant.length >= 5) { - sb.append('+').append(toUpper(localeVariant)); - } - } - return sb.toString(); - } + // check for old style non BCP47 tags + // allows values-xx-rXX, values-xx, values-xxx-rXX + // denies values-xxx, anything else + if (localeVariant == null && localeScript == null && (region[0] != '\00' || language[0] != '\00') && + region.length != 3) { + sb.append('-').append(language); + if (region[0] != '\00') { + sb.append("-r").append(region); + } + } else { // BCP47 + if (language[0] == '\00' && region[0] == '\00') { + return sb.toString(); // early return, no language or region + } + sb.append("-b+"); + if (language[0] != '\00') { + sb.append(language); + } + if (localeScript != null && localeScript.length == 4) { + sb.append('+').append(localeScript); + } + if ((region.length == 2 || region.length == 3) && region[0] != '\00') { + sb.append('+').append(region); + } + if (localeVariant != null && localeVariant.length >= 5) { + sb.append('+').append(toUpper(localeVariant)); + } + } + return sb.toString(); + } - private String toUpper(char[] character) { - StringBuilder sb = new StringBuilder(); - for (char ch : character) { - sb.append(Character.toUpperCase(ch)); - } - return sb.toString(); - } + private String toUpper(char[] character) { + StringBuilder sb = new StringBuilder(); + for (char ch : character) { + sb.append(Character.toUpperCase(ch)); + } + return sb.toString(); + } + @Override + public String toString() { + return !getQualifiers().equals("") ? getQualifiers() : "[DEFAULT]"; + } - @Override - public String toString() { - return !getQualifiers().equals("") ? getQualifiers() : "[DEFAULT]"; - } + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final EntryConfig other = (EntryConfig) obj; + return this.mQualifiers.equals(other.mQualifiers); + } - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final EntryConfig other = (EntryConfig) obj; - return this.mQualifiers.equals(other.mQualifiers); - } + @Override + public int hashCode() { + int hash = 17; + hash = 31 * hash + this.mQualifiers.hashCode(); + return hash; + } - @Override - public int hashCode() { - int hash = 17; - hash = 31 * hash + this.mQualifiers.hashCode(); - return hash; - } + // TODO: Dirty static hack. This counter should be a part of ResPackage, + // but it would be hard right now and this feature is very rarely used. + private static int sErrCounter = 0; - // TODO: Dirty static hack. This counter should be a part of ResPackage, - // but it would be hard right now and this feature is very rarely used. - private static int sErrCounter = 0; + public final static byte SDK_BASE = 1; + public final static byte SDK_BASE_1_1 = 2; + public final static byte SDK_CUPCAKE = 3; + public final static byte SDK_DONUT = 4; + public final static byte SDK_ECLAIR = 5; + public final static byte SDK_ECLAIR_0_1 = 6; + public final static byte SDK_ECLAIR_MR1 = 7; + public final static byte SDK_FROYO = 8; + public final static byte SDK_GINGERBREAD = 9; + public final static byte SDK_GINGERBREAD_MR1 = 10; + public final static byte SDK_HONEYCOMB = 11; + public final static byte SDK_HONEYCOMB_MR1 = 12; + public final static byte SDK_HONEYCOMB_MR2 = 13; + public final static byte SDK_ICE_CREAM_SANDWICH = 14; + public final static byte SDK_ICE_CREAM_SANDWICH_MR1 = 15; + public final static byte SDK_JELLY_BEAN = 16; + public final static byte SDK_JELLY_BEAN_MR1 = 17; + public final static byte SDK_JELLY_BEAN_MR2 = 18; + public final static byte SDK_KITKAT = 19; + public final static byte SDK_LOLLIPOP = 21; + public final static byte SDK_LOLLIPOP_MR1 = 22; + public final static byte SDK_MNC = 23; + public final static byte SDK_NOUGAT = 24; + public final static byte SDK_NOUGAT_MR1 = 25; + public final static byte SDK_OREO = 26; + public final static byte SDK_OREO_MR1 = 27; + public final static byte SDK_P = 28; - public final static byte SDK_BASE = 1; - public final static byte SDK_BASE_1_1 = 2; - public final static byte SDK_CUPCAKE = 3; - public final static byte SDK_DONUT = 4; - public final static byte SDK_ECLAIR = 5; - public final static byte SDK_ECLAIR_0_1 = 6; - public final static byte SDK_ECLAIR_MR1 = 7; - public final static byte SDK_FROYO = 8; - public final static byte SDK_GINGERBREAD = 9; - public final static byte SDK_GINGERBREAD_MR1 = 10; - public final static byte SDK_HONEYCOMB = 11; - public final static byte SDK_HONEYCOMB_MR1 = 12; - public final static byte SDK_HONEYCOMB_MR2 = 13; - public final static byte SDK_ICE_CREAM_SANDWICH = 14; - public final static byte SDK_ICE_CREAM_SANDWICH_MR1 = 15; - public final static byte SDK_JELLY_BEAN = 16; - public final static byte SDK_JELLY_BEAN_MR1 = 17; - public final static byte SDK_JELLY_BEAN_MR2 = 18; - public final static byte SDK_KITKAT = 19; - public final static byte SDK_LOLLIPOP = 21; - public final static byte SDK_LOLLIPOP_MR1 = 22; - public final static byte SDK_MNC = 23; - public final static byte SDK_NOUGAT = 24; - public final static byte SDK_NOUGAT_MR1 = 25; - public final static byte SDK_OREO = 26; - public final static byte SDK_OREO_MR1 = 27; - public final static byte SDK_P = 28; + public final static byte ORIENTATION_ANY = 0; + public final static byte ORIENTATION_PORT = 1; + public final static byte ORIENTATION_LAND = 2; + public final static byte ORIENTATION_SQUARE = 3; - public final static byte ORIENTATION_ANY = 0; - public final static byte ORIENTATION_PORT = 1; - public final static byte ORIENTATION_LAND = 2; - public final static byte ORIENTATION_SQUARE = 3; + public final static byte TOUCHSCREEN_ANY = 0; + public final static byte TOUCHSCREEN_NOTOUCH = 1; + public final static byte TOUCHSCREEN_STYLUS = 2; + public final static byte TOUCHSCREEN_FINGER = 3; - public final static byte TOUCHSCREEN_ANY = 0; - public final static byte TOUCHSCREEN_NOTOUCH = 1; - public final static byte TOUCHSCREEN_STYLUS = 2; - public final static byte TOUCHSCREEN_FINGER = 3; + public final static int DENSITY_DEFAULT = 0; + public final static int DENSITY_LOW = 120; + public final static int DENSITY_MEDIUM = 160; + public final static int DENSITY_400 = 190; + public final static int DENSITY_TV = 213; + public final static int DENSITY_HIGH = 240; + public final static int DENSITY_XHIGH = 320; + public final static int DENSITY_XXHIGH = 480; + public final static int DENSITY_XXXHIGH = 640; + public final static int DENSITY_ANY = 0xFFFE; + public final static int DENSITY_NONE = 0xFFFF; - public final static int DENSITY_DEFAULT = 0; - public final static int DENSITY_LOW = 120; - public final static int DENSITY_MEDIUM = 160; - public final static int DENSITY_400 = 190; - public final static int DENSITY_TV = 213; - public final static int DENSITY_HIGH = 240; - public final static int DENSITY_XHIGH = 320; - public final static int DENSITY_XXHIGH = 480; - public final static int DENSITY_XXXHIGH = 640; - public final static int DENSITY_ANY = 0xFFFE; - public final static int DENSITY_NONE = 0xFFFF; + public final static int MNC_ZERO = -1; - public final static int MNC_ZERO = -1; + public final static short MASK_LAYOUTDIR = 0xc0; + public final static short SCREENLAYOUT_LAYOUTDIR_ANY = 0x00; + public final static short SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; + public final static short SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; + public final static short SCREENLAYOUT_LAYOUTDIR_SHIFT = 0x06; - public final static short MASK_LAYOUTDIR = 0xc0; - public final static short SCREENLAYOUT_LAYOUTDIR_ANY = 0x00; - public final static short SCREENLAYOUT_LAYOUTDIR_LTR = 0x40; - public final static short SCREENLAYOUT_LAYOUTDIR_RTL = 0x80; - public final static short SCREENLAYOUT_LAYOUTDIR_SHIFT = 0x06; + public final static short MASK_SCREENROUND = 0x03; + public final static short SCREENLAYOUT_ROUND_ANY = 0; + public final static short SCREENLAYOUT_ROUND_NO = 0x1; + public final static short SCREENLAYOUT_ROUND_YES = 0x2; - public final static short MASK_SCREENROUND = 0x03; - public final static short SCREENLAYOUT_ROUND_ANY = 0; - public final static short SCREENLAYOUT_ROUND_NO = 0x1; - public final static short SCREENLAYOUT_ROUND_YES = 0x2; + public final static byte KEYBOARD_ANY = 0; + public final static byte KEYBOARD_NOKEYS = 1; + public final static byte KEYBOARD_QWERTY = 2; + public final static byte KEYBOARD_12KEY = 3; - public final static byte KEYBOARD_ANY = 0; - public final static byte KEYBOARD_NOKEYS = 1; - public final static byte KEYBOARD_QWERTY = 2; - public final static byte KEYBOARD_12KEY = 3; + public final static byte NAVIGATION_ANY = 0; + public final static byte NAVIGATION_NONAV = 1; + public final static byte NAVIGATION_DPAD = 2; + public final static byte NAVIGATION_TRACKBALL = 3; + public final static byte NAVIGATION_WHEEL = 4; - public final static byte NAVIGATION_ANY = 0; - public final static byte NAVIGATION_NONAV = 1; - public final static byte NAVIGATION_DPAD = 2; - public final static byte NAVIGATION_TRACKBALL = 3; - public final static byte NAVIGATION_WHEEL = 4; + public final static byte MASK_KEYSHIDDEN = 0x3; + public final static byte KEYSHIDDEN_ANY = 0x0; + public final static byte KEYSHIDDEN_NO = 0x1; + public final static byte KEYSHIDDEN_YES = 0x2; + public final static byte KEYSHIDDEN_SOFT = 0x3; - public final static byte MASK_KEYSHIDDEN = 0x3; - public final static byte KEYSHIDDEN_ANY = 0x0; - public final static byte KEYSHIDDEN_NO = 0x1; - public final static byte KEYSHIDDEN_YES = 0x2; - public final static byte KEYSHIDDEN_SOFT = 0x3; + public final static byte MASK_NAVHIDDEN = 0xc; + public final static byte NAVHIDDEN_ANY = 0x0; + public final static byte NAVHIDDEN_NO = 0x4; + public final static byte NAVHIDDEN_YES = 0x8; - public final static byte MASK_NAVHIDDEN = 0xc; - public final static byte NAVHIDDEN_ANY = 0x0; - public final static byte NAVHIDDEN_NO = 0x4; - public final static byte NAVHIDDEN_YES = 0x8; + public final static byte MASK_SCREENSIZE = 0x0f; + public final static byte SCREENSIZE_ANY = 0x00; + public final static byte SCREENSIZE_SMALL = 0x01; + public final static byte SCREENSIZE_NORMAL = 0x02; + public final static byte SCREENSIZE_LARGE = 0x03; + public final static byte SCREENSIZE_XLARGE = 0x04; - public final static byte MASK_SCREENSIZE = 0x0f; - public final static byte SCREENSIZE_ANY = 0x00; - public final static byte SCREENSIZE_SMALL = 0x01; - public final static byte SCREENSIZE_NORMAL = 0x02; - public final static byte SCREENSIZE_LARGE = 0x03; - public final static byte SCREENSIZE_XLARGE = 0x04; + public final static byte MASK_SCREENLONG = 0x30; + public final static byte SCREENLONG_ANY = 0x00; + public final static byte SCREENLONG_NO = 0x10; + public final static byte SCREENLONG_YES = 0x20; - public final static byte MASK_SCREENLONG = 0x30; - public final static byte SCREENLONG_ANY = 0x00; - public final static byte SCREENLONG_NO = 0x10; - public final static byte SCREENLONG_YES = 0x20; + public final static byte MASK_UI_MODE_TYPE = 0x0f; + public final static byte UI_MODE_TYPE_ANY = 0x00; + public final static byte UI_MODE_TYPE_NORMAL = 0x01; + public final static byte UI_MODE_TYPE_DESK = 0x02; + public final static byte UI_MODE_TYPE_CAR = 0x03; + public final static byte UI_MODE_TYPE_TELEVISION = 0x04; + public final static byte UI_MODE_TYPE_APPLIANCE = 0x05; + public final static byte UI_MODE_TYPE_WATCH = 0x06; + public final static byte UI_MODE_TYPE_VR_HEADSET = 0x07; - public final static byte MASK_UI_MODE_TYPE = 0x0f; - public final static byte UI_MODE_TYPE_ANY = 0x00; - public final static byte UI_MODE_TYPE_NORMAL = 0x01; - public final static byte UI_MODE_TYPE_DESK = 0x02; - public final static byte UI_MODE_TYPE_CAR = 0x03; - public final static byte UI_MODE_TYPE_TELEVISION = 0x04; - public final static byte UI_MODE_TYPE_APPLIANCE = 0x05; - public final static byte UI_MODE_TYPE_WATCH = 0x06; - public final static byte UI_MODE_TYPE_VR_HEADSET = 0x07; + // start - miui + public final static byte UI_MODE_TYPE_GODZILLAUI = 0x0b; + public final static byte UI_MODE_TYPE_SMALLUI = 0x0c; + public final static byte UI_MODE_TYPE_MEDIUMUI = 0x0d; + public final static byte UI_MODE_TYPE_LARGEUI = 0x0e; + public final static byte UI_MODE_TYPE_HUGEUI = 0x0f; + // end - miui - // start - miui - public final static byte UI_MODE_TYPE_GODZILLAUI = 0x0b; - public final static byte UI_MODE_TYPE_SMALLUI = 0x0c; - public final static byte UI_MODE_TYPE_MEDIUMUI = 0x0d; - public final static byte UI_MODE_TYPE_LARGEUI = 0x0e; - public final static byte UI_MODE_TYPE_HUGEUI = 0x0f; - // end - miui + public final static byte MASK_UI_MODE_NIGHT = 0x30; + public final static byte UI_MODE_NIGHT_ANY = 0x00; + public final static byte UI_MODE_NIGHT_NO = 0x10; + public final static byte UI_MODE_NIGHT_YES = 0x20; - public final static byte MASK_UI_MODE_NIGHT = 0x30; - public final static byte UI_MODE_NIGHT_ANY = 0x00; - public final static byte UI_MODE_NIGHT_NO = 0x10; - public final static byte UI_MODE_NIGHT_YES = 0x20; + public final static byte COLOR_HDR_MASK = 0xC; + public final static byte COLOR_HDR_NO = 0x4; + public final static byte COLOR_HDR_SHIFT = 0x2; + public final static byte COLOR_HDR_UNDEFINED = 0x0; + public final static byte COLOR_HDR_YES = 0x8; - public final static byte COLOR_HDR_MASK = 0xC; - public final static byte COLOR_HDR_NO = 0x4; - public final static byte COLOR_HDR_SHIFT = 0x2; - public final static byte COLOR_HDR_UNDEFINED = 0x0; - public final static byte COLOR_HDR_YES = 0x8; + public final static byte COLOR_UNDEFINED = 0x0; - public final static byte COLOR_UNDEFINED = 0x0; + public final static byte COLOR_WIDE_UNDEFINED = 0x0; + public final static byte COLOR_WIDE_NO = 0x1; + public final static byte COLOR_WIDE_YES = 0x2; + public final static byte COLOR_WIDE_MASK = 0x3; - public final static byte COLOR_WIDE_UNDEFINED = 0x0; - public final static byte COLOR_WIDE_NO = 0x1; - public final static byte COLOR_WIDE_YES = 0x2; - public final static byte COLOR_WIDE_MASK = 0x3; - - private static final Logger LOG = LoggerFactory.getLogger(EntryConfig.class); + private static final Logger LOG = LoggerFactory.getLogger(EntryConfig.class); } diff --git a/jadx-core/src/test/java/jadx/NotYetImplemented.java b/jadx-core/src/test/java/jadx/NotYetImplemented.java index 4ed618821..de3f415d9 100644 --- a/jadx-core/src/test/java/jadx/NotYetImplemented.java +++ b/jadx-core/src/test/java/jadx/NotYetImplemented.java @@ -21,4 +21,5 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface NotYetImplemented { + String value() default ""; } diff --git a/jadx-core/src/test/java/jadx/NotYetImplementedExtension.java b/jadx-core/src/test/java/jadx/NotYetImplementedExtension.java index 2a2ab2977..1bff2ae13 100644 --- a/jadx-core/src/test/java/jadx/NotYetImplementedExtension.java +++ b/jadx-core/src/test/java/jadx/NotYetImplementedExtension.java @@ -35,5 +35,4 @@ public class NotYetImplementedExtension implements AfterTestExecutionCallback, T return context.getTestMethod().get().getAnnotation(NotYetImplemented.class) != null || context.getTestClass().get().getAnnotation(NotYetImplemented.class) != null; } - } diff --git a/jadx-core/src/test/java/jadx/api/JadxArgsValidatorOutDirsTest.java b/jadx-core/src/test/java/jadx/api/JadxArgsValidatorOutDirsTest.java index b23ec1861..130ed8656 100644 --- a/jadx-core/src/test/java/jadx/api/JadxArgsValidatorOutDirsTest.java +++ b/jadx-core/src/test/java/jadx/api/JadxArgsValidatorOutDirsTest.java @@ -7,8 +7,8 @@ import org.slf4j.LoggerFactory; import jadx.core.utils.files.FileUtils; import static jadx.core.utils.files.FileUtils.toFile; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class JadxArgsValidatorOutDirsTest { @@ -44,8 +44,8 @@ public class JadxArgsValidatorOutDirsTest { setOutDirs(null, null, null); String inputFileBase = args.getInputFiles().get(0).getName().replace(".apk", ""); checkOutDirs(inputFileBase, - inputFileBase + "/" + JadxArgs.DEFAULT_SRC_DIR, - inputFileBase + "/" + JadxArgs.DEFAULT_RES_DIR); + inputFileBase + '/' + JadxArgs.DEFAULT_SRC_DIR, + inputFileBase + '/' + JadxArgs.DEFAULT_RES_DIR); } private void setOutDirs(String outDir, String srcDir, String resDir) { diff --git a/jadx-core/src/test/java/jadx/api/JadxInternalAccess.java b/jadx-core/src/test/java/jadx/api/JadxInternalAccess.java index 8bfb46072..5ef6daf4a 100644 --- a/jadx-core/src/test/java/jadx/api/JadxInternalAccess.java +++ b/jadx-core/src/test/java/jadx/api/JadxInternalAccess.java @@ -1,10 +1,17 @@ package jadx.api; +import java.util.List; + import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.IDexTreeVisitor; public class JadxInternalAccess { public static RootNode getRoot(JadxDecompiler d) { return d.getRoot(); } + + public static List getPassList(JadxDecompiler d) { + return d.getPasses(); + } } diff --git a/jadx-core/src/test/java/jadx/core/deobf/NameMapperTest.java b/jadx-core/src/test/java/jadx/core/deobf/NameMapperTest.java index 5b3c2e4d6..14278a707 100644 --- a/jadx-core/src/test/java/jadx/core/deobf/NameMapperTest.java +++ b/jadx-core/src/test/java/jadx/core/deobf/NameMapperTest.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import static jadx.core.deobf.NameMapper.isValidIdentifier; import static jadx.core.deobf.NameMapper.removeInvalidChars; import static jadx.core.deobf.NameMapper.removeInvalidCharsMiddle; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class NameMapperTest { diff --git a/jadx-core/src/test/java/jadx/core/dex/info/AccessInfoTest.java b/jadx-core/src/test/java/jadx/core/dex/info/AccessInfoTest.java index 9e697ed8b..f0e984ecc 100644 --- a/jadx-core/src/test/java/jadx/core/dex/info/AccessInfoTest.java +++ b/jadx-core/src/test/java/jadx/core/dex/info/AccessInfoTest.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.info.AccessInfo.AFType; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertSame; -import static org.hamcrest.MatcherAssert.assertThat; public class AccessInfoTest { diff --git a/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java b/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java new file mode 100644 index 000000000..53d1a016c --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/dex/visitors/typeinference/TypeCompareTest.java @@ -0,0 +1,128 @@ +package jadx.core.dex.visitors.typeinference; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import jadx.NotYetImplemented; +import jadx.NotYetImplementedExtension; +import jadx.api.JadxArgs; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.RootNode; + +import static jadx.core.dex.instructions.args.ArgType.BOOLEAN; +import static jadx.core.dex.instructions.args.ArgType.CHAR; +import static jadx.core.dex.instructions.args.ArgType.INT; +import static jadx.core.dex.instructions.args.ArgType.NARROW; +import static jadx.core.dex.instructions.args.ArgType.NARROW_INTEGRAL; +import static jadx.core.dex.instructions.args.ArgType.OBJECT; +import static jadx.core.dex.instructions.args.ArgType.UNKNOWN; +import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_ARRAY; +import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_OBJECT; +import static jadx.core.dex.instructions.args.ArgType.array; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@ExtendWith(NotYetImplementedExtension.class) +public class TypeCompareTest { + private TypeCompare compare; + + @BeforeEach + public void init() { + JadxArgs args = new JadxArgs(); + RootNode root = new RootNode(args); + root.load(Collections.emptyList()); + root.initClassPath(); + compare = new TypeCompare(root); + } + + @Test + public void compareTypes() { + firstIsNarrow(INT, UNKNOWN); + + firstIsNarrow(BOOLEAN, INT); + + firstIsNarrow(array(UNKNOWN), UNKNOWN); + firstIsNarrow(array(UNKNOWN), NARROW); + } + + @Test + public void comparePrimitives() { + check(INT, UNKNOWN_OBJECT, TypeCompareEnum.CONFLICT); + check(INT, OBJECT, TypeCompareEnum.CONFLICT); + check(INT, CHAR, TypeCompareEnum.WIDER); + + firstIsNarrow(CHAR, NARROW_INTEGRAL); + firstIsNarrow(array(CHAR), UNKNOWN_OBJECT); + } + + @Test + public void compareArrays() { + firstIsNarrow(array(CHAR), OBJECT); + firstIsNarrow(array(CHAR), array(UNKNOWN)); + + firstIsNarrow(array(OBJECT), OBJECT); + firstIsNarrow(array(OBJECT), array(UNKNOWN_OBJECT)); + + firstIsNarrow(UNKNOWN_ARRAY, OBJECT); + } + + @Test + public void compareGenerics() { + ArgType mapCls = ArgType.object("java.util.Map"); + ArgType setCls = ArgType.object("java.util.Set"); + + ArgType keyType = ArgType.genericType("K"); + ArgType valueType = ArgType.genericType("V"); + ArgType mapGeneric = ArgType.generic(mapCls.getObject(), new ArgType[]{keyType, valueType}); + + check(mapGeneric, mapCls, TypeCompareEnum.NARROW_BY_GENERIC); + check(mapCls, mapGeneric, TypeCompareEnum.WIDER_BY_GENERIC); + + check(mapCls, setCls, TypeCompareEnum.CONFLICT); + } + + @Test + public void compareGenericTypes() { + ArgType vType = ArgType.genericType("V"); + ArgType rType = ArgType.genericType("R"); + + check(vType, ArgType.OBJECT, TypeCompareEnum.NARROW_BY_GENERIC); + check(ArgType.OBJECT, vType, TypeCompareEnum.WIDER_BY_GENERIC); + + check(vType, rType, TypeCompareEnum.CONFLICT); + check(vType, vType, TypeCompareEnum.EQUAL); + + ArgType tType = ArgType.genericType("T"); + tType.setExtendTypes(Collections.singletonList(ArgType.STRING)); + + check(tType, ArgType.STRING, TypeCompareEnum.NARROW_BY_GENERIC); + check(ArgType.STRING, tType, TypeCompareEnum.WIDER_BY_GENERIC); + + check(tType, ArgType.OBJECT, TypeCompareEnum.NARROW_BY_GENERIC); + check(ArgType.OBJECT, tType, TypeCompareEnum.WIDER_BY_GENERIC); + } + + @Test + @NotYetImplemented + public void compareGenericTypesNYI() { + ArgType vType = ArgType.genericType("V"); + // TODO: use extend types from generic declaration for more strict checks + check(vType, ArgType.STRING, TypeCompareEnum.CONFLICT); + check(ArgType.STRING, vType, TypeCompareEnum.CONFLICT); + } + + private void firstIsNarrow(ArgType first, ArgType second) { + check(first, second, TypeCompareEnum.NARROW); + // reverse + check(second, first, TypeCompareEnum.WIDER); + } + + private void check(ArgType first, ArgType second, TypeCompareEnum expectedResult) { + TypeCompareEnum result = compare.compareTypes(first, second); + assertThat("Compare '" + first + "' vs '" + second + '\'', + result, is(expectedResult)); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 7e8562b81..017982355 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -17,17 +17,17 @@ import java.util.jar.JarOutputStream; import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.JadxInternalAccess; -import jadx.core.Jadx; import jadx.core.ProcessClass; import jadx.core.codegen.CodeGen; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.AttrList; +import jadx.core.dex.attributes.IAttributeNode; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; -import jadx.core.utils.exceptions.JadxException; import jadx.core.xmlgen.ResourceStorage; import jadx.core.xmlgen.entry.ResourceEntry; import jadx.tests.api.compiler.DynamicCompiler; @@ -35,13 +35,14 @@ import jadx.tests.api.compiler.StaticCompiler; import jadx.tests.api.utils.TestUtils; import static jadx.core.utils.files.FileUtils.addFileToJar; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -53,7 +54,7 @@ public abstract class IntegrationTest extends TestUtils { /** * Run auto check method if defined: *

-	 *     public static void check()
+	 *     public void check() {}
 	 * 
*/ public static final String CHECK_METHOD_NAME = "check"; @@ -148,34 +149,19 @@ public abstract class IntegrationTest extends TestUtils { } protected void decompile(JadxDecompiler jadx, ClassNode cls) { - List passes = getPassesList(jadx); + List passes = JadxInternalAccess.getPassList(jadx); ProcessClass.process(cls, passes, true); } protected void decompileWithoutUnload(JadxDecompiler jadx, ClassNode cls) { cls.load(); - List passes = getPassesList(jadx); - for (IDexTreeVisitor visitor : passes) { + for (IDexTreeVisitor visitor : JadxInternalAccess.getPassList(jadx)) { DepthTraversal.visit(visitor, cls); } generateClsCode(cls); // don't unload class } - private List getPassesList(JadxDecompiler jadx) { - RootNode root = JadxInternalAccess.getRoot(jadx); - List passesList = Jadx.getPassesList(jadx.getArgs()); - passesList.forEach(pass -> { - try { - pass.init(root); - } catch (JadxException e) { - e.printStackTrace(); - fail(e.getMessage()); - } - }); - return passesList; - } - protected void generateClsCode(ClassNode cls) { try { CodeGen.generate(cls); @@ -186,17 +172,30 @@ public abstract class IntegrationTest extends TestUtils { } protected static void checkCode(ClassNode cls) { - assertTrue( - !cls.contains(AFlag.INCONSISTENT_CODE) && !cls.contains(AType.JADX_ERROR), - "Inconsistent cls: " + cls); + assertFalse(hasErrors(cls), "Inconsistent cls: " + cls); for (MethodNode mthNode : cls.getMethods()) { - assertTrue( - !mthNode.contains(AFlag.INCONSISTENT_CODE) && !mthNode.contains(AType.JADX_ERROR), - "Inconsistent method: " + mthNode); + assertFalse(hasErrors(mthNode), "Method with problems: " + mthNode); } assertThat(cls.getCode().toString(), not(containsString("inconsistent"))); } + private static boolean hasErrors(IAttributeNode node) { + if (node.contains(AFlag.INCONSISTENT_CODE) + || node.contains(AType.JADX_ERROR) + || node.contains(AType.JADX_WARN)) { + return true; + } + AttrList commentsAttr = node.get(AType.COMMENTS); + if (commentsAttr != null) { + for (String comment : commentsAttr.getList()) { + if (comment.contains("JADX WARN")) { + return true; + } + } + } + return false; + } + private void runAutoCheck(String clsName) { try { // run 'check' method from original class @@ -221,7 +220,7 @@ public abstract class IntegrationTest extends TestUtils { return; } try { - checkMth.invoke(origCls.newInstance()); + checkMth.invoke(origCls.getConstructor().newInstance()); } catch (InvocationTargetException ie) { rethrow("Original check failed", ie); } @@ -318,7 +317,7 @@ public abstract class IntegrationTest extends TestUtils { File temp = createTempFile(".jar"); try (JarOutputStream jo = new JarOutputStream(new FileOutputStream(temp))) { for (File file : list) { - addFileToJar(jo, file, path + "/" + file.getName()); + addFileToJar(jo, file, path + '/' + file.getName()); } } return temp; @@ -341,7 +340,7 @@ public abstract class IntegrationTest extends TestUtils { private static File createTempDir(String prefix) throws IOException { File baseDir = new File(System.getProperty("java.io.tmpdir")); - String baseName = prefix + "-" + System.nanoTime(); + String baseName = prefix + '-' + System.nanoTime(); for (int counter = 1; counter < 1000; counter++) { File tempDir = new File(baseDir, baseName + counter); if (tempDir.mkdir()) { @@ -361,7 +360,7 @@ public abstract class IntegrationTest extends TestUtils { File directory = new File(pkgResource.toURI()); String[] files = directory.list(); for (String file : files) { - String fullName = pkgName + "." + file; + String fullName = pkgName + '.' + file; if (fullName.startsWith(clsName)) { list.add(new File(directory, file)); } @@ -438,6 +437,11 @@ public abstract class IntegrationTest extends TestUtils { protected void setOutputCFG() { this.args.setCfgOutput(true); this.args.setRawCFGOutput(true); + } // Use only for debug purpose + + @Deprecated + protected void setOutputRawCFG() { + this.args.setRawCFGOutput(true); } // Use only for debug purpose diff --git a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java index dbc7b1133..5c88cddd1 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -1,8 +1,5 @@ package jadx.tests.api; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.notNullValue; - import java.io.File; import java.util.Arrays; import java.util.Collections; @@ -16,6 +13,9 @@ import org.jf.smali.SmaliOptions; import jadx.api.JadxDecompiler; import jadx.core.dex.nodes.ClassNode; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.notNullValue; + public abstract class SmaliTest extends IntegrationTest { private static final String SMALI_TESTS_PROJECT = "jadx-core"; @@ -44,7 +44,7 @@ public abstract class SmaliTest extends IntegrationTest { protected ClassNode getClassNodeFromSmaliFiles(String pkg, String testName, String clsName) { File outDex = createTempFile(".dex"); compileSmali(outDex, collectSmaliFiles(pkg, testName)); - return getClassNodeFromFile(outDex, pkg + "." + clsName); + return getClassNodeFromFile(outDex, pkg + '.' + clsName); } protected JadxDecompiler loadSmaliFile(String pkg, String smaliFileName) { diff --git a/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java b/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java index d07e1511a..da3c61610 100644 --- a/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java +++ b/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java @@ -22,7 +22,7 @@ public class ClassFileManager extends ForwardingJavaFileManager passes = Jadx.getPassesList(jadx.getArgs()); + private void processByPatterns(JadxDecompiler jadx, String clsPattern, @Nullable String mthPattern) { + List passes = JadxInternalAccess.getPassList(jadx); RootNode root = JadxInternalAccess.getRoot(jadx); int processed = 0; for (ClassNode classNode : root.getClasses(true)) { String clsFullName = classNode.getClassInfo().getFullName(); - if (clsPattern.matcher(clsFullName).matches()) { - if (processCls(jadx, mthPattern, passes, classNode)) { + if (clsFullName.equals(clsPattern)) { + if (processCls(mthPattern, passes, classNode)) { processed++; } } @@ -84,14 +82,14 @@ public abstract class BaseExternalTest extends IntegrationTest { assertThat("No classes processed", processed, greaterThan(0)); } - private boolean processCls(JadxDecompiler jadx, @Nullable Pattern mthPattern, List passes, ClassNode classNode) { + private boolean processCls(@Nullable String mthPattern, List passes, ClassNode classNode) { classNode.load(); boolean decompile = false; if (mthPattern == null) { decompile = true; } else { for (MethodNode mth : classNode.getMethods()) { - if (mthPattern.matcher(mth.getName()).matches()) { + if (isMthMatch(mth, mthPattern)) { decompile = true; break; } @@ -103,9 +101,10 @@ public abstract class BaseExternalTest extends IntegrationTest { try { ProcessClass.process(classNode, passes, true); } catch (Exception e) { - throw new JadxRuntimeException("Codegen failed", e); + throw new JadxRuntimeException("Class process failed", e); } - LOG.warn("\n Print class: {}, {}", classNode.getFullName(), classNode.dex()); + LOG.info("----------------------------------------------------------------"); + LOG.info("Print class: {}, {}", classNode.getFullName(), classNode.dex()); if (mthPattern != null) { printMethods(classNode, mthPattern); } else { @@ -115,21 +114,42 @@ public abstract class BaseExternalTest extends IntegrationTest { return true; } - private void printMethods(ClassNode classNode, @NotNull Pattern mthPattern) { + private boolean isMthMatch(MethodNode mth, String mthPattern) { + String shortId = mth.getMethodInfo().getShortId(); + return isMatch(shortId, mthPattern); + } + + private boolean isMatch(String str, String pattern) { + if (str.equals(pattern)) { + return true; + } + return str.startsWith(pattern); + } + + private void printMethods(ClassNode classNode, @NotNull String mthPattern) { String code = classNode.getCode().getCodeStr(); if (code == null) { return; } + + String dashLine = "======================================================================================"; + Map methodsMap = getMethodsMap(classNode); String[] lines = code.split(CodeWriter.NL); for (MethodNode mth : classNode.getMethods()) { - if (mthPattern.matcher(mth.getName()).matches()) { + if (isMthMatch(mth, mthPattern)) { int decompiledLine = mth.getDecompiledLine() - 1; StringBuilder mthCode = new StringBuilder(); int startLine = getCommentLinesCount(lines, decompiledLine); int brackets = 0; for (int i = startLine; i > 0 && i < lines.length; i++) { + // stop if next method started + MethodNode mthAtLine = methodsMap.get(i); + if (mthAtLine != null && !mthAtLine.equals(mth)) { + break; + } String line = lines[i]; mthCode.append(line).append(CodeWriter.NL); + // also count brackets for detect method end if (i >= decompiledLine) { brackets += StringUtils.countMatches(line, '{'); brackets -= StringUtils.countMatches(line, '}'); @@ -138,11 +158,23 @@ public abstract class BaseExternalTest extends IntegrationTest { } } } - LOG.info("Print method: {}\n{}", mth.getMethodInfo().getShortId(), mthCode); + LOG.info("Print method: {}\n{}\n{}\n{}", mth.getMethodInfo().getShortId(), + dashLine, + mthCode, + dashLine + ); } } } + public Map getMethodsMap(ClassNode classNode) { + Map linesMap = new HashMap<>(); + for (MethodNode method : classNode.getMethods()) { + linesMap.put(method.getDecompiledLine() - 1, method); + } + return linesMap; + } + protected int getCommentLinesCount(String[] lines, int line) { for (int i = line - 1; i > 0 && i < lines.length; i--) { String str = lines[i]; diff --git a/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java b/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java index 847f75afd..e9911980e 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/JadxClasspathTest.java @@ -1,12 +1,5 @@ package jadx.tests.functional; -import static jadx.core.dex.instructions.args.ArgType.STRING; -import static jadx.core.dex.instructions.args.ArgType.object; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.io.IOException; import org.junit.jupiter.api.BeforeEach; @@ -18,6 +11,13 @@ import jadx.core.dex.nodes.DexNode; import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.DecodeException; +import static jadx.core.dex.instructions.args.ArgType.STRING; +import static jadx.core.dex.instructions.args.ArgType.object; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class JadxClasspathTest { private static final String JAVA_LANG_EXCEPTION = "java.lang.Exception"; diff --git a/jadx-core/src/test/java/jadx/tests/functional/JadxVisitorsOrderTest.java b/jadx-core/src/test/java/jadx/tests/functional/JadxVisitorsOrderTest.java index 4e433b1ee..afeeb9b01 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/JadxVisitorsOrderTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/JadxVisitorsOrderTest.java @@ -14,8 +14,8 @@ import jadx.core.Jadx; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.JadxVisitor; -import static org.hamcrest.Matchers.empty; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; public class JadxVisitorsOrderTest { @@ -40,16 +40,19 @@ public class JadxVisitorsOrderTest { List errors = new ArrayList<>(); Set names = new HashSet<>(); + Set> passClsSet = new HashSet<>(); for (int i = 0; i < passes.size(); i++) { IDexTreeVisitor pass = passes.get(i); - JadxVisitor info = pass.getClass().getAnnotation(JadxVisitor.class); + Class passClass = pass.getClass(); + JadxVisitor info = passClass.getAnnotation(JadxVisitor.class); if (info == null) { - LOG.warn("No JadxVisitor annotation for visitor: {}", pass.getClass().getName()); + LOG.warn("No JadxVisitor annotation for visitor: {}", passClass.getName()); continue; } - String passName = pass.getClass().getSimpleName(); - if (!names.add(passName)) { - errors.add("Visitor name conflict: " + passName + ", class: " + pass.getClass().getName()); + boolean firstOccurrence = passClsSet.add(passClass); + String passName = passClass.getSimpleName(); + if (firstOccurrence && !names.add(passName)) { + errors.add("Visitor name conflict: " + passName + ", class: " + passClass.getName()); } for (Class cls : info.runBefore()) { if (classList.indexOf(cls) < i) { diff --git a/jadx-core/src/test/java/jadx/tests/functional/StringUtilsTest.java b/jadx-core/src/test/java/jadx/tests/functional/StringUtilsTest.java index bd3283c21..34fef46a9 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/StringUtilsTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/StringUtilsTest.java @@ -32,7 +32,7 @@ class StringUtilsTest { } private void checkStringUnescape(String input, String result) { - assertThat(stringUtils.unescapeString(input), is("\"" + result + "\"")); + assertThat(stringUtils.unescapeString(input), is('"' + result + '"')); } @Test @@ -47,6 +47,6 @@ class StringUtilsTest { } private void checkCharUnescape(char input, String result) { - assertThat(stringUtils.unescapeChar(input), is("'" + result + "'")); + assertThat(stringUtils.unescapeChar(input), is('\'' + result + '\'')); } } diff --git a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java index 74b4c357a..b6e39144f 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.Test; import jadx.core.export.TemplateFile; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; public class TemplateFileTest { diff --git a/jadx-core/src/test/java/jadx/tests/functional/TestIfCondition.java b/jadx-core/src/test/java/jadx/tests/functional/TestIfCondition.java index 3f3235f69..b74782976 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/TestIfCondition.java +++ b/jadx-core/src/test/java/jadx/tests/functional/TestIfCondition.java @@ -19,8 +19,8 @@ import static jadx.core.dex.regions.conditions.IfCondition.Mode.OR; import static jadx.core.dex.regions.conditions.IfCondition.merge; import static jadx.core.dex.regions.conditions.IfCondition.not; import static jadx.core.dex.regions.conditions.IfCondition.simplify; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestIfCondition { diff --git a/jadx-core/src/test/java/jadx/tests/functional/TypeMergeTest.java b/jadx-core/src/test/java/jadx/tests/functional/TypeMergeTest.java deleted file mode 100644 index ab99dcc4d..000000000 --- a/jadx-core/src/test/java/jadx/tests/functional/TypeMergeTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package jadx.tests.functional; - -import static jadx.core.dex.instructions.args.ArgType.BOOLEAN; -import static jadx.core.dex.instructions.args.ArgType.BYTE; -import static jadx.core.dex.instructions.args.ArgType.CHAR; -import static jadx.core.dex.instructions.args.ArgType.INT; -import static jadx.core.dex.instructions.args.ArgType.LONG; -import static jadx.core.dex.instructions.args.ArgType.NARROW; -import static jadx.core.dex.instructions.args.ArgType.OBJECT; -import static jadx.core.dex.instructions.args.ArgType.STRING; -import static jadx.core.dex.instructions.args.ArgType.UNKNOWN; -import static jadx.core.dex.instructions.args.ArgType.UNKNOWN_OBJECT; -import static jadx.core.dex.instructions.args.ArgType.array; -import static jadx.core.dex.instructions.args.ArgType.genericType; -import static jadx.core.dex.instructions.args.ArgType.object; -import static jadx.core.dex.instructions.args.ArgType.unknown; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.IOException; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import jadx.core.clsp.ClspGraph; -import jadx.core.dex.instructions.args.ArgType; -import jadx.core.dex.instructions.args.PrimitiveType; -import jadx.core.dex.nodes.DexNode; -import jadx.core.dex.nodes.RootNode; -import jadx.core.utils.exceptions.DecodeException; - -public class TypeMergeTest { - - private DexNode dex; - - @BeforeEach - public void initClsp() throws IOException, DecodeException { - ClspGraph clsp = new ClspGraph(); - clsp.load(); - dex = mock(DexNode.class); - RootNode rootNode = mock(RootNode.class); - when(rootNode.getClsp()).thenReturn(clsp); - when(dex.root()).thenReturn(rootNode); - } - - @Test - public void testMerge() throws IOException, DecodeException { - first(INT, INT); - first(BOOLEAN, INT); - reject(INT, LONG); - first(INT, UNKNOWN); - reject(INT, UNKNOWN_OBJECT); - - first(INT, NARROW); - first(CHAR, INT); - - check(unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN, PrimitiveType.FLOAT), - unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN), - unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN)); - - check(unknown(PrimitiveType.INT, PrimitiveType.FLOAT), - unknown(PrimitiveType.INT, PrimitiveType.BOOLEAN), - INT); - - check(unknown(PrimitiveType.INT, PrimitiveType.OBJECT), - unknown(PrimitiveType.OBJECT, PrimitiveType.ARRAY), - unknown(PrimitiveType.OBJECT)); - - ArgType objExc = object("java.lang.Exception"); - ArgType objThr = object("java.lang.Throwable"); - ArgType objIO = object("java.io.IOException"); - ArgType objArr = object("java.lang.ArrayIndexOutOfBoundsException"); - ArgType objList = object("java.util.List"); - - first(objExc, objExc); - check(objExc, objList, OBJECT); - first(objExc, OBJECT); - - check(objExc, objThr, objThr); - check(objIO, objArr, objExc); - - ArgType generic = genericType("T"); - first(generic, objExc); - } - - @Test - public void testArrayMerge() { - check(array(INT), array(BYTE), ArgType.OBJECT); - first(array(INT), array(INT)); - first(array(STRING), array(STRING)); - - first(OBJECT, array(INT)); - first(OBJECT, array(STRING)); - - first(array(unknown(PrimitiveType.INT, PrimitiveType.FLOAT)), unknown(PrimitiveType.ARRAY)); - } - - private void first(ArgType t1, ArgType t2) { - check(t1, t2, t1); - } - - private void reject(ArgType t1, ArgType t2) { - check(t1, t2, null); - } - - private void check(ArgType t1, ArgType t2, ArgType exp) { - merge(t1, t2, exp); - merge(t2, t1, exp); - } - - private void merge(ArgType t1, ArgType t2, ArgType exp) { - ArgType res = ArgType.merge(dex, t1, t2); - String msg = format(t1, t2, exp, res); - if (exp == null) { - assertNull(res, "Incorrect accept: " + msg); - } else { - assertNotNull(res, "Incorrect reject: " + msg); - assertTrue(exp.equals(res), "Incorrect result: " + msg); - } - } - - private String format(ArgType t1, ArgType t2, ArgType exp, ArgType res) { - return t1 + " <+> " + t2 + " = '" + res + "', expected: " + exp; - } -} diff --git a/jadx-core/src/test/java/jadx/tests/integration/TestDuplicateCast.java b/jadx-core/src/test/java/jadx/tests/integration/TestDuplicateCast.java index c1b04ee94..147e6fab1 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/TestDuplicateCast.java +++ b/jadx-core/src/test/java/jadx/tests/integration/TestDuplicateCast.java @@ -12,10 +12,10 @@ import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; /** diff --git a/jadx-core/src/test/java/jadx/tests/integration/TestFloatValue.java b/jadx-core/src/test/java/jadx/tests/integration/TestFloatValue.java index fd9c96420..84c944a7f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/TestFloatValue.java +++ b/jadx-core/src/test/java/jadx/tests/integration/TestFloatValue.java @@ -8,6 +8,7 @@ import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestFloatValue extends IntegrationTest { @@ -17,6 +18,10 @@ public class TestFloatValue extends IntegrationTest { fa[0] /= 2; return fa; } + + public void check() { + assertEquals(0.275f, method()[0], 0.0001f); + } } @Test diff --git a/jadx-core/src/test/java/jadx/tests/integration/TestRedundantBrackets.java b/jadx-core/src/test/java/jadx/tests/integration/TestRedundantBrackets.java index 3931ff252..e9c2e8bca 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/TestRedundantBrackets.java +++ b/jadx-core/src/test/java/jadx/tests/integration/TestRedundantBrackets.java @@ -17,7 +17,10 @@ public class TestRedundantBrackets extends IntegrationTest { } public int method2(Object obj) { - return obj instanceof String ? ((String) obj).length() : 0; + if (obj instanceof String) { + return ((String) obj).length(); + } + return 0; } public int method3(int a, int b) { @@ -50,7 +53,8 @@ public class TestRedundantBrackets extends IntegrationTest { assertThat(code, not(containsString("(-1)"))); assertThat(code, not(containsString("return;"))); - assertThat(code, containsString("return obj instanceof String ? ((String) obj).length() : 0;")); + assertThat(code, containsString("if (obj instanceof String) {")); + assertThat(code, containsString("return ((String) obj).length();")); assertThat(code, containsString("a + b < 10")); assertThat(code, containsString("(a & b) != 0")); assertThat(code, containsString("if (num == 4 || num == 6 || num == 8 || num == 10)")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/TestReturnWrapping.java b/jadx-core/src/test/java/jadx/tests/integration/TestReturnWrapping.java index 911ad5668..c9e87efd5 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/TestReturnWrapping.java +++ b/jadx-core/src/test/java/jadx/tests/integration/TestReturnWrapping.java @@ -54,7 +54,11 @@ public class TestReturnWrapping extends IntegrationTest { assertThat(code, containsString("return 255;")); assertThat(code, containsString("return arg0 + 1;")); - assertThat(code, containsString("return i > 128 ? arg0.toString() + ret.toString() : Integer.valueOf(i);")); + + // TODO: reduce code vars by name +// assertThat(code, containsString("return i > 128 ? arg0.toString() + ret.toString() : Integer.valueOf(i);")); + assertThat(code, containsString("return i2 > 128 ? arg0.toString() + ret.toString() : Integer.valueOf(i2);")); + assertThat(code, containsString("return arg0 + 2;")); assertThat(code, containsString("arg0 -= 951;")); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/SimplifyVisitorStringBuilderTest.java b/jadx-core/src/test/java/jadx/tests/integration/TestStringBuilderElimination2.java similarity index 69% rename from jadx-core/src/test/java/jadx/tests/integration/SimplifyVisitorStringBuilderTest.java rename to jadx-core/src/test/java/jadx/tests/integration/TestStringBuilderElimination2.java index 272993b82..f3ce0d525 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/SimplifyVisitorStringBuilderTest.java +++ b/jadx-core/src/test/java/jadx/tests/integration/TestStringBuilderElimination2.java @@ -1,10 +1,10 @@ package jadx.tests.integration; +import org.junit.jupiter.api.Test; + import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.visitors.SimplifyVisitor; -import jadx.core.utils.exceptions.JadxException; import jadx.tests.api.IntegrationTest; -import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -14,20 +14,18 @@ import static org.hamcrest.MatcherAssert.assertThat; * * @author Jan Peter Stotz */ -public class SimplifyVisitorStringBuilderTest extends IntegrationTest { +public class TestStringBuilderElimination2 extends IntegrationTest { public static class TestCls1 { public String test() { - return new StringBuilder("[init]").append("a1").append('c').append(2).append(0l).append(1.0f). + return new StringBuilder("[init]").append("a1").append('c').append(2).append(0L).append(1.0f). append(2.0d).append(true).toString(); } } @Test - public void test1() throws JadxException { - ClassNode cls = getClassNode(SimplifyVisitorStringBuilderTest.TestCls1.class); - SimplifyVisitor visitor = new SimplifyVisitor(); - visitor.visit(cls); + public void test1() { + ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls1.class); String code = cls.getCode().toString(); assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 2 + 0 + 1.0f + 2.0d + true;")); } @@ -49,10 +47,8 @@ public class SimplifyVisitorStringBuilderTest extends IntegrationTest { } @Test - public void test2() throws JadxException { - ClassNode cls = getClassNode(SimplifyVisitorStringBuilderTest.TestCls2.class); - SimplifyVisitor visitor = new SimplifyVisitor(); - visitor.visit(cls); + public void test2() { + ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls2.class); String code = cls.getCode().toString(); assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 1 + 2 + 1.0f + 2.0d + true;")); } @@ -68,10 +64,8 @@ public class SimplifyVisitorStringBuilderTest extends IntegrationTest { } @Test - public void test3() throws JadxException { - ClassNode cls = getClassNode(SimplifyVisitorStringBuilderTest.TestClsStringUtilsReverse.class); - SimplifyVisitor visitor = new SimplifyVisitor(); - visitor.visit(cls); + public void test3() { + ClassNode cls = getClassNode(TestClsStringUtilsReverse.class); String code = cls.getCode().toString(); assertThat(code, containsString("return new StringBuilder(str).reverse().toString();")); } @@ -84,10 +78,8 @@ public class SimplifyVisitorStringBuilderTest extends IntegrationTest { } @Test - public void testChainWithDelete() throws JadxException { + public void testChainWithDelete() { ClassNode cls = getClassNode(TestClsChainWithDelete.class); - SimplifyVisitor visitor = new SimplifyVisitor(); - visitor.visit(cls); String code = cls.getCode().toString(); assertThat(code, containsString("return new StringBuilder(\"[init]\").append(\"a1\").delete(1, 2).toString();")); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsMix.java b/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsMix.java new file mode 100644 index 000000000..5b26b31bb --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/annotations/TestAnnotationsMix.java @@ -0,0 +1,123 @@ +package jadx.tests.integration.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import jadx.NotYetImplemented; +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestAnnotationsMix extends IntegrationTest { + + public static class TestCls { + + public boolean test() throws Exception { + Class cls = TestCls.class; + new Thread(); + + Method err = cls.getMethod("error"); + assertTrue(err.getExceptionTypes().length > 0); + assertSame(err.getExceptionTypes()[0], Exception.class); + + Method d = cls.getMethod("depr", String[].class); + assertTrue(d.getAnnotations().length > 0); + assertSame(d.getAnnotations()[0].annotationType(), Deprecated.class); + + Method ma = cls.getMethod("test", String[].class); + assertTrue(ma.getAnnotations().length > 0); + MyAnnotation a = (MyAnnotation) ma.getAnnotations()[0]; + assertEquals(7, a.num()); + assertSame(a.state(), Thread.State.TERMINATED); + return true; + } + + @Deprecated + public int a; + + public void error() throws Exception { + throw new Exception("error"); + } + + @Deprecated + public static Object depr(String[] a) { + return Arrays.asList(a); + } + + @MyAnnotation(name = "b", + num = 7, + cls = Exception.class, + doubles = {0.0, 1.1}, + value = 9.87f, + simple = @SimpleAnnotation(false)) + public static Object test(String[] a) { + return Arrays.asList(a); + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface MyAnnotation { + String name() default "a"; + + String str() default "str"; + + int num(); + + float value(); + + double[] doubles(); + + Class cls(); + + SimpleAnnotation simple(); + + Thread.State state() default Thread.State.TERMINATED; + } + + public @interface SimpleAnnotation { + boolean value(); + } + + public void check() throws Exception { + test(); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("int i = false;"))); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } + + @Test + @NotYetImplemented + public void testNYI() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("Thread thread = new Thread();"))); + assertThat(code, containsString("new Thread();")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith.java index 7aca8445b..a2618978f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith.java @@ -5,20 +5,23 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; - public class TestArith extends IntegrationTest { public static class TestCls { - public void method(int a) { + public int test(int a) { a += 2; + use(a); + return a; } - public void method2(int a) { + public int test2(int a) { a++; + use(a); + return a; } + + private static void use(int i) {} } @Test @@ -26,7 +29,19 @@ public class TestArith extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - assertThat(code, containsString("a += 2;")); - assertThat(code, containsString("a++;")); + // TODO: reduce code vars by name +// assertThat(code, containsString("a += 2;")); +// assertThat(code, containsString("a++;")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + // TODO: simplify for variables without debug names +// assertThat(code, containsString("i += 2;")); +// assertThat(code, containsString("i++;")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith3.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith3.java index 17cbbc962..7132e0699 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArith3.java @@ -39,5 +39,15 @@ public class TestArith3 extends IntegrationTest { assertThat(code, containsOne("while (n + 4 < buffer.length) {")); assertThat(code, containsOne("n += len + 5;")); assertThat(code, not(containsString("; n += len + 5) {"))); + assertThat(code, not(containsString("default:"))); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("while (")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArithNot.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArithNot.java index 7931ec598..9eedade4f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestArithNot.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestArithNot.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.Matchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; public class TestArithNot extends SmaliTest { /* diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement2.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement2.java index 95d865eda..72820efba 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestFieldIncrement2.java @@ -1,14 +1,14 @@ package jadx.tests.integration.arith; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; - import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + public class TestFieldIncrement2 extends IntegrationTest { public static class TestCls { @@ -33,7 +33,7 @@ public class TestFieldIncrement2 extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, containsString("this.a.f += n;")); - assertThat(code, containsString("a.f *= n;")); + assertThat(code, containsString("a2.f *= n;")); } @Test diff --git a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill2.java b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill2.java index 70a75cba3..1ef487398 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrayFill2.java @@ -1,14 +1,14 @@ package jadx.tests.integration.arrays; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; - import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + public class TestArrayFill2 extends IntegrationTest { public static class TestCls { @@ -41,5 +41,4 @@ public class TestArrayFill2 extends IntegrationTest { assertThat(code, containsString("return new int[]{1, a++, a * 2};")); } - } diff --git a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays3.java b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays3.java index 72c1a8bc3..28d10cd7a 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays3.java @@ -8,6 +8,7 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestArrays3 extends IntegrationTest { public static class TestCls { @@ -17,12 +18,23 @@ public class TestArrays3 extends IntegrationTest { } public void check() { - assertThat(test(new byte[]{1, 2}), instanceOf(Object[].class)); + byte[] inputArr = {1, 2}; + Object result = test(inputArr); + assertThat(result, instanceOf(Object[].class)); + assertThat(((Object[]) result)[0], is(inputArr)); } } @Test public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("return new Object[]{bArr};")); + } + + @Test + public void testNoDebug() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays4.java b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays4.java index 1b1091f89..9a8b768e4 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestArrays4.java @@ -1,34 +1,35 @@ package jadx.tests.integration.arrays; -import jadx.core.dex.nodes.ClassNode; -import jadx.tests.api.SmaliTest; -import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.Test; -public class TestArrays4 extends SmaliTest { +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; - public static class TestCls { - char[] payload; +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; - public TestCls(byte[] bytes) { - char[] a = toChars(bytes); - this.payload = new char[a.length]; - System.arraycopy(a, 0, this.payload, 0, bytes.length); - } +public class TestArrays4 extends IntegrationTest { - private static char[] toChars(byte[] bArr) { - return new char[bArr.length]; - } - } + public static class TestCls { + char[] payload; - @Test - public void testArrayTypeInference() { - noDebugInfo(); - ClassNode cls = getClassNode(TestCls.class); - String code = cls.getCode().toString(); + public TestCls(byte[] bytes) { + char[] a = toChars(bytes); + this.payload = new char[a.length]; + System.arraycopy(a, 0, this.payload, 0, bytes.length); + } - assertThat(code, containsOne("char[] toChars = toChars(bArr);")); - } + private static char[] toChars(byte[] bArr) { + return new char[bArr.length]; + } + } + @Test + public void testArrayTypeInference() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("char[] chars = toChars(bArr);")); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestMultiDimArrayFill.java b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestMultiDimArrayFill.java new file mode 100644 index 000000000..c4f9fcb21 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestMultiDimArrayFill.java @@ -0,0 +1,42 @@ +package jadx.tests.integration.arrays; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestMultiDimArrayFill extends IntegrationTest { + + public static class TestCls { + + public static Obj test(int a, int b) { + return new Obj( + new int[][]{ + new int[]{1}, + new int[]{2}, + {3}, + new int[]{4, 5}, + new int[0] + }, + new int[]{a, a, a, a, b} + ); + } + + private static class Obj { + public Obj(int[][] ints, int[] ints2) {} + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("return new Obj(" + + "new int[][]{new int[]{1}, new int[]{2}, new int[]{3}, new int[]{4, 5}, new int[0]}, " + + "new int[]{a, a, a, a, b});")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf2.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf2.java new file mode 100644 index 000000000..8b0609c42 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf2.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.conditions; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestComplexIf2 extends SmaliTest { + +/* + public void test() { + if (this.isSaved) { + throw new RuntimeException("Error"); + } + if (LoaderUtils.isContextLoaderAvailable()) { + this.savedContextLoader = LoaderUtils.getContextClassLoader(); + ClassLoader loader = this; + if (this.project != null && "simple".equals(this.project)) { + loader = getClass().getClassLoader(); + } + LoaderUtils.setContextClassLoader(loader); + this.isSaved = true; + } + } +*/ + + @Test + public void test() { + disableCompilation(); + ClassNode cls = getClassNodeFromSmaliWithPkg("conditions", "TestComplexIf2"); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("if (this.project != null && \"simple\".equals(this.project)) {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditionInLoop.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditionInLoop.java new file mode 100644 index 000000000..a369f6ef0 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditionInLoop.java @@ -0,0 +1,52 @@ +package jadx.tests.integration.conditions; + +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.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class TestConditionInLoop extends IntegrationTest { + + public static class TestCls { + private static int test(int a, int b) { + int c = a + b; + for (int i = a; i < b; i++) { + if (i == 7) { + c += 2; + } else { + c *= 2; + } + } + c--; + return c; + } + + public void check() { + assertThat(test(5, 9), is(115)); + assertThat(test(8, 23), is(1015807)); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("for (int i = a; i < b; i++) {")); + assertThat(code, containsOne("c += 2;")); + assertThat(code, containsOne("c *= 2;")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("while")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions11.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions11.java index f1642c5ac..bb9b6104d 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions11.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions11.java @@ -33,6 +33,5 @@ public class TestConditions11 extends IntegrationTest { assertThat(code, containsOne("f();")); assertThat(code, not(containsString("return"))); assertThat(code, not(containsString("else"))); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions12.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions12.java index b1a94bbba..2d386159d 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions12.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions12.java @@ -65,6 +65,5 @@ public class TestConditions12 extends IntegrationTest { assertThat(code, containsOne("if (quality >= 30 && autoStop) {")); assertThat(code, containsOne("if (!autoStop && lastValidRaw > -1 && quality < 10) {")); assertThat(code, not(containsString("return"))); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions13.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions13.java index b840025ad..7c67f1a69 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions13.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions13.java @@ -37,6 +37,5 @@ public class TestConditions13 extends IntegrationTest { assertThat(code, containsOne("qualityReading = false;")); assertThat(code, containsOne("} else if (raw == 0 || quality < 6 || !qualityReading) {")); assertThat(code, not(containsString("return"))); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions14.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions14.java index e9a8b5b64..df524fd15 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions14.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions14.java @@ -30,6 +30,5 @@ public class TestConditions14 extends IntegrationTest { assertThat(code, containsOne("boolean r = a == null ? b != null : !a.equals(b);")); assertThat(code, containsOne("if (r) {")); assertThat(code, containsOne("System.out.println(\"1\");")); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions15.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions15.java index 603307034..030c79e3e 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions15.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions15.java @@ -64,6 +64,5 @@ public class TestConditions15 extends IntegrationTest { assertThat(code, containsOne("\"1\".equals(name)")); assertThat(code, containsOne("\"30\".equals(name)")); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions16.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions16.java index afcc453c4..d584f22d1 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions16.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions16.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestConditions16 extends IntegrationTest { @@ -32,6 +32,5 @@ public class TestConditions16 extends IntegrationTest { // assertThat(code, containsOne("return a < 0 || (b % 2 != 0 && a > 28) || b < 0;")); assertThat(code, containsOne("return a < 0 || ((b % 2 != 0 && a > 28) || b < 0);")); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions5.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions5.java index 607004ac9..c7b8281ef 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestConditions5.java @@ -12,7 +12,7 @@ import static org.hamcrest.MatcherAssert.assertThat; public class TestConditions5 extends IntegrationTest { public static class TestCls { - public static void assertEquals(Object a1, Object a2) { + public static void test(Object a1, Object a2) { if (a1 == null) { if (a2 != null) { throw new AssertionError(a1 + " != " + a2); @@ -22,7 +22,7 @@ public class TestConditions5 extends IntegrationTest { } } - public static void assertEquals2(Object a1, Object a2) { + public static void test2(Object a1, Object a2) { if (a1 != null) { if (!a1.equals(a2)) { throw new AssertionError(a1 + " != " + a2); diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf.java index 3928e6ca3..148bdb608 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestNestedIf.java @@ -42,6 +42,5 @@ public class TestNestedIf extends IntegrationTest { assertThat(code, countString(2, "return false;")); assertThat(code, containsOne("test1();")); assertThat(code, containsOne("return true;")); - } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary.java index fc17b5ddb..c49624612 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary.java @@ -22,7 +22,7 @@ public class TestTernary extends IntegrationTest { } public int test3(int a) { - return a > 0 ? 1 : (a + 2) * 3; + return a > 0 ? a : (a + 2) * 3; } } @@ -34,6 +34,6 @@ public class TestTernary extends IntegrationTest { assertThat(code, not(containsString("else"))); assertThat(code, containsString("return a != 2;")); assertThat(code, containsString("assertTrue(a == 3)")); - assertThat(code, containsString("return a > 0 ? 1 : (a + 2) * 3;")); + assertThat(code, containsString("return a > 0 ? a : (a + 2) * 3;")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary2.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary2.java index 3382284a7..4080695d3 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestTernary2.java @@ -1,27 +1,32 @@ package jadx.tests.integration.conditions; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class TestTernary2 extends IntegrationTest { public static class TestCls { public void test() { - assertTrue(f(1, 0) == 0); + checkFalse(f(1, 0) == 0); } private int f(int a, int b) { return a + b; } + + private void checkFalse(boolean b) { + if (b) { + throw new AssertionError("Must be false"); + } + } } @Test @@ -29,7 +34,6 @@ public class TestTernary2 extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - assertEquals(1, count(code, "assertTrue")); assertEquals(1, count(code, "f(1, 0)")); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java index 330ce08b0..703fccdfc 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java +++ b/jadx-core/src/test/java/jadx/tests/integration/debuginfo/TestLineNumbers.java @@ -10,8 +10,8 @@ import jadx.core.dex.nodes.MethodNode; import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestLineNumbers extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums.java index eb1b066f1..54accdbe2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums.java @@ -46,10 +46,10 @@ public class TestEnums extends IntegrationTest { assertThat(code, containsLines(1, "public enum EmptyEnum {", "}")); assertThat(code, containsLines(1, "public enum EmptyEnum2 {", - indent(1) + ";", + indent(1) + ';', "", indent(1) + "public static void mth() {", - indent(1) + "}", + indent(1) + '}', "}")); assertThat(code, containsLines(1, "public enum Direction {", @@ -64,7 +64,7 @@ public class TestEnums extends IntegrationTest { "", indent(1) + "public String test() {", indent(2) + "return \"\";", - indent(1) + "}", + indent(1) + '}', "}")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2.java index 11f65a0e1..2aafe0245 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums2.java @@ -38,12 +38,12 @@ public class TestEnums2 extends IntegrationTest { indent(1) + "PLUS {", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x + y;", - indent(2) + "}", + indent(2) + '}', indent(1) + "},", indent(1) + "MINUS {", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x - y;", - indent(2) + "}", + indent(2) + '}', indent(1) + "};", "", indent(1) + "public abstract int apply(int i, int i2);", diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums3.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums3.java index 2384d53f7..e55d7118b 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums3.java @@ -7,7 +7,7 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestEnums3 extends IntegrationTest { @@ -30,9 +30,9 @@ public class TestEnums3 extends IntegrationTest { } public void check() { - assertTrue(Numbers.ONE.getNum() == 1); - assertTrue(Numbers.THREE.getNum() == 3); - assertTrue(Numbers.FOUR.getNum() == 4); + assertEquals(1, Numbers.ONE.getNum()); + assertEquals(3, Numbers.THREE.getNum()); + assertEquals(4, Numbers.FOUR.getNum()); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums4.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums4.java index 1fc4be97f..0fb338610 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnums4.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestEnums4 extends IntegrationTest { @@ -24,8 +24,8 @@ public class TestEnums4 extends IntegrationTest { private final String[] exts; - private ResType(String... exts) { - this.exts = exts; + private ResType(String... extensions) { + this.exts = extensions; } public String[] getExts() { @@ -44,7 +44,7 @@ public class TestEnums4 extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, containsOne("CODE(\".dex\", \".class\"),")); - assertThat(code, containsOne("ResType(String... exts) {")); + assertThat(code, containsOne("ResType(String... extensions) {")); // assertThat(code, not(containsString("private ResType"))); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsInterface.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsInterface.java index 62a09be8b..1e3d44a8e 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsInterface.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestEnumsInterface.java @@ -40,13 +40,13 @@ public class TestEnumsInterface extends IntegrationTest { indent(1) + "PLUS {", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x + y;", - indent(2) + "}", + indent(2) + '}', indent(1) + "},", indent(1) + "MINUS {", indent(2) + "public int apply(int x, int y) {", indent(3) + "return x - y;", - indent(2) + "}", - indent(1) + "}", + indent(2) + '}', + indent(1) + '}', "}" )); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java index ec4ae9e89..10fa01f09 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchOverEnum extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum2.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum2.java index 2af797122..7115a3f77 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum2.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchOverEnum2 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackMode.java b/jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackMode.java index b10af3cf0..aff039140 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackMode.java +++ b/jadx-core/src/test/java/jadx/tests/integration/fallback/TestFallbackMode.java @@ -5,6 +5,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; @@ -30,8 +31,10 @@ public class TestFallbackMode extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, containsString("public int test(int r2) {")); - assertThat(code, containsString("r1 = this;")); - assertThat(code, containsString("L_0x0004:")); + assertThat(code, containsOne("r1 = this;")); + assertThat(code, containsOne("L_0x0000:")); + assertThat(code, containsOne("L_0x0007:")); + assertThat(code, containsOne("int r2 = r2 + 1")); assertThat(code, not(containsString("throw new UnsupportedOperationException"))); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/generics/MissingGenericsTypesTest.java b/jadx-core/src/test/java/jadx/tests/integration/generics/MissingGenericsTypesTest.java index 35e139d65..c1521da29 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/generics/MissingGenericsTypesTest.java +++ b/jadx-core/src/test/java/jadx/tests/integration/generics/MissingGenericsTypesTest.java @@ -1,14 +1,14 @@ package jadx.tests.integration.generics; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; - import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + public class MissingGenericsTypesTest extends SmaliTest { /* diff --git a/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics2.java b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics2.java index 7d5f379f5..427a7666c 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics2.java @@ -6,6 +6,7 @@ import java.util.Map; import org.junit.jupiter.api.Test; +import jadx.NotYetImplemented; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; @@ -18,9 +19,9 @@ public class TestGenerics2 extends IntegrationTest { private static class ItemReference extends WeakReference { private Object id; - public ItemReference(V item, Object id, ReferenceQueue queue) { + public ItemReference(V item, Object objId, ReferenceQueue queue) { super(item, queue); - this.id = id; + this.id = objId; } } @@ -29,7 +30,10 @@ public class TestGenerics2 extends IntegrationTest { public V get(Object id) { WeakReference ref = this.items.get(id); - return (ref != null) ? ref.get() : null; + if (ref != null) { + return ref.get(); + } + return null; } } } @@ -39,9 +43,19 @@ public class TestGenerics2 extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - assertThat(code, containsString("public ItemReference(V item, Object id, ReferenceQueue queue) {")); + assertThat(code, containsString("public ItemReference(V item, Object objId, ReferenceQueue queue) {")); assertThat(code, containsString("public V get(Object id) {")); assertThat(code, containsString("WeakReference ref = ")); - assertThat(code, containsString("return ref != null ? ref.get() : null;")); + assertThat(code, containsString("return ref.get();")); + } + + @Test + @NotYetImplemented("Make generic info propagation for methods (like Map.get)") + public void testDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("WeakReference ref = ")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics6.java b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics6.java index 4ef2966d7..51d09927b 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics6.java +++ b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenerics6.java @@ -25,7 +25,7 @@ public class TestGenerics6 extends IntegrationTest { } } - private interface I { + private interface I { void f(); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericsInArgs.java b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericsInArgs.java new file mode 100644 index 000000000..8fdfdeec0 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/generics/TestGenericsInArgs.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.generics; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.IntegrationTest; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +public class TestGenericsInArgs extends IntegrationTest { + + public static class TestCls { + public static void test(List genericList, Set set) { + if (genericList == null) { + throw new RuntimeException("list is null"); + } + if (set == null) { + throw new RuntimeException("set is null"); + } + genericList.clear(); + use(genericList); + set.clear(); + } + + private static void use(List l) { + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("public static void test(List genericList, Set set) {")); + assertThat(code, containsString("if (genericList == null) {")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("public static void test(List list, Set set) {")); + assertThat(code, containsString("if (list == null) {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/inline/TestInlineInLoop.java b/jadx-core/src/test/java/jadx/tests/integration/inline/TestInlineInLoop.java index dc7ceaaf0..2efd2e74a 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inline/TestInlineInLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inline/TestInlineInLoop.java @@ -6,13 +6,12 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static jadx.tests.api.utils.JadxMatchers.countString; import static org.hamcrest.MatcherAssert.assertThat; public class TestInlineInLoop extends IntegrationTest { public static class TestCls { - public static void main(String[] args) throws Exception { + public static void main(String[] args) { int a = 0; int b = 4; int c = 0; @@ -37,11 +36,12 @@ public class TestInlineInLoop extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - assertThat(code, containsOne("int c")); - assertThat(code, containsOne("c = b + 1")); - assertThat(code, countString(2, "c = b;")); - assertThat(code, containsOne("b++;")); - assertThat(code, containsOne("b = c")); + // TODO: remove unused variables from test + assertThat(code, containsOne("int c = b + 1")); + assertThat(code, containsOne("int c2 = b;")); + assertThat(code, containsOne("int c3 = b;")); + assertThat(code, containsOne("int b2 = b + 1;")); + assertThat(code, containsOne("b = c3")); assertThat(code, containsOne("a++;")); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass10.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass10.java index afda095ee..bb7a07abd 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass10.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass10.java @@ -8,9 +8,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass10 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass11.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass11.java index 34b027b22..70cafc6ec 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass11.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass11.java @@ -8,9 +8,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass11 extends IntegrationTest { @@ -36,7 +36,6 @@ public class TestAnonymousClass11 extends IntegrationTest { } public abstract void m(); - } private void func(A a) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass4.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass4.java index 6c53a59a3..62c74eedf 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass4.java @@ -37,9 +37,9 @@ public class TestAnonymousClass4 extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, containsOne(indent(3) + "new Thread() {")); - assertThat(code, containsOne(indent(4) + "{")); + assertThat(code, containsOne(indent(4) + '{')); assertThat(code, containsOne("f = 1;")); - assertThat(code, countString(2, indent(4) + "}")); + assertThat(code, countString(2, indent(4) + '}')); assertThat(code, containsOne(indent(4) + "public void run() {")); assertThat(code, containsOne("d = 7.5")); assertThat(code, containsOne(indent(3) + "}.start();")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass5.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass5.java index 3027c4d9a..faf5b6f59 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass5.java @@ -10,11 +10,11 @@ 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; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass5 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass6.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass6.java index fb0a207d6..30441b1dd 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass6.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass6.java @@ -6,9 +6,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass6 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass7.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass7.java index 0a1eafdf9..b5e40a82f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass7.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass7.java @@ -6,9 +6,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass7 extends IntegrationTest { @@ -20,7 +20,6 @@ public class TestAnonymousClass7 extends IntegrationTest { } }; } - } @Test diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass8.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass8.java index f17a28d58..1dbf88d47 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass8.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass8.java @@ -6,9 +6,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass8 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass9.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass9.java index ddf1160cd..5733292e2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass9.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass9.java @@ -9,9 +9,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestAnonymousClass9 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestInner2Samples.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestInner2Samples.java index 0c77bc0ca..a8b457e6c 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestInner2Samples.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestInner2Samples.java @@ -6,9 +6,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestInner2Samples extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticRename.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticRename.java index 63aecf84e..345bc2df8 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticRename.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestInnerClassSyntheticRename.java @@ -6,9 +6,9 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; /** * Issue: https://github.com/skylot/jadx/issues/336 diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestOuterConstructorCall.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestOuterConstructorCall.java index e67e787b3..5d4a44b40 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestOuterConstructorCall.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestOuterConstructorCall.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestOuterConstructorCall extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java index ab9b0f80a..785bb7875 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestRFieldRestore.java @@ -9,9 +9,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestRFieldRestore extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestSyntheticMthRename.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestSyntheticMthRename.java index aa3c5c9c0..33033b0d7 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestSyntheticMthRename.java +++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestSyntheticMthRename.java @@ -6,9 +6,9 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; /** * Issue: https://github.com/skylot/jadx/issues/397 diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java index 40097f8ae..27e740edf 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestCastInOverloadedInvoke.java @@ -1,9 +1,5 @@ package jadx.tests.integration.invoke; -import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - import java.util.ArrayList; import java.util.List; @@ -13,6 +9,10 @@ import jadx.NotYetImplemented; 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.is; + public class TestCastInOverloadedInvoke extends IntegrationTest { public static class TestCls { @@ -51,11 +51,22 @@ public class TestCastInOverloadedInvoke extends IntegrationTest { } @Test - @NotYetImplemented public void test() { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); + assertThat(code, containsOne("call((ArrayList) new ArrayList());")); + assertThat(code, containsOne("call((List) new ArrayList());")); + + assertThat(code, containsOne("call((String) obj);")); + } + + @Test + @NotYetImplemented + public void testNYI() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + assertThat(code, containsOne("call(new ArrayList<>());")); assertThat(code, containsOne("call((List) new ArrayList());")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestConstructorInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestConstructorInvoke.java index 99645997f..6f5b598d4 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestConstructorInvoke.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestConstructorInvoke.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; public class TestConstructorInvoke extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestInheritedStaticInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestInheritedStaticInvoke.java new file mode 100644 index 000000000..308457db9 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestInheritedStaticInvoke.java @@ -0,0 +1,36 @@ +package jadx.tests.integration.invoke; + +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.MatcherAssert.assertThat; + +public class TestInheritedStaticInvoke extends IntegrationTest { + + public static class TestCls { + public static class A { + public static int a() { + return 1; + } + } + + public static class B extends A { + } + + public int test() { + return B.a(); // not A.a() + } + } + + @Test + public void test() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("return B.a();")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke.java index 1a2aaa891..fbabf3b55 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestOverloadedMethodInvoke.java @@ -8,8 +8,8 @@ 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.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestOverloadedMethodInvoke extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke.java b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke.java index 5aa6ea568..70b464efb 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke.java +++ b/jadx-core/src/test/java/jadx/tests/integration/invoke/TestSuperInvoke.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSuperInvoke extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach2.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach2.java index 989388462..0de2c74ee 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestArrayForEach2.java @@ -7,6 +7,8 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsLines; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; public class TestArrayForEach2 extends IntegrationTest { @@ -26,12 +28,14 @@ public class TestArrayForEach2 extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); + assertThat(code, not(containsString("int "))); + assertThat(code, containsLines(2, "for (String s : str.split(\"\\n\")) {", indent(1) + "String t = s.trim();", indent(1) + "if (t.length() > 0) {", indent(2) + "System.out.println(t);", - indent(1) + "}", + indent(1) + '}', "}" )); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf.java index a96fa60bc..00f4c5847 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf.java @@ -9,8 +9,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestBreakInComplexIf extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf2.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf2.java index 822c4c0c8..dc6d3fe8c 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInComplexIf2.java @@ -9,8 +9,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestBreakInComplexIf2 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java index 345addaf5..482634910 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java @@ -8,8 +8,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestBreakWithLabel extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop.java index 352d1e25e..68b62d92e 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestEndlessLoop.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; -import static org.hamcrest.Matchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; public class TestEndlessLoop extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexForLoop.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexForLoop.java index 082abdd99..f85ff09fc 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexForLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIndexForLoop.java @@ -7,6 +7,7 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsLines; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestIndexForLoop extends IntegrationTest { @@ -19,6 +20,13 @@ public class TestIndexForLoop extends IntegrationTest { } return sum; } + + public void check() { + int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + assertEquals(0, test(array, 0)); + assertEquals(6, test(array, 3)); + assertEquals(36, test(array, 8)); + } } @Test diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach3.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach3.java index df35a797e..782f02887 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIterableForEach3.java @@ -40,4 +40,10 @@ public class TestIterableForEach3 extends IntegrationTest { assertThat(code, containsOne("if (str.length() == 0) {")); // TODO move return outside 'if' } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition5.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition5.java index 43e94887e..1dd0c215c 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestLoopCondition5.java @@ -7,8 +7,8 @@ import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; public class TestLoopCondition5 extends SmaliTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops3.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops3.java index b13567704..0f73339f8 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops3.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestNestedLoops3 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops2.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops2.java index 83915a9c8..28fa94350 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestSequentialLoops2.java @@ -48,4 +48,15 @@ public class TestSequentialLoops2 extends IntegrationTest { assertThat(code, containsOne("return c")); assertThat(code, countString(2, "<= 127")); } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, countString(2, "while (")); + assertThat(code, containsString("break;")); + assertThat(code, countString(2, "<= 127")); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestSynchronizedInEndlessLoop.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestSynchronizedInEndlessLoop.java index a5392267f..0c574aaff 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestSynchronizedInEndlessLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestSynchronizedInEndlessLoop.java @@ -1,14 +1,14 @@ package jadx.tests.integration.loops; -import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.MatcherAssert.assertThat; - import org.junit.jupiter.api.Test; import jadx.NotYetImplemented; 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; + public class TestSynchronizedInEndlessLoop extends IntegrationTest { public static class TestCls { diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop.java index a211eb921..7d318e347 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestTryCatchInLoop.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestTryCatchInLoop extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java b/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java index 0c2b41d99..b0f808322 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java +++ b/jadx-core/src/test/java/jadx/tests/integration/names/TestDuplicatedNames.java @@ -9,21 +9,21 @@ import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.MatcherAssert.assertThat; public class TestDuplicatedNames extends SmaliTest { -/* - public static class TestCls { + /* + public static class TestCls { - public Object fieldName; - public String fieldName; + public Object fieldName; + public String fieldName; - public Object run() { - return this.fieldName; + public Object run() { + return this.fieldName; + } + + public String run() { + return this.fieldName; + } } - - public String run() { - return this.fieldName; - } - } -*/ + */ @Test public void test() { commonChecks(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/names/TestNameAssign2.java b/jadx-core/src/test/java/jadx/tests/integration/names/TestNameAssign2.java index 425c352bb..0e7070dfa 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/names/TestNameAssign2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/names/TestNameAssign2.java @@ -13,8 +13,9 @@ import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.ssa.LiveVarAnalysis; 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; +import static org.hamcrest.Matchers.not; public class TestNameAssign2 extends IntegrationTest { @@ -58,7 +59,6 @@ public class TestNameAssign2 extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - // TODO: - assertThat(code, containsOne("int id;")); + assertThat(code, not(containsString("int id;"))); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedClassNames.java b/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedClassNames.java index c83d8eca6..79db7246e 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedClassNames.java +++ b/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedClassNames.java @@ -1,15 +1,15 @@ package jadx.tests.integration.names; +import java.io.File; + import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; - -import java.io.File; public class TestReservedClassNames extends SmaliTest { /* diff --git a/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedNames.java b/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedNames.java index 71f6d700b..c359657a2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedNames.java +++ b/jadx-core/src/test/java/jadx/tests/integration/names/TestReservedNames.java @@ -5,9 +5,9 @@ import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestReservedNames extends SmaliTest { /* diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestBadMethodAccessModifiers.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestBadMethodAccessModifiers.java index 441f621f0..201b4c808 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestBadMethodAccessModifiers.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestBadMethodAccessModifiers.java @@ -6,9 +6,9 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestBadMethodAccessModifiers extends SmaliTest { /* diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestDefConstructorNotRemoved.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestDefConstructorNotRemoved.java new file mode 100644 index 000000000..9c0185199 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestDefConstructorNotRemoved.java @@ -0,0 +1,60 @@ +package jadx.tests.integration.others; + +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.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +public class TestDefConstructorNotRemoved extends IntegrationTest { + + public static class TestCls { + + static { + // empty + } + + public static class A { + private final String s; + + public A() { + s = "a"; + } + + public A(String str) { + s = str; + } + } + + public static class B extends A { + public B() { + super(); + } + + public B(String s) { + super(s); + } + } + + public void check() { + new A(); + new A("a"); + new B(); + new B("b"); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("super();"))); + assertThat(code, not(containsString("static {"))); + assertThat(code, containsOne("public B() {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit.java index f9f1a1a48..6b8087242 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestFieldInit.java @@ -10,9 +10,9 @@ 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; import static org.hamcrest.Matchers.not; -import static org.hamcrest.MatcherAssert.assertThat; public class TestFieldInit extends IntegrationTest { @@ -24,7 +24,7 @@ public class TestFieldInit extends IntegrationTest { private static List s = new ArrayList<>(); private A a = new A(); - private int i = 1 + Random.class.getSimpleName().length(); + private int i = 1 + Random.class.getSimpleName().length(); private int n = 0; public TestCls(int z) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13a.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13a.java index 186261f2b..b2d63d5e9 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13a.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestIssue13a.java @@ -1,10 +1,5 @@ package jadx.tests.integration.others; -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; - import java.lang.reflect.Field; import java.util.HashMap; @@ -13,6 +8,11 @@ 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; + public class TestIssue13a extends IntegrationTest { public static class TestCls { @@ -41,18 +41,18 @@ public class TestIssue13a extends IntegrationTest { Field f = c.getField("CREATOR"); creator = (Parcelable.Creator) f.get(null); } catch (IllegalAccessException e) { - Log.e(TAG, "1" + name + ", e: " + e); - throw new RuntimeException("2" + name); + Log.e(TAG, '1' + name + ", e: " + e); + throw new RuntimeException('2' + name); } catch (ClassNotFoundException e) { - Log.e(TAG, "3" + name + ", e: " + e); - throw new RuntimeException("4" + name); + Log.e(TAG, '3' + name + ", e: " + e); + throw new RuntimeException('4' + name); } catch (ClassCastException e) { - throw new RuntimeException("5" + name); + throw new RuntimeException('5' + name); } catch (NoSuchFieldException e) { - throw new RuntimeException("6" + name); + throw new RuntimeException('6' + name); } if (creator == null) { - throw new RuntimeException("7" + name); + throw new RuntimeException('7' + name); } map.put(name, creator); } @@ -95,7 +95,7 @@ public class TestIssue13a extends IntegrationTest { String code = cls.getCode().toString(); for (int i = 1; i <= 7; i++) { - assertThat(code, containsOne("\"" + i + "\"")); + assertThat(code, containsOne("'" + i + '\'')); } // TODO: add additional checks diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch.java index a1c9480c0..b5d3cbccc 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitch extends IntegrationTest { public static class TestCls { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch3.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch3.java index 96251f713..6935b97aa 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitch3.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestSwitch3 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak.java index 98900d24c..135f4ec32 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchBreak.java @@ -7,8 +7,10 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchBreak extends IntegrationTest { @@ -34,6 +36,10 @@ public class TestSwitchBreak extends IntegrationTest { } return s; } + + public void check() { + assertThat(test(9), is("1--4--1--4--1-")); + } } @Test @@ -43,7 +49,8 @@ public class TestSwitchBreak extends IntegrationTest { assertThat(code, containsString("switch (a % 4) {")); assertEquals(4, count(code, "case ")); - assertEquals(3, count(code, "break;")); + assertEquals(2, count(code, "break;")); + assertThat(code, not(containsString("default:"))); // TODO finish break with label from switch assertThat(code, containsOne("return s + \"+\";")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchContinue.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchContinue.java index 5fbfb7bc0..c2d29b114 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchContinue.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchContinue.java @@ -7,8 +7,8 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchContinue extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop.java index 19b3ee906..d09b39eb0 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchInLoop extends IntegrationTest { public static class TestCls { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchLabels.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchLabels.java index 50bb7816f..d2d9148b2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchLabels.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchLabels.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.Matchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; public class TestSwitchLabels extends IntegrationTest { public static class TestCls { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java index 4fdacfbe1..c77012de0 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase.java @@ -7,18 +7,18 @@ import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchReturnFromCase extends IntegrationTest { public static class TestCls { public void test(int a) { - String s = null; if (a > 1000) { return; } - switch (a % 4) { + String s = null; + switch (a % 10) { case 1: s = "1"; break; @@ -30,9 +30,14 @@ public class TestSwitchReturnFromCase extends IntegrationTest { s = "4"; break; case 5: + break; + case 6: return; } - s = "5"; + if (s == null) { + s = "5"; + } + System.out.println(s); } } @@ -41,7 +46,7 @@ public class TestSwitchReturnFromCase extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - assertThat(code, containsString("switch (a % 4) {")); + assertThat(code, containsString("switch (a % 10) {")); assertEquals(5, count(code, "case ")); assertEquals(3, count(code, "break;")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase2.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase2.java index d2dc6b217..bad3cc7b2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchReturnFromCase2.java @@ -1,12 +1,13 @@ package jadx.tests.integration.switches; +import org.junit.jupiter.api.Test; + import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; -import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestSwitchReturnFromCase2 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchSimple.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchSimple.java index 0e5cacde4..dafb59e46 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchSimple.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchSimple.java @@ -7,8 +7,8 @@ import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchSimple extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java index fb2648d06..3c6bb05ae 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchWithFallThroughCase extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase2.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase2.java index ddc1618e1..fe90991c3 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchWithFallThroughCase2.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestSwitchWithFallThroughCase2 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized.java b/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized.java index 6896780a9..6260dd953 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized.java +++ b/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized.java @@ -5,6 +5,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; @@ -22,7 +23,7 @@ public class TestSynchronized extends IntegrationTest { public int test2() { synchronized (this.o) { - return i; + return this.i; } } } @@ -33,11 +34,11 @@ public class TestSynchronized extends IntegrationTest { String code = cls.getCode().toString(); assertThat(code, not(containsString("synchronized (this) {"))); - assertThat(code, containsString("public synchronized boolean test1() {")); - assertThat(code, containsString("return this.f")); - assertThat(code, containsString("synchronized (this.o) {")); + assertThat(code, containsOne("public synchronized boolean test1() {")); + assertThat(code, containsOne("return this.f")); + assertThat(code, containsOne("synchronized (this.o) {")); - assertThat(code, not(containsString(indent(3) + ";"))); + assertThat(code, not(containsString(indent(3) + ';'))); assertThat(code, not(containsString("try {"))); assertThat(code, not(containsString("} catch (Throwable th) {"))); assertThat(code, not(containsString("throw th;"))); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinallyExtract.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinallyExtract.java index b421f4cec..2c4db000f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinallyExtract.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestFinallyExtract.java @@ -1,7 +1,5 @@ package jadx.tests.integration.trycatch; -import java.io.IOException; - import org.junit.jupiter.api.Test; import jadx.core.dex.nodes.ClassNode; @@ -9,23 +7,37 @@ 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; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestFinallyExtract extends IntegrationTest { public static class TestCls { + private int result = 0; - public String test() throws IOException { + public String test() { boolean success = false; try { - String value = test(); + String value = call(); + result++; success = true; return value; } finally { if (!success) { - test(); + result -= 2; } } } + + private String call() { + return "call"; + } + + public void check() { + test(); + assertEquals(result, 1); + } } @Test @@ -33,10 +45,38 @@ public class TestFinallyExtract extends IntegrationTest { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); + assertThat(code, not(containsString("if (0 == 0) {"))); + + assertThat(code, containsOne("boolean success = false;")); + assertThat(code, containsOne("try {")); assertThat(code, containsOne("success = true;")); assertThat(code, containsOne("return value;")); - assertThat(code, containsOne("try {")); assertThat(code, containsOne("} finally {")); assertThat(code, containsOne("if (!success) {")); } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + // java compiler optimization: 'success' variable completely removed and no code duplication: + /* + public String test() { + try { + String call = call(); + this.result++; + return call; + } catch (Throwable th) { + this.result -= 2; + throw th; + } + } + */ + assertThat(code, containsOne("this.result++;")); + assertThat(code, containsOne("} catch (Throwable th) {")); + assertThat(code, containsOne("this.result -= 2;")); + assertThat(code, containsOne("throw th;")); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestInlineInCatch.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestInlineInCatch.java index 4c42426e8..e692227b4 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestInlineInCatch.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestInlineInCatch.java @@ -19,6 +19,9 @@ public class TestInlineInCatch extends IntegrationTest { File output = null; try { output = File.createTempFile("f", "a", dir); + if (!output.exists()) { + return 1; + } return 0; } catch (Exception e) { if (output != null) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java new file mode 100644 index 000000000..d769978db --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatch2.java @@ -0,0 +1,48 @@ +package jadx.tests.integration.trycatch; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +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.MatcherAssert.assertThat; + +public class TestMultiExceptionCatch2 extends IntegrationTest { + + public static class TestCls { + public void test(Constructor constructor) { + try { + constructor.newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + @Test + public void test() { + commonChecks(); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + commonChecks(); + } + + private void commonChecks() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("try {")); + assertThat(code, containsOne("} catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {")); + assertThat(code, containsOne("e.printStackTrace();")); + + // TODO: store vararg attribute for methods from classpath +// assertThat(code, containsOne("constructor.newInstance();")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java index ef8b718cb..a1c707fa6 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestMultiExceptionCatchSameJump.java @@ -11,17 +11,17 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; public class TestMultiExceptionCatchSameJump extends SmaliTest { -/* - public static class TestCls { - public void test() { - try { - System.out.println("Test"); - } catch (ProviderException | DateTimeException e) { - throw new RuntimeException(e); + /* + public static class TestCls { + public void test() { + try { + System.out.println("Test"); + } catch (ProviderException | DateTimeException e) { + throw new RuntimeException(e); + } } } - } -*/ + */ @Test public void test() { ClassNode cls = getClassNodeFromSmaliWithPkg("trycatch", "TestMultiExceptionCatchSameJump"); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch6.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch6.java index 01f3b5e35..ae1e98ca8 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch6.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch6.java @@ -44,6 +44,14 @@ public class TestTryCatch6 extends IntegrationTest { @Test public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("try {")); + } + + @Test + public void testNoDebug() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch7.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch7.java index 7c91eaad0..8ae2205e9 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch7.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch7.java @@ -25,16 +25,26 @@ public class TestTryCatch7 extends IntegrationTest { @Test public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + check(code, "e", "ex"); + } + + @Test + public void testNoDebug() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - String excVarName = "e"; - String catchExcVarName = "e2"; + check(code, "e", "e2"); + } + + private void check(String code, String excVarName, String catchExcVarName) { assertThat(code, containsOne("Exception " + excVarName + " = new Exception();")); assertThat(code, containsOne("} catch (Exception " + catchExcVarName + ") {")); - assertThat(code, containsOne(excVarName + " = " + catchExcVarName + ";")); + assertThat(code, containsOne(excVarName + " = " + catchExcVarName + ';')); assertThat(code, containsOne(excVarName + ".printStackTrace();")); - assertThat(code, containsOne("return " + excVarName + ";")); + assertThat(code, containsOne("return " + excVarName + ';')); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch8.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch8.java index 2f73c6258..d51da9eaa 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch8.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch8.java @@ -6,10 +6,10 @@ 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.isA; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; public class TestTryCatch8 extends IntegrationTest { @@ -31,8 +31,8 @@ public class TestTryCatch8 extends IntegrationTest { synchronized (this) { try { throw new MyException(); - } catch (MyException e) { - this.e = e; + } catch (MyException myExc) { + this.e = myExc; } catch (Exception x) { this.e = new MyException("MyExc", x); } @@ -54,9 +54,19 @@ public class TestTryCatch8 extends IntegrationTest { assertThat(code, containsOne("synchronized (this) {")); assertThat(code, containsOne("throw new MyException();")); - assertThat(code, containsOne("} catch (MyException e) {")); - assertThat(code, containsOne("this.e = e;")); + assertThat(code, containsOne("} catch (MyException myExc) {")); + assertThat(code, containsOne("this.e = myExc;")); assertThat(code, containsOne("} catch (Exception x) {")); assertThat(code, containsOne("this.e = new MyException(\"MyExc\", x);")); } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("synchronized (this) {")); + assertThat(code, containsOne("throw new MyException();")); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java index 301333190..580720cd2 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally2.java @@ -1,9 +1,5 @@ package jadx.tests.integration.trycatch; -import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static jadx.tests.api.utils.JadxMatchers.countString; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -15,6 +11,10 @@ import jadx.core.clsp.NClass; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static jadx.tests.api.utils.JadxMatchers.countString; +import static org.hamcrest.MatcherAssert.assertThat; + public class TestTryCatchFinally2 extends IntegrationTest { public static class TestCls { diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally6.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally6.java index dca852311..e6b01178a 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally6.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally6.java @@ -18,6 +18,7 @@ public class TestTryCatchFinally6 extends IntegrationTest { public static void test() throws IOException { InputStream is = null; try { + call(); is = new FileInputStream("1.txt"); } finally { if (is != null) { @@ -25,6 +26,9 @@ public class TestTryCatchFinally6 extends IntegrationTest { } } } + + private static void call() { + } } @Test @@ -35,11 +39,31 @@ public class TestTryCatchFinally6 extends IntegrationTest { assertThat(code, containsLines(2, "InputStream is = null;", "try {", + indent(1) + "call();", indent(1) + "is = new FileInputStream(\"1.txt\");", "} finally {", indent(1) + "if (is != null) {", indent(2) + "is.close();", - indent(1) + "}", + indent(1) + '}', + "}" + )); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsLines(2, + "FileInputStream fileInputStream = null;", + "try {", + indent() + "call();", + indent() + "fileInputStream = new FileInputStream(\"1.txt\");", + "} finally {", + indent() + "if (fileInputStream != null) {", + indent() + indent() + "fileInputStream.close();", + indent() + '}', "}" )); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch3.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally7.java similarity index 94% rename from jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch3.java rename to jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally7.java index e3ffd4129..d49af08a8 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch3.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally7.java @@ -7,11 +7,11 @@ import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.not; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -public class TestTryCatch3 extends IntegrationTest { +public class TestTryCatchFinally7 extends IntegrationTest { public static class TestCls { private int f = 0; @@ -63,7 +63,7 @@ public class TestTryCatch3 extends IntegrationTest { } @Test - public void test2() { + public void testNoDebug() { noDebugInfo(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch5.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally8.java similarity index 91% rename from jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch5.java rename to jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally8.java index 7e1170f4d..c563faa3f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally8.java @@ -13,7 +13,7 @@ import jadx.tests.api.IntegrationTest; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; -public class TestTryCatch5 extends IntegrationTest { +public class TestTryCatchFinally8 extends IntegrationTest { public static class TestCls { private Object test(Object obj) { @@ -43,8 +43,8 @@ public class TestTryCatch5 extends IntegrationTest { } @Test + @NotYetImplemented("Fix merged catch blocks (shared code between catches)") public void test() { - disableCompilation(); ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf.java index 1ad8efd4f..a2ac81ffb 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchInIf.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; public class TestTryCatchInIf extends IntegrationTest { @@ -23,7 +23,7 @@ public class TestTryCatchInIf extends IntegrationTest { } else { key = Integer.parseInt(value); } - return name + "=" + key; + return name + '=' + key; } catch (NumberFormatException e) { return "Failed to parse number"; } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchMultiException.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchMultiException.java new file mode 100644 index 000000000..57febf7aa --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchMultiException.java @@ -0,0 +1,36 @@ +package jadx.tests.integration.trycatch; + +import java.security.ProviderException; +import java.time.DateTimeException; + +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.MatcherAssert.assertThat; + +public class TestTryCatchMultiException extends IntegrationTest { + + public static class TestCls { + public void test() { + try { + System.out.println("Test"); + } catch (ProviderException | DateTimeException e) { + throw new RuntimeException(e); + } + } + } + + @Test + public void test() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + String catchExcVarName = "e"; + assertThat(code, containsOne("} catch (ProviderException | DateTimeException " + catchExcVarName + ") {")); + assertThat(code, containsOne("throw new RuntimeException(" + catchExcVarName + ");")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMove.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc.java similarity index 82% rename from jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMove.java rename to jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc.java index d92d65759..291ed9e11 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMove.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc.java @@ -8,7 +8,7 @@ import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; import static org.hamcrest.MatcherAssert.assertThat; -public class TestTryCatchNoMove extends SmaliTest { +public class TestTryCatchNoMoveExc extends SmaliTest { // private static void test(AutoCloseable closeable) { // if (closeable != null) { @@ -21,7 +21,7 @@ public class TestTryCatchNoMove extends SmaliTest { @Test public void test() { - ClassNode cls = getClassNodeFromSmaliWithPath("trycatch", "TestTryCatchNoMove"); + ClassNode cls = getClassNodeFromSmaliWithPkg("trycatch", "TestTryCatchNoMoveExc"); String code = cls.getCode().toString(); assertThat(code, containsOne("if (autoCloseable != null) {")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc2.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc2.java index 65213533f..3333705b9 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchNoMoveExc2.java @@ -26,7 +26,7 @@ public class TestTryCatchNoMoveExc2 extends SmaliTest { @Test public void test() { - ClassNode cls = getClassNodeFromSmaliWithPath("trycatch", "TestTryCatchNoMoveExc2"); + ClassNode cls = getClassNodeFromSmaliWithPkg("trycatch", "TestTryCatchNoMoveExc2"); String code = cls.getCode().toString(); assertThat(code, containsOne("try {")); diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TryAfterDeclaration.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TryAfterDeclaration.java index a43a91659..70b5e1423 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TryAfterDeclaration.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TryAfterDeclaration.java @@ -1,8 +1,5 @@ package jadx.tests.integration.trycatch; -import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.MatcherAssert.assertThat; - import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -13,6 +10,9 @@ import jadx.NotYetImplemented; 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; + public class TryAfterDeclaration extends IntegrationTest { /** @@ -30,7 +30,7 @@ public class TryAfterDeclaration extends IntegrationTest { class TestClass { public static void consume() throws IOException { - InputStream bis = null; + InputStream bis = null; try { bis = new FileInputStream("1.txt"); while (bis != null) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestArrayTypes.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestArrayTypes.java new file mode 100644 index 000000000..1849dd320 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestArrayTypes.java @@ -0,0 +1,44 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestArrayTypes extends IntegrationTest { + + public static class TestCls { + + public void test() { + Exception e = new Exception(); + System.out.println(e); + use(new Object[]{e}); + } + + public void use(Object[] arr) {} + + public void check() { + test(); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("use(new Object[]{e});")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("use(new Object[]{exc});")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestConstInline.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestConstInline.java new file mode 100644 index 000000000..fe111f56d --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestConstInline.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.types; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.JadxMatchers.containsOne; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestConstInline extends SmaliTest { + +// private static String test(boolean b) { +// List list; +// String str; +// if (b) { +// list = Collections.emptyList(); +// str = "1"; +// } else { +// list = null; +// str = list; // not correct assign in java but bytecode allow it +// } +// return use(list, str); +// } +// +// private static String use(List list, String str) { +// return list + str; +// } + + @Test + public void test() { + ClassNode cls = getClassNodeFromSmaliWithPkg("types", "TestConstInline"); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("list = null;")); + assertThat(code, containsOne("str = null;")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestConstTypeInference.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestConstTypeInference.java new file mode 100644 index 000000000..a13273776 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestConstTypeInference.java @@ -0,0 +1,65 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +public class TestConstTypeInference extends IntegrationTest { + + @SuppressWarnings("overrides") + public static class TestCls { + private final int a; + + public TestCls() { + this(0); + } + + public TestCls(int a) { + this.a = a; + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj != null) { + if (getClass() == obj.getClass()) { + TestCls other = (TestCls) obj; + return this.a == other.a; + } + } + return false; + } + + public void check() { + TestCls seven = new TestCls(7); + assertEquals(seven, seven); + assertNotEquals(seven, null); + + TestCls six = new TestCls(6); + assertNotEquals(seven, six); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("obj == this")); + assertThat(code, anyOf(containsOne("obj == null"), containsOne("obj != null"))); + } + + @Test + public void test2() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics.java new file mode 100644 index 000000000..5a4a5e922 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestGenerics.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestGenerics extends IntegrationTest { + + public static class TestCls { + private T data; + + public TestCls data(T t) { + this.data = t; + return this; + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("TestCls data(T t) {")); + } + + @Test + public void test2() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("TestCls data(T t) {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java new file mode 100644 index 000000000..20c86d1eb --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestPrimitivesInIf.java @@ -0,0 +1,46 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestPrimitivesInIf extends IntegrationTest { + + public static class TestCls { + + public boolean test(String str) { + short sh = Short.parseShort(str); + int i = Integer.parseInt(str); + System.out.println(sh + " vs " + i); + return sh == i; + } + + public void check() { + assertTrue(test("1")); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("short sh = Short.parseShort(str);")); + assertThat(code, containsOne("int i = Integer.parseInt(str);")); + assertThat(code, containsOne("return sh == i;")); + } + + @Test + public void test2() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("short parseShort = Short.parseShort(str);")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java new file mode 100644 index 000000000..8dce850b4 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeInheritance.java @@ -0,0 +1,62 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestTypeInheritance extends IntegrationTest { + + public static class TestCls { + + public interface IRoot { + } + + public interface IBase extends IRoot { + } + + public static class A implements IBase { + } + + public static class B implements IBase { + public void b() {} + } + + private static void test(boolean z) { + IBase impl; + if (z) { + impl = new A(); + } else { + B b = new B(); + b.b(); + impl = b; // this move is removed in no-debug byte-code + } + useBase(impl); + useRoot(impl); + } + + private static void useRoot(IRoot root) {} + + private static void useBase(IBase base) {} + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("IBase impl;")); + assertThat(code, containsOne("impl = new A();")); + assertThat(code, containsOne("B b = new B();")); + assertThat(code, containsOne("impl = b;")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver4.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver4.java index 1ba7e613d..cf2985a0d 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver4.java @@ -8,8 +8,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestTypeResolver4 extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver5.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver5.java new file mode 100644 index 000000000..12665a8fc --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver5.java @@ -0,0 +1,24 @@ +package jadx.tests.integration.types; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; + +public class TestTypeResolver5 extends SmaliTest { + + @Test + public void test() { + disableCompilation(); + + ClassNode cls = getClassNodeFromSmaliWithPath("types", "TestTypeResolver5"); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("Object string2"))); + assertThat(code, not(containsString("r1v2"))); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java new file mode 100644 index 000000000..f30740484 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6.java @@ -0,0 +1,38 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestTypeResolver6 extends IntegrationTest { + + public static class TestCls { + private final Object obj; + + public TestCls(boolean b) { + this.obj = b ? this : makeObj(); + } + + public Object makeObj() { + return new Object(); + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("this.obj = b ? this : makeObj();")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java new file mode 100644 index 000000000..85c2f1c5f --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver6a.java @@ -0,0 +1,48 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestTypeResolver6a extends IntegrationTest { + + public static class TestCls implements Runnable { + private final Runnable runnable; + + public TestCls(boolean b) { + this.runnable = b ? this : makeRunnable(); + } + + public Runnable makeRunnable() { + return new Runnable() { + @Override + public void run() { + // do nothing + } + }; + } + + @Override + public void run() { + // do nothing + } + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("this.runnable = b ? this : makeRunnable();")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver7.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver7.java new file mode 100644 index 000000000..08473d49f --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver7.java @@ -0,0 +1,49 @@ +package jadx.tests.integration.types; + +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.MatcherAssert.assertThat; + +public class TestTypeResolver7 extends IntegrationTest { + + public static class TestCls { + public void test(boolean a, boolean b) { + Object obj = null; + if (a) { + use(b ? (Exception) getObj() : (Exception) obj); + } else { + Runnable r = (Runnable) obj; + if (b) { + r = (Runnable) getObj(); + } + use(r); + } + } + + private Object getObj() { + return null; + } + + private void use(Exception e) {} + + private void use(Runnable r) {} + } + + @Test + public void test() { + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsOne("use(b ? (Exception) getObj() : null);")); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + getClassNode(TestCls.class); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables2.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables2.java index 8a1584f96..bdd8eaf1f 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables2.java @@ -28,4 +28,13 @@ public class TestVariables2 extends IntegrationTest { assertThat(code, containsString("Object store = s != null ? s : null;")); } + + @Test + public void testNoDebug() { + noDebugInfo(); + ClassNode cls = getClassNode(TestCls.class); + String code = cls.getCode().toString(); + + assertThat(code, containsString("Object obj2 = obj != null ? obj : null;")); + } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables4.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables4.java index 9baa1de13..2674e9583 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables4.java @@ -37,7 +37,7 @@ public class TestVariables4 extends IntegrationTest { msg = "not extends AbstractTest"; } System.err.println(">> " - + (pass ? "PASS" : "FAIL") + "\t" + + (pass ? "PASS" : "FAIL") + '\t' + clsName + (msg == null ? "" : "\t - " + msg)); if (exc != null) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables6.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables6.java new file mode 100644 index 000000000..52b4b2f8c --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariables6.java @@ -0,0 +1,26 @@ +package jadx.tests.integration.variables; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +public class TestVariables6 extends SmaliTest { + + @Test + public void test() { + disableCompilation(); + ClassNode cls = getClassNodeFromSmaliWithPath("variables", "TestVariables6"); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("r4"))); + assertThat(code, not(containsString("r1v1"))); + assertThat(code, containsString("DateStringParser dateStringParser")); + assertThat(code, containsString("FinancialInstrumentMetadataAttribute startYear = this.mFinancialInstrumentMetadataDefinition" + + ".getStartYear();")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions.java index 37401af2c..a28b4a99d 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions.java +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesDefinitions.java @@ -22,7 +22,7 @@ public class TestVariablesDefinitions extends IntegrationTest { private ClassNode cls; private List passes; - public void run() { + public void test() { try { cls.load(); for (IDexTreeVisitor pass : this.passes) { @@ -41,5 +41,6 @@ public class TestVariablesDefinitions extends IntegrationTest { assertThat(code, containsOne(indent(3) + "for (IDexTreeVisitor pass : this.passes) {")); assertThat(code, not(containsString("iterator;"))); + assertThat(code, not(containsString("Iterator"))); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesGeneric.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesGeneric.java new file mode 100644 index 000000000..c09cd6a43 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesGeneric.java @@ -0,0 +1,34 @@ +package jadx.tests.integration.variables; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +public class TestVariablesGeneric extends SmaliTest { + /* + public static j a(i iVar, c cVar) { + if (iVar == null) { + throw new IllegalArgumentException("subscriber can not be null"); + } + if (cVar.a == null) { + throw new IllegalStateException("onSubscribe function can not be null."); + } + ... + */ + + @Test + public void test() { + disableCompilation(); + ClassNode cls = getClassNodeFromSmaliWithPkg("variables", "TestVariablesGeneric"); + String code = cls.getCode().toString(); + + assertThat(code, not(containsString("iVar2"))); + assertThat(code, containsString("public static j a(i iVar, c cVar) {")); + assertThat(code, containsString("if (iVar == null) {")); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesIfElseChain.java b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesIfElseChain.java index f402665fc..f2b0ec69b 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesIfElseChain.java +++ b/jadx-core/src/test/java/jadx/tests/integration/variables/TestVariablesIfElseChain.java @@ -6,8 +6,8 @@ import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.JadxMatchers.containsOne; -import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; public class TestVariablesIfElseChain extends IntegrationTest { diff --git a/jadx-core/src/test/smali/conditions/TestComplexIf2.smali b/jadx-core/src/test/smali/conditions/TestComplexIf2.smali new file mode 100644 index 000000000..b8a9190ad --- /dev/null +++ b/jadx-core/src/test/smali/conditions/TestComplexIf2.smali @@ -0,0 +1,85 @@ +.class public final Lconditions/TestComplexIf2; +.super Ljava/lang/ClassLoader; + + +# instance fields +.field private isSaved:Z + +.field private project:Ljava/lang/String; + + +.method public test()V + .locals 4 + + .line 415 + iget-boolean v0, p0, Lconditions/TestComplexIf2;->isSaved:Z + + if-eqz v0, :cond_0 + + .line 416 + new-instance v0, Ljava/lang/RuntimeException; + + const-string v1, "Error" + + invoke-direct {v0, v1}, Ljava/lang/RuntimeException;->(Ljava/lang/String;)V + + throw v0 + + .line 418 + :cond_0 + invoke-static {}, Lorg/apache/tools/ant/util/LoaderUtils;->isContextLoaderAvailable()Z + + move-result v0 + + if-eqz v0, :cond_2 + + .line 419 + invoke-static {}, Lorg/apache/tools/ant/util/LoaderUtils;->getContextClassLoader()Ljava/lang/ClassLoader; + + move-result-object v0 + + iput-object v0, p0, Lconditions/TestComplexIf2;->savedContextLoader:Ljava/lang/ClassLoader; + + .line 420 + move-object v0, p0 + + .line 421 + .local v0, "loader":Ljava/lang/ClassLoader; + iget-object v1, p0, Lconditions/TestComplexIf2;->project:Ljava/lang/String; + + if-eqz v1, :cond_1 + + const-string v1, "simple" + + iget-object v2, p0, Lconditions/TestComplexIf2;->project:Ljava/lang/String; + + invoke-virtual {v1, v2}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result v1 + + if-eqz v1, :cond_1 + + .line 423 + invoke-virtual {p0}, Ljava/lang/Object;->getClass()Ljava/lang/Class; + + move-result-object v1 + + invoke-virtual {v1}, Ljava/lang/Class;->getClassLoader()Ljava/lang/ClassLoader; + + move-result-object v0 + + .line 425 + :cond_1 + invoke-static {v0}, Lorg/apache/tools/ant/util/LoaderUtils;->setContextClassLoader(Ljava/lang/ClassLoader;)V + + .line 426 + const/4 v1, 0x1 + + iput-boolean v1, p0, Lconditions/TestComplexIf2;->isSaved:Z + + .line 428 + .end local v0 # "loader":Ljava/lang/ClassLoader; + :cond_2 + return-void +.end method + diff --git a/jadx-core/src/test/smali/trycatch/TestTryCatchNoMove.smali b/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc.smali similarity index 88% rename from jadx-core/src/test/smali/trycatch/TestTryCatchNoMove.smali rename to jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc.smali index dd27a3bbd..63abe321c 100644 --- a/jadx-core/src/test/smali/trycatch/TestTryCatchNoMove.smali +++ b/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc.smali @@ -1,4 +1,4 @@ -.class public LTestTryCatchNoMove; +.class public Ltrycatch/TestTryCatchNoMoveExc; .super Ljava/lang/Object; .method private static test(Ljava/lang/AutoCloseable;)V diff --git a/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc2.smali b/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc2.smali index 4d2b9f836..e42f87e4d 100644 --- a/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc2.smali +++ b/jadx-core/src/test/smali/trycatch/TestTryCatchNoMoveExc2.smali @@ -1,4 +1,4 @@ -.class public LTestTryCatchNoMoveExc2; +.class public Ltrycatch/TestTryCatchNoMoveExc2; .super Ljava/lang/Object; .method private static test(Ljava/lang/AutoCloseable;)V diff --git a/jadx-core/src/test/smali/types/TestConstInline.smali b/jadx-core/src/test/smali/types/TestConstInline.smali new file mode 100644 index 000000000..0a6f2e90d --- /dev/null +++ b/jadx-core/src/test/smali/types/TestConstInline.smali @@ -0,0 +1,72 @@ +.class public Ltypes/TestConstInline; +.super Ljava/lang/Object; + +.method private static test(Z)Ljava/lang/String; + .registers 4 + .param p0, "b" # Z + + if-eqz p0, :cond_d + invoke-static {}, Ltypes/TestConstInline;->list()Ljava/util/List; + move-result-object v0 + const-string v1, "1" + goto :goto_return + + :cond_d + const/4 v2, 0x0 + # chained move instead zero const loading + move v0, v2 + move v1, v0 + goto :goto_return + + :goto_return + invoke-static {v0, v1}, Ltypes/TestConstInline;->use(Ljava/util/List;Ljava/lang/String;)Ljava/lang/String; + move-result-object v2 + return-object v2 +.end method + +.method private static use(Ljava/util/List;Ljava/lang/String;)Ljava/lang/String; + .registers 3 + .annotation system Ldalvik/annotation/Signature; + value = { + "(", + "Ljava/util/List", + "<", + "Ljava/lang/String;", + ">;", + "Ljava/lang/String;", + ")", + "Ljava/lang/String;" + } + .end annotation + + new-instance v0, Ljava/lang/StringBuilder; + invoke-direct {v0}, Ljava/lang/StringBuilder;->()V + + invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder; + move-result-object v0 + + invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; + move-result-object v0 + + invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; + move-result-object v0 + + return-object v0 +.end method + +.method private static list()Ljava/util/List; + .registers 1 + .annotation system Ldalvik/annotation/Signature; + value = { + "()", + "Ljava/util/List", + "<", + "Ljava/lang/String;", + ">;" + } + .end annotation + + invoke-static {}, Ljava/util/Collections;->emptyList()Ljava/util/List; + move-result-object v0 + return-object v0 +.end method diff --git a/jadx-core/src/test/smali/types/TestTypeResolver5.smali b/jadx-core/src/test/smali/types/TestTypeResolver5.smali new file mode 100644 index 000000000..7d02817ec --- /dev/null +++ b/jadx-core/src/test/smali/types/TestTypeResolver5.smali @@ -0,0 +1,176 @@ +.class public LTestTypeResolver5; +.super Landroid/content/Context; +.source "SourceFile" + + +# static fields +.field public static final EXTERNAL_SOURCE:Ljava/lang/String; = "externalsource" + +.field public static final IS_APPBOY_CAMPAIGN:Ljava/lang/String; = "appBoyCampaign" + +.field public static final IS_NEWS_FEED:Ljava/lang/String; = "isNewsFeed" + + +# direct methods +.method public constructor ()V + .locals 0 + + .prologue + .line 35 + invoke-direct {p0}, Landroid/content/Context;->()V + + return-void +.end method + +.method private openNextScreen(Landroid/os/Bundle;)V + .locals 3 + + .prologue + const/4 v1, 0x0 + + .line 56 + if-eqz p1, :cond_2 + + const-string v0, "externalsource" + + invoke-virtual {p1, v0}, Landroid/os/Bundle;->containsKey(Ljava/lang/String;)Z + + move-result v0 + + if-eqz v0, :cond_2 + + const-string v0, "externalsource" + + .line 57 + invoke-virtual {p1, v0}, Landroid/os/Bundle;->getString(Ljava/lang/String;)Ljava/lang/String; + + move-result-object v0 + + move-object v2, v0 + + .line 58 + :goto_0 + if-eqz p1, :cond_3 + + const-string v0, "isNewsFeed" + + invoke-virtual {p1, v0}, Landroid/os/Bundle;->containsKey(Ljava/lang/String;)Z + + move-result v0 + + if-eqz v0, :cond_3 + + const-string v0, "isNewsFeed" + + .line 59 + invoke-virtual {p1, v0}, Landroid/os/Bundle;->getBoolean(Ljava/lang/String;)Z + + move-result v0 + + .line 61 + :goto_1 + invoke-static {v2}, Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z + + move-result v2 + + if-nez v2, :cond_0 + + const/4 v1, 0x1 + + .line 64 + :cond_0 + if-eqz p1, :cond_1 + + .line 65 + new-instance v2, Landroid/webkit/WebView; + + invoke-direct {v2, p0}, Landroid/webkit/WebView;->(Landroid/content/Context;)V + + .line 66 + invoke-direct {p0, v2, p1}, LTestTypeResolver5;->runJavaScriptForCampaign(Landroid/webkit/WebView;Landroid/os/Bundle;)V + + .line 70 + :cond_1 + if-eqz v0, :cond_4 + + .line 72 + invoke-direct {p0, p1}, LTestTypeResolver5;->startHomeActivity(Landroid/os/Bundle;)V + + .line 73 + invoke-virtual {p0}, LTestTypeResolver5;->finish()V + + .line 80 + :goto_2 + return-void + + .line 57 + :cond_2 + const-string v0, "" + + move-object v2, v0 + + goto :goto_0 + + :cond_3 + move v0, v1 + + .line 59 + goto :goto_1 + + .line 74 + :cond_4 + invoke-virtual {p0}, LTestTypeResolver5;->isTaskRoot()Z + + move-result v0 + + if-nez v0, :cond_5 + + if-eqz v1, :cond_6 + + .line 76 + :cond_5 + invoke-direct {p0, p1}, LTestTypeResolver5;->openSplash(Landroid/os/Bundle;)V + + goto :goto_2 + + .line 78 + :cond_6 + invoke-virtual {p0}, LTestTypeResolver5;->finish()V + + goto :goto_2 +.end method + +.method private openSplash(Landroid/os/Bundle;)V + .locals 1 + return-void +.end method + +.method private runJavaScriptForCampaign(Landroid/webkit/WebView;Landroid/os/Bundle;)V + .locals 1 + return-void +.end method + +.method private startHomeActivity(Landroid/os/Bundle;)V + .locals 1 + return-void +.end method + +.method public onBackPressed()V + .locals 1 + return-void +.end method + +.method public onCreate(Landroid/os/Bundle;)V + .locals 1 + return-void +.end method + +.method protected onPause()V + .locals 1 + return-void +.end method + +.method protected onStart()V + .locals 1 + return-void +.end method diff --git a/jadx-core/src/test/smali/variables/TestVariables6.smali b/jadx-core/src/test/smali/variables/TestVariables6.smali new file mode 100644 index 000000000..2139aff29 --- /dev/null +++ b/jadx-core/src/test/smali/variables/TestVariables6.smali @@ -0,0 +1,144 @@ +.class public LTestVariables6; +.super Lcom/paypal/android/p2pmobile/wallet/banksandcards/fragments/BasePaymentFragment; +.source "SourceFile" + +# interfaces +.implements Landroid/support/v13/app/FragmentCompat$OnRequestPermissionsResultCallback; +.implements Landroid/widget/TextView$OnEditorActionListener; +.implements Lcom/paypal/android/p2pmobile/common/utils/ISafeClickVerifierListener; +.implements Lcom/paypal/android/p2pmobile/common/widgets/CSCTextWatcher$ICSCTextWatcherListener; + + +# annotations +.annotation system Ldalvik/annotation/MemberClasses; + value = { + Lcom/paypal/android/p2pmobile/wallet/banksandcards/fragments/EnterCardFragment$IEnterCardFragmentListener; + } +.end annotation + + +.field private static final DATE_SEPARATOR:C = '/' + +.field mDateFormatOrder:Lcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder; + + +# direct methods +.method static constructor ()V + .locals 0 + + return-void +.end method + +.method public constructor ()V + .locals 1 + + return-void +.end method + +.method private bindStartDateToMutableCredebitCard(Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard;)Z + .locals 10 + .param p1 # Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard; + .annotation build Landroid/support/annotation/NonNull; + .end annotation + .end param + + .line 1024 + iget-object v0, p0, LTestVariables6;->mFinancialInstrumentMetadataDefinition:Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition; + + invoke-virtual {v0}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition;->getStartMonth()Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; + + move-result-object v0 + + .line 1025 + iget-object v1, p0, LTestVariables6;->mFinancialInstrumentMetadataDefinition:Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition; + + invoke-virtual {v1}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataDefinition;->getStartYear()Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; + + move-result-object v1 + + const/4 v2, 0x2 + + .line 1026 + new-array v2, v2, [Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute; + + const/4 v3, 0x0 + + aput-object v0, v2, v3 + + const/4 v0, 0x1 + + aput-object v1, v2, v0 + + invoke-static {v2}, Lcom/paypal/android/p2pmobile/wallet/banksandcards/utils/EnterCardFragmentUtils;->attributesAreRequired([Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute;)Z + + move-result v2 + + if-nez v2, :cond_0 + + return v0 + + .line 1030 + :cond_0 + invoke-virtual {p0}, LTestVariables6;->getView()Landroid/view/View; + + move-result-object v2 + + if-nez v2, :cond_1 + + return v3 + + .line 1035 + :cond_1 + invoke-virtual {v1}, Lcom/paypal/android/foundation/wallet/model/FinancialInstrumentMetadataAttribute;->getMaximumLength()I + + move-result v6 + + .line 1036 + sget v1, Lcom/paypal/android/p2pmobile/wallet/R$id;->enter_card_start_date:I + + invoke-virtual {v2, v1}, Landroid/view/View;->findViewById(I)Landroid/view/View; + + move-result-object v1 + + check-cast v1, Landroid/widget/TextView; + + .line 1038 + new-instance v2, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser; + + invoke-virtual {v1}, Landroid/widget/TextView;->getText()Ljava/lang/CharSequence; + + move-result-object v1 + + invoke-interface {v1}, Ljava/lang/CharSequence;->toString()Ljava/lang/String; + + move-result-object v5 + + iget-object v7, p0, LTestVariables6;->mDateFormatOrder:Lcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder; + + const/16 v8, 0x2f + + const/4 v9, 0x0 + + move-object v4, v2 + + invoke-direct/range {v4 .. v9}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->(Ljava/lang/String;ILcom/paypal/android/p2pmobile/common/utils/ValidatedDateFormatOrder;CZ)V + + .line 1039 + invoke-virtual {v2}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->isError()Z + + move-result v1 + + if-nez v1, :cond_2 + + .line 1040 + invoke-virtual {v2}, Lcom/paypal/android/p2pmobile/common/utils/DateStringParser;->getDate()Ljava/util/Date; + + move-result-object v1 + + invoke-virtual {p1, v1}, Lcom/paypal/android/foundation/wallet/model/MutableCredebitCard;->setIssueDate(Ljava/util/Date;)V + + return v0 + + :cond_2 + return v3 +.end method diff --git a/jadx-core/src/test/smali/variables/TestVariablesGeneric.smali b/jadx-core/src/test/smali/variables/TestVariablesGeneric.smali new file mode 100644 index 000000000..dd9c78ea8 --- /dev/null +++ b/jadx-core/src/test/smali/variables/TestVariablesGeneric.smali @@ -0,0 +1,159 @@ +.class public Lvariables/TestVariablesGeneric; +.super Ljava/lang/Object; +.source "SourceFile" + +.method public static a(Lrx/i;Lrx/c;)Lrx/j; + .locals 3 + .annotation system Ldalvik/annotation/Signature; + value = { + "(", + "Lrx/i<", + "-TT;>;", + "Lrx/c<", + "TT;>;)", + "Lrx/j;" + } + .end annotation + + if-nez p0, :cond_0 + + .line 10325 + new-instance p0, Ljava/lang/IllegalArgumentException; + + const-string p1, "subscriber can not be null" + + invoke-direct {p0, p1}, Ljava/lang/IllegalArgumentException;->(Ljava/lang/String;)V + + throw p0 + + .line 10327 + :cond_0 + iget-object v0, p1, Lrx/c;->a:Lrx/c$a; + + if-nez v0, :cond_1 + + .line 10328 + new-instance p0, Ljava/lang/IllegalStateException; + + const-string p1, "onSubscribe function can not be null." + + invoke-direct {p0, p1}, Ljava/lang/IllegalStateException;->(Ljava/lang/String;)V + + throw p0 + + .line 10336 + :cond_1 + invoke-virtual {p0}, Lrx/i;->onStart()V + + .line 10343 + instance-of v0, p0, Lrx/c/c; + + if-nez v0, :cond_2 + + .line 10345 + new-instance v0, Lrx/c/c; + + invoke-direct {v0, p0}, Lrx/c/c;->(Lrx/i;)V + + move-object p0, v0 + + .line 10352 + :cond_2 + :try_start_0 + iget-object v0, p1, Lrx/c;->a:Lrx/c$a; + + invoke-static {p1, v0}, Lrx/d/c;->a(Lrx/c;Lrx/c$a;)Lrx/c$a; + + move-result-object p1 + + invoke-interface {p1, p0}, Lrx/c$a;->call(Ljava/lang/Object;)V + + .line 10353 + invoke-static {p0}, Lrx/d/c;->a(Lrx/j;)Lrx/j; + + move-result-object p1 + :try_end_0 + .catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0 + + return-object p1 + + :catch_0 + move-exception p1 + + .line 10356 + invoke-static {p1}, Lrx/exceptions/a;->b(Ljava/lang/Throwable;)V + + .line 10358 + invoke-virtual {p0}, Lrx/i;->isUnsubscribed()Z + + move-result v0 + + if-eqz v0, :cond_3 + + .line 10359 + invoke-static {p1}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; + + move-result-object p0 + + invoke-static {p0}, Lrx/d/c;->a(Ljava/lang/Throwable;)V + + goto :goto_0 + + .line 10363 + :cond_3 + :try_start_1 + invoke-static {p1}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; + + move-result-object v0 + + invoke-virtual {p0, v0}, Lrx/i;->onError(Ljava/lang/Throwable;)V + :try_end_1 + .catch Ljava/lang/Throwable; {:try_start_1 .. :try_end_1} :catch_1 + + .line 10375 + :goto_0 + invoke-static {}, Lrx/f/e;->b()Lrx/j; + + move-result-object p0 + + return-object p0 + + :catch_1 + move-exception p0 + + .line 10365 + invoke-static {p0}, Lrx/exceptions/a;->b(Ljava/lang/Throwable;)V + + .line 10368 + new-instance v0, Lrx/exceptions/OnErrorFailedException; + + new-instance v1, Ljava/lang/StringBuilder; + + const-string v2, "Error occurred attempting to subscribe [" + + invoke-direct {v1, v2}, Ljava/lang/StringBuilder;->(Ljava/lang/String;)V + + invoke-virtual {p1}, Ljava/lang/Throwable;->getMessage()Ljava/lang/String; + + move-result-object p1 + + invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; + + const-string p1, "] and then again while trying to pass to onError." + + invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder; + + invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String; + + move-result-object p1 + + invoke-direct {v0, p1, p0}, Lrx/exceptions/OnErrorFailedException;->(Ljava/lang/String;Ljava/lang/Throwable;)V + + .line 10370 + invoke-static {v0}, Lrx/d/c;->b(Ljava/lang/Throwable;)Ljava/lang/Throwable; + + .line 10372 + throw v0 +.end method diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java index a64ec479f..43096f2ec 100755 --- a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java @@ -126,13 +126,13 @@ public class CertificateManager { StringBuilder builder = new StringBuilder(); if (x509cert != null) { builder.append(generateHeader()); - builder.append("\n"); + builder.append('\n'); builder.append(generatePublicKey()); - builder.append("\n"); + builder.append('\n'); builder.append(generateSignature()); - builder.append("\n"); + builder.append('\n'); builder.append(generateFingerprint()); } return builder.toString(); @@ -150,7 +150,7 @@ public class CertificateManager { } static void append(StringBuilder str, String name, String value) { - str.append(name).append(": ").append(value).append("\n"); + str.append(name).append(": ").append(value).append('\n'); } public static String getThumbPrint(X509Certificate cert, String type) diff --git a/jadx-samples/src/main/java/jadx/samples/RunTests.java b/jadx-samples/src/main/java/jadx/samples/RunTests.java index 6ab2c55d6..6dcdecb91 100644 --- a/jadx-samples/src/main/java/jadx/samples/RunTests.java +++ b/jadx-samples/src/main/java/jadx/samples/RunTests.java @@ -65,7 +65,7 @@ public class RunTests { msg = "not extends AbstractTest"; } System.err.println(">> " - + (pass ? "PASS" : "FAIL") + "\t" + + (pass ? "PASS" : "FAIL") + '\t' + clsName + (msg == null ? "" : "\t - " + msg)); if (exc != null) { diff --git a/jadx-samples/src/main/java/jadx/samples/TestStringProcessing.java b/jadx-samples/src/main/java/jadx/samples/TestStringProcessing.java index c32928492..4c4d80e06 100644 --- a/jadx-samples/src/main/java/jadx/samples/TestStringProcessing.java +++ b/jadx-samples/src/main/java/jadx/samples/TestStringProcessing.java @@ -12,7 +12,7 @@ public class TestStringProcessing extends AbstractTest { public void testStringConcat() { String s = "1"; - assertEquals("a" + s, "a1"); + assertEquals('a' + s, "a1"); } @Override