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 40cc182672a..012068838b6 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/RegionGen.java @@ -48,6 +48,7 @@ import jadx.core.dex.regions.loops.LoopRegion; import jadx.core.dex.regions.loops.LoopType; import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.utils.BlockUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; @@ -365,11 +366,18 @@ private void makeCatchBlock(ICodeWriter code, ExceptionHandler handler) throws C if (arg == null) { code.add("unknown"); // throwing exception is too late at this point } else if (arg instanceof RegisterArg) { - SSAVar ssaVar = ((RegisterArg) arg).getSVar(); - if (code.isMetadataSupported()) { + RegisterArg regArg = (RegisterArg) arg; + SSAVar ssaVar = regArg.getSVar(); + if (ssaVar == null) { + InitCodeVariables.initCodeVar(mth, regArg); + ssaVar = regArg.getSVar(); + } else if (!ssaVar.isCodeVarSet()) { + InitCodeVariables.initCodeVar(ssaVar); + } + if (ssaVar != null && code.isMetadataSupported()) { code.attachDefinition(VarNode.get(mth, ssaVar)); } - code.add(mgen.getNameGen().assignArg(ssaVar.getCodeVar())); + code.add(ssaVar != null ? mgen.getNameGen().assignArg(ssaVar.getCodeVar()) : "unknown"); } else if (arg instanceof NamedArg) { code.add(mgen.getNameGen().assignNamedArg((NamedArg) arg)); } else { 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 171d9324761..9b9e86c5059 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 @@ -34,7 +34,7 @@ public SwitchRegion(IRegion parent, BlockNode header) { public static final class CaseInfo { private final List keys; - private final IContainer container; + private IContainer container; public CaseInfo(List keys, IContainer container) { this.keys = keys; @@ -85,6 +85,18 @@ public List getBranches() { return Collections.unmodifiableList(getCaseContainers()); } + @Override + public boolean replaceSubBlock(IContainer oldBlock, IContainer newBlock) { + for (CaseInfo caseInfo : cases) { + if (caseInfo.container == oldBlock) { + caseInfo.container = newBlock; + updateParent(newBlock, this); + return true; + } + } + return false; + } + @Override public void generate(RegionGen regionGen, ICodeWriter code) throws CodegenException { regionGen.makeSwitch(this, code); 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 9ec4adf40e7..623fb516039 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 @@ -414,6 +414,10 @@ private static void removeCheckCast(MethodNode mth, BlockNode block, int i, Inde ArgType castType = (ArgType) insn.getIndex(); if (!ArgType.isCastNeeded(mth.root(), castArg.getType(), castType)) { RegisterArg result = insn.getResult(); + if (!canUpdateCastResultType(result, castArg.getType())) { + insn.add(AFlag.EXPLICIT_CAST); + return; + } result.setType(castArg.getType()); InsnNode move = new InsnNode(InsnType.MOVE, 1); @@ -432,6 +436,14 @@ private static void removeCheckCast(MethodNode mth, BlockNode block, int i, Inde } } + private static boolean canUpdateCastResultType(@Nullable RegisterArg result, ArgType type) { + if (result == null || result.getSVar() == null) { + return false; + } + ArgType immutableType = result.getImmutableType(); + return immutableType == null || Objects.equals(immutableType, type); + } + private static @Nullable InsnNode isCastDuplicate(IndexInsnNode castInsn) { InsnArg arg = castInsn.getArg(0); if (arg.isRegister()) { @@ -520,19 +532,29 @@ private static void processAnonymousConstructor(MethodNode mth, ConstructorInsn int argsCount = Math.min(callMth.getMethodInfo().getArgsCount(), co.getArgsCount()); for (int i = 0; i < argsCount; i++) { if (attr.isSkip(i)) { - anonymousCallArgMod(co.getArg(i)); + anonymousCallArgMod(mth, co.getArg(i)); } } } else { // additional info not available apply mods to all args (the safest solution) - co.getArguments().forEach(ModVisitor::anonymousCallArgMod); + co.getArguments().forEach(arg -> anonymousCallArgMod(mth, arg)); } } - private static void anonymousCallArgMod(InsnArg arg) { + private static void anonymousCallArgMod(MethodNode mth, InsnArg arg) { arg.add(AFlag.DONT_INLINE); if (arg.isRegister()) { - ((RegisterArg) arg).getSVar().getCodeVar().setFinal(true); + RegisterArg regArg = (RegisterArg) arg; + SSAVar ssaVar = regArg.getSVar(); + if (ssaVar == null) { + InitCodeVariables.initCodeVar(mth, regArg); + ssaVar = regArg.getSVar(); + } else if (!ssaVar.isCodeVarSet()) { + InitCodeVariables.initCodeVar(ssaVar); + } + if (ssaVar != null) { + ssaVar.getCodeVar().setFinal(true); + } } } 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 1ea5fa02569..72db2feaf09 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 @@ -115,6 +115,12 @@ private static void removeInstructions(BlockNode block) { case MOVE: // remove redundant moves: unused result and same args names (a = a;) RegisterArg result = insn.getResult(); + if (result == null) { + if (!insn.getArg(0).isInsnWrap()) { + it.remove(); + } + break; + } if (result.getSVar().getUseCount() == 0 && result.isNameEquals(insn.getArg(0))) { it.remove(); @@ -254,7 +260,7 @@ private static void modifyArith(BlockNode block) { * Otherwise, move to the top and add a warning. */ private void moveConstructorInConstructor(MethodNode mth) { - if (!mth.isConstructor()) { + if (!mth.isConstructor() || mth.getRegion() == null) { return; } ConstructorInsn ctrInsn = searchConstructorCall(mth); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockProcessor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockProcessor.java index 5209ed80cf9..83f88fb4103 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockProcessor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockProcessor.java @@ -140,7 +140,9 @@ private static void checkForUnreachableBlocks(MethodNode mth) { fixed = true; break; } - throw new JadxRuntimeException("Unreachable block: " + block); + removeUnreachableBlock(block, mth); + fixed = true; + break; } } if (!fixed) { 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 00d667d727f..314426b980f 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 @@ -9,6 +9,7 @@ import jadx.core.dex.visitors.regions.maker.SynchronizedRegionMaker; import jadx.core.dex.visitors.shrink.CodeShrinkVisitor; import jadx.core.utils.exceptions.JadxException; +import jadx.core.utils.exceptions.JadxOverflowException; @JadxVisitor( name = "RegionMakerVisitor", @@ -21,17 +22,25 @@ public void visit(MethodNode mth) throws JadxException { if (mth.isNoCode() || mth.getBasicBlocks().isEmpty()) { return; } - RegionMaker rm = new RegionMaker(mth); - mth.setRegion(rm.makeMthRegion()); - if (!mth.isNoExceptionHandlers()) { - new ExcHandlersRegionMaker(mth, rm).process(); - } - processForceInlineInsns(mth); - ProcessTryCatchRegions.process(mth); - PostProcessRegions.process(mth); - CleanRegions.process(mth); - if (mth.getAccessFlags().isSynchronized()) { - SynchronizedRegionMaker.removeSynchronized(mth); + try { + RegionMaker rm = new RegionMaker(mth); + mth.setRegion(rm.makeMthRegion()); + if (!mth.isNoExceptionHandlers()) { + new ExcHandlersRegionMaker(mth, rm).process(); + } + processForceInlineInsns(mth); + ProcessTryCatchRegions.process(mth); + PostProcessRegions.process(mth); + CleanRegions.process(mth); + if (mth.getAccessFlags().isSynchronized()) { + SynchronizedRegionMaker.removeSynchronized(mth); + } + } catch (JadxOverflowException e) { + mth.setRegion(null); + mth.addWarnComment("Code restructure failed: " + e.getMessage() + ", using fallback mode"); + } catch (StackOverflowError e) { + mth.setRegion(null); + mth.addWarnComment("Code restructure failed: StackOverflowError, using fallback mode", e); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchBreakVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchBreakVisitor.java index 69236447cf8..75b7a9c5b09 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchBreakVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/SwitchBreakVisitor.java @@ -207,7 +207,7 @@ public boolean enterRegion(MethodNode mth, IRegion region) { public void leaveRegion(MethodNode mth, IRegion region) { if (addBreakRegion.contains(region)) { addBreakRegion.remove(region); - region.getSubBlocks().add(SwitchRegionMaker.buildBreakContainer(currentSwitch)); + SwitchRegionMaker.appendBreak(mth, region, currentSwitch); } if (cleanupSet.contains(region)) { cleanupSet.remove(region); 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 a66a1959210..231de20c388 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 @@ -312,7 +312,7 @@ private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockN // forcing ternary inline for constructors (will help in moving super call to the top) and enums // skip code style checks } else { - if (elseAssign != null && elseAssign.isConstInsn()) { + if (canInlineConstAssign(elseAssign)) { if (!verifyLineHints(mth, insn, elseAssign)) { return; } @@ -330,7 +330,7 @@ private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockN return; } InsnArg elseArg; - if (elseAssign != null && elseAssign.isConstInsn()) { + if (canInlineConstAssign(elseAssign)) { // inline constant elseArg = InsnArg.wrapInsnIntoArg(elseAssign.copyWithoutResult()); SSAVar elseVar = elseAssign.getResult().getSVar(); @@ -359,6 +359,10 @@ private static void replaceWithTernary(MethodNode mth, IfRegion ifRegion, BlockN CodeShrinkVisitor.shrinkMethod(mth); } + private static boolean canInlineConstAssign(InsnNode insn) { + return insn != null && insn.isConstInsn() && insn.getResult() != null; + } + private TernaryMod() { } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/ExcHandlersRegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/ExcHandlersRegionMaker.java index c289d6f8193..e7a95d3051c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/ExcHandlersRegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/ExcHandlersRegionMaker.java @@ -50,7 +50,12 @@ public void process() { BlockNode handlerBlock = handler.getHandlerBlock(); if (handlerBlock != null) { blocks.add(handlerBlock); - splitters.add(BlockUtils.getTopSplitterForHandler(handlerBlock)); + BlockNode splitter = BlockUtils.searchTopSplitterForHandler(handlerBlock); + if (splitter != null) { + splitters.add(splitter); + } else { + mth.addDebugComment("Can't find top splitter block for handler: " + handlerBlock); + } } else { mth.addDebugComment("No exception handler block: " + handler); } @@ -124,7 +129,11 @@ private void processExcHandler(ExceptionHandler handler, Set exits) { RegionStack stack = regionMaker.getStack().clear(); BlockNode dom; if (handler.isFinally()) { - dom = BlockUtils.getTopSplitterForHandler(start); + dom = BlockUtils.searchTopSplitterForHandler(start); + if (dom == null) { + mth.addDebugComment("Can't find top splitter block for finally handler: " + start); + dom = start; + } } else { dom = start; stack.addExits(exits); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SwitchRegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SwitchRegionMaker.java index 3099853aebd..5ae096249bc 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SwitchRegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/SwitchRegionMaker.java @@ -158,13 +158,13 @@ private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitS outs.clear(mth.getExitBlock().getId()); BlockNode out = null; + LoopInfo loop = mth.getLoopForBlock(block); if (outs.cardinality() == 1) { // single exit out = BlockUtils.bitSetToOneBlock(mth, outs); } else { // several switch exits // possible 'return', 'continue' or fallthrough in one of the cases - LoopInfo loop = mth.getLoopForBlock(block); if (loop != null) { outs.andNot(loop.getStart().getPostDoms()); outs.andNot(loop.getEnd().getPostDoms()); @@ -220,12 +220,27 @@ private BlockNode getOneIntersectionBlock(BlockNode out, BitSet caseBlocks, BitS // 'out' block already processed, prevent endless loop // in this case it might be that 'out' is the LOOP_START of a loop and occurs before 'block' // just try the immediate post dominator as a fallback - mth.addWarnComment("Switch 'out' block " + out + " for " + block + " already processed. Defaulting to fallback option."); - out = block.getIPostDom(); + BlockNode fallbackOut = block.getIPostDom(); + if (isLoopBackEdgeOut(loop, out, fallbackOut)) { + // Dominance frontier can point to the current loop header instead of the switch exit. + out = fallbackOut; + } else { + mth.addWarnComment("Switch 'out' block " + out + " for " + block + + " already processed. Defaulting to fallback option."); + out = fallbackOut; + } } return out; } + private boolean isLoopBackEdgeOut(@Nullable LoopInfo loop, BlockNode out, @Nullable BlockNode fallbackOut) { + return loop != null + && out == loop.getStart() + && fallbackOut != null + && fallbackOut != out + && !regionMaker.isProcessed(fallbackOut); + } + private BlockNode allSameReturns(RegionStack stack) { BlockNode exitBlock = mth.getExitBlock(); List preds = exitBlock.getPredecessors(); @@ -416,7 +431,7 @@ public void leaveRegion(MethodNode mth, IRegion region) { } } if (insertBreak && canAppendBreak(region)) { - region.getSubBlocks().add(buildBreakContainer(switchRegion)); + appendBreak(mth, region, switchRegion); } } }); @@ -426,6 +441,30 @@ public static boolean canAppendBreak(IRegion region) { return !region.contains(AFlag.FALL_THROUGH) && !RegionUtils.hasExitBlock(region); } + public static boolean appendBreak(MethodNode mth, IRegion region, SwitchRegion switchRegion) { + return appendToRegion(mth, region, buildBreakContainer(switchRegion)); + } + + private static boolean appendToRegion(MethodNode mth, IRegion region, IContainer container) { + if (region instanceof Region) { + ((Region) region).add(container); + return true; + } + IRegion parent = region.getParent(); + if (parent == null) { + mth.addWarnComment("Failed to append 'break' to region without parent"); + return false; + } + Region wrapper = new Region(parent); + if (!parent.replaceSubBlock(region, wrapper)) { + mth.addWarnComment("Failed to append 'break' to region: parent doesn't support replacement"); + return false; + } + wrapper.add(region); + wrapper.add(container); + return true; + } + public static InsnContainer buildBreakContainer(SwitchRegion switchRegion) { InsnNode breakInsn = new InsnNode(InsnType.BREAK, 0); breakInsn.add(AFlag.SYNTHETIC); 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 index c848f20ce43..a0e910a9e0d 100644 --- 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 @@ -3,6 +3,7 @@ import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.InitCodeVariables; import jadx.core.dex.visitors.JadxVisitor; @JadxVisitor( @@ -20,6 +21,9 @@ public void visit(MethodNode mth) { return; } mth.getSVars().forEach(var -> { + if (!var.isCodeVarSet()) { + InitCodeVariables.initCodeVar(var); + } ArgType type = var.getTypeInfo().getType(); if (!type.isTypeKnown()) { mth.addWarnComment("Type inference failed for: " + var.getDetailedVarInfo(mth)); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FixTypesVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FixTypesVisitor.java index 551b42468af..b2e99424d9b 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FixTypesVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/typeinference/FixTypesVisitor.java @@ -95,6 +95,8 @@ public void visit(MethodNode mth) { break; } } + } catch (JadxOverflowException e) { + mth.addWarnComment("Type fixes stopped after reaching update limit: " + e.getMessage()); } catch (Exception e) { mth.addError("Types fix failed", e); } 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 index d93d117e3bd..66989933e6b 100644 --- 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 @@ -75,6 +75,8 @@ public void visit(MethodNode mth) { runTypePropagation(mth); } catch (StackOverflowError | BootstrapMethodError e) { mth.addError("Type inference failed with stack overflow", new JadxOverflowException(e.getMessage())); + } catch (JadxOverflowException e) { + mth.addWarnComment("Type inference stopped after reaching update limit: " + e.getMessage()); } catch (Exception e) { mth.addError("Type inference failed", e); } 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 index 32d8f6f0e68..b80776feb6d 100644 --- 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 @@ -268,6 +268,9 @@ private TypeUpdateResult updateTypeForArg(TypeUpdateInfo updateInfo, InsnArg arg } private TypeUpdateResult updateTypeForSsaVar(TypeUpdateInfo updateInfo, SSAVar ssaVar, ArgType candidateType) { + if (updateInfo.isProcessed(ssaVar.getAssign())) { + return CHANGED; + } TypeInfo typeInfo = ssaVar.getTypeInfo(); ArgType immutableType = ssaVar.getImmutableType(); if (immutableType != null && !Objects.equals(immutableType, candidateType)) { @@ -448,6 +451,9 @@ private TypeUpdateResult invokeListener(TypeUpdateInfo updateInfo, InsnNode insn private TypeUpdateResult sameFirstArgListener(TypeUpdateInfo updateInfo, InsnNode insn, InsnArg arg, ArgType candidateType) { InsnArg changeArg = isAssign(insn, arg) ? insn.getArg(0) : insn.getResult(); + if (changeArg == null) { + return CHANGED; + } if (updateInfo.hasUpdateWithType(changeArg, candidateType)) { return 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 5ba00807b93..0019903cd6c 100644 --- a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java @@ -1473,13 +1473,18 @@ public static boolean replaceInsn(MethodNode mth, InsnNode oldInsn, InsnNode new } public static BlockNode getTopSplitterForHandler(BlockNode handlerBlock) { - BlockNode block = getBlockWithFlag(handlerBlock.getPredecessors(), AFlag.EXC_TOP_SPLITTER); + BlockNode block = searchTopSplitterForHandler(handlerBlock); if (block == null) { throw new JadxRuntimeException("Can't find top splitter block for handler:" + handlerBlock); } return block; } + @Nullable + public static BlockNode searchTopSplitterForHandler(BlockNode handlerBlock) { + return getBlockWithFlag(handlerBlock.getPredecessors(), AFlag.EXC_TOP_SPLITTER); + } + /** * Return out block of try catch, by finding where try branch meets catch branch. * It traverse domFrontier start from handler block, find the first frontier @@ -1492,7 +1497,10 @@ public static BlockNode getTopSplitterForHandler(BlockNode handlerBlock) { @Nullable public static BlockNode getTryAndHandlerCrossBlock(MethodNode mth, ExceptionHandler handler) { BlockNode start = handler.getHandlerBlock(); - BlockNode topSplitter = BlockUtils.getTopSplitterForHandler(start); + BlockNode topSplitter = BlockUtils.searchTopSplitterForHandler(start); + if (topSplitter == null) { + return null; + } List allHandlers = handler.getTryBlock().getHandlers(); List handlerExitsCandidate = new ArrayList<>(BlockUtils.bitSetToBlocks(mth, start.getDomFrontier())); BitSet visited = newBlocksBitSet(mth); diff --git a/jadx-core/src/main/java/jadx/core/utils/Pair.java b/jadx-core/src/main/java/jadx/core/utils/Pair.java index 2a5f38242cb..b92a6c6b5fe 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Pair.java +++ b/jadx-core/src/main/java/jadx/core/utils/Pair.java @@ -1,5 +1,7 @@ package jadx.core.utils; +import java.util.Objects; + public class Pair { private final T first; @@ -27,12 +29,12 @@ public boolean equals(Object o) { return false; } Pair other = (Pair) o; - return first.equals(other.first) && second.equals(other.second); + return Objects.equals(first, other.first) && Objects.equals(second, other.second); } @Override public int hashCode() { - return first.hashCode() + 31 * second.hashCode(); + return Objects.hashCode(first) + 31 * Objects.hashCode(second); } @Override diff --git a/jadx-core/src/test/java/jadx/tests/functional/TestDecompileGuards.java b/jadx-core/src/test/java/jadx/tests/functional/TestDecompileGuards.java new file mode 100644 index 00000000000..edc77513155 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/functional/TestDecompileGuards.java @@ -0,0 +1,87 @@ +package jadx.tests.functional; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +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.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.InsnNode; +import jadx.core.dex.visitors.ModVisitor; +import jadx.core.dex.visitors.PrepareForCodeGen; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.Pair; +import jadx.core.utils.exceptions.JadxRuntimeException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestDecompileGuards { + + @Test + public void testSearchTopSplitterForHandlerMissing() { + BlockNode handler = new BlockNode(1, 1, 0); + + assertThat(BlockUtils.searchTopSplitterForHandler(handler)).isNull(); + assertThatThrownBy(() -> BlockUtils.getTopSplitterForHandler(handler)) + .isInstanceOf(JadxRuntimeException.class) + .hasMessageContaining("Can't find top splitter block"); + } + + @Test + public void testSearchTopSplitterForHandlerFound() { + BlockNode splitter = new BlockNode(1, 1, 0); + splitter.add(AFlag.EXC_TOP_SPLITTER); + + BlockNode handler = new BlockNode(2, 2, 0); + handler.getPredecessors().add(splitter); + + assertThat(BlockUtils.searchTopSplitterForHandler(handler)).isSameAs(splitter); + assertThat(BlockUtils.getTopSplitterForHandler(handler)).isSameAs(splitter); + } + + @Test + public void testPrepareForCodeGenRemovesMoveWithoutResult() throws Exception { + BlockNode block = new BlockNode(1, 1, 0); + InsnNode move = new InsnNode(InsnType.MOVE, 1); + move.addArg(LiteralArg.make(0, ArgType.INT)); + block.getInstructions().add(move); + + Method removeInstructions = PrepareForCodeGen.class.getDeclaredMethod("removeInstructions", BlockNode.class); + removeInstructions.setAccessible(true); + removeInstructions.invoke(null, block); + + assertThat(block.getInstructions()).isEmpty(); + } + + @Test + public void testPairAllowsNullValues() { + Pair pair = new Pair<>(new BlockNode(1, 1, 0), null); + Pair samePair = new Pair<>(pair.getFirst(), null); + Pair otherPair = new Pair<>(null, pair.getFirst()); + + assertThat(pair).isEqualTo(samePair); + assertThat(pair.hashCode()).isEqualTo(samePair.hashCode()); + assertThat(pair).isNotEqualTo(otherPair); + } + + @Test + public void testCheckCastResultTypeRejectsImmutableConflict() throws Exception { + Method check = ModVisitor.class.getDeclaredMethod("canUpdateCastResultType", RegisterArg.class, ArgType.class); + check.setAccessible(true); + + RegisterArg result = InsnArg.reg(0, ArgType.OBJECT); + result.add(AFlag.IMMUTABLE_TYPE); + new SSAVar(0, 0, result); + + assertThat(check.invoke(null, result, ArgType.OBJECT)).isEqualTo(true); + assertThat(check.invoke(null, result, ArgType.STRING)).isEqualTo(false); + assertThat(check.invoke(null, InsnArg.reg(1, ArgType.OBJECT), ArgType.OBJECT)).isEqualTo(false); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/functional/TestSwitchRegionMaker.java b/jadx-core/src/test/java/jadx/tests/functional/TestSwitchRegionMaker.java new file mode 100644 index 00000000000..c1568576d22 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/functional/TestSwitchRegionMaker.java @@ -0,0 +1,59 @@ +package jadx.tests.functional; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IContainer; +import jadx.core.dex.nodes.InsnContainer; +import jadx.core.dex.regions.Region; +import jadx.core.dex.regions.SwitchRegion; +import jadx.core.dex.regions.conditions.IfRegion; +import jadx.core.dex.visitors.regions.maker.SwitchRegionMaker; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestSwitchRegionMaker { + + @Test + public void testAppendBreakWrapsIfRegion() { + Region parent = new Region(null); + IfRegion ifRegion = new IfRegion(parent); + ifRegion.updateCondition(new BlockNode(1, 1, 0)); + parent.add(ifRegion); + + SwitchRegion switchRegion = new SwitchRegion(parent, new BlockNode(2, 2, 0)); + + assertThat(SwitchRegionMaker.appendBreak(null, ifRegion, switchRegion)).isTrue(); + + Region wrapper = (Region) parent.getSubBlocks().get(0); + assertThat(wrapper.getSubBlocks().get(0)).isSameAs(ifRegion); + assertThat(ifRegion.getParent()).isSameAs(wrapper); + assertBreakContainer(wrapper.getSubBlocks().get(1)); + } + + @Test + public void testAppendBreakReplacesSwitchCaseContainer() { + SwitchRegion switchRegion = new SwitchRegion(null, new BlockNode(1, 1, 0)); + IfRegion caseRegion = new IfRegion(switchRegion); + caseRegion.updateCondition(new BlockNode(2, 2, 0)); + switchRegion.addCase(Collections.singletonList(1), caseRegion); + + assertThat(SwitchRegionMaker.appendBreak(null, caseRegion, switchRegion)).isTrue(); + + Region wrapper = (Region) switchRegion.getCases().get(0).getContainer(); + assertThat(wrapper.getSubBlocks().get(0)).isSameAs(caseRegion); + assertThat(caseRegion.getParent()).isSameAs(wrapper); + assertBreakContainer(wrapper.getSubBlocks().get(1)); + } + + private static void assertBreakContainer(IContainer container) { + assertThat(container).isInstanceOf(InsnContainer.class); + InsnContainer insnContainer = (InsnContainer) container; + assertThat(insnContainer.getInstructions()) + .singleElement() + .satisfies(insn -> assertThat(insn.getType()).isEqualTo(InsnType.BREAK)); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/functional/TestTypeUpdate.java b/jadx-core/src/test/java/jadx/tests/functional/TestTypeUpdate.java new file mode 100644 index 00000000000..fc70c1e6440 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/functional/TestTypeUpdate.java @@ -0,0 +1,40 @@ +package jadx.tests.functional; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import jadx.api.JadxArgs; +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.LiteralArg; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.visitors.typeinference.TypeUpdate; +import jadx.core.dex.visitors.typeinference.TypeUpdateInfo; +import jadx.core.dex.visitors.typeinference.TypeUpdateResult; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestTypeUpdate { + + @Test + public void testConstListenerWithMissingResult() throws Exception { + TypeUpdate typeUpdate = new TypeUpdate(new RootNode(new JadxArgs())); + InsnNode constInsn = new InsnNode(InsnType.CONST, 1); + LiteralArg arg = LiteralArg.make(0, ArgType.UNKNOWN); + constInsn.addArg(arg); + + Method listener = TypeUpdate.class.getDeclaredMethod("sameFirstArgListener", + TypeUpdateInfo.class, + InsnNode.class, + InsnArg.class, + ArgType.class); + listener.setAccessible(true); + + Object result = listener.invoke(typeUpdate, null, constInsn, arg, ArgType.DOUBLE); + + assertThat(result).isEqualTo(TypeUpdateResult.CHANGED); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestBlockProcessorGuards.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestBlockProcessorGuards.java new file mode 100644 index 00000000000..b413e511677 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestBlockProcessorGuards.java @@ -0,0 +1,44 @@ +package jadx.tests.integration.others; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.blocks.BlockProcessor; +import jadx.tests.api.IntegrationTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestBlockProcessorGuards extends IntegrationTest { + + public static class TestCls { + public int test() { + return 1; + } + } + + @Test + public void testCheckForUnreachableBlocksRemovesOrphanBlock() throws Exception { + disableCompilation(); + + ClassNode cls = getClassNode(TestCls.class); + MethodNode mth = getMethod(cls, "test"); + List blocks = new ArrayList<>(mth.getBasicBlocks()); + BlockNode orphanBlock = new BlockNode(1000, blocks.size(), 0x100); + blocks.add(orphanBlock); + mth.setBasicBlocks(blocks); + + Method check = BlockProcessor.class.getDeclaredMethod("checkForUnreachableBlocks", MethodNode.class); + check.setAccessible(true); + check.invoke(null, mth); + + assertThat(mth.getBasicBlocks()).doesNotContain(orphanBlock); + assertThat(orphanBlock.getPredecessors()).isEmpty(); + assertThat(orphanBlock.getSuccessors()).isEmpty(); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop10.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop10.java new file mode 100644 index 00000000000..d771f32b5ed --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop10.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.switches; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestSwitchInLoop10 extends IntegrationTest { + + public static class TestCls { + private int pos; + + public TestCls mergeFrom() { + int tag; + do { + tag = readTag(); + switch (tag) { + case 0: + return this; + default: + break; + } + } while (parseUnknownField(tag)); + return this; + } + + private int readTag() { + return pos++ == 0 ? 1 : 0; + } + + private boolean parseUnknownField(int tag) { + return tag > 0 && pos < 3; + } + + public void check() { + org.assertj.core.api.Assertions.assertThat(mergeFrom()).isSameAs(this); + } + } + + @Test + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("do {") + .containsOne("switch (tag) {") + .containsOne("case 0:") + .containsOne("while (parseUnknownField(tag));") + .doesNotContain("Switch 'out' block"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver27.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver27.java new file mode 100644 index 00000000000..bd4fa64c573 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver27.java @@ -0,0 +1,136 @@ +package jadx.tests.integration.types; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTypeResolver27 extends IntegrationTest { + + public static class TestCls { + public static A test(String[] args) { + A a = new A(); + int i = 0; + while (i < args.length) { + String arg = args[i++]; + switch (arg) { + case "a0": + a = a.set(args[i++]); + if (a != null) { + a.mark(0); + } + break; + + case "a1": + a = a.set(args[i++]); + if (a != null) { + a.mark(1); + } + break; + + case "a2": + a = a.set(args[i++]); + if (a != null) { + a.mark(2); + } + break; + + case "a3": + a = a.set(args[i++]); + if (a != null) { + a.mark(3); + } + break; + + case "a4": + a = a.set(args[i++]); + if (a != null) { + a.mark(4); + } + break; + + case "a5": + a = a.set(args[i++]); + if (a != null) { + a.mark(5); + } + break; + + case "a6": + a = a.set(args[i++]); + if (a != null) { + a.mark(6); + } + break; + + case "a7": + a = a.set(args[i++]); + if (a != null) { + a.mark(7); + } + break; + + case "a8": + a = a.set(args[i++]); + if (a != null) { + a.mark(8); + } + break; + + case "a9": + a = a.set(args[i++]); + if (a != null) { + a.mark(9); + } + break; + + case "a10": + a = a.set(args[i++]); + if (a != null) { + a.mark(10); + } + break; + + case "a11": + a = a.set(args[i++]); + if (a != null) { + a.mark(11); + } + break; + + default: + a.mark(-1); + break; + } + } + return a; + } + + public static class A { + private int value; + + public A set(String s) { + value += s.length(); + return this; + } + + public void mark(int value) { + this.value += value; + } + } + } + + @Test + public void test() { + args.setTypeUpdatesLimitCount(1); + noDebugInfo(); + disableCompilation(); + assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("type inference failed") + .doesNotContain("Type inference failed") + .doesNotContain("Types fix failed") + .containsOne("return a;"); + } +}