diff --git a/jadx-core/build.gradle.kts b/jadx-core/build.gradle.kts index e2c54200f..9b6821ec6 100644 --- a/jadx-core/build.gradle.kts +++ b/jadx-core/build.gradle.kts @@ -11,10 +11,12 @@ dependencies { testImplementation("org.apache.commons:commons-lang3:3.20.0") testImplementation(project(":jadx-plugins:jadx-dex-input")) - testRuntimeOnly(project(":jadx-plugins:jadx-smali-input")) - testRuntimeOnly(project(":jadx-plugins:jadx-java-convert")) - testRuntimeOnly(project(":jadx-plugins:jadx-java-input")) - testRuntimeOnly(project(":jadx-plugins:jadx-raung-input")) + // 'ClassNotFound' error is raised if set as 'testRuntime' + // for the plugins below when running the tests from vscode. + testImplementation(project(":jadx-plugins:jadx-smali-input")) + testImplementation(project(":jadx-plugins:jadx-java-convert")) + testImplementation(project(":jadx-plugins:jadx-java-input")) + testImplementation(project(":jadx-plugins:jadx-raung-input")) testImplementation("org.eclipse.jdt:ecj") { version { diff --git a/jadx-core/src/main/java/jadx/api/JavaClass.java b/jadx-core/src/main/java/jadx/api/JavaClass.java index d64de1e5b..d5ee9818a 100644 --- a/jadx-core/src/main/java/jadx/api/JavaClass.java +++ b/jadx-core/src/main/java/jadx/api/JavaClass.java @@ -362,4 +362,21 @@ public final class JavaClass implements JavaNode { public String toString() { return getFullName(); } + + /** + * Detect if calling load() would trigger a potentially expensive decompilation operation. + */ + public boolean loadingWouldRequireDecompilation() { + if (listsLoaded) { + // lists are already poplulated, so it's safe regardless of the state of the class itself + return false; + } + + if (cls.getState().isProcessComplete()) { + // decompilation has already finished + return false; + } + + return true; + } } diff --git a/jadx-core/src/main/java/jadx/api/JavaMethod.java b/jadx-core/src/main/java/jadx/api/JavaMethod.java index bfc5f96fa..43b06e698 100644 --- a/jadx-core/src/main/java/jadx/api/JavaMethod.java +++ b/jadx-core/src/main/java/jadx/api/JavaMethod.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import jadx.api.metadata.ICodeAnnotation; import jadx.api.metadata.ICodeNodeRef; +import jadx.api.plugins.input.data.IMethodRef; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.MethodOverrideAttr; import jadx.core.dex.info.AccessInfo; @@ -72,6 +73,18 @@ public final class JavaMethod implements JavaNode { return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUseIn()); } + public List getUsed() { + return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed()); + } + + public List getUnresolvedUsed() { + return mth.getUnresolvedUsed(); + } + + public boolean callsSelf() { + return mth.callsSelf(); + } + public List getOverrideRelatedMethods() { MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE); if (ovrdAttr == null) { diff --git a/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java b/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java index 4a6f21bf0..226d0ab27 100644 --- a/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java +++ b/jadx-core/src/main/java/jadx/api/metadata/impl/CodeMetadataStorage.java @@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata { return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap()); } + // -> private final Map lines; + // -> + // the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel. private final NavigableMap navMap; private CodeMetadataStorage(Map lines, NavigableMap navMap) { diff --git a/jadx-core/src/main/java/jadx/api/usage/IUsageInfoVisitor.java b/jadx-core/src/main/java/jadx/api/usage/IUsageInfoVisitor.java index 0495a052e..d632315cf 100644 --- a/jadx-core/src/main/java/jadx/api/usage/IUsageInfoVisitor.java +++ b/jadx-core/src/main/java/jadx/api/usage/IUsageInfoVisitor.java @@ -2,6 +2,7 @@ package jadx.api.usage; import java.util.List; +import jadx.api.plugins.input.data.IMethodRef; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.MethodNode; @@ -18,5 +19,11 @@ public interface IUsageInfoVisitor { void visitMethodsUsage(MethodNode mth, List methods); + void visitMethodsUses(MethodNode mth, List methods); + + void visitUnresolvedMethodsUsage(MethodNode mth, List methods); + + void visitIsSelfCall(MethodNode mth, boolean isSelfCall); + void visitComplete(); } diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index fd1c9f3a7..65a62f372 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -15,6 +15,7 @@ import jadx.api.JadxArgs; import jadx.core.deobf.DeobfuscatorVisitor; import jadx.core.deobf.SaveDeobfMapping; import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.visitors.AdjustForIfMergeVisitor; import jadx.core.dex.visitors.AnonymousClassVisitor; import jadx.core.dex.visitors.ApplyVariableNames; import jadx.core.dex.visitors.AttachCommentsVisitor; @@ -156,6 +157,8 @@ public class Jadx { passes.add(new FixTypesVisitor()); passes.add(new FinishTypeInference()); + passes.add(new AdjustForIfMergeVisitor()); + if (args.getUseKotlinMethodsForVarNames() != JadxArgs.UseKotlinMethodsForVarNames.DISABLE) { passes.add(new ProcessKotlinInternals()); } 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 174feae18..afb15e133 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 @@ -18,6 +18,7 @@ public enum AFlag { DONT_WRAP, DONT_INLINE, DONT_INLINE_CONST, + DONT_INVERT, // don't invert this if statement DONT_GENERATE, // process as usual, but don't output to generated code COMMENT_OUT, // process as usual, but comment insn in generated code REMOVE, // can be completely removed @@ -30,6 +31,9 @@ public enum AFlag { ADDED_TO_REGION, + // this loop condition has been merged or otherwise shouldn't be subject to the 1 instruction limit + ALLOW_MULTIPLE_INSNS_LOOP_COND, + EXC_TOP_SPLITTER, EXC_BOTTOM_SPLITTER, FINALLY_INSNS, @@ -67,6 +71,11 @@ public enum AFlag { */ FORCE_ASSIGN_INLINE, + /** + * A MOVE instruction has been inlined + */ + MOVE_INLINED, + CUSTOM_DECLARE, // variable for this register don't need declaration DECLARE_VAR, 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 e6622651b..5ca1a3189 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 @@ -11,6 +11,7 @@ import jadx.core.dex.attributes.nodes.DecompileModeOverrideAttr; import jadx.core.dex.attributes.nodes.EdgeInsnAttr; import jadx.core.dex.attributes.nodes.EnumClassAttr; import jadx.core.dex.attributes.nodes.EnumMapAttr; +import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr; import jadx.core.dex.attributes.nodes.FieldReplaceAttr; import jadx.core.dex.attributes.nodes.ForceReturnAttr; import jadx.core.dex.attributes.nodes.GenericInfoAttr; @@ -92,6 +93,7 @@ public final class AType implements IJadxAttrType { public static final AType> SPECIAL_EDGE = new AType<>(); public static final AType TMP_EDGE = new AType<>(); public static final AType TRY_BLOCK = new AType<>(); + public static final AType EXC_SPLIT_CROSS = new AType<>(); // block or insn public static final AType EXC_HANDLER = 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 8b976753b..4c1fdc33c 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 @@ -9,6 +9,8 @@ import jadx.core.utils.Utils; public class AttrList implements IJadxAttribute { + private static final int MAX_ATTRLIST_LENGTH = 300; + private final IJadxAttrType> type; private final List list = new ArrayList<>(); @@ -27,6 +29,11 @@ public class AttrList implements IJadxAttribute { @Override public String toString() { - return Utils.listToString(list, ", "); + String commaDelimited = Utils.listToString(list, ", "); + // if the comma delimited list is too long, use newlines instead to maintain readability + if (commaDelimited.length() > MAX_ATTRLIST_LENGTH) { + return Utils.listToString(list, "\n "); + } + return commaDelimited; } } diff --git a/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ExcSplitCrossAttr.java b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ExcSplitCrossAttr.java new file mode 100644 index 000000000..80559f613 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/attributes/nodes/ExcSplitCrossAttr.java @@ -0,0 +1,35 @@ +package jadx.core.dex.attributes.nodes; + +import jadx.api.plugins.input.data.attributes.IJadxAttrType; +import jadx.api.plugins.input.data.attributes.IJadxAttribute; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.nodes.BlockNode; + +/** + * This attribute is set on the new synthetic node that BlockExceptionHandler creates at the bottom + * of certain try regions. It stores a reference to the original path cross of the bottom of the try + * region, so that blocks can be restructured to not pass through it when that would create an + * erroneous loop. + */ +public class ExcSplitCrossAttr implements IJadxAttribute { + + private final BlockNode originalPathCross; + + public ExcSplitCrossAttr(BlockNode originalPathCross) { + this.originalPathCross = originalPathCross; + } + + public BlockNode getOriginalPathCross() { + return this.originalPathCross; + } + + @Override + public IJadxAttrType getAttrType() { + return AType.EXC_SPLIT_CROSS; + } + + @Override + public String toString() { + return "ExcSplitCross -> " + originalPathCross.toString(); + } +} 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 d6d38a6fa..359c8a7f6 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 @@ -348,6 +348,7 @@ public class InsnDecoder { return insn(InsnType.THROW, null, InsnArg.reg(insn, 0, ArgType.THROWABLE)); case MOVE_EXCEPTION: + method.add(AFlag.COMPUTE_POST_DOM); // Post dominators required for try/catch block processing return insn(InsnType.MOVE_EXCEPTION, InsnArg.reg(insn, 0, ArgType.UNKNOWN_OBJECT_NO_ARRAY)); case RETURN_VOID: 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 d25a4e1a8..94337ea55 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 @@ -12,6 +12,7 @@ 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.IBlock; import jadx.core.dex.nodes.InsnNode; import jadx.core.utils.InsnRemover; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -110,6 +111,18 @@ public final class PhiInsn extends InsnNode { return null; } + @Nullable + public RegisterArg getArgByBlock(IBlock block) { + if (getArgsCount() == 0) { + return null; + } + int index = blockBinds.indexOf(block); + if (index == -1) { + return null; + } + return getArg(index); + } + @Override public boolean replaceArg(InsnArg from, InsnArg to) { if (!(from instanceof RegisterArg) || !(to instanceof RegisterArg)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/Edge.java b/jadx-core/src/main/java/jadx/core/dex/nodes/Edge.java index 4ed1ff57a..0eee83086 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/Edge.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/Edge.java @@ -1,12 +1,24 @@ package jadx.core.dex.nodes; -public class Edge { +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AttrNode; + +public class Edge extends AttrNode { private final BlockNode source; private final BlockNode target; public Edge(BlockNode source, BlockNode target) { + this(source, target, false); + } + + public Edge(BlockNode source, BlockNode target, boolean isSynthetic) { + if (isSynthetic) { + this.add(AFlag.SYNTHETIC); + } + this.source = source; this.target = target; + } public BlockNode getSource() { @@ -17,6 +29,10 @@ public class Edge { return target; } + public boolean isSynthetic() { + return this.contains(AFlag.SYNTHETIC); + } + @Override public boolean equals(Object o) { if (this == o) { 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 55fde5fe1..13f0b645c 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 @@ -2,8 +2,10 @@ package jadx.core.dex.nodes; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -19,6 +21,7 @@ import jadx.api.metadata.annotations.VarNode; import jadx.api.plugins.input.data.ICodeReader; import jadx.api.plugins.input.data.IDebugInfo; import jadx.api.plugins.input.data.IMethodData; +import jadx.api.plugins.input.data.IMethodRef; import jadx.api.plugins.input.data.attributes.JadxAttrType; import jadx.api.plugins.input.data.attributes.types.ExceptionsAttr; import jadx.api.utils.CodeUtils; @@ -81,7 +84,14 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, private List loops; private Region region; + // Methods that use this method private List useIn = Collections.emptyList(); + // Unresolved methods that use this method + private List unresolvedUsed = Collections.emptyList(); + // Methods that this method uses + private Set methodsUsed = new HashSet<>(); + // True if this method contains a self call + private boolean callsSelf = false; private JavaMethod javaNode; @@ -702,12 +712,60 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails, return codeReader; } + // Cannot modify through get, use setUseIn public List getUseIn() { - return useIn; + return Collections.unmodifiableList(useIn); } + // Do not modify passed list after setting public void setUseIn(List useIn) { this.useIn = useIn; + + // Notify all methods (callers) this method (calee) is used in + for (MethodNode methodUsedIn : useIn) { + methodUsedIn.addUsed(this); + } + } + + public void addUsed(MethodNode used) { + if (used != null) { + this.methodsUsed.add(used); + } + } + + public void setUsed(List methodsUsed) { + this.methodsUsed = new HashSet<>(methodsUsed); + } + + public Set getUsed() { + this.removeInavlidMethodsUsed(); + return methodsUsed; + } + + public List getUnresolvedUsed() { + return unresolvedUsed; + } + + public void setUnresolvedUsed(List unresolvedUsed) { + this.unresolvedUsed = unresolvedUsed; + } + + public void setCallsSelf(boolean callsSelf) { + this.callsSelf = callsSelf; + } + + public boolean callsSelf() { + return this.callsSelf; + } + + // Remove any methods from the list of used methods (calees) if this method (caller) has been + // removed from the calee's list of callers + private void removeInavlidMethodsUsed() { + for (MethodNode methodUsed : new ArrayList<>(methodsUsed)) { + if (!methodUsed.getUseIn().contains(this)) { + methodsUsed.remove(methodUsed); + } + } } public JavaMethod getJavaNode() { 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 c86350656..e88501d07 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 @@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable; import jadx.api.ICodeWriter; import jadx.core.codegen.RegionGen; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.BlockNode; @@ -126,6 +127,7 @@ public final class LoopRegion extends ConditionRegion { preCondInsns.addAll(condInsns); condInsns.clear(); condInsns.addAll(preCondInsns); + header.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND); preCondInsns.clear(); preCondition = null; } 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 ff8fbe3bc..ee7162be3 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 @@ -6,9 +6,12 @@ import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.core.dex.instructions.args.InsnArg; @@ -20,6 +23,8 @@ import jadx.core.utils.Utils; public class ExceptionHandler { + private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class); + private final List catchTypes = new ArrayList<>(1); private final int handlerOffset; @@ -153,6 +158,42 @@ public class ExceptionHandler { return removed; } + @Nullable + public BlockNode getBottomSplitter() { + TryCatchBlockAttr handlerTryBlock = getTryBlock(); + // TODO: Implement support for finding bottom splitter of catch with inner tries + if (handlerTryBlock.getInnerTryBlocks().size() > 1) { + LOG.warn("No support yet for finding bottom block of try body with multipe inner trys"); + return null; + } + final TryCatchBlockAttr searchForTryBody; + if (handlerTryBlock.getInnerTryBlocks().isEmpty()) { + searchForTryBody = handlerTryBlock; + } else { + searchForTryBody = Utils.getOne(handlerTryBlock.getInnerTryBlocks()); + } + + BlockNode splitter = null; + for (BlockNode handlerPredecessor : getHandlerBlock().getPredecessors()) { + if (!handlerPredecessor.contains(AFlag.EXC_BOTTOM_SPLITTER)) { + continue; + } + + for (BlockNode splitterPredecessor : handlerPredecessor.getPredecessors()) { + TryCatchBlockAttr tryBody = splitterPredecessor.get(AType.TRY_BLOCK); + if (tryBody == searchForTryBody) { + splitter = handlerPredecessor; + break; + } + } + + if (splitter != null) { + break; + } + } + return splitter; + } + public void markForRemove() { this.removed = true; this.blocks.forEach(b -> b.add(AFlag.REMOVE)); diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlockAttr.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlockAttr.java index c1df755ad..dfd3fcecd 100644 --- a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlockAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryCatchBlockAttr.java @@ -2,17 +2,34 @@ package jadx.core.dex.trycatch; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; import jadx.api.plugins.input.data.attributes.IJadxAttrType; import jadx.api.plugins.input.data.attributes.IJadxAttribute; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.LoopInfo; import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; +import jadx.core.utils.exceptions.JadxRuntimeException; public class TryCatchBlockAttr implements IJadxAttribute { + public static boolean isImplicitOrMerged(TryCatchBlockAttr tryBlock) { + return tryBlock.isMerged() || tryBlock.getHandlers().isEmpty(); + } + private final int id; private final List handlers; private List blocks; @@ -134,6 +151,244 @@ public class TryCatchBlockAttr implements IJadxAttribute { return id; } + public List getHandlerTryEdges() { + List mergedHandlers = getMergedHandlers(); + List edges = new ArrayList<>(mergedHandlers.size()); + for (ExceptionHandler handler : mergedHandlers) { + BlockNode handlerBlock = handler.getHandlerBlock(); + BlockNode handlerSplitter = handler.getBottomSplitter(); + if (handlerSplitter == null) { + // If we cannot find a bottom splitter, there might be none. In this case, assume that the top + // splitter of this try catch is the source of the exit. + List allChildren = ListUtils.filter(handlerBlock.getPredecessors(), blk -> getBlocks().contains(blk)); + handlerSplitter = BlockUtils.getBottomBlock(allChildren); + if (handlerSplitter == null) { + handlerSplitter = getTopSplitter(); + } + } + TryEdge edge = new TryEdge(handlerSplitter, handlerBlock, handler); + edges.add(edge); + } + return edges; + } + + public List getFallthroughTryEdges() { + List edges = new LinkedList<>(); + List exploredBlocks = new ArrayList<>(); + List exploredTrys = new LinkedList<>(); + + getFallthroughTryEdges(edges, exploredBlocks, exploredTrys); + return edges; + } + + public void getFallthroughTryEdges(List edges, List exploredBlocks, List exploredTrys) { + List mergedHandlers = getMergedHandlers(); + Set searchBlocks = new HashSet<>(); + searchBlocks.addAll(getBlocks()); + for (ExceptionHandler handler : mergedHandlers) { + searchBlocks.removeAll(handler.getBlocks()); + } + + BlockNode sourceBlock = BlockUtils.getTopBlock(new ArrayList<>(searchBlocks)); + + exploredTrys.add(this); + + exploreTryPath(edges, sourceBlock, searchBlocks, exploredBlocks, exploredTrys); + } + + public List getTryEdges() { + List handlerEdges = getHandlerTryEdges(); + List fallthroughEdges = getFallthroughTryEdges(); + List edges = new ArrayList<>(handlerEdges.size() + fallthroughEdges.size()); + edges.addAll(handlerEdges); + edges.addAll(fallthroughEdges); + return Collections.unmodifiableList(edges); + } + + private void exploreTryPath(List edges, BlockNode blk, Set searchBlocks, List exploredBlocks, + List exploredTrys) { + for (BlockNode successor : blk.getSuccessors()) { + // If a separate branch has already explored this block, we don't need to recalculate its exits. + if (exploredBlocks.contains(successor)) { + continue; + } + + // If this is a bottom splitter, ignore - we only care about non-handler edges. + if (successor.contains(AFlag.EXC_BOTTOM_SPLITTER)) { + continue; + } + + exploredBlocks.add(successor); + + if (successor.contains(AFlag.LOOP_END)) { + final var loopsAttrList = successor.get(AType.LOOP); + final List loops = loopsAttrList.getList(); + final List loopStartBlocks = new LinkedList<>(); + for (final LoopInfo loop : loops) { + loopStartBlocks.add(loop.getStart()); + final List loopEdges = loop.getExitEdges(); + for (final Edge loopEdge : loopEdges) { + if (loopEdge.getTarget() == successor) { + loopStartBlocks.add(loopEdge.getSource()); + } + } + } + final boolean includesAllLoopStart = ListUtils.allMatch(loopStartBlocks, exploredBlocks::contains); + if (!includesAllLoopStart) { + edges.add(new TryEdge(blk, successor, TryEdgeType.LOOP_EXIT)); + continue; + } + } + + boolean isPathToAnySearchBlock = false; + for (final BlockNode searchBlock : searchBlocks) { + if (BlockUtils.isPathExists(successor, searchBlock)) { + isPathToAnySearchBlock = true; + break; + } + } + if (!searchBlocks.contains(successor) && !isPathToAnySearchBlock) { + // This block is not contained within this try's block list. This can either be since it is an exit + // to the try or it is a block which leads to an exit (for example, an exception handler). + + // If this block (successor) leads to an exit, then the "bottom block" of all try blocks and this + // block will be + // equal to the bottom block of all try blocks. If this block is an exit, then either: + // - a path does not exist from all try blocks to this block, thus making the bottom block null. + // - a path does exist from all try blocks to this block but no more try blocks follow, thus making + // the bottom block this block. + List allBlocksWithCurrent = new ArrayList<>(getBlocks().size() + 1); + allBlocksWithCurrent.addAll(getBlocks()); + allBlocksWithCurrent.add(successor); + BlockNode bottomBlock = BlockUtils.getBottomBlock(allBlocksWithCurrent); + + if (!(bottomBlock == null || bottomBlock == successor)) { + // This block leads to an exit. + exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys); + continue; + } + + BlockNode emptyPathEndOfSuccessor = BlockUtils.followEmptyPath(successor, false, false); + + if (emptyPathEndOfSuccessor.contains(AFlag.EXC_TOP_SPLITTER)) { + // This block is an exit which enters another try catch. In this case, the next try catch is within + // the same scope. Thus, we will take all of the edges out of that try and add them to the list of + // edges of this try. + Set nestedTrys = new HashSet<>(); + List allSuccessorsOnTryBody = ListUtils.filter(emptyPathEndOfSuccessor.getSuccessors(), + potentialTryBlock -> potentialTryBlock.contains(AFlag.TRY_ENTER)); + for (BlockNode tryBodyEnter : allSuccessorsOnTryBody) { + TryCatchBlockAttr nestedTry = tryBodyEnter.get(AType.TRY_BLOCK); + if (nestedTry == null) { + continue; + } + + // If we have already added a try's edges, skip over it to avoid infinite recursion. + if (exploredTrys.contains(nestedTry)) { + continue; + } + + // Unsure of why these top splitters have to be the same for them to be "nested" trys, but this + // seems to work (?) + if (nestedTry.getTopSplitter() != getTopSplitter()) { + continue; + } + + nestedTrys.add(nestedTry); + } + + // Only will we attempt to add nested inners if there exists any. If none exist, perform normal + // handling of the edge. + if (!nestedTrys.isEmpty()) { + for (TryCatchBlockAttr nestedTry : nestedTrys) { + nestedTry.getFallthroughTryEdges(edges, exploredBlocks, exploredTrys); + } + continue; + } + } + + if (bottomBlock == null) { + // This block is an exit which occurs before all try blocks are logically executed. + edges.add(new TryEdge(blk, successor, TryEdgeType.PREMATURE_EXIT)); + } else if (bottomBlock == successor) { + // This block is an exit which occurs after all try blocks are logically executed. + edges.add(new TryEdge(blk, successor, TryEdgeType.TRUE_FALLTHROUGH)); + } else { + // All possible cases should have been caught by the above if / else and the preceeding if. + // If this is hit, any changes made to this algorithm must aptly handle all possible code paths + // before executing this. + throw new JadxRuntimeException( + "Unexpected code execution branch taken during try edge resolution: blk=" + + blk + ",successor=" + successor); + } + } else { + exploreTryPath(edges, successor, searchBlocks, exploredBlocks, exploredTrys); + } + } + } + + public List getMergedHandlers() { + boolean hasInnerBlocks = !getInnerTryBlocks().isEmpty(); + final List mergedHandlers; + if (hasInnerBlocks) { + // collect handlers from this and all inner blocks + // (intentionally not using recursive collect for now) + mergedHandlers = new ArrayList<>(getHandlers()); + for (TryCatchBlockAttr innerTryBlock : getInnerTryBlocks()) { + mergedHandlers.addAll(innerTryBlock.getHandlers()); + } + } else { + mergedHandlers = getHandlers(); + } + return Collections.unmodifiableList(mergedHandlers); + } + + public Map getEdgeBlockMap(MethodNode mth) { + List edges = getTryEdges(); + Map blockMap = new HashMap<>(); + for (TryEdge edge : edges) { + blockMap.put(edge, edge.getTarget()); + } + return blockMap; + } + + public TryEdgeScopeGroupMap getExecutionScopeGroups(MethodNode mth) { + Map handlerBlocks = getEdgeBlockMap(mth); + TryEdgeScopeGroupMap scopeGroups = new TryEdgeScopeGroupMap(mth, this, handlerBlocks.size()); + scopeGroups.populateFromEdges(handlerBlocks); + + return scopeGroups; + } + + public Map> getHandlerFallthroughGroups(MethodNode mth, TryEdgeScopeGroupMap scopeGroups) { + return scopeGroups.getScopeEnds(mth); + } + + public List getSearchBlocksFromFallthroughGroups(MethodNode mth, ExceptionHandler finallyHandler, + Map> fallthroughGroups) { + + List searchBlocks = new LinkedList<>(); + for (Map.Entry> entry : fallthroughGroups.entrySet()) { + BlockNode scopeEndBlock = entry.getKey(); + List sourceHandlers = entry.getValue(); + + for (BlockNode scopeEndPredecessor : scopeEndBlock.getPredecessors()) { + // Add all predecessors to the scope end which are connected to some handler's scope start + try (Stream stream = sourceHandlers.stream()) { + Object[] matchedHandlerPaths = + stream.filter(handler -> !(handler.isHandlerExit() && handler.getExceptionHandler() == finallyHandler)) + .map(handler -> handler.getTarget()) + .filter(scopeStart -> BlockUtils.isPathExists(scopeStart, scopeEndPredecessor)) + .toArray(); + if (matchedHandlerPaths.length != 0) { + searchBlocks.add(scopeEndPredecessor); + } + } + } + } + return searchBlocks; + } + @Override public IJadxAttrType getAttrType() { return AType.TRY_BLOCK; diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdge.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdge.java new file mode 100644 index 000000000..f577c6be5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdge.java @@ -0,0 +1,110 @@ +package jadx.core.dex.trycatch; + +import java.util.Objects; +import java.util.Optional; + +import org.jetbrains.annotations.NotNull; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * Represents an edge between two blocks representing an exit out of a try body. + * The source block will be within the try body. + */ +public final class TryEdge { + + private final BlockNode source; + private final BlockNode target; + private final Optional handler; + private final TryEdgeType type; + + public TryEdge(final BlockNode source, final BlockNode target, final TryEdgeType type) { + this(source, target, type, Optional.empty()); + } + + public TryEdge(final BlockNode source, final BlockNode target, final @NotNull ExceptionHandler handler) { + this(source, target, TryEdgeType.HANDLER, Optional.of(handler)); + } + + public TryEdge(final BlockNode source, final BlockNode target, final TryEdgeType type, final Optional handler) { + this.source = source; + this.target = target; + this.handler = handler; + this.type = type; + + if (isHandlerExit() && handler.isEmpty()) { + throw new JadxRuntimeException("Attempted to add a null exception handler as an edge of \"" + type.toString() + "\" type"); + } else if (isNotHandlerExit() && handler.isPresent()) { + throw new JadxRuntimeException("Attempted to add an exception handler as an edge of \"" + type.toString() + "\" type"); + } + } + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder("TryEdge: ["); + sb.append(type); + sb.append(' '); + sb.append(source.toString()); + sb.append(" -> "); + sb.append(target.toString()); + sb.append("] - Handler: "); + if (handler.isEmpty()) { + sb.append("None"); + } else { + sb.append(handler.get().toString()); + } + return sb.toString(); + } + + @Override + public final boolean equals(Object obj) { + if (!(obj instanceof TryEdge)) { + return false; + } + + final TryEdge other = (TryEdge) obj; + + return source.equals(other.source) + && target.equals(other.target) + && handler.equals(other.handler) + && type.equals(other.type); + } + + @Override + public final int hashCode() { + return Objects.hash(source, target, type, handler); + } + + public final BlockNode getSource() { + return source; + } + + public final BlockNode getTarget() { + return target; + } + + public final TryEdgeType getType() { + return type; + } + + public final boolean isHandlerExit() { + return type == TryEdgeType.HANDLER; + } + + public final boolean isNotHandlerExit() { + return !isHandlerExit(); + } + + public final ExceptionHandler getExceptionHandler() { + if (!isHandlerExit()) { + throw new JadxRuntimeException("Attempted to get the exception handler of a non-handler edge type"); + } + + if (handler.isEmpty()) { + throw new JadxRuntimeException("Attempted to get the exception handler of a handler edge type, however none was present"); + } + + return handler.get(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeScopeGroupMap.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeScopeGroupMap.java new file mode 100644 index 000000000..927cbeb02 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeScopeGroupMap.java @@ -0,0 +1,377 @@ +package jadx.core.dex.trycatch; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.Pair; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * A map which stores the information of how try edges correlate with each other. + * K is a try edge and V contains all other try edges whose who share the same logical scope. + */ +public final class TryEdgeScopeGroupMap implements Map> { + + private static final class TryEdgeScope { + + private final TryEdge edge; + private final BlockNode block; + + public TryEdgeScope(final TryEdge edge, final BlockNode block) { + this.edge = edge; + this.block = block; + } + } + + private final List> mergedEdges = new ArrayList<>(); + private final TryCatchBlockAttr tryCatch; + private final Map> underlyingMap; + + public TryEdgeScopeGroupMap(final MethodNode mth, final TryCatchBlockAttr tryCatch, final int initialCapacity) { + this.tryCatch = tryCatch; + underlyingMap = new HashMap<>(initialCapacity); + } + + @Override + public final void clear() { + underlyingMap.clear(); + } + + @Override + public final boolean containsKey(Object key) { + return underlyingMap.containsKey(key); + } + + @Override + public final boolean containsValue(Object value) { + if (!(value instanceof TryEdge)) { + return false; + } + + final TryEdge edge = (TryEdge) value; + return underlyingMap.containsKey(edge); + } + + @Override + public final Set>> entrySet() { + return underlyingMap.entrySet(); + } + + @Override + public final Map get(Object key) { + return underlyingMap.get(key); + } + + @Override + public final boolean isEmpty() { + return underlyingMap.isEmpty(); + } + + @Override + public final Set keySet() { + return underlyingMap.keySet(); + } + + @Override + public final Map put(TryEdge key, Map value) { + return underlyingMap.put(key, value); + } + + @Override + public final void putAll(Map> otherMap) { + underlyingMap.putAll(otherMap); + } + + @Override + public final Map remove(Object key) { + return underlyingMap.remove(key); + } + + @Override + public final int size() { + return underlyingMap.size(); + } + + @Override + public final Collection> values() { + return underlyingMap.values(); + } + + public final boolean hasMergedEdges() { + return !mergedEdges.isEmpty(); + } + + public final List> getMergedScopes() { + return mergedEdges; + } + + public final void populateFromEdges(final Map edges) { + mergeSameScopes(edges); + + for (final TryEdge edge : edges.keySet()) { + final BlockNode edgeBlock = edges.get(edge); + + final Map handlerFallthroughMap = createEdgeTerminusMap(edges, edge, edgeBlock); + put(edge, handlerFallthroughMap); + } + } + + /** + * Returns a map of all points where edges meet with each other, dictating the end of that + * edge's scope. + * + * @param mth + * @return + */ + public Map> getScopeEnds(final MethodNode mth) { + final Map> groups = new HashMap<>(); + + // A list containing pairs of edges where there are no shared common clean successors between the + // two handlers. This usually indicates that these edge pairs must be processed differently. + final List isolatedEdgePairs = new LinkedList<>(); + + for (final TryEdge mergeEdgeA : keySet()) { + final Pair edgeMergedPair = getMergedNodeFromEdge(mergeEdgeA); + + if (edgeMergedPair != null) { + continue; + } + + final Map handlerRelations = get(mergeEdgeA); + + final List scopeEnds = new ArrayList<>(handlerRelations.size()); + for (final TryEdge mergeEdgeB : handlerRelations.keySet()) { + final Pair mergedPairFromRelation = getMergedNodeFromEdge(mergeEdgeB); + if (mergedPairFromRelation != null && mergedPairFromRelation.getFirst() == mergeEdgeA) { + continue; + } + + final BlockNode sharedTerminator = handlerRelations.get(mergeEdgeB); + + if (sharedTerminator == null) { + // There are no common clean succesors between the two handlers. + isolatedEdgePairs.add(mergeEdgeB); + } else { + scopeEnds.add(sharedTerminator); + } + } + + if (scopeEnds.isEmpty()) { + // Isolated edge pairs found - we will deal with them later + continue; + } + + final BlockNode topGrouping = BlockUtils.getTopBlock(scopeEnds); + + if (groups.containsKey(topGrouping)) { + groups.get(topGrouping).add(mergeEdgeA); + } else { + final List groupingHandlers = new LinkedList<>(); + groupingHandlers.add(mergeEdgeA); + groups.put(topGrouping, groupingHandlers); + } + } + + for (final TryEdge isolatedEdge : isolatedEdgePairs) { + boolean isInList = false; + for (final List foundEdges : groups.values()) { + if (foundEdges.contains(isolatedEdge)) { + isInList = true; + break; + } + } + + if (isInList) { + // The isolated edge is not isolated with another handler - we can ignore this edge. + break; + } + + // If an isolated edge has not been added to the groupings, we will add it now. + // This will be added by locating the point where the search for a common successor stops. + // Since a common successor of all blocks which do have some clean path can be found in the method + // exit node, the mentioned point will be the farthest successor of the edge target which has no + // clean successors. + + final BlockNode target = isolatedEdge.getTarget(); + final List successorBlocks = BlockUtils.collectAllSuccessors(mth, target, true); + final BlockNode cleanSuccessorEnd = BlockUtils.getBottomBlock(successorBlocks); + if (cleanSuccessorEnd == null) { + throw new JadxRuntimeException("Could not find bottom clean successor for isolated try edge"); + } + + final List scopeTerminusList; + if (groups.containsKey(cleanSuccessorEnd)) { + scopeTerminusList = groups.get(cleanSuccessorEnd); + } else { + scopeTerminusList = new LinkedList<>(); + groups.put(cleanSuccessorEnd, scopeTerminusList); + } + scopeTerminusList.add(isolatedEdge); + } + + if (groups.size() == 1) { + for (final Pair pair : mergedEdges) { + final TryEdge keptEdge = pair.getFirst(); + final TryEdge removedEdge = pair.getSecond(); + + if (keptEdge.isHandlerExit() && !tryCatch.getHandlers().contains(keptEdge.getExceptionHandler())) { + continue; + } + if (removedEdge.isHandlerExit() && !tryCatch.getHandlers().contains(removedEdge.getExceptionHandler())) { + continue; + } + + // If both handlers are not handler exits, we can assume that the code paths merge at some Phi node + // which begins the finally duplicated code. + if (keptEdge.isNotHandlerExit() && removedEdge.isNotHandlerExit()) { + continue; + } + + for (final List edgesWithTerminus : groups.values()) { + if (edgesWithTerminus.contains(keptEdge)) { + edgesWithTerminus.remove(keptEdge); + } + } + + final BlockNode terminus = get(keptEdge).get(removedEdge); + final List terminusEdges; + if (!groups.containsKey(terminus)) { + terminusEdges = new LinkedList<>(); + terminusEdges.add(keptEdge); + groups.put(terminus, terminusEdges); + } else { + terminusEdges = groups.get(terminus); + } + terminusEdges.add(removedEdge); + } + } + + return groups; + } + + @Nullable + private Pair getMergedNodeFromEdge(final TryEdge edge) { + for (Pair pair : mergedEdges) { + if (pair.getSecond() == edge) { + return pair; + } + } + return null; + } + + private Map createEdgeTerminusMap(final Map edgeStartMap, final TryEdge edge, + final BlockNode edgeStart) { + final Map scopeRelations = new HashMap<>(edgeStartMap.size() - 1); + for (final TryEdge otherEdge : edgeStartMap.keySet()) { + if (edge == otherEdge) { + continue; + } + + final BlockNode otherEdgeStart = edgeStartMap.get(otherEdge); + + final boolean eitherEdgeIsHandler = edge.isHandlerExit() || otherEdge.isHandlerExit(); + if (otherEdgeStart == edgeStart && eitherEdgeIsHandler) { + continue; + } + + if (otherEdgeStart.isMthExitBlock()) { + scopeRelations.put(otherEdge, otherEdgeStart); + // Everything leads to the exit node so merged edges are no longer needed + mergedEdges.clear(); + continue; + } + if (edgeStart.isMthExitBlock()) { + scopeRelations.put(otherEdge, edgeStart); + // Everything leads to the exit node so merged edges are no longer needed + mergedEdges.clear(); + continue; + } + + final BitSet sharedPostDominators = (BitSet) edgeStart.getPostDoms().clone(); + final BitSet otherPostDoms = otherEdgeStart.getPostDoms(); + if (sharedPostDominators.isEmpty() || otherPostDoms.isEmpty()) { + continue; + } + sharedPostDominators.and(otherPostDoms); + + final List postDomHandler = new LinkedList<>(); + BlockNode currentBlock = edgeStart; + while (currentBlock != null) { + postDomHandler.add(currentBlock); + currentBlock = currentBlock.getIPostDom(); + } + + BlockNode commonPostDom = null; + currentBlock = otherEdgeStart; + while (currentBlock != null) { + if (postDomHandler.contains(currentBlock)) { + commonPostDom = currentBlock; + break; + } + currentBlock = currentBlock.getIPostDom(); + } + + final BlockNode scopeEnd = commonPostDom; + scopeRelations.put(otherEdge, scopeEnd); + } + return scopeRelations; + } + + /** + * If two scopes ever merge, as in if one edge leads to the same execution point as the target of + * another edge, this function will record it. + * + * @param handlers + * @return + */ + private Map mergeSameScopes(final Map handlers) { + final List> exceptionHandlers = new ArrayList<>(handlers.entrySet()); + + final List> handlerPairs = new LinkedList<>(); + for (int i = 0; i < exceptionHandlers.size(); i++) { + for (int j = i + 1; j < exceptionHandlers.size(); j++) { + TryEdgeScope a = new TryEdgeScope(exceptionHandlers.get(i).getKey(), exceptionHandlers.get(i).getValue()); + TryEdgeScope b = new TryEdgeScope(exceptionHandlers.get(j).getKey(), exceptionHandlers.get(j).getValue()); + handlerPairs.add(new Pair<>(a, b)); + } + } + + final Map simplifiedScopes = new HashMap<>(handlers); + + int i = 0; + while (i < handlerPairs.size()) { + final Pair handlerPair = handlerPairs.get(i); + + final TryEdgeScope edgeScopeA = handlerPair.getFirst(); + final TryEdgeScope edgeScopeB = handlerPair.getSecond(); + final BlockNode edgeBlockA = edgeScopeA.block; + final BlockNode edgeBlockB = edgeScopeB.block; + final boolean pathExists = BlockUtils.isPathExists(edgeBlockA, edgeBlockB) || BlockUtils.isPathExists(edgeBlockB, edgeBlockA); + if (pathExists) { + BlockNode bottomBlock = BlockUtils.getBottomBlock(List.of(edgeBlockA, edgeBlockB)); + // The two blocks are within the same scope - remove these from the matrix + final TryEdge removeHandler = edgeBlockA != bottomBlock ? edgeScopeA.edge : edgeScopeB.edge; + final TryEdge keepHandler = edgeBlockA == bottomBlock ? edgeScopeA.edge : edgeScopeB.edge; + simplifiedScopes.remove(removeHandler); + handlerPairs.remove(i); + + mergedEdges.add(new Pair<>(keepHandler, removeHandler)); + } else { + i++; + } + } + + return simplifiedScopes; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeType.java b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeType.java new file mode 100644 index 000000000..256a90003 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/trycatch/TryEdgeType.java @@ -0,0 +1,8 @@ +package jadx.core.dex.trycatch; + +public enum TryEdgeType { + TRUE_FALLTHROUGH, + PREMATURE_EXIT, + LOOP_EXIT, + HANDLER +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/AdjustForIfMergeVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/AdjustForIfMergeVisitor.java new file mode 100644 index 000000000..4240fb8c6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/AdjustForIfMergeVisitor.java @@ -0,0 +1,136 @@ +package jadx.core.dex.visitors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.SpecialEdgeAttr; +import jadx.core.dex.attributes.nodes.SpecialEdgeAttr.SpecialEdgeType; +import jadx.core.dex.instructions.InsnType; +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.visitors.regions.RegionMakerVisitor; +import jadx.core.dex.visitors.typeinference.FinishTypeInference; +import jadx.core.utils.BlockUtils; + +@JadxVisitor( + name = "AdjustForIfMergeVisitor", + desc = "Move instructions between if blocks that can't be inlined but are safe to push through the if to allow the ifs to merge", + runBefore = { RegionMakerVisitor.class }, + runAfter = { FinishTypeInference.class } +) +public class AdjustForIfMergeVisitor extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) { + if (mth.isNoCode() || mth.getBasicBlocks() == null) { + return; + } + // Find candidates for adjustment by selecting blocks between two if statements + List blocks = mth.getBasicBlocks(); + + for (BlockNode blk : blocks) { + if (areSurroundingsCorrectShape(blk)) { + BlockNode pred = blk.getPredecessors().get(0); + BlockNode succ = blk.getCleanSuccessors().get(0); + + if (isSimpleIf(pred) && isSimpleIf(succ)) { + List movableInstructions = getMovableInstructions(blk, succ); + + if (!movableInstructions.isEmpty() && couldMerge(mth, pred, blk, succ)) { + doMove(mth, blk, succ, movableInstructions); + } + } + } + } + + } + + private boolean areSurroundingsCorrectShape(BlockNode blk) { + return (blk.getPredecessors().size() == 1 && blk.getCleanSuccessors().size() == 1); + } + + private boolean isSimpleIf(BlockNode blk) { + return blk.getInstructions().size() == 1 && blk.getInstructions().get(0).getType() == InsnType.IF; + } + + private boolean couldMerge(MethodNode mth, BlockNode pred, BlockNode blk, BlockNode succ) { + // we cannot merge if the edge from blk to succ is a back edge + // there's a function in BlockUtils that purports to check if something is a back edge but it + // doesn't so do it by hand here + + List specialEdges = mth.getAll(AType.SPECIAL_EDGE); + for (SpecialEdgeAttr edge : specialEdges) { + if (edge.getStart() == blk && edge.getEnd() == succ && edge.getType() == SpecialEdgeType.BACK_EDGE) { + mth.addDebugComment("Refusing to push insns through at block " + blk.toString() + " : edge to successor is a back edge."); + return false; + } + } + + return true; + } + + private List getMovableInstructions(BlockNode blk, BlockNode succ) { + // A 'movable instruction' is one that does not impact either codegen or the semantics of the + // following block, so it can be pushed through into the new synthetics. + + // For now, we just look for nop moves along the same register such that the target variable is not + // used in the succ block. + + List movableInstructions = new ArrayList<>(); + for (InsnNode insn : blk.getInstructions()) { + if (insn.getType() == InsnType.MOVE) { + if (!(insn.getArg(0) instanceof RegisterArg)) { + // could be a LiteralArg + continue; + } + RegisterArg source = (RegisterArg) insn.getArg(0); + RegisterArg target = insn.getResult(); + + List uses = target.getSVar().getUseList(); + for (RegisterArg use : uses) { + if (BlockUtils.blockContains(succ, use.getParentInsn())) { + // the target is used inside the successor, so we can't cleanly do the assignment afterwards + continue; + } + } + + // we don't want to just push everything through, e.g. + // if (condition) { return; } + // x = 123456 + // if (condition) { return; } + // would be a less clean result if the assignment was pushed into the block of the 2nd if. + + if (source.getRegNum() == target.getRegNum()) { + movableInstructions.add(insn); + } + } + } + + return movableInstructions; + } + + private void doMove(MethodNode mth, BlockNode target, BlockNode bottomIf, List movableInstructions) { + // Move instructions from the list out of blk and into new synthetics on each edge out of succ + + // preserving instruction ordering, although it's unlikely that it would ever matter here + Collections.reverse(movableInstructions); + for (InsnNode insn : movableInstructions) { + target.getInstructions().remove(insn); + for (BlockNode succ : bottomIf.getCleanSuccessors()) { + succ.getInstructions().add(0, insn); // add at start + + if (succ.contains(AFlag.LOOP_START)) { + // if we're merging into a loop condition, silence the warning when there's more than one + // instruction in the loop header + succ.add(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND); + } + } + } + } + +} 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 74d4cd0e9..0fd081d17 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 @@ -1,32 +1,12 @@ package jadx.core.dex.visitors; import java.io.File; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.Optional; import java.util.regex.Matcher; -import jadx.api.ICodeWriter; -import jadx.api.impl.SimpleCodeWriter; -import jadx.core.codegen.MethodGen; -import jadx.core.dex.attributes.IAttributeNode; -import jadx.core.dex.instructions.IfNode; -import jadx.core.dex.instructions.InsnType; -import jadx.core.dex.nodes.BlockNode; -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.trycatch.ExceptionHandler; -import jadx.core.utils.BlockUtils; -import jadx.core.utils.InsnUtils; -import jadx.core.utils.RegionUtils; -import jadx.core.utils.StringUtils; -import jadx.core.utils.Utils; - -import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP; +import jadx.core.utils.DotGraphUtils; public class DotGraphVisitor extends AbstractVisitor { @@ -38,6 +18,9 @@ public class DotGraphVisitor extends AbstractVisitor { private final boolean useRegions; private final boolean rawInsn; + // if present, this region and it's children will still be drawn when not in regions mode. + private Optional highlightRegion; + public static DotGraphVisitor dump() { return new DotGraphVisitor(false, false); } @@ -54,9 +37,26 @@ public class DotGraphVisitor extends AbstractVisitor { return new DotGraphVisitor(true, true); } + /** + * Helper function to generate a cfg at a given point showing only one of the regions in the graph. + * Intended to be called during a debugging session to produce a CFG with only a region of interest, + * with DotGraphVisitor.debugDumpWithRegionHiglight(region).visit(mth); + * + * @param region the region to show + * @return the visitor, to be invoked with `.visit(mth);` + */ + public static DotGraphVisitor debugDumpWithRegionHighlight(IRegion region) { + return new DotGraphVisitor(false, false, Optional.of(region)); + } + private DotGraphVisitor(boolean useRegions, boolean rawInsn) { + this(useRegions, rawInsn, Optional.empty()); + } + + private DotGraphVisitor(boolean useRegions, boolean rawInsn, Optional highlightRegion) { this.useRegions = useRegions; this.rawInsn = rawInsn; + this.highlightRegion = highlightRegion; } @Override @@ -69,264 +69,13 @@ public class DotGraphVisitor extends AbstractVisitor { if (mth.isNoCode()) { return; } - File outRootDir = mth.root().getArgs().getOutDir(); - new DumpDotGraph(outRootDir).process(mth); + new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth); } public void save(File dir, MethodNode mth) { if (mth.isNoCode()) { return; } - new DumpDotGraph(dir).process(mth); - } - - private class DumpDotGraph { - private final ICodeWriter dot = new SimpleCodeWriter(); - private final ICodeWriter conn = new SimpleCodeWriter(); - private final File dir; - - public DumpDotGraph(File dir) { - this.dir = dir; - } - - public void process(MethodNode mth) { - dot.startLine("digraph \"CFG for"); - dot.add(escape(mth.getMethodInfo().getFullId())); - dot.add("\" {"); - - BlockNode enterBlock = mth.getEnterBlock(); - if (useRegions) { - if (mth.getRegion() == null) { - return; - } - processMethodRegion(mth); - } else { - List blocks = mth.getBasicBlocks(); - if (blocks == null) { - InsnNode[] insnArr = mth.getInstructions(); - if (insnArr == null) { - return; - } - BlockNode block = new BlockNode(0, 0, 0); - List insnList = block.getInstructions(); - for (InsnNode insn : insnArr) { - if (insn != null) { - insnList.add(insn); - } - } - enterBlock = block; - blocks = Collections.singletonList(block); - } - for (BlockNode block : blocks) { - processBlock(mth, block, false); - } - } - - dot.startLine("MethodNode[shape=record,label=\"{"); - dot.add(escape(mth.getAccessFlags().makeString(true))); - dot.add(escape(mth.getReturnType() + " " - + mth.getParentClass() + '.' + mth.getName() - + '(' + Utils.listToString(mth.getAllArgRegs()) + ") ")); - - String attrs = attributesString(mth); - if (!attrs.isEmpty()) { - dot.add(" | ").add(attrs); - } - dot.add("}\"];"); - - dot.startLine("MethodNode -> ").add(makeName(enterBlock)).add(';'); - - dot.add(conn.toString()); - - dot.startLine('}'); - dot.startLine(); - - String fileName = StringUtils.escape(mth.getMethodInfo().getShortId()) - + (useRegions ? ".regions" : "") - + (rawInsn ? ".raw" : "") - + ".dot"; - File file = dir.toPath() - .resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs") - .resolve(fileName) - .toFile(); - SaveCode.save(dot.finish(), file); - } - - private void processMethodRegion(MethodNode mth) { - processRegion(mth, mth.getRegion()); - for (ExceptionHandler h : mth.getExceptionHandlers()) { - if (h.getHandlerRegion() != null) { - processRegion(mth, h.getHandlerRegion()); - } - } - Set regionsBlocks = new HashSet<>(mth.getBasicBlocks().size()); - RegionUtils.getAllRegionBlocks(mth.getRegion(), regionsBlocks); - for (ExceptionHandler handler : mth.getExceptionHandlers()) { - IContainer handlerRegion = handler.getHandlerRegion(); - if (handlerRegion != null) { - RegionUtils.getAllRegionBlocks(handlerRegion, regionsBlocks); - } - } - for (BlockNode block : mth.getBasicBlocks()) { - if (!regionsBlocks.contains(block)) { - processBlock(mth, block, true); - } - } - } - - private void processRegion(MethodNode mth, IContainer region) { - if (region instanceof IRegion) { - IRegion r = (IRegion) region; - dot.startLine("subgraph " + makeName(region) + " {"); - dot.startLine("label = \"").add(r.toString()); - String attrs = attributesString(r); - if (!attrs.isEmpty()) { - dot.add(" | ").add(attrs); - } - dot.add("\";"); - dot.startLine("node [shape=record,color=blue];"); - - for (IContainer c : r.getSubBlocks()) { - processRegion(mth, c); - } - - dot.startLine('}'); - } else if (region instanceof BlockNode) { - processBlock(mth, (BlockNode) region, false); - } else if (region instanceof IBlock) { - processIBlock(mth, (IBlock) region, false); - } - } - - private void processBlock(MethodNode mth, BlockNode block, boolean error) { - String attrs = attributesString(block); - dot.startLine(makeName(block)); - dot.add(" [shape=record,"); - if (error) { - dot.add("color=red,"); - } - dot.add("label=\"{"); - dot.add(String.valueOf(block.getCId())).add("\\:\\ "); - dot.add(InsnUtils.formatOffset(block.getStartOffset())); - if (!attrs.isEmpty()) { - dot.add('|').add(attrs); - } - if (PRINT_DOMINATORS_INFO) { - dot.add('|'); - dot.startLine("doms: ").add(escape(block.getDoms())); - dot.startLine("\\lidom: ").add(escape(block.getIDom())); - dot.startLine("\\lpost-doms: ").add(escape(block.getPostDoms())); - dot.startLine("\\lpost-idom: ").add(escape(block.getIPostDom())); - dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier())); - dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn()))); - dot.startLine("\\l"); - } - String insns = insertInsns(mth, block); - if (!insns.isEmpty()) { - dot.add('|').add(insns); - } - dot.add("}\"];"); - - BlockNode falsePath = null; - 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]" : ""; - addEdge(block, next, style); - } - - if (PRINT_DOMINATORS) { - for (BlockNode c : block.getDominatesOn()) { - conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];"); - } - for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) { - conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];"); - } - } - } - - private void processIBlock(MethodNode mth, IBlock block, boolean error) { - String attrs = attributesString(block); - dot.startLine(makeName(block)); - dot.add(" [shape=record,"); - if (error) { - dot.add("color=red,"); - } - dot.add("label=\"{"); - if (!attrs.isEmpty()) { - dot.add(attrs); - } - String insns = insertInsns(mth, block); - if (!insns.isEmpty()) { - dot.add('|').add(insns); - } - dot.add("}\"];"); - } - - private void addEdge(BlockNode from, BlockNode to, String style) { - conn.startLine(makeName(from)).add(" -> ").add(makeName(to)); - conn.add(style); - conn.add(';'); - } - - private String attributesString(IAttributeNode block) { - StringBuilder attrs = new StringBuilder(); - for (String attr : block.getAttributesStringsList()) { - attrs.append(escape(attr)).append(NL); - } - return attrs.toString(); - } - - private String makeName(IContainer c) { - String name; - if (c instanceof BlockNode) { - name = "Node_" + ((BlockNode) c).getCId(); - } else if (c instanceof IBlock) { - name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode(); - } else { - name = "cluster_" + c.getClass().getSimpleName() + '_' + c.hashCode(); - } - return name; - } - - private String insertInsns(MethodNode mth, IBlock block) { - if (rawInsn) { - StringBuilder sb = new StringBuilder(); - for (InsnNode insn : block.getInstructions()) { - sb.append(escape(insn)).append(NL); - } - return sb.toString(); - } else { - ICodeWriter code = new SimpleCodeWriter(); - List instructions = block.getInstructions(); - MethodGen.addFallbackInsns(code, mth, instructions.toArray(new InsnNode[0]), BLOCK_DUMP); - String str = escape(code.newLine().toString()); - if (str.startsWith(NL)) { - str = str.substring(NL.length()); - } - return str; - } - } - - private String escape(Object obj) { - if (obj == null) { - return "null"; - } - return escape(obj.toString()); - } - - private String escape(String string) { - return string - .replace("\\", "") // TODO replace \" - .replace("/", "\\/") - .replace(">", "\\>").replace("<", "\\<") - .replace("{", "\\{").replace("}", "\\}") - .replace("\"", "\\\"") - .replace("-", "\\-") - .replace("|", "\\|") - .replaceAll("\\R", NLQR); - } + new DotGraphUtils(useRegions, rawInsn, highlightRegion).dumpToFile(mth, dir); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/InlineMethods.java b/jadx-core/src/main/java/jadx/core/dex/visitors/InlineMethods.java index 3aa77090a..d249e53a4 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/InlineMethods.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/InlineMethods.java @@ -158,7 +158,9 @@ public class InlineMethods extends AbstractVisitor { } private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) { - inlinedMth.getUseIn().remove(mth); + List newUseIn = new ArrayList<>(inlinedMth.getUseIn()); + newUseIn.remove(mth); + inlinedMth.setUseIn(newUseIn); insn.visitInsns(innerInsn -> { // TODO: share code with UsageInfoVisitor switch (innerInsn.getType()) { @@ -167,7 +169,7 @@ public class InlineMethods extends AbstractVisitor { MethodInfo callMth = ((BaseInvokeNode) innerInsn).getCallMth(); MethodNode callMthNode = mth.root().resolveMethod(callMth); if (callMthNode != null) { - callMthNode.setUseIn(ListUtils.safeReplace(callMthNode.getUseIn(), inlinedMth, mth)); + callMthNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(callMthNode.getUseIn()), inlinedMth, mth)); replaceClsUsage(mth, inlinedMth, callMthNode.getParentClass()); } break; @@ -179,7 +181,7 @@ public class InlineMethods extends AbstractVisitor { FieldInfo fieldInfo = (FieldInfo) ((IndexInsnNode) innerInsn).getIndex(); FieldNode fieldNode = mth.root().resolveField(fieldInfo); if (fieldNode != null) { - fieldNode.setUseIn(ListUtils.safeReplace(fieldNode.getUseIn(), inlinedMth, mth)); + fieldNode.setUseIn(ListUtils.safeReplace(new ArrayList<>(fieldNode.getUseIn()), inlinedMth, mth)); replaceClsUsage(mth, inlinedMth, fieldNode.getParentClass()); } break; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/MoveInlineVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/MoveInlineVisitor.java index c06dbd21a..740fd4387 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/MoveInlineVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/MoveInlineVisitor.java @@ -2,6 +2,7 @@ package jadx.core.dex.visitors; import java.util.ArrayList; +import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.InsnType; @@ -39,6 +40,7 @@ public class MoveInlineVisitor extends AbstractVisitor { continue; } if (processMove(mth, insn)) { + block.add(AFlag.MOVE_INLINED); remover.addAndUnbind(insn); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockExceptionHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockExceptionHandler.java index 75cc71c71..cae778242 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockExceptionHandler.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockExceptionHandler.java @@ -23,6 +23,7 @@ import jadx.api.plugins.utils.Utils; import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.ExcSplitCrossAttr; import jadx.core.dex.attributes.nodes.TmpEdgeAttr; import jadx.core.dex.info.ClassInfo; import jadx.core.dex.instructions.InsnType; @@ -380,6 +381,23 @@ public class BlockExceptionHandler { } connectSplittersAndHandlers(tryCatchBlock, topSplitterBlock, bottomSplitterBlock); + // At this point, it's possible that a cross edge to the original bottom has been turned into a back + // edge by the insertion of the new bottom. This causes problems because back edges usually signifiy + // loops, but this is not a loop. To fix this, predecessors of the bottom that also have a path from + // the bottom are rewritten to point to the original path crossing point (before synthetic blocks). + if (bottom != null && bottom.contains(AType.EXC_SPLIT_CROSS)) { + List convertBlocks = new ArrayList<>(); + for (BlockNode b : bottom.getPredecessors()) { + if (BlockUtils.isAnyPathExists(bottom, b)) { + convertBlocks.add(b); + } + } + for (BlockNode b : convertBlocks) { + // The connection can't be replaced during the first loop because it would modify the preds list. + BlockSplitter.replaceConnection(b, bottom, bottom.get(AType.EXC_SPLIT_CROSS).getOriginalPathCross()); + } + } + for (BlockNode block : blocks) { TryCatchBlockAttr currentTCBAttr = block.get(AType.TRY_BLOCK); if (currentTCBAttr == null || currentTCBAttr.getInnerTryBlocks().contains(tryCatchBlock)) { @@ -460,12 +478,15 @@ public class BlockExceptionHandler { List outsidePredecessors = preds.stream() .filter(p -> !BlockUtils.atLeastOnePathExists(blocks, p)) .collect(Collectors.toList()); - if (outsidePredecessors.isEmpty()) { + // if we have no predecessors or every predecessor is outside (which would mean that inserting the + // new synthetic block does nothing), just return the existing path cross instead. + if (outsidePredecessors.isEmpty() || outsidePredecessors.size() == pathCross.getPredecessors().size()) { return pathCross; } // some predecessors outside of input set paths -> split block only for input set BlockNode splitCross = BlockSplitter.blockSplitTop(mth, pathCross); splitCross.add(AFlag.SYNTHETIC); + splitCross.addAttr(new ExcSplitCrossAttr(pathCross)); for (BlockNode outsidePredecessor : outsidePredecessors) { // return predecessors to split bottom block (original) BlockSplitter.replaceConnection(outsidePredecessor, splitCross, pathCross); 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 3ee6718e2..cae57c51e 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 @@ -129,11 +129,48 @@ public class BlockProcessor extends AbstractVisitor { private static void checkForUnreachableBlocks(MethodNode mth) { for (BlockNode block : mth.getBasicBlocks()) { if (block.getPredecessors().isEmpty() && block != mth.getEnterBlock()) { - throw new JadxRuntimeException("Unreachable block: " + block); + // Sometimes a split cross block will have all it's predecessors moved elsewhere after it's been + // created. This is usually detected at the time of it's creation, but in certain edge cases it + // is difficult to do so. In those cases it will be cleanly removed here, along with the associated + // bottom splitter. + if (block.contains(AType.EXC_SPLIT_CROSS) && fixUnreachableSplitCross(mth, block)) { + mth.addInfoComment("Removed unreachable split cross block " + block.toString()); + } else { + throw new JadxRuntimeException("Unreachable block: " + block); + } } } } + /** + * Attempts to remove an unreachable synthetic split cross block that has been added previously, + * along with the associated bottom splitter. + * + * @param mth the method containing the unreachable block + * @param splitCross the unreachable block + * @return true if the operation was successful, false if a precondition was not satisfied and no + * changes were made. + */ + private static boolean fixUnreachableSplitCross(MethodNode mth, BlockNode splitCross) { + BlockNode bottomSplitter = null; + for (BlockNode succ : splitCross.getSuccessors()) { + if (succ.contains(AFlag.EXC_BOTTOM_SPLITTER)) { + bottomSplitter = succ; + break; + } + } + + if (bottomSplitter == null || bottomSplitter.getPredecessors().size() != 1) { + return false; + } + Set removeSet = new HashSet<>(); + removeSet.add(bottomSplitter); + removeSet.add(splitCross); + removeFromMethod(removeSet, mth); + + return true; + } + private static boolean deduplicateBlockInsns(MethodNode mth, BlockNode block) { if (block.contains(AFlag.LOOP_START) || block.contains(AFlag.LOOP_END)) { // search for same instruction at end of all predecessors blocks diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockSplitter.java b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockSplitter.java index 3464fe716..d9f18d367 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockSplitter.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/blocks/BlockSplitter.java @@ -179,7 +179,7 @@ public class BlockSplitter extends AbstractVisitor { replaceTarget(source, oldDest, newDest); } - static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) { + public static BlockNode insertBlockBetween(MethodNode mth, BlockNode source, BlockNode target) { BlockNode newBlock = startNewBlock(mth, target.getStartOffset()); newBlock.add(AFlag.SYNTHETIC); removeConnection(source, target); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/CentralityState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/CentralityState.java new file mode 100644 index 000000000..363c33e29 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/CentralityState.java @@ -0,0 +1,162 @@ +package jadx.core.dex.visitors.finaly; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.InsnNode; + +/** + * A centrality state is an object which helps track how instructions can be skipped. + * When looking for a finally, one of the things we have to do is make sure that instructions + * are not part of the return and are actually part of the "finally" block. + * This object helps keep track of registers, instructions etc to see if instructions can be + * skipped. + */ +public final class CentralityState { + + private final Set allowableOutputArguments = new HashSet<>(); + private final SameInstructionsStrategy sameInstructionsStrategy; + private boolean allowsCentral = true; + private boolean allowsNonStartingNode; + + public CentralityState(final SameInstructionsStrategy sameInstructionsStrategy, final boolean allowsNonStartingNode) { + this.sameInstructionsStrategy = sameInstructionsStrategy; + this.allowsNonStartingNode = allowsNonStartingNode; + } + + @Override + public final String toString() { + final StringBuilder sb = new StringBuilder("CentralityState - "); + if (allowsCentral) { + sb.append("allows central"); + } else { + sb.append("disallows central"); + } + sb.append(" | "); + for (RegisterArg registerArg : allowableOutputArguments) { + sb.append(registerArg.getName()); + sb.append(" "); + } + return sb.toString(); + } + + public final SameInstructionsStrategy getSameInstructionsStrategy() { + return sameInstructionsStrategy; + } + + public final boolean getAllowsCentral() { + return allowsCentral; + } + + public final void setAllowsCentral(final boolean allowsCentral) { + this.allowsCentral = allowsCentral; + } + + public final boolean getAllowsNonStartingNode() { + return allowsNonStartingNode; + } + + public final void setAllowsNonStartingNode(final boolean allowsNonStartingNode) { + this.allowsNonStartingNode = allowsNonStartingNode; + } + + public final void addAllowableOutput(final RegisterArg allowableOutput) { + allowableOutputArguments.add(allowableOutput); + } + + public final void addAllowableOutputs(final Collection allowableOutputs) { + allowableOutputArguments.addAll(allowableOutputs); + } + + /** + * Adds all inputs register arguments from an instruction as allowable output arguments. + * + * @param allowableOutputInsn The instruction to retrieve the list of inputs from. + */ + public final void addAllowableOutputs(final InsnNode allowableOutputInsn) { + final List registerArgs = new LinkedList<>(); + for (final InsnArg arg : allowableOutputInsn.getArgList()) { + if (!(arg instanceof RegisterArg)) { + continue; + } + + registerArgs.add((RegisterArg) arg); + } + + registerArgs.forEach(this::addAllowableOutput); + } + + public final boolean hasAllowableOutput(final InsnNode insn) { + if (allowableOutputArguments.isEmpty()) { + return false; + } + + final RegisterArg registerArg; + if (insn.getResult() != null) { + registerArg = insn.getResult(); + } else { + registerArg = null; + } + + if (registerArg == null) { + return false; + } + + for (final RegisterArg allowableOutput : allowableOutputArguments) { + if (allowableOutput.equals(registerArg)) { + return true; + } + } + return false; + } + + @SuppressWarnings("unused") + public final boolean hasAllowableInputs(final InsnNode insn) { + if (allowableOutputArguments.isEmpty()) { + return false; + } + + final List registerArgs = new ArrayList<>(); + + for (final InsnArg arg : insn.getArgList()) { + if (arg instanceof RegisterArg) { + registerArgs.add((RegisterArg) arg); + } + } + + if (registerArgs.isEmpty() || allowableOutputArguments.isEmpty()) { + return false; + } + + for (final RegisterArg regArg : registerArgs) { + boolean foundMatch = false; + for (final RegisterArg allowableOutput : allowableOutputArguments) { + if (regArg.equals(allowableOutput)) { + foundMatch = true; + break; + } + } + if (!foundMatch) { + return false; + } + } + return true; + } + + public final CentralityState duplicate() { + final CentralityState state = new CentralityState(sameInstructionsStrategy, allowsNonStartingNode); + state.allowsCentral = allowsCentral; + state.allowableOutputArguments.addAll(allowableOutputArguments); + return state; + } + + public final Set getAllowableOutputArguments() { + return allowableOutputArguments; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java index e4908a206..38b884b7e 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/MarkFinallyVisitor.java @@ -2,19 +2,18 @@ package jadx.core.dex.visitors.finaly; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import java.util.Set; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.core.Consts; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.AType; -import jadx.core.dex.attributes.nodes.RegDebugInfoAttr; import jadx.core.dex.instructions.InsnType; import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; @@ -24,18 +23,44 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.trycatch.ExceptionHandler; import jadx.core.dex.trycatch.TryCatchBlockAttr; +import jadx.core.dex.trycatch.TryEdge; +import jadx.core.dex.trycatch.TryEdgeScopeGroupMap; import jadx.core.dex.visitors.AbstractVisitor; import jadx.core.dex.visitors.ConstInlineVisitor; import jadx.core.dex.visitors.DepthTraversal; import jadx.core.dex.visitors.IDexTreeVisitor; import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.finaly.traverser.TraverserController; +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; import jadx.core.dex.visitors.ssa.SSATransform; import jadx.core.utils.BlockUtils; -import jadx.core.utils.InsnList; import jadx.core.utils.ListUtils; -import jadx.core.utils.Utils; -import jadx.core.utils.blocks.BlockPair; +import jadx.core.utils.Pair; +import jadx.core.utils.exceptions.DecodeException; +import jadx.core.utils.exceptions.JadxRuntimeException; +/** + * This visitor is responsible for extracting finally blocks from duplicated instructions located + * within the ends of each branch leading to the terminating point of all code paths. + * + * To do this, the terminating point of each handler / exit from try body is found relative to every + * other handler / exit from try body. This, in effect, is used to identify the "scopes" of each + * possible path within the try block and thus can be used to find a common series of included + * blocks + * within the "scope" of each handler and a block to start searching from in reverse to find common + * instructions between that and the "nominated finally" handler. These groups are described by the + * {@link TryEdgeScopeGroupMap} object. + * + * After this, the {@link TraverserController} is responsible for traversing the block graphs from + * each "scope terminus" along the blocks contained with each handlers "scope", comparing them + * against + * the "nominated finally" block. If the control flow and instructions of each block match, then + * they + * are added as duplicate instructions. At the end, the visitor will mark the identified duplicated + * instructions and identified finally instructions with the respective {@link AFlag} to be handled + * during regioning of the block graph. + */ @JadxVisitor( name = "MarkFinallyVisitor", desc = "Search and mark duplicate code generated for finally block", @@ -43,211 +68,407 @@ import jadx.core.utils.blocks.BlockPair; runBefore = ConstInlineVisitor.class ) public class MarkFinallyVisitor extends AbstractVisitor { + private static final Logger LOG = LoggerFactory.getLogger(MarkFinallyVisitor.class); + private static final class TryExtractInfo { + + private final TryCatchBlockAttr tryBlock; + private final TryEdgeScopeGroupMap scopeGroups; + private final ExceptionHandler finallyHandler; + private final Map> scopeTerminusGroups; + private final TryCatchEdgeBlockMap handlerScopes; + private final Set allHandlerBlocks; + private final Set rethrowBlocks; + + private Set completeFinallyBlocks = null; + private Set completeCandidateBlocks = null; + + private TryExtractInfo(final TryCatchBlockAttr tryBlock, final TryEdgeScopeGroupMap scopeGroups, + final ExceptionHandler finallyHandler, final Map> fallthroughGroups, + final TryCatchEdgeBlockMap handlerScopes) { + this.tryBlock = tryBlock; + this.scopeGroups = scopeGroups; + this.finallyHandler = finallyHandler; + this.scopeTerminusGroups = fallthroughGroups; + this.handlerScopes = handlerScopes; + this.allHandlerBlocks = new HashSet<>(); + this.rethrowBlocks = new HashSet<>(); + + for (final List handlerBlocks : handlerScopes.values()) { + allHandlerBlocks.addAll(handlerBlocks); + } + } + } + @Override - public void visit(MethodNode mth) { + public void visit(final MethodNode mth) { if (mth.isNoCode() || mth.isNoExceptionHandlers()) { return; } try { - boolean applied = false; - List tryBlocks = mth.getAll(AType.TRY_BLOCKS_LIST); - for (TryCatchBlockAttr tryBlock : tryBlocks) { - applied |= processTryBlock(mth, tryBlock); - } - if (applied) { - mth.clearExceptionHandlers(); - // remove merged or empty try blocks from list in method attribute - List clearedTryBlocks = new ArrayList<>(tryBlocks); - if (clearedTryBlocks.removeIf(tb -> tb.isMerged() || tb.getHandlers().isEmpty())) { - mth.remove(AType.TRY_BLOCKS_LIST); - mth.addAttr(AType.TRY_BLOCKS_LIST, clearedTryBlocks); + boolean implicitHandlerRemoved = false; + final List tryBlocks = mth.getAll(AType.TRY_BLOCKS_LIST); + + final List processRequiredTryBlocks = new ArrayList<>(); + + // Search through all exception handlers and: + // - Remove implicit handlers + // - Mark non-implicit handlers to be searched for a finally block + for (final TryCatchBlockAttr tryBlock : tryBlocks) { + final TryExtractInfo tryInfo = getTryBlockData(mth, tryBlock); + if (tryInfo == null) { + continue; + } + final List cutHandlerBlocks = cutHandlerBlocks(tryInfo, tryInfo.finallyHandler); + if (cutHandlerBlocks == null) { + continue; + } + if (attemptRemoveImplicitHandlers(cutHandlerBlocks, tryInfo)) { + implicitHandlerRemoved = true; + } else { + processRequiredTryBlocks.add(tryBlock); } } - } catch (Exception e) { - mth.addWarnComment("Undo finally extract visitor", e); + // If any implicit handlers have been found, remove them + if (implicitHandlerRemoved) { + resetTryBlocks(mth, tryBlocks); + } + + // Search through all non-implicit handlers and search for a finally block. + boolean finallyExtracted = false; + for (final TryCatchBlockAttr tryBlock : processRequiredTryBlocks) { + // Refresh scope groups now due to implicit handlers + final TryExtractInfo tryInfo = getTryBlockData(mth, tryBlock); + + if (tryInfo == null) { + continue; + } + + cutHandlerBlocks(tryInfo, tryInfo.finallyHandler); + + finallyExtracted |= processTryBlock(mth, tryInfo); + } + // If any handlers have been merged, remove them + if (finallyExtracted) { + resetTryBlocks(mth, tryBlocks); + } + } catch (final Exception e) { + LOG.error(e.getMessage()); undoFinallyVisitor(mth); + mth.addWarnComment("Undo finally extract visitor", e); } } - private static boolean processTryBlock(MethodNode mth, TryCatchBlockAttr tryBlock) { - if (tryBlock.isMerged()) { - return false; + private static void resetTryBlocks(final MethodNode mth, final List tryBlocks) { + mth.clearExceptionHandlers(); + // remove merged or empty try blocks from list in method attribute + final List clearedTryBlocks = new ArrayList<>(tryBlocks); + if (clearedTryBlocks.removeIf(TryCatchBlockAttr::isImplicitOrMerged)) { + mth.remove(AType.TRY_BLOCKS_LIST); + mth.addAttr(AType.TRY_BLOCKS_LIST, clearedTryBlocks); } + } + + /** + * For a given try block, attempts to calculate try block data. This includes the handler blocks for + * each try branch, data regarding the scope of each try branch relative to every other branch, and + * the blocks logically contained within each try branch. This information is stored via internal + * class members and is not returned by the function. + * + * @param mth The method containing the try block. + * @param tryBlock The try block to determine the scope information of. + * @return The handler identified as the "all" handler. + */ + @Nullable + private static TryExtractInfo getTryBlockData(final MethodNode mth, final TryCatchBlockAttr tryBlock) { + if (tryBlock.isMerged()) { + return null; + } + + // Find the all handler ExceptionHandler allHandler = null; - InsnNode reThrowInsn = null; - for (ExceptionHandler excHandler : tryBlock.getHandlers()) { + for (final ExceptionHandler excHandler : tryBlock.getHandlers()) { if (excHandler.isCatchAll()) { allHandler = excHandler; - for (BlockNode excBlock : excHandler.getBlocks()) { - InsnNode lastInsn = BlockUtils.getLastInsn(excBlock); - if (lastInsn != null && lastInsn.getType() == InsnType.THROW) { - reThrowInsn = BlockUtils.getLastInsn(excBlock); - } - } break; } } - if (allHandler != null && reThrowInsn != null) { - if (extractFinally(mth, tryBlock, allHandler)) { - reThrowInsn.add(AFlag.DONT_GENERATE); - return true; + + if (allHandler == null) { + return null; + } + + final TryEdgeScopeGroupMap scopeGroups = tryBlock.getExecutionScopeGroups(mth); + final var fallthroughGroups = tryBlock.getHandlerFallthroughGroups(mth, scopeGroups); + final var handlerScopes = TryCatchEdgeBlockMap.getAllInScope(mth, tryBlock, scopeGroups, allHandler, fallthroughGroups); + return new TryExtractInfo(tryBlock, scopeGroups, allHandler, fallthroughGroups, handlerScopes); + } + + /** + * Processes a try block, attempting to extract a finally by locating common instruction patterns + * between all + * try branches. + * + * @param mth The method containing the try block. + * @param tryInfo The try block information. + * @return Whether a finally block has been successfully extracted. + */ + private static boolean processTryBlock(final MethodNode mth, final TryExtractInfo tryInfo) { + if (tryInfo.rethrowBlocks.isEmpty()) { + return false; + } + + if (extractFinally(mth, tryInfo)) { + for (final BlockNode rethrowBlock : tryInfo.rethrowBlocks) { + final InsnNode lastInsn = BlockUtils.getLastInsn(rethrowBlock); + if (lastInsn == null) { + continue; + } + lastInsn.add(AFlag.DONT_GENERATE); } + return true; } return false; } + @Nullable + private static List cutHandlerBlocks(final TryExtractInfo tryInfo, final ExceptionHandler handler) { + final BlockNode handlerBlock = handler.getHandlerBlock(); + final List handlerBlocks = tryInfo.handlerScopes.getBlocksForHandler(handler); + if (handlerBlocks == null) { + return null; + } + + final InsnNode handlerFinalInsn = BlockUtils.getFirstInsn(handlerBlock); + if (handlerFinalInsn != null && handlerFinalInsn.getType() == InsnType.MOVE_EXCEPTION) { + handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception' + } + + final BlockNode bottomBlock = BlockUtils.getBottomBlock(handlerBlocks); + final List pathExits = BlockUtils.followEmptyUpPathWithinSet(bottomBlock, handlerBlocks); + if (pathExits.isEmpty()) { + return handlerBlocks; + } + for (final BlockNode pathExit : pathExits) { + // For this to be able to extract a finally, we must ensure that all paths into the handler's logic + // end with a THROW equal to the output of the move-exception instruction located at the start of + // this handler, if any. + final InsnNode bottomBlockLastInsn = BlockUtils.getLastInsn(pathExit); + final boolean isValidPathExit = bottomBlockLastInsn != null + && handlerFinalInsn != null + && bottomBlockLastInsn.getType() == InsnType.THROW + && bottomBlockLastInsn.getArgsCount() > 0 + && bottomBlockLastInsn.getArg(0).equals(handlerFinalInsn.getResult()); + if (!isValidPathExit) { + return handlerBlocks; + } + } + final List cutHandlerBlocks = new ArrayList<>(handlerBlocks); + for (final BlockNode pathExit : pathExits) { + cutHandlerBlocks.remove(pathExit); + removeEmptyUpPath(cutHandlerBlocks, pathExit); + tryInfo.rethrowBlocks.add(pathExit); + } + return cutHandlerBlocks; + } + + /** + * Attempts to identify and remove an implicit try catch block. + * + * @param cutHandlerBlocks The cut handler blocks of the all handler. + * @return Whether the try block is implicit and has been removed. + */ + private static boolean attemptRemoveImplicitHandlers(final List cutHandlerBlocks, final TryExtractInfo tryInfo) { + if (!(cutHandlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(cutHandlerBlocks))) { + return false; + } + // remove empty catch + tryInfo.finallyHandler.getTryBlock().removeHandler(tryInfo.finallyHandler); + return true; + } + /** * Search and mark common code from 'try' block and 'handlers'. */ - private static boolean extractFinally(MethodNode mth, TryCatchBlockAttr tryBlock, ExceptionHandler allHandler) { - BlockNode handlerBlock = allHandler.getHandlerBlock(); - List handlerBlocks = - new ArrayList<>(BlockUtils.collectBlocksDominatedByWithExcHandlers(mth, handlerBlock, handlerBlock)); - handlerBlocks.remove(handlerBlock); // exclude block with 'move-exception' - cutPathEnds(mth, handlerBlocks); - if (handlerBlocks.isEmpty() || BlockUtils.isAllBlocksEmpty(handlerBlocks)) { - // remove empty catch - allHandler.getTryBlock().removeHandler(allHandler); - return true; - } - BlockNode startBlock = Utils.getOne(handlerBlock.getCleanSuccessors()); - FinallyExtractInfo extractInfo = new FinallyExtractInfo(mth, allHandler, startBlock, handlerBlocks); - if (Consts.DEBUG_FINALLY) { - LOG.debug("Finally info: handler=({}), start={}, blocks={}", allHandler, startBlock, handlerBlocks); + private static boolean extractFinally(final MethodNode mth, final TryExtractInfo tryInfo) { + // Get all handlers from this and inner try blocks. + final boolean hasInnerBlocks = !tryInfo.tryBlock.getInnerTryBlocks().isEmpty(); + final List handlers = getHandlersForTryCatch(tryInfo.tryBlock); + if (handlers.isEmpty()) { + return false; } - boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty(); - List handlers; + final Map> insns = findCommonInsns(mth, tryInfo); + if (insns == null || insns.isEmpty()) { + return false; + } + + final Set ignoredFinallyInsns = new HashSet<>(); + final Set ignoredCandidateInsns = new HashSet<>(); + final Map> insnMap = new HashMap<>(); + for (final InsnNode finallyInsn : insns.keySet()) { + final List candidateInsns = insns.get(finallyInsn); + + // For an instruction to have matched, the number of times it has been found must be + // equal to the number of edges that the exception handler has which aren't the + // finally handler. + if (candidateInsns.size() != tryInfo.handlerScopes.size() - 1) { + ignoredFinallyInsns.add(finallyInsn); + ignoredCandidateInsns.addAll(candidateInsns); + // TODO: Add support for partial `catch (Throwable)` finally clauses. + // continue; + return false; + } + + insnMap.put(finallyInsn, candidateInsns); + } + + for (final InsnNode finallyInsn : insnMap.keySet()) { + finallyInsn.add(AFlag.FINALLY_INSNS); + final List candidateInsns = insnMap.get(finallyInsn); + for (final InsnNode candidateInsn : candidateInsns) { + copyCodeVars(finallyInsn, candidateInsn); + candidateInsn.add(AFlag.DONT_GENERATE); + } + } + + for (final BlockNode finallyBlock : tryInfo.completeFinallyBlocks) { + if (ListUtils.anyMatch(finallyBlock.getInstructions(), ignoredFinallyInsns::contains)) { + // If this block contains an instruction which was not found in all try edges, + // don't mark it as a finally block. + continue; + } + finallyBlock.add(AFlag.FINALLY_INSNS); + } + for (final BlockNode candidateBlock : tryInfo.completeCandidateBlocks) { + if (ListUtils.anyMatch(candidateBlock.getInstructions(), ignoredCandidateInsns::contains)) { + // If this block contains an instruction which was found to "duplicate" a finally + // instruction which was not found in all try edges, don't mark it as a duplicated + // block. + continue; + } + candidateBlock.add(AFlag.DONT_GENERATE); + } + + // If any scope has been merged with the fallthrough case of the try catch, don't merge inner trys. + // Otherwise, merge inner trys. + final boolean mergedFallthroughScope = + ListUtils.anyMatch(tryInfo.scopeGroups.getMergedScopes(), scopePair -> scopePair.getFirst().isNotHandlerExit()); + final boolean mergeInnerTryBlocks = hasInnerBlocks && !mergedFallthroughScope; + + tryInfo.finallyHandler.setFinally(true); + + if (mergeInnerTryBlocks) { + final List innerTryBlocks = tryInfo.tryBlock.getInnerTryBlocks(); + for (final TryCatchBlockAttr innerTryBlock : innerTryBlocks) { + tryInfo.tryBlock.getHandlers().addAll(innerTryBlock.getHandlers()); + tryInfo.tryBlock.getBlocks().addAll(innerTryBlock.getBlocks()); + innerTryBlock.setMerged(true); + } + tryInfo.tryBlock.setBlocks(ListUtils.distinctList(tryInfo.tryBlock.getBlocks())); + innerTryBlocks.clear(); + } + + return true; + } + + /** + * Gets a list of every exception handler attached to this try block, including handlers of inner + * try blocks. + * + * @param tryBlock The source try block to get the list of exception handlers for + * @return The list of exception handlers. + */ + private static List getHandlersForTryCatch(final TryCatchBlockAttr tryBlock) { + final boolean hasInnerBlocks = !tryBlock.getInnerTryBlocks().isEmpty(); + final List handlers; if (hasInnerBlocks) { // collect handlers from this and all inner blocks - // (intentionally not using recursive collect for now) handlers = new ArrayList<>(tryBlock.getHandlers()); - for (TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) { - handlers.addAll(innerTryBlock.getHandlers()); + for (final TryCatchBlockAttr innerTryBlock : tryBlock.getInnerTryBlocks()) { + handlers.addAll(getHandlersForTryCatch(innerTryBlock)); } } else { handlers = tryBlock.getHandlers(); } - if (handlers.isEmpty()) { - return false; - } - // search 'finally' instructions in other handlers - for (ExceptionHandler otherHandler : handlers) { - if (otherHandler == allHandler) { + return handlers; + } + + @Nullable + private static Map> findCommonInsns(final MethodNode mth, final TryExtractInfo tryInfo) { + final List allHandlerBlocks = tryInfo.handlerScopes.getBlocksForHandler(tryInfo.finallyHandler); + final BlockNode finallyScopeTerminus = getTerminusForHandler(tryInfo.finallyHandler, tryInfo); + + final Map> matchingInsns = new HashMap<>(); + for (final TryEdge edge : tryInfo.handlerScopes.keySet()) { + if (edge.isHandlerExit() && edge.getExceptionHandler() == tryInfo.finallyHandler) { continue; } - for (BlockNode checkBlock : otherHandler.getBlocks()) { - if (searchDuplicateInsns(checkBlock, extractInfo)) { + + final List handlerBlocks = tryInfo.handlerScopes.get(edge); + BlockNode scopeTerminus = null; + for (final BlockNode edgeTerminusBlock : tryInfo.scopeTerminusGroups.keySet()) { + final List edgesWithTerminus = tryInfo.scopeTerminusGroups.get(edgeTerminusBlock); + if (edgesWithTerminus.contains(edge)) { + scopeTerminus = edgeTerminusBlock; break; - } else { - extractInfo.getFinallyInsnsSlice().resetIncomplete(); } } - } - if (Consts.DEBUG_FINALLY) { - LOG.debug("Handlers slices:\n{}", extractInfo); - } - boolean mergeInnerTryBlocks; - int duplicatesCount = extractInfo.getDuplicateSlices().size(); - if (duplicatesCount == (handlers.size() - 1)) { - // all collected handlers have duplicate block - mergeInnerTryBlocks = hasInnerBlocks; - } else { - // some handlers don't have duplicated blocks - if (!hasInnerBlocks || duplicatesCount != (tryBlock.getHandlers().size() - 1)) { - // unexpected count of duplicated slices - return false; + if (scopeTerminus == null) { + throw new JadxRuntimeException("Expected to find fallthrough terminus for handler " + edge); } - mergeInnerTryBlocks = false; - } - // remove 'finally' from 'try' blocks, - // check all up paths on each exit (connected with 'finally' exit) - List tryBlocks = allHandler.getTryBlock().getBlocks(); - BlockNode bottomBlock = BlockUtils.getBottomBlock(allHandler.getBlocks()); - if (bottomBlock == null) { - if (Consts.DEBUG_FINALLY) { - LOG.warn("No bottom block for handler: {} and blocks: {}", allHandler, allHandler.getBlocks()); + final TraverserActivePathState comparatorState = + new TraverserActivePathState(mth, new SameInstructionsStrategyImpl(), finallyScopeTerminus, + scopeTerminus, allHandlerBlocks, handlerBlocks); + final TraverserController controller = new TraverserController(); + final List pathResults; + try { + pathResults = controller.process(comparatorState); + } catch (final TraverserException e) { + LOG.error("Could not search for finally duplicate instructions in path", e); + return null; } - return false; - } - BlockNode bottomFinallyBlock = BlockUtils.followEmptyPath(bottomBlock); - BlockNode bottom = BlockUtils.getNextBlock(bottomFinallyBlock); - if (bottom == null) { - if (Consts.DEBUG_FINALLY) { - LOG.warn("Finally bottom block not found for: {} and: {}", bottomBlock, bottomFinallyBlock); - } - return false; - } - boolean found = false; - List pathBlocks = getPathStarts(mth, bottom, bottomFinallyBlock); - for (BlockNode pred : pathBlocks) { - List upPath = BlockUtils.collectPredecessors(mth, pred, tryBlocks); - if (upPath.size() < handlerBlocks.size()) { - continue; - } - if (Consts.DEBUG_FINALLY) { - LOG.debug("Checking dup path starts: {} from {}", upPath, pred); - } - for (BlockNode block : upPath) { - if (searchDuplicateInsns(block, extractInfo)) { - found = true; - if (Consts.DEBUG_FINALLY) { - LOG.debug("Found dup in: {} from {}", block, pred); + + final Set completeFinally = new HashSet<>(); + final Set completeCandidate = new HashSet<>(); + for (final TraverserActivePathState pathResult : pathResults) { + for (final Pair matchingInsnPair : pathResult.getMatchedInsns()) { + final InsnNode finallyInsn = matchingInsnPair.getFirst(); + final InsnNode candidateInsn = matchingInsnPair.getSecond(); + final List candidateInsnsList; + if (!matchingInsns.containsKey(finallyInsn)) { + candidateInsnsList = new LinkedList<>(); + matchingInsns.put(finallyInsn, candidateInsnsList); + } else { + candidateInsnsList = matchingInsns.get(finallyInsn); } - break; - } else { - extractInfo.getFinallyInsnsSlice().resetIncomplete(); + candidateInsnsList.add(candidateInsn); } + + completeFinally.addAll(pathResult.getAllFullyMatchedFinallyBlocks()); + completeCandidate.addAll(pathResult.getAllFullyMatchedCandidateBlocks()); } - } - if (!found) { - if (Consts.DEBUG_FINALLY) { - LOG.info("Dup not found for all handler: {}", allHandler); + + if (tryInfo.completeFinallyBlocks == null) { + tryInfo.completeFinallyBlocks = completeFinally; + } else { + tryInfo.completeFinallyBlocks.retainAll(completeFinally); + } + + if (tryInfo.completeCandidateBlocks == null) { + tryInfo.completeCandidateBlocks = completeCandidate; + } else { + tryInfo.completeCandidateBlocks.addAll(completeCandidate); } - return false; - } - if (Consts.DEBUG_FINALLY) { - LOG.debug("Result slices:\n{}", extractInfo); - } - if (!checkSlices(extractInfo)) { - mth.addWarnComment("Finally extract failed"); - return false; } - // 'finally' extract confirmed, apply - apply(extractInfo); - allHandler.setFinally(true); - - if (mergeInnerTryBlocks) { - List innerTryBlocks = tryBlock.getInnerTryBlocks(); - for (TryCatchBlockAttr innerTryBlock : innerTryBlocks) { - tryBlock.getHandlers().addAll(innerTryBlock.getHandlers()); - tryBlock.getBlocks().addAll(innerTryBlock.getBlocks()); - innerTryBlock.setMerged(true); - } - tryBlock.setBlocks(ListUtils.distinctList(tryBlock.getBlocks())); - innerTryBlocks.clear(); - } - return true; + return matchingInsns; } - private static void cutPathEnds(MethodNode mth, List handlerBlocks) { - List throwBlocks = ListUtils.filter(handlerBlocks, - b -> BlockUtils.checkLastInsnType(b, InsnType.THROW)); - if (throwBlocks.size() != 1) { - mth.addDebugComment("Finally have unexpected throw blocks count: " + throwBlocks.size() + ", expect 1"); - return; - } - BlockNode throwBlock = throwBlocks.get(0); - handlerBlocks.remove(throwBlock); - removeEmptyUpPath(handlerBlocks, throwBlock); - } - - private static void removeEmptyUpPath(List handlerBlocks, BlockNode startBlock) { - for (BlockNode pred : startBlock.getPredecessors()) { + private static void removeEmptyUpPath(final List handlerBlocks, final BlockNode startBlock) { + for (final BlockNode pred : startBlock.getPredecessors()) { if (pred.isEmpty()) { if (handlerBlocks.remove(pred) && !BlockUtils.isBackEdge(pred, startBlock)) { removeEmptyUpPath(handlerBlocks, pred); @@ -256,394 +477,28 @@ public class MarkFinallyVisitor extends AbstractVisitor { } } - private static List getPathStarts(MethodNode mth, BlockNode bottom, BlockNode bottomFinallyBlock) { - Stream preds = bottom.getPredecessors().stream().filter(b -> b != bottomFinallyBlock); - if (bottom == mth.getExitBlock()) { - preds = preds.flatMap(r -> r.getPredecessors().stream()); - } - return preds.collect(Collectors.toList()); - } - - 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()) { - extractInfo.getMth().addDebugComment( - "Incorrect finally slice size: " + dupSlice + ", expected: " + 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()) { - extractInfo.getMth().addDebugComment( - "Incorrect finally slice insn: " + dupInsn + ", expected: " + 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) { + private static void copyCodeVars(final InsnNode fromInsn, final InsnNode toInsn) { copyCodeVars(fromInsn.getResult(), toInsn.getResult()); - int argsCount = fromInsn.getArgsCount(); + final 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) { + private static void copyCodeVars(final InsnArg fromArg, final InsnArg toArg) { if (fromArg == null || toArg == null || !fromArg.isRegister() || !toArg.isRegister()) { return; } - SSAVar fromSsaVar = ((RegisterArg) fromArg).getSVar(); - SSAVar toSsaVar = ((RegisterArg) toArg).getSVar(); + final SSAVar fromSsaVar = ((RegisterArg) fromArg).getSVar(); + final 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()) { - Map checkCache = new HashMap<>(); - if (checkBlocksTree(dupBlock, startBlock, dupSlice, extractInfo, checkCache)) { - dupSlice.setComplete(true); - extractInfo.getFinallyInsnsSlice().setComplete(true); - } else { - return null; - } - } - return checkTempSlice(dupSlice); - } - - @Nullable - private static InsnsSlice checkTempSlice(InsnsSlice slice) { - List insnsList = slice.getInsnsList(); - if (insnsList.isEmpty()) { - return null; - } - // ignore slice with only one 'if' insn - if (insnsList.size() == 1) { - InsnNode insnNode = insnsList.get(0); - if (insnNode.getType() == InsnType.IF) { - return null; - } - } - return slice; - } - - /** - * 'Finally' instructions can start in the middle of the first block. - */ - private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) { - extractInfo.setCurDupSlice(null); - List dupInsns = dupBlock.getInstructions(); - List finallyInsns = finallyBlock.getInstructions(); - int dupSize = dupInsns.size(); - int finSize = finallyInsns.size(); - if (dupSize < finSize) { - return null; - } - int startPos; - int endPos = 0; - if (dupSize == finSize) { - if (!checkInsns(extractInfo, dupInsns, finallyInsns, 0)) { - return null; - } - startPos = 0; - } else { - // dupSize > finSize - startPos = dupSize - finSize; - // fast check from end of block - if (!checkInsns(extractInfo, dupInsns, finallyInsns, startPos)) { - // search start insn - boolean found = false; - for (int i = 1; i < startPos; i++) { - if (checkInsns(extractInfo, dupInsns, finallyInsns, i)) { - startPos = i; - endPos = finSize + i; - found = true; - break; - } - } - if (!found) { - return null; - } - } - } - - // put instructions into slices - boolean complete; - InsnsSlice slice = new InsnsSlice(); - extractInfo.setCurDupSlice(slice); - int endIndex; - if (endPos != 0) { - endIndex = endPos + 1; - // both slices completed - complete = true; - } else { - endIndex = dupSize; - 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()) { - extractInfo.getMth().addDebugComment( - "Another duplicated slice has different insns count: " + slice + ", finally: " + 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(FinallyExtractInfo extractInfo, List dupInsns, List finallyInsns, int delta) { - extractInfo.setCurDupInsns(dupInsns, delta); - for (int i = finallyInsns.size() - 1; i >= 0; i--) { - InsnNode startInsn = finallyInsns.get(i); - InsnNode dupInsn = dupInsns.get(delta + i); - if (!sameInsns(extractInfo, dupInsn, startInsn)) { - return false; - } - } - return true; - } - - private static boolean checkBlocksTree(BlockNode dupBlock, BlockNode finallyBlock, - InsnsSlice dupSlice, FinallyExtractInfo extractInfo, - Map checksCache) { - BlockPair checkBlocks = new BlockPair(dupBlock, finallyBlock); - Boolean checked = checksCache.get(checkBlocks); - if (checked != null) { - return checked; - } - boolean same; - InsnsSlice finallySlice = extractInfo.getFinallyInsnsSlice(); - List finallyCS = getSuccessorsWithoutLoop(finallyBlock); - List dupCS = getSuccessorsWithoutLoop(dupBlock); - if (finallyCS.size() == dupCS.size()) { - same = true; - 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)) { - same = false; - break; - } - if (!checkBlocksTree(dupSBlock, finSBlock, dupSlice, extractInfo, checksCache)) { - same = false; - break; - } - dupSlice.addBlock(dupSBlock); - finallySlice.addBlock(finSBlock); - } - } - } else { - // stop checks at start blocks (already compared) - same = true; - } - checksCache.put(checkBlocks, same); - return same; - } - - private static List getSuccessorsWithoutLoop(BlockNode block) { - if (block.contains(AFlag.LOOP_END)) { - return block.getCleanSuccessors(); - } - return block.getSuccessors(); - } - - private static boolean compareBlocks(BlockNode dupBlock, BlockNode finallyBlock, InsnsSlice dupSlice, FinallyExtractInfo extractInfo) { - List dupInsns = dupBlock.getInstructions(); - List finallyInsns = finallyBlock.getInstructions(); - int dupInsnCount = dupInsns.size(); - int finallyInsnCount = finallyInsns.size(); - if (finallyInsnCount == 0) { - return dupInsnCount == 0; - } - if (dupInsnCount < finallyInsnCount) { - return false; - } - extractInfo.setCurDupInsns(dupInsns, 0); - for (int i = 0; i < finallyInsnCount; i++) { - if (!sameInsns(extractInfo, dupInsns.get(i), finallyInsns.get(i))) { - return false; - } - } - if (dupInsnCount > finallyInsnCount) { - dupSlice.addInsns(dupBlock, 0, finallyInsnCount); - dupSlice.setComplete(true); - InsnsSlice finallyInsnsSlice = extractInfo.getFinallyInsnsSlice(); - finallyInsnsSlice.addBlock(finallyBlock); - finallyInsnsSlice.setComplete(true); - } - return true; - } - - private static boolean sameInsns(FinallyExtractInfo extractInfo, InsnNode dupInsn, InsnNode fInsn) { - if (!dupInsn.isSame(fInsn)) { - return false; - } - // TODO: check instance arg in ConstructorInsn - for (int i = 0; i < dupInsn.getArgsCount(); i++) { - InsnArg dupArg = dupInsn.getArg(i); - InsnArg fArg = fInsn.getArg(i); - if (!isSameArgs(extractInfo, dupArg, fArg)) { - return false; - } - } - return true; - } - - @SuppressWarnings("RedundantIfStatement") - private static boolean isSameArgs(FinallyExtractInfo extractInfo, InsnArg dupArg, InsnArg fArg) { - boolean isReg = dupArg.isRegister(); - if (isReg != fArg.isRegister()) { - return false; - } - if (isReg) { - RegisterArg dupReg = (RegisterArg) dupArg; - RegisterArg fReg = (RegisterArg) fArg; - if (!dupReg.sameCodeVar(fReg) - && !sameDebugInfo(dupReg, fReg) - && assignedOutsideHandler(extractInfo, dupReg, fReg) - && assignInsnDifferent(dupReg, fReg)) { - return false; - } - } - boolean remConst = dupArg.isConst(); - if (remConst != fArg.isConst()) { - return false; - } - if (remConst && !dupArg.isSameConst(fArg)) { - return false; - } - return true; - } - - private static boolean sameDebugInfo(RegisterArg dupReg, RegisterArg fReg) { - RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO); - RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO); - if (fDbgInfo == null || dupDbgInfo == null) { - return false; - } - return dupDbgInfo.equals(fDbgInfo); - } - - private static boolean assignInsnDifferent(RegisterArg dupReg, RegisterArg fReg) { - InsnNode assignInsn = fReg.getAssignInsn(); - InsnNode dupAssign = dupReg.getAssignInsn(); - if (assignInsn == null || dupAssign == null) { - return true; - } - if (!assignInsn.isSame(dupAssign)) { - return true; - } - if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) { - return !assignInsn.isDeepEquals(dupAssign); - } - return false; - } - - @SuppressWarnings("RedundantIfStatement") - private static boolean assignedOutsideHandler(FinallyExtractInfo extractInfo, RegisterArg dupReg, RegisterArg fReg) { - if (InsnList.contains(extractInfo.getFinallyInsnsSlice().getInsnsList(), fReg.getAssignInsn())) { - return false; - } - InsnNode dupAssign = dupReg.getAssignInsn(); - InsnsSlice curDupSlice = extractInfo.getCurDupSlice(); - if (curDupSlice != null && InsnList.contains(curDupSlice.getInsnsList(), dupAssign)) { - return false; - } - List curDupInsns = extractInfo.getCurDupInsns(); - if (Utils.notEmpty(curDupInsns) && InsnList.contains(curDupInsns, dupAssign, extractInfo.getCurDupInsnsOffset())) { - return false; - } - return true; - } - /** * Reload method without applying this visitor */ - private static void undoFinallyVisitor(MethodNode mth) { + private static void undoFinallyVisitor(final MethodNode mth) { try { // TODO: make more common and less hacky mth.unload(); @@ -651,11 +506,29 @@ public class MarkFinallyVisitor extends AbstractVisitor { for (IDexTreeVisitor visitor : mth.root().getPasses()) { if (visitor instanceof MarkFinallyVisitor) { break; + // All visitors after MarkFinally will be invoked as usual after this because + // the original decompilation request will proceed. } DepthTraversal.visit(visitor, mth); } - } catch (Exception e) { + } catch (final DecodeException e) { mth.addError("Undo finally extract failed", e); } } + + @Nullable + private static BlockNode getTerminusForHandler(final ExceptionHandler handler, final TryExtractInfo tryInfo) { + for (final BlockNode terminus : tryInfo.scopeTerminusGroups.keySet()) { + final List edgesWithTerminus = tryInfo.scopeTerminusGroups.get(terminus); + for (final TryEdge edge : edgesWithTerminus) { + if (edge.isNotHandlerExit()) { + continue; + } + if (edge.getExceptionHandler().equals(handler)) { + return terminus; + } + } + } + return null; + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategy.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategy.java new file mode 100644 index 000000000..3db34eb3e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategy.java @@ -0,0 +1,11 @@ +package jadx.core.dex.visitors.finaly; + +import jadx.core.dex.instructions.args.InsnArg; +import jadx.core.dex.nodes.InsnNode; + +public abstract class SameInstructionsStrategy { + + public abstract boolean sameInsns(InsnNode dupInsn, InsnNode fInsn); + + public abstract boolean isSameArgs(InsnArg dupArg, InsnArg fArg); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategyImpl.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategyImpl.java new file mode 100644 index 000000000..dcd5e895c --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/SameInstructionsStrategyImpl.java @@ -0,0 +1,81 @@ +package jadx.core.dex.visitors.finaly; + +import java.util.Objects; + +import jadx.core.dex.attributes.AType; +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; + +public final class SameInstructionsStrategyImpl extends SameInstructionsStrategy { + + private static boolean sameDebugInfo(final RegisterArg dupReg, final RegisterArg fReg) { + final RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO); + final RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO); + if (fDbgInfo == null || dupDbgInfo == null) { + return false; + } + return dupDbgInfo.equals(fDbgInfo); + } + + private static boolean assignInsnDifferent(final RegisterArg dupReg, final RegisterArg fReg) { + final InsnNode assignInsn = fReg.getAssignInsn(); + final InsnNode dupAssign = dupReg.getAssignInsn(); + if (assignInsn == null || dupAssign == null) { + return true; + } + if (!assignInsn.isSame(dupAssign)) { + return true; + } + if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) { + // Do this and not deep equals since we already know that the result is the same and that the insn + // type is the same + return !(Objects.equals(assignInsn.getArguments(), assignInsn.getArguments())); + } + return false; + } + + @Override + public final boolean sameInsns(final InsnNode dupInsn, final InsnNode fInsn) { + if (!dupInsn.isSame(fInsn)) { + return false; + } + + for (int i = 0; i < dupInsn.getArgsCount(); i++) { + final InsnArg dupArg = dupInsn.getArg(i); + final InsnArg fArg = fInsn.getArg(i); + if (!isSameArgs(dupArg, fArg)) { + return false; + } + } + + return true; + } + + @Override + public final boolean isSameArgs(final InsnArg dupArg, final InsnArg fArg) { + if (dupArg == null) { + return false; + } + final boolean isReg = dupArg.isRegister(); + if (isReg != fArg.isRegister()) { + return false; + } + if (isReg) { + final RegisterArg dupReg = (RegisterArg) dupArg; + final RegisterArg fReg = (RegisterArg) fArg; + + if (!dupReg.sameCodeVar(fReg) + && !sameDebugInfo(dupReg, fReg) + && assignInsnDifferent(dupReg, fReg)) { + return false; + } + } + final boolean remConst = dupArg.isConst(); + if (remConst != fArg.isConst()) { + return false; + } + return !(remConst && !dupArg.isSameConst(fArg)); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/TryCatchEdgeBlockMap.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/TryCatchEdgeBlockMap.java new file mode 100644 index 000000000..7728f64b6 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/TryCatchEdgeBlockMap.java @@ -0,0 +1,200 @@ +package jadx.core.dex.visitors.finaly; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.trycatch.TryCatchBlockAttr; +import jadx.core.dex.trycatch.TryEdge; +import jadx.core.dex.trycatch.TryEdgeScopeGroupMap; +import jadx.core.utils.BlockUtils; +import jadx.core.utils.ListUtils; + +/** + * A map containing all edges within a try catch block as the key and all blocks that the + * respective edge can traverse to within the "scope" of that edge (relative to the + * entire try / catch). + */ +public final class TryCatchEdgeBlockMap implements Map> { + + public static boolean anyBlockHasNonImplicitTry(final List blocks) { + final List blocksWithTries = ListUtils.filter(blocks, blk -> blk.contains(AFlag.EXC_TOP_SPLITTER)); + if (blocksWithTries.isEmpty()) { + return false; + } + + for (final BlockNode topSplitter : blocksWithTries) { + TryCatchBlockAttr block = null; + for (final BlockNode topSplitterSuccessor : topSplitter.getCleanSuccessors()) { + if (topSplitterSuccessor.contains(AType.TRY_BLOCK)) { + block = topSplitterSuccessor.get(AType.TRY_BLOCK); + } + } + if (block == null) { + continue; + } + if (!TryCatchBlockAttr.isImplicitOrMerged(block)) { + return true; + } + } + return false; + } + + public static TryCatchEdgeBlockMap getAllInScope(final MethodNode mth, final TryCatchBlockAttr tryCatch, + final TryEdgeScopeGroupMap scopeGroups, final ExceptionHandler finallyHandler, + final Map> scopeTerminusGroups) { + final Map edgeBlocks = tryCatch.getEdgeBlockMap(mth); + + final TryCatchEdgeBlockMap result = new TryCatchEdgeBlockMap(); + for (final BlockNode scopeTerminus : scopeTerminusGroups.keySet()) { + final List sourceEdges = scopeTerminusGroups.get(scopeTerminus); + for (final TryEdge sourceEdge : sourceEdges) { + final BlockNode edgeBlock = edgeBlocks.get(sourceEdge); + + final boolean useClean = !(sourceEdge.isNotHandlerExit() + && ListUtils.anyMatch(scopeGroups.getMergedScopes(), pair -> pair.getSecond().isNotHandlerExit())); + List allBlocks = + BlockUtils.collectAllSuccessorsUntil(mth, edgeBlock, useClean, (block) -> block == scopeTerminus); + final boolean anyBlockHasTry = anyBlockHasNonImplicitTry(allBlocks); + + if (anyBlockHasTry && useClean) { + // If there's a try edge in the found blocks, collect all successors, not just clean successors. + allBlocks = BlockUtils.collectAllSuccessorsUntil(mth, edgeBlock, false, (block) -> block == scopeTerminus); + } + if (sourceEdge.isNotHandlerExit()) { + // If source edge is a fallthrough case, add the try body. + allBlocks = new ArrayList<>(allBlocks); + allBlocks.addAll(tryCatch.getBlocks()); + } + + result.put(sourceEdge, allBlocks); + } + } + + final List finallyBlocks = result.getBlocksForHandler(finallyHandler); + for (final TryEdge edge : result.keySet()) { + if (edge.isHandlerExit() && edge.getExceptionHandler() == finallyHandler) { + continue; + } + + final List blocks = result.get(edge); + blocks.removeAll(finallyBlocks); + } + + return result; + } + + private final Map> underlying; + + public TryCatchEdgeBlockMap() { + underlying = new HashMap<>(); + } + + @Override + public final void clear() { + underlying.clear(); + } + + @Override + public final boolean containsKey(Object key) { + return underlying.containsKey(key); + } + + @Override + public final boolean containsValue(Object value) { + if (!(value instanceof TryEdge)) { + return false; + } + + final TryEdge edge = (TryEdge) value; + return underlying.containsKey(edge); + } + + @Override + public final Set>> entrySet() { + return underlying.entrySet(); + } + + @Override + public final List get(Object key) { + return underlying.get(key); + } + + @Override + public final boolean isEmpty() { + return underlying.isEmpty(); + } + + @Override + public final Set keySet() { + return underlying.keySet(); + } + + @Override + public final List put(TryEdge key, List value) { + return underlying.put(key, value); + } + + @Override + public final void putAll(Map> otherMap) { + underlying.putAll(otherMap); + } + + @Override + public final List remove(Object key) { + return underlying.remove(key); + } + + @Override + public final int size() { + return underlying.size(); + } + + @Override + public final Collection> values() { + return underlying.values(); + } + + @Nullable + public final List getBlocksForHandler(final ExceptionHandler handler) { + TryEdge edgeWithHandler = null; + for (final TryEdge edge : keySet()) { + if (edge.isNotHandlerExit()) { + continue; + } + + if (!edge.getExceptionHandler().equals(handler)) { + continue; + } + + edgeWithHandler = edge; + break; + } + if (edgeWithHandler == null) { + return null; + } + return get(edgeWithHandler); + } + + public final List getBlocksForAllFallthroughs() { + final List blks = new ArrayList<>(); + for (final TryEdge edge : keySet()) { + if (edge.isHandlerExit()) { + continue; + } + + blks.addAll(get(edge)); + } + return blks; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/GlobalTraverserSourceState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/GlobalTraverserSourceState.java new file mode 100644 index 000000000..179c76e9d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/GlobalTraverserSourceState.java @@ -0,0 +1,26 @@ +package jadx.core.dex.visitors.finaly.traverser; + +import java.util.Set; + +import jadx.core.dex.nodes.BlockNode; + +/** + * A state used by the traverser controller for storing information regarding an entire path to + * take during traversal. This should be static amongst all states following the same path. + */ +public final class GlobalTraverserSourceState { + + private final Set containedBlocks; + + public GlobalTraverserSourceState(final Set containedBlocks) { + this.containedBlocks = containedBlocks; + } + + public final boolean isBlockContained(final BlockNode block) { + return containedBlocks.contains(block); + } + + public final Set getContainedBlocks() { + return containedBlocks; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserController.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserController.java new file mode 100644 index 000000000..ca3f88ed0 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserController.java @@ -0,0 +1,211 @@ +package jadx.core.dex.visitors.finaly.traverser; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractActivePathTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.state.RecoveredFromCacheTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * Responsible for determining if two distinct subgraphs are the same within a graph by comparing + * all blocks and their instructions. + * This is used for identifying duplicated instructions for extracting finally blocks. + * + * The terms "finally" and "candidate" are used to represent the two distinct subgraphs explored + * by this controller; the "finally" subgraph, which is the subgraph which is what is being used + * as a finally block, and the "candidate" subgraph, which is the subgraph which is being + * compared to the "finally" subgraph to see if they are the same. There is only ever one + * "finally" subgraph, however it is run against multiple different "candidate" subgraphs depending + * on the complexity of the try catch block that this is being run for. + */ +public final class TraverserController { + + private static List processHandlerImplementations(final TraverserActivePathState state, + final AbstractBlockTraverserHandler handler) throws TraverserException { + if (handler instanceof AbstractBlockPathTraverserHandler) { + ((AbstractBlockPathTraverserHandler) handler).process(); + return List.of(state); + } else if (handler instanceof AbstractActivePathTraverserHandler) { + return ((AbstractActivePathTraverserHandler) handler).process(); + } else { + throw new JadxRuntimeException( + "A sealed class, " + AbstractBlockPathTraverserHandler.class.getSimpleName() + ", has an unknown implementation"); + } + } + + private final @Nullable Function stateAbortCondition; + + public TraverserController() { + this(null); + } + + public TraverserController(final @Nullable Function stateAbortCondition) { + this.stateAbortCondition = stateAbortCondition; + } + + /** + * Processes a traverser path state using from a {@link TraverserActivePathState}. This + * function will continue evaluating an active path until either: + *
    + *
  • The state abort condition is met by both "finally" and "candidate" path, if there is + * one.
  • + *
  • The path state of either the "finally" or "candidate" path has terminated.
  • + *
  • The path has began a comparison of two blocks which have already been compared.
  • + *
  • The "finally" and "candidate" states, on two different executions of + * {@link TraverserController#advance}, did not change. + *
+ * This function will return a list of all of the different paths taken at the point of + * termination of each individual branch. + * + * @param state + * @return + */ + public final List process(final TraverserActivePathState state) throws TraverserException { + TraverserActivePathState nextState = state; + final AtomicReference previousFinallyState = new AtomicReference<>(null); + final AtomicReference previousCandidateState = new AtomicReference<>(null); + while (true) { + final List advancedStates = advance(nextState, previousFinallyState, previousCandidateState); + if (advancedStates == null || advancedStates.isEmpty()) { + break; + } + + if (advancedStates.size() != 1) { + final TraverserController nextController = new TraverserController(stateAbortCondition); + final List returnStates = new ArrayList<>(); + for (final TraverserActivePathState advancedState : advancedStates) { + final List childStates = nextController.process(advancedState); + returnStates.addAll(childStates); + } + return returnStates; + } + + nextState = advancedStates.get(0); + } + return List.of(nextState); + } + + /** + * Processes a singular traverser state once. + * + * @param state + * @param previousFinallyState + * @param previousCandidateState + * @return + */ + public final List advance(final TraverserActivePathState state, + final AtomicReference previousFinallyState, + final AtomicReference previousCandidateState) throws TraverserException { + final TraverserGlobalCommonState commonState = state.getGlobalCommonState(); + final TraverserState finallyState = state.getFinallyState(); + final TraverserState candidateState = state.getCandidateState(); + + if (previousFinallyState.get() == finallyState && previousCandidateState.get() == candidateState) { + final TraverserStateFactory finallyStateProducer = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNRESOLVABLE_STATES); + final TraverserStateFactory candidateStateProducer = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNRESOLVABLE_STATES); + return List.of(TraverserActivePathState.produceFromFactories(state, finallyStateProducer, candidateStateProducer)); + } + + previousFinallyState.set(finallyState); + previousCandidateState.set(candidateState); + + if (finallyState.isTerminal() || candidateState.isTerminal()) { + return null; + } + + if (finallyState.getClass().equals(candidateState.getClass()) + && finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE + && candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE) { + + final BlockNode finallyBlock; + final BlockNode candidateBlock; + final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); + final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); + if (finallyBlockInfo != null && candidateBlockInfo != null) { + finallyBlock = finallyBlockInfo.getBlock(); + candidateBlock = candidateBlockInfo.getBlock(); + } else { + finallyBlock = null; + candidateBlock = null; + } + + final boolean isCached; + if (finallyBlock != null && candidateBlock != null) { + isCached = commonState.hasBlocksBeenCached(finallyBlock, candidateBlock); + } else { + isCached = false; + } + + if (isCached) { + final List dupStates = commonState.getCachedStateFor(finallyBlock, candidateBlock); + final List recoveredFromCacheStates = new ArrayList<>(dupStates.size()); + for (final TraverserActivePathState dupState : dupStates) { + final TraverserState reusedFinallyState = dupState.getFinallyState(); + final TraverserState reusedCandidateState = dupState.getCandidateState(); + final TraverserStateFactory finallyStateProducer = RecoveredFromCacheTraverserState.getFactory(reusedFinallyState); + final TraverserStateFactory candidateStateProducer = + RecoveredFromCacheTraverserState.getFactory(reusedCandidateState); + final TraverserActivePathState recoveredFromCacheState = + TraverserActivePathState.produceFromFactories(state, finallyStateProducer, candidateStateProducer); + recoveredFromCacheState.mergeWith(dupStates); + recoveredFromCacheStates.add(recoveredFromCacheState); + } + return recoveredFromCacheStates; + } + + final AbstractBlockTraverserHandler handler = candidateState.getNextHandler(); + final List resultingStates = processHandlerImplementations(state, handler); + return resultingStates; + } + + final boolean hasReadyToCompare = finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE + || candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; + + final boolean finallyStateAborted = advanceSingleState(state, finallyState, hasReadyToCompare); + final boolean candidateStateAborted = advanceSingleState(state, candidateState, hasReadyToCompare); + + if (finallyStateAborted && candidateStateAborted) { + return null; + } + + return List.of(state); + } + + /** + * Advances a singular state once. + * + * @return Whether this state has been aborted by the state abort function. + */ + private boolean advanceSingleState(final TraverserActivePathState activePathState, final TraverserState singleState, + final boolean hasReadyToCompare) throws TraverserException { + final boolean stateAborted = stateAbortCondition != null && stateAbortCondition.apply(singleState); + if (stateAbortCondition == null || !stateAborted) { + if (singleState.getCompareState() == TraverserState.ComparisonState.NOT_READY + || (singleState.getCompareState() == TraverserState.ComparisonState.AWAITING_OPTIONAL_PREDECESSOR_MERGE + && hasReadyToCompare)) { + final AbstractBlockTraverserHandler handler = singleState.getNextHandler(); + final List results = processHandlerImplementations(activePathState, handler); + if (results.size() != 1 || results.get(0) != activePathState) { + throw new JadxRuntimeException("A traverser handler which was not expected to change path states actually did"); + } + } + } + return stateAborted; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserException.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserException.java new file mode 100644 index 000000000..3c097a8aa --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/TraverserException.java @@ -0,0 +1,8 @@ +package jadx.core.dex.visitors.finaly.traverser; + +public class TraverserException extends Exception { + + public TraverserException(final String msg) { + super(msg); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/DuplicatedTraverserStateFactory.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/DuplicatedTraverserStateFactory.java new file mode 100644 index 000000000..fcad4a41b --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/DuplicatedTraverserStateFactory.java @@ -0,0 +1,26 @@ +package jadx.core.dex.visitors.finaly.traverser.factory; + +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public final class DuplicatedTraverserStateFactory extends TraverserStateFactory { + + private final T baseState; + + public DuplicatedTraverserStateFactory(final T baseState) { + this.baseState = baseState; + } + + @Override + public final T generateInternalState(final TraverserActivePathState state) { + final Class baseStateClass = (Class) baseState.getClass(); + final TraverserState duplicated = baseState.duplicate(state); + if (!baseStateClass.isInstance(duplicated)) { + throw new JadxRuntimeException( + "A state of class " + baseState.getClass() + " has duplicated to produce a class of " + duplicated.getClass()); + } + return baseStateClass.cast(duplicated); + } + +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/TraverserStateFactory.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/TraverserStateFactory.java new file mode 100644 index 000000000..72a4e4608 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/factory/TraverserStateFactory.java @@ -0,0 +1,14 @@ +package jadx.core.dex.visitors.finaly.traverser.factory; + +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; + +public abstract class TraverserStateFactory { + + protected abstract T generateInternalState(final TraverserActivePathState state); + + public final T generateState(final TraverserActivePathState state) { + final T generatedState = generateInternalState(state); + return generatedState; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractActivePathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractActivePathTraverserHandler.java new file mode 100644 index 000000000..dae9ce6a3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractActivePathTraverserHandler.java @@ -0,0 +1,25 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.List; + +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; + +public abstract class AbstractActivePathTraverserHandler extends AbstractBlockTraverserHandler { + + private final TraverserActivePathState comparatorState; + + public AbstractActivePathTraverserHandler(final TraverserActivePathState comparatorState) { + this.comparatorState = comparatorState; + } + + protected abstract List handle() throws TraverserException; + + public final List process() throws TraverserException { + return handle(); + } + + public final TraverserActivePathState getComparator() { + return comparatorState; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockPathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockPathTraverserHandler.java new file mode 100644 index 000000000..bbce55d65 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockPathTraverserHandler.java @@ -0,0 +1,38 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.concurrent.atomic.AtomicReference; + +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; + +/** + * Traverser handlers are responsible for deducing how blocks should be searched within a path + * whilst + * searching for duplicate 'finally' instructions. + */ +public abstract class AbstractBlockPathTraverserHandler extends AbstractBlockTraverserHandler { + + private final AtomicReference stateRef; + + public AbstractBlockPathTraverserHandler(final TraverserState initialState) { + this.stateRef = new AtomicReference<>(initialState); + } + + public AbstractBlockPathTraverserHandler(final AtomicReference initialStateRef) { + this.stateRef = initialStateRef; + } + + protected abstract void handle() throws TraverserException; + + public final void process() throws TraverserException { + handle(); + } + + public final TraverserState getState() { + return stateRef.get(); + } + + public final AtomicReference getStateReference() { + return stateRef; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockTraverserHandler.java new file mode 100644 index 000000000..f67776aa3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/AbstractBlockTraverserHandler.java @@ -0,0 +1,7 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +/** + * Sealed class + */ +public abstract class AbstractBlockTraverserHandler { +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/BaseBlockTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/BaseBlockTraverserHandler.java new file mode 100644 index 000000000..e0b05baae --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/BaseBlockTraverserHandler.java @@ -0,0 +1,46 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.concurrent.atomic.AtomicReference; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.dex.visitors.finaly.traverser.visitors.ImplicitInsnBlockTraverserVisitor; +import jadx.core.dex.visitors.finaly.traverser.visitors.PathEndBlockTraverserVisitor; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public class BaseBlockTraverserHandler extends AbstractBlockPathTraverserHandler { + + public BaseBlockTraverserHandler(final TraverserState initialState) { + super(initialState); + } + + public BaseBlockTraverserHandler(final AtomicReference initialStateRef) { + super(initialStateRef); + } + + @Override + protected void handle() { + final TraverserBlockInfo blockInsnInfo = getState().getBlockInsnInfo(); + if (blockInsnInfo == null) { + throw new JadxRuntimeException("Expected to find block info within " + getClass().getSimpleName()); + } + + final TraverserActivePathState comparator = getState().getComparatorState(); + final AtomicReference stateRef = comparator.getReferenceForState(getState()); + + if (stateRef == null) { + throw new JadxRuntimeException("Orphaned traverser state"); + } + + final BlockNode block = blockInsnInfo.getBlock(); + + final ImplicitInsnBlockTraverserVisitor implicitVisitor = new ImplicitInsnBlockTraverserVisitor(getState()); + final TraverserState stateAfterImplicit = implicitVisitor.visit(block); + final PathEndBlockTraverserVisitor pathEndVisitor = new PathEndBlockTraverserVisitor(stateAfterImplicit); + final TraverserState nextState = pathEndVisitor.visit(block); + + stateRef.set(nextState); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/InstructionActivePathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/InstructionActivePathTraverserHandler.java new file mode 100644 index 000000000..f1f72768f --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/InstructionActivePathTraverserHandler.java @@ -0,0 +1,48 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.dex.visitors.finaly.traverser.visitors.comparator.InstructionBlockComparatorTraverserVisitor; + +public final class InstructionActivePathTraverserHandler extends AbstractActivePathTraverserHandler { + + public static final class UnresolvableBlockException extends TraverserException { + public UnresolvableBlockException(final BlockNode block, final String reason) { + super("A block, " + block.toString() + ", could not have instructions compared.\n\t" + reason); + } + } + + public InstructionActivePathTraverserHandler(final TraverserActivePathState state) { + super(state); + } + + @Override + protected List handle() throws TraverserException { + final TraverserActivePathState comparator = getComparator(); + final TraverserGlobalCommonState commonState = comparator.getGlobalCommonState(); + + final TraverserState finallyState = comparator.getFinallyState(); + final TraverserState candidateState = comparator.getCandidateState(); + + final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); + final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); + final BlockNode finallyBlock = finallyBlockInfo.getBlock(); + final BlockNode candidateBlock = candidateBlockInfo.getBlock(); + + final InstructionBlockComparatorTraverserVisitor visitor = new InstructionBlockComparatorTraverserVisitor(); + final TraverserActivePathState newState = visitor.visit(comparator); + + if (finallyBlock != null && candidateBlock != null) { + commonState.addCachedStateFor(finallyBlock, candidateBlock, List.of(newState)); + } + + return List.of(newState); + } + +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/MergePathActivePathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/MergePathActivePathTraverserHandler.java new file mode 100644 index 000000000..b1cc8ab07 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/MergePathActivePathTraverserHandler.java @@ -0,0 +1,237 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Stack; +import java.util.function.Function; + +import jadx.core.dex.instructions.args.RegisterArg; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; +import jadx.core.dex.visitors.finaly.traverser.TraverserController; +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.state.IdentifiedScopeWithTerminatorTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.RecoveredFromCacheTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserGlobalCommonState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public final class MergePathActivePathTraverserHandler extends AbstractActivePathTraverserHandler { + + private static TraverserActivePathState createNonMatchingTerminator(final TraverserActivePathState state) { + final TraverserStateFactory finallyStateFactory = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_PATHS); + final TraverserStateFactory candidateStateFactory = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_PATHS); + + return TraverserActivePathState.produceFromFactories(state, finallyStateFactory, candidateStateFactory); + } + + private static boolean isStateOnTerminus(final TraverserState state, final BlockNode terminus) { + final TraverserBlockInfo blockInfo = state.getBlockInsnInfo(); + if (blockInfo == null) { + return false; + } + return blockInfo.getBlock() == terminus; + } + + private static Function getStateAbortOnTerminusFunction( + final IdentifiedScopeWithTerminatorTraverserState finallyState, + final IdentifiedScopeWithTerminatorTraverserState candidateState) { + final BlockNode finallyTerminus = finallyState.getTerminus(); + final BlockNode candidateTerminus = candidateState.getTerminus(); + final GlobalTraverserSourceState finallyGlobalState = finallyState.getGlobalState(); + final GlobalTraverserSourceState candidateGlobalState = candidateState.getGlobalState(); + + return (final TraverserState state) -> { + if (state.getGlobalState() == finallyGlobalState) { + return isStateOnTerminus(state, finallyTerminus); + } else if (state.getGlobalState() == candidateGlobalState) { + return isStateOnTerminus(state, candidateTerminus); + } else { + throw new JadxRuntimeException("Unknown global traverser state. Has a global state been duplicated?"); + } + }; + } + + private static PostMergeStatus getScopeSplitPostMergeStatus(final List pathsTaken) { + // If the scope split is the same, all branches must not end in a terminator. + + final PostMergeStatus status = new PostMergeStatus(); + for (final TraverserActivePathState path : pathsTaken) { + final TraverserState finallyState; + final TraverserState candidateState; + if (path.getFinallyState().isTerminal() || path.getCandidateState().isTerminal()) { + final TraverserState rawFinallyState = path.getFinallyState(); + final TraverserState rawCandidateState = path.getCandidateState(); + final boolean finallyIsCached = rawFinallyState instanceof RecoveredFromCacheTraverserState; + final boolean candidateIsCached = rawCandidateState instanceof RecoveredFromCacheTraverserState; + if (!(finallyIsCached && candidateIsCached)) { + status.perfectMatch = false; + continue; + } + + final RecoveredFromCacheTraverserState finallyCachedState = (RecoveredFromCacheTraverserState) rawFinallyState; + final RecoveredFromCacheTraverserState candidateCachedState = (RecoveredFromCacheTraverserState) rawCandidateState; + if (finallyCachedState.canContinue() || candidateCachedState.canContinue()) { + status.perfectMatch = false; + continue; + } + finallyState = finallyCachedState.getUnderlying(); + candidateState = candidateCachedState.getUnderlying(); + } else { + finallyState = path.getFinallyState(); + candidateState = path.getCandidateState(); + } + + final CentralityState finallyCentralityState = finallyState.getCentralityState(); + final CentralityState candidateCentralityState = candidateState.getCentralityState(); + status.finallyAllowsCentral &= finallyCentralityState.getAllowsCentral(); + status.candidateAllowsCentral &= candidateCentralityState.getAllowsCentral(); + status.finallyAllowableOutputs.addAll(finallyCentralityState.getAllowableOutputArguments()); + status.candidateAllowableOutputs.addAll(candidateCentralityState.getAllowableOutputArguments()); + } + + return status; + } + + private static final class PostMergeStatus { + + public final Set finallyAllowableOutputs = new HashSet<>(); + public final Set candidateAllowableOutputs = new HashSet<>(); + public boolean finallyAllowsCentral; + public boolean candidateAllowsCentral; + public boolean perfectMatch = true; + } + + public MergePathActivePathTraverserHandler(final TraverserActivePathState comparatorState) { + super(comparatorState); + } + + @Override + protected final List handle() { + final TraverserActivePathState comparator = getComparator().duplicate(); + final TraverserGlobalCommonState commonState = comparator.getGlobalCommonState(); + final IdentifiedScopeWithTerminatorTraverserState finallyState = + (IdentifiedScopeWithTerminatorTraverserState) comparator.getFinallyState(); + final IdentifiedScopeWithTerminatorTraverserState candidateState = + (IdentifiedScopeWithTerminatorTraverserState) comparator.getCandidateState(); + + final BlockNode finallyTerminus = finallyState.getTerminus(); + final BlockNode candidateTerminus = candidateState.getTerminus(); + + final Function abortFunction = getStateAbortOnTerminusFunction(finallyState, candidateState); + + final List allPermutationsPaths = getAllPermutationsOfCollection(candidateState.getRoots()); + List paths = null; + PostMergeStatus postMerge = null; + for (final BlockNode[] candidateRootsPermutation : allPermutationsPaths) { + final List traversalPaths = new ArrayList<>(); + for (int i = 0; i < finallyState.getRoots().size(); i++) { + final var finallyRoot = finallyState.getRoots().get(i); + final var candidateRoot = candidateRootsPermutation[i]; + + final var finallyCentrality = finallyState.getCentralityState().duplicate(); + final var candidateCentrality = candidateState.getCentralityState().duplicate(); + + final var finallyBlockInfo = new TraverserBlockInfo(finallyRoot); + final var candidateBlockInfo = new TraverserBlockInfo(candidateRoot); + + final var finallyStateFactory = NewBlockTraverserState.getFactory(finallyCentrality, finallyBlockInfo); + final var candidateStateFactory = NewBlockTraverserState.getFactory(candidateCentrality, candidateBlockInfo); + + final var newState = TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); + traversalPaths.add(newState); + } + + final List currentPaths = new ArrayList<>(); + boolean errorOccurred = false; + for (final TraverserActivePathState pathState : traversalPaths) { + final TraverserController branchController = new TraverserController(abortFunction); + final List out; + try { + out = branchController.process(pathState); + } catch (final TraverserException e) { + errorOccurred = true; + break; + } + currentPaths.addAll(out); + } + + if (errorOccurred) { + // If an error occurred, this path was not successful. + continue; + } + + // If the finally terminus and candidate terminus have been cached at this stage, it means that a + // path that we searched evaluated the two termini. At this point, we can ignore a non-perfect + // match if the path could continue from the point of the termini. + final boolean hasTerminusBeenEvaluatedInPaths = commonState.hasBlocksBeenCached(finallyTerminus, candidateTerminus); + final PostMergeStatus currentPostMerge = getScopeSplitPostMergeStatus(currentPaths); + if (!currentPostMerge.perfectMatch && !hasTerminusBeenEvaluatedInPaths) { + // No match + continue; + } + + paths = currentPaths; + postMerge = currentPostMerge; + break; + } + + if (paths == null || postMerge == null) { + final TraverserActivePathState nonMatchingState = createNonMatchingTerminator(comparator); + return List.of(nonMatchingState); + } + + final CentralityState newFinallyCentralityState = finallyState.getCentralityState().duplicate(); + newFinallyCentralityState.setAllowsCentral(postMerge.finallyAllowsCentral); + newFinallyCentralityState.addAllowableOutputs(postMerge.finallyAllowableOutputs); + final CentralityState newCandidateCentralityState = candidateState.getCentralityState().duplicate(); + newCandidateCentralityState.setAllowsCentral(postMerge.candidateAllowsCentral); + newCandidateCentralityState.addAllowableOutputs(postMerge.candidateAllowableOutputs); + + final TraverserBlockInfo finallyTerminusBlockInfo = new TraverserBlockInfo(finallyState.getTerminus()); + final TraverserBlockInfo candidateTerminusBlockInfo = new TraverserBlockInfo(candidateState.getTerminus()); + + final TraverserStateFactory finallyStateFactory = + NewBlockTraverserState.getFactory(newFinallyCentralityState, finallyTerminusBlockInfo); + final TraverserStateFactory candidateStateFactory = + NewBlockTraverserState.getFactory(newCandidateCentralityState, candidateTerminusBlockInfo); + + final TraverserActivePathState nextState = + TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); + nextState.mergeWith(paths); + return List.of(nextState); + } + + public static List getAllPermutationsOfCollection(final Collection elements) { + final Stack permutationStack = new Stack<>(); + final List permutations = new ArrayList<>(); + permutations(permutations, elements, permutationStack, elements.size()); + return permutations; + } + + public static void permutations(final List permutations, final Collection elements, + final Stack permutationStack, final int size) { + if (permutationStack.size() == size) { + permutations.add(permutationStack.toArray(BlockNode[]::new)); + } + + BlockNode[] availableItems = elements.toArray(BlockNode[]::new); + for (BlockNode i : availableItems) { + permutationStack.push(i); + elements.remove(i); + permutations(permutations, elements, permutationStack, size); + elements.add(permutationStack.pop()); + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorBlockPathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorBlockPathTraverserHandler.java new file mode 100644 index 000000000..77e65896e --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorBlockPathTraverserHandler.java @@ -0,0 +1,47 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.concurrent.atomic.AtomicReference; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.traverser.state.ISourceBlockState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.dex.visitors.finaly.traverser.visitors.AbstractBlockTraverserVisitor; +import jadx.core.dex.visitors.finaly.traverser.visitors.PredecessorBlockTraverserVisitor; +import jadx.core.utils.exceptions.JadxRuntimeException; + +public final class PredecessorBlockPathTraverserHandler + extends AbstractBlockPathTraverserHandler { + + private final ISourceBlockState sourceBlockState; + + public PredecessorBlockPathTraverserHandler(final T initialState) { + super(initialState); + + this.sourceBlockState = initialState; + } + + public PredecessorBlockPathTraverserHandler(final AtomicReference initialStateRef) { + super(initialStateRef); + + this.sourceBlockState = initialStateRef.get(); + } + + @Override + protected final void handle() { + final TraverserState baseState = getState(); + final TraverserActivePathState comparator = baseState.getComparatorState(); + final AtomicReference stateRef = comparator.getReferenceForState(baseState); + + if (stateRef == null) { + throw new JadxRuntimeException("Orphaned traverser state"); + } + + final BlockNode sourceBlock = sourceBlockState.getSourceBlock(); + final AbstractBlockTraverserVisitor visitor = new PredecessorBlockTraverserVisitor(baseState); + final TraverserState nextState = visitor.visit(sourceBlock); + + stateRef.set(nextState); + } + +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorMergeActivePathTraverserHandler.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorMergeActivePathTraverserHandler.java new file mode 100644 index 000000000..b1fd22d53 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/handlers/PredecessorMergeActivePathTraverserHandler.java @@ -0,0 +1,154 @@ +package jadx.core.dex.visitors.finaly.traverser.handlers; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; +import jadx.core.dex.visitors.finaly.traverser.TraverserException; +import jadx.core.dex.visitors.finaly.traverser.factory.DuplicatedTraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.state.IdentifiedScopeWithTerminatorTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.UnknownAdvanceStrategyTraverserState; +import jadx.core.utils.BlockUtils; + +public final class PredecessorMergeActivePathTraverserHandler extends AbstractActivePathTraverserHandler { + + private static List orderBlocks(final List blocks) { + final List dup = new ArrayList<>(blocks); + + // Collections.sort(dup, (blk1, blk2) -> Integer.compare(blk1.getCId(), blk2.getCId())); + + return dup; + } + + public PredecessorMergeActivePathTraverserHandler(TraverserActivePathState initialState) { + super(initialState); + } + + @Override + protected final List handle() throws TraverserException { + // At this point, we expect the handler to contain the block state of the path which is + // requesting a predecessor merge. If the other handler also requests a predecessor merge, + // we can merge the two. If not, we'll split the active handler to support the multiple + // paths. + + final TraverserActivePathState comparator = getComparator(); + final TraverserState finallyState = comparator.getFinallyState(); + final TraverserState candidateState = comparator.getCandidateState(); + + final boolean finallyNeedsDuplicate = finallyState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; + final boolean candidateNeedsDuplicate = candidateState.getCompareState() == TraverserState.ComparisonState.READY_TO_COMPARE; + final boolean shouldMerge = finallyNeedsDuplicate && candidateNeedsDuplicate; + + if (shouldMerge) { + return mergeScopes((UnknownAdvanceStrategyTraverserState) finallyState, (UnknownAdvanceStrategyTraverserState) candidateState); + } else { + final UnknownAdvanceStrategyTraverserState advancingState; + final TraverserState otherState; + if (finallyNeedsDuplicate) { + advancingState = (UnknownAdvanceStrategyTraverserState) finallyState; + otherState = candidateState; + } else { + advancingState = (UnknownAdvanceStrategyTraverserState) candidateState; + otherState = finallyState; + } + return duplicateForPaths(comparator, advancingState, otherState, finallyNeedsDuplicate); + } + } + + private List mergeScopes(final UnknownAdvanceStrategyTraverserState finallyState, + final UnknownAdvanceStrategyTraverserState candidateState) throws TraverserException { + final List finallyBlocks = finallyState.getNextBlocks(); + final List candidateBlocks = candidateState.getNextBlocks(); + + final int finallyBlocksSize = finallyBlocks.size(); + final int candidateBlocksSize = candidateBlocks.size(); + + final List states; + if (candidateBlocksSize % finallyBlocksSize == 0 && candidateBlocksSize == finallyBlocksSize) { + final List finallyBlocksOrdered = orderBlocks(finallyBlocks); + final List candidateBlocksOrdered = orderBlocks(candidateBlocks); + + final int duplicationCount = candidateBlocksSize / finallyBlocksSize; + + states = new ArrayList<>(duplicationCount); + for (int i = 0; i < duplicationCount; i++) { + final List candidateBlocksSubset = new ArrayList<>(finallyBlocksSize); + for (int j = 0; j < finallyBlocksSize; j++) { + candidateBlocksSubset.add(candidateBlocksOrdered.get(i * finallyBlocksSize + j)); + } + + final TraverserActivePathState comparatorState = getScopeForBlocks(finallyBlocksOrdered, candidateBlocksSubset); + states.add(comparatorState); + } + } else { + final TraverserStateFactory finallyStateFactory = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE); + final TraverserStateFactory candidateStateFactory = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE); + final TraverserActivePathState newState = + TraverserActivePathState.produceFromFactories(getComparator(), finallyStateFactory, candidateStateFactory); + states = List.of(newState); + } + return states; + } + + private List duplicateForPaths(final TraverserActivePathState comparator, + final UnknownAdvanceStrategyTraverserState advancingState, final TraverserState otherState, + final boolean duplicateIsFromFinally) { + final List nextPredecessors = advancingState.getNextBlocks(); + final List newPaths = new ArrayList<>(nextPredecessors.size()); + for (final BlockNode predecessor : nextPredecessors) { + final CentralityState centralityState = advancingState.getCentralityState(); + final TraverserBlockInfo duplicatePathBlockInfo = new TraverserBlockInfo(predecessor); + final TraverserStateFactory duplicatePathStateFactory = + NewBlockTraverserState.getFactory(centralityState, duplicatePathBlockInfo); + final TraverserStateFactory otherStateFactory = new DuplicatedTraverserStateFactory<>(otherState); + + final TraverserActivePathState comparatorDuplicated = comparator.duplicate(); + final TraverserActivePathState newPathState; + if (duplicateIsFromFinally) { + newPathState = + TraverserActivePathState.produceFromFactories(comparatorDuplicated, duplicatePathStateFactory, otherStateFactory); + } else { + newPathState = + TraverserActivePathState.produceFromFactories(comparatorDuplicated, otherStateFactory, duplicatePathStateFactory); + } + newPaths.add(newPathState); + } + + return newPaths; + } + + private TraverserActivePathState getScopeForBlocks(final List finallyBlocks, final List candidateBlocks) { + final TraverserActivePathState comparator = getComparator(); + final MethodNode mth = getComparator().getGlobalCommonState().getMethodNode(); + + final TraverserState finallyState = comparator.getFinallyState(); + final TraverserState candidateState = comparator.getCandidateState(); + + final GlobalTraverserSourceState finallyGlobalState = comparator.getGlobalStateFor(finallyState); + final CentralityState finallyCentralityState = finallyState.getCentralityState(); + final BlockNode finallyTerminator = + BlockUtils.getBottomCommonPredecessor(mth, finallyBlocks, finallyGlobalState.getContainedBlocks()); + final TraverserStateFactory finallyStateFactory = + IdentifiedScopeWithTerminatorTraverserState.getFactory(finallyCentralityState, finallyBlocks, finallyTerminator); + + final GlobalTraverserSourceState candidateGlobalState = comparator.getGlobalStateFor(candidateState); + final CentralityState candidateCentralityState = candidateState.getCentralityState(); + final BlockNode candidateTerminator = + BlockUtils.getBottomCommonPredecessor(mth, candidateBlocks, candidateGlobalState.getContainedBlocks()); + final TraverserStateFactory candidateStateFactory = + IdentifiedScopeWithTerminatorTraverserState.getFactory(candidateCentralityState, candidateBlocks, candidateTerminator); + + return TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/AwaitingInsnCompareTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/AwaitingInsnCompareTraverserState.java new file mode 100644 index 000000000..c5a6385e7 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/AwaitingInsnCompareTraverserState.java @@ -0,0 +1,56 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.InstructionActivePathTraverserHandler; + +public final class AwaitingInsnCompareTraverserState extends TraverserState { + + private final CentralityState centralityState; + private final @Nullable TraverserBlockInfo blockInsnInfo; + + public AwaitingInsnCompareTraverserState(final TraverserActivePathState state, final CentralityState centralityState, + final TraverserBlockInfo blockInsnInfo) { + super(state); + + this.centralityState = centralityState; + this.blockInsnInfo = blockInsnInfo; + } + + @Override + public final @Nullable AbstractBlockTraverserHandler getNextHandler() { + return new InstructionActivePathTraverserHandler(getComparatorState()); + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.READY_TO_COMPARE; + } + + @Override + public final boolean isTerminal() { + return false; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return centralityState; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return blockInsnInfo; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + final CentralityState dCentralityState = centralityState.duplicate(); + final TraverserBlockInfo dBlockInsnInfo = blockInsnInfo.duplicate(); + + final TraverserState duplicated = new AwaitingInsnCompareTraverserState(comparatorState, dCentralityState, dBlockInsnInfo); + + return duplicated; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/ISourceBlockState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/ISourceBlockState.java new file mode 100644 index 000000000..4f119baf5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/ISourceBlockState.java @@ -0,0 +1,8 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import jadx.core.dex.nodes.BlockNode; + +public interface ISourceBlockState { + + public abstract BlockNode getSourceBlock(); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/IdentifiedScopeWithTerminatorTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/IdentifiedScopeWithTerminatorTraverserState.java new file mode 100644 index 000000000..79b893414 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/IdentifiedScopeWithTerminatorTraverserState.java @@ -0,0 +1,89 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.MergePathActivePathTraverserHandler; + +public final class IdentifiedScopeWithTerminatorTraverserState extends TraverserState { + + public static TraverserStateFactory getFactory(final CentralityState centralityState, + final List roots, final BlockNode scopeTerminator) { + return new IdentifiedScopeWithTerminatorStateFactory(centralityState, roots, scopeTerminator); + } + + private static final class IdentifiedScopeWithTerminatorStateFactory + extends TraverserStateFactory { + + private final CentralityState centralityState; + private final List roots; + private final BlockNode scopeTerminator; + + public IdentifiedScopeWithTerminatorStateFactory(final CentralityState centralityState, final List roots, + final BlockNode scopeTerminator) { + this.centralityState = centralityState; + this.roots = roots; + this.scopeTerminator = scopeTerminator; + } + + @Override + public final IdentifiedScopeWithTerminatorTraverserState generateInternalState(final TraverserActivePathState state) { + return new IdentifiedScopeWithTerminatorTraverserState(state, centralityState, roots, scopeTerminator); + } + } + + private final CentralityState centralityState; + private final List roots; + private final BlockNode scopeTerminator; + + public IdentifiedScopeWithTerminatorTraverserState(final TraverserActivePathState state, final CentralityState centralityState, + final List roots, final BlockNode scopeTerminator) { + super(state); + this.roots = roots; + this.scopeTerminator = scopeTerminator; + this.centralityState = centralityState; + } + + @Override + public final @Nullable AbstractBlockTraverserHandler getNextHandler() { + return new MergePathActivePathTraverserHandler(getComparatorState()); + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.READY_TO_COMPARE; + } + + @Override + public final boolean isTerminal() { + return false; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return centralityState; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return null; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + return new IdentifiedScopeWithTerminatorTraverserState(comparatorState, centralityState, roots, scopeTerminator); + } + + public final BlockNode getTerminus() { + return scopeTerminator; + } + + public final List getRoots() { + return roots; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NewBlockTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NewBlockTraverserState.java new file mode 100644 index 000000000..43b0c03d2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NewBlockTraverserState.java @@ -0,0 +1,79 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.BaseBlockTraverserHandler; + +public final class NewBlockTraverserState extends TraverserState { + + public static final TraverserStateFactory getFactory(final CentralityState centralityState, + final TraverserBlockInfo blockInsnInfo) { + return new NewBlockStateFactory(centralityState, blockInsnInfo); + } + + private static class NewBlockStateFactory extends TraverserStateFactory { + + private final CentralityState centralityState; + private final TraverserBlockInfo blockInsnInfo; + + public NewBlockStateFactory(final CentralityState centralityState, final TraverserBlockInfo blockInsnInfo) { + this.centralityState = centralityState; + this.blockInsnInfo = blockInsnInfo; + } + + @Override + public NewBlockTraverserState generateInternalState(final TraverserActivePathState state) { + return new NewBlockTraverserState(state, centralityState, blockInsnInfo); + } + } + + private final CentralityState centralityState; + private final @Nullable TraverserBlockInfo blockInsnInfo; + + public NewBlockTraverserState(final TraverserActivePathState state, final CentralityState centralityState, + final TraverserBlockInfo blockInsnInfo) { + super(state); + this.centralityState = centralityState; + this.blockInsnInfo = blockInsnInfo; + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.NOT_READY; + } + + @Override + public final boolean isTerminal() { + return false; + } + + @Override + public @Nullable AbstractBlockPathTraverserHandler getNextHandler() { + // We have no data on this block, so we'll give it the base block handler to gain + // information about it to gather more state information regarding the block. + return new BaseBlockTraverserHandler(this); + } + + @Override + protected @Nullable CentralityState getUnderlyingCentralityState() { + return centralityState; + } + + @Override + protected @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return blockInsnInfo; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + final CentralityState dCentralityState = centralityState.duplicate(); + final TraverserBlockInfo dBlockInsnInfo = blockInsnInfo.duplicate(); + + final TraverserState duplicated = new NewBlockTraverserState(comparatorState, dCentralityState, dBlockInsnInfo); + + return duplicated; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NoBlockTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NoBlockTraverserState.java new file mode 100644 index 000000000..43dc11bec --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/NoBlockTraverserState.java @@ -0,0 +1,78 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.PredecessorBlockPathTraverserHandler; + +public final class NoBlockTraverserState extends TraverserState implements ISourceBlockState { + + public static TraverserStateFactory getFactory(final CentralityState centralityState, + final BlockNode sourceBlock) { + return new NoBlockStateFactory(centralityState, sourceBlock); + } + + private static class NoBlockStateFactory extends TraverserStateFactory { + + private final CentralityState centralityState; + private final BlockNode sourceBlock; + + public NoBlockStateFactory(final CentralityState centralityState, final BlockNode sourceBlock) { + this.centralityState = centralityState; + this.sourceBlock = sourceBlock; + } + + @Override + public NoBlockTraverserState generateInternalState(final TraverserActivePathState state) { + return new NoBlockTraverserState(state, centralityState, sourceBlock); + } + } + + private final BlockNode sourceBlock; + private final CentralityState centralityState; + + public NoBlockTraverserState(final TraverserActivePathState state, final CentralityState centralityState, final BlockNode sourceBlock) { + super(state); + this.sourceBlock = sourceBlock; + this.centralityState = centralityState; + } + + @Override + public final @Nullable AbstractBlockPathTraverserHandler getNextHandler() { + return new PredecessorBlockPathTraverserHandler<>(this); + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.NOT_READY; + } + + @Override + public final boolean isTerminal() { + return false; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return centralityState; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return null; + } + + @Override + public final BlockNode getSourceBlock() { + return sourceBlock; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + final CentralityState dCentralityState = centralityState.duplicate(); + return new NoBlockTraverserState(comparatorState, dCentralityState, sourceBlock); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/RecoveredFromCacheTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/RecoveredFromCacheTraverserState.java new file mode 100644 index 000000000..76baffba3 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/RecoveredFromCacheTraverserState.java @@ -0,0 +1,75 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; + +public final class RecoveredFromCacheTraverserState extends TraverserState { + + public static TraverserStateFactory getFactory(final TraverserState underlying) { + return new RecoveredFromCacheStateFactory(underlying); + } + + private static final class RecoveredFromCacheStateFactory extends TraverserStateFactory { + + private final TraverserState underlying; + + private RecoveredFromCacheStateFactory(final TraverserState underlying) { + this.underlying = underlying; + } + + @Override + protected final RecoveredFromCacheTraverserState generateInternalState(final TraverserActivePathState state) { + return new RecoveredFromCacheTraverserState(underlying); + } + + } + + private final TraverserState underlying; + + public RecoveredFromCacheTraverserState(final TraverserState underlying) { + super(underlying.getComparatorState()); + + this.underlying = underlying; + } + + @Override + public final @Nullable AbstractBlockTraverserHandler getNextHandler() { + return null; + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.NOT_READY; + } + + @Override + public final boolean isTerminal() { + return true; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return null; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return null; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + return new RecoveredFromCacheTraverserState(underlying); + } + + public final TraverserState getUnderlying() { + return underlying; + } + + public final boolean canContinue() { + return underlying.isTerminal(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TerminalTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TerminalTraverserState.java new file mode 100644 index 000000000..879ba40a9 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TerminalTraverserState.java @@ -0,0 +1,97 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockPathTraverserHandler; + +public final class TerminalTraverserState extends TraverserState { + + public static TraverserStateFactory getFactory(final TerminationReason terminationReason) { + return new TerminalStateFactory(terminationReason); + } + + public static enum TerminationReason { + /** + * When comparing instructions within a finally and candidate block, non-matching + * instructions were found calling for the termination of the Traverser. + */ + NON_MATCHING_INSTRUCTIONS, + + NON_MATCHING_PATHS, + + /** + * When a handler was requested to find the predecessors of a block, no predecessors within + * the scope existed. + */ + END_OF_PATH, + /** + * When a handler was requested to process a block, a cached result for that handler + * already existed. + */ + USING_CACHED_RESULTS, + + UNMERGEABLE_STATE, + + UNRESOLVABLE_STATES, + } + + private static class TerminalStateFactory extends TraverserStateFactory { + + private final TerminationReason terminationReason; + + public TerminalStateFactory(final TerminationReason terminationReason) { + this.terminationReason = terminationReason; + } + + @Override + public TerminalTraverserState generateInternalState(TraverserActivePathState state) { + return new TerminalTraverserState(state, terminationReason); + } + } + + private final TerminationReason terminationReason; + + public TerminalTraverserState(final TraverserActivePathState state, final TerminationReason terminationReason) { + super(state); + this.terminationReason = terminationReason; + } + + @Override + public final boolean isTerminal() { + return true; + } + + @Override + @Nullable + public final AbstractBlockPathTraverserHandler getNextHandler() { + return null; + } + + public final TerminationReason getTerminationReason() { + return terminationReason; + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.NOT_READY; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return null; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return null; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + final TraverserState duplicated = new TerminalTraverserState(comparatorState, terminationReason); + + return duplicated; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserActivePathState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserActivePathState.java new file mode 100644 index 000000000..730f3659d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserActivePathState.java @@ -0,0 +1,412 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.SameInstructionsStrategy; +import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.utils.Pair; +import jadx.core.utils.exceptions.JadxRuntimeException; + +/** + * A state used by the traverser controller. For two given branches, the "finally" branch and the + * "candidate" branch whilst determining similar instructions and blocks, the active path state + * contains information regarding the current matched instructions and blocks, as well as the + * current state of both the finally and candidate path being explored. + */ +public class TraverserActivePathState { + + /** + * Produces a shallow clone of the {@link TraverserActivePathState}. Since this is a shallow clone, + * it should only be used for a singular branch. If a branch is used and this needs to be + * duplicated, be sure to use the deep clone duplication on the previous + * {@link TraverserActivePathState} before invoking this method on it. + * + * @param previousTraverserState The previous active path state to create a shallow clone of. + * @param finallyStateProducer The factory responsible for producing the new finally state to be + * held by the resulting active path state. + * @param candidateStateProducer The factory responsible for producing the new candidate state to + * be held by the resulting active path state. + * @return The cloned active path state. + */ + public static TraverserActivePathState produceFromFactories(final TraverserActivePathState previousTraverserState, + final TraverserStateFactory finallyStateProducer, final TraverserStateFactory candidateStateProducer) { + final TraverserActivePathState dState = + new TraverserActivePathState(previousTraverserState.matchedInsns, previousTraverserState.finallyCompletionMonitor, + previousTraverserState.candidateCompletionMonitor, previousTraverserState.commonGlobalState, + previousTraverserState.finallyGlobalState, + previousTraverserState.candidateGlobalState); + + final TraverserState dFinallyState = finallyStateProducer.generateState(dState); + final TraverserState dCandidateState = candidateStateProducer.generateState(dState); + + dState.candidateStateRef.set(dCandidateState); + dState.finallyStateRef.set(dFinallyState); + + return dState; + } + + /** + * Tracks the comparison state of a given block. + * i.e. how many of the instructions have been compared in the Traversal. + */ + private static final class BlockCompletionMonitor { + + private final BlockNode block; + private final Set matchedIndices; + private final int insnCount; + + private BlockCompletionMonitor(final BlockNode block) { + this.block = block; + this.insnCount = block.getInstructions().size(); + this.matchedIndices = new HashSet<>(insnCount); + for (int i = 0; i < insnCount; i++) { + matchedIndices.add(i); + } + } + + private void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { + if (info.getBlock() != block) { + return; + } + + final int botPointer = info.getBottomOffset(); + for (int i = 0; i < numberMatched; i++) { + final int indexMatched = botPointer + i; + matchedIndices.remove(indexMatched); + } + + final int bottomImplicitCount = info.getBottomImplicitCount(); + final boolean noPathEndInsns = botPointer - bottomImplicitCount == 0; + if (noPathEndInsns) { + for (int i = 0; i < bottomImplicitCount; i++) { + matchedIndices.remove(i); + } + } + } + + private BlockCompletionMonitor duplicate() { + final BlockCompletionMonitor dup = new BlockCompletionMonitor(block); + dup.matchedIndices.retainAll(matchedIndices); + return dup; + } + + private void mergeWith(final BlockCompletionMonitor other) { + if (other.block != block) { + return; + } + + matchedIndices.retainAll(other.matchedIndices); + } + + private boolean isEntireBlock() { + return matchedIndices.isEmpty(); + } + } + + private static final class BlockCompletionMonitorMap implements Map { + + private final Map underlying; + + public BlockCompletionMonitorMap() { + underlying = new HashMap<>(); + } + + @Override + public final void clear() { + underlying.clear(); + } + + @Override + public final boolean containsKey(Object key) { + return underlying.containsKey(key); + } + + @Override + public final boolean containsValue(Object value) { + if (!(value instanceof BlockNode)) { + return false; + } + + final BlockNode edge = (BlockNode) value; + return underlying.containsKey(edge); + } + + @Override + public final Set> entrySet() { + return underlying.entrySet(); + } + + @Override + public final BlockCompletionMonitor get(Object key) { + return underlying.get(key); + } + + @Override + public final boolean isEmpty() { + return underlying.isEmpty(); + } + + @Override + public final Set keySet() { + return underlying.keySet(); + } + + @Override + public final BlockCompletionMonitor put(BlockNode key, BlockCompletionMonitor value) { + return underlying.put(key, value); + } + + @Override + public final void putAll(Map otherMap) { + underlying.putAll(otherMap); + } + + @Override + public final BlockCompletionMonitor remove(Object key) { + return underlying.remove(key); + } + + @Override + public final int size() { + return underlying.size(); + } + + @Override + public final Collection values() { + return underlying.values(); + } + + private void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { + final BlockNode block = info.getBlock(); + if (containsKey(block)) { + get(block).registerWithBlockInfo(info, numberMatched); + } else { + final BlockCompletionMonitor monitor = new BlockCompletionMonitor(block); + monitor.registerWithBlockInfo(info, numberMatched); + put(block, monitor); + } + } + + private void mergeEntry(final BlockCompletionMonitor other) { + final BlockNode block = other.block; + if (containsKey(block)) { + get(block).mergeWith(other); + } else { + final BlockCompletionMonitor monitor = other.duplicate(); + put(block, monitor); + } + } + + private void mergeMap(final BlockCompletionMonitorMap other) { + for (final BlockCompletionMonitor monitor : other.values()) { + mergeEntry(monitor); + } + } + + private BlockCompletionMonitorMap duplicate() { + final BlockCompletionMonitorMap dup = new BlockCompletionMonitorMap(); + for (final BlockNode sourceBlock : keySet()) { + final BlockCompletionMonitor monitor = get(sourceBlock); + dup.put(sourceBlock, monitor.duplicate()); + } + return dup; + } + } + + private final AtomicReference finallyStateRef; + private final AtomicReference candidateStateRef; + private final GlobalTraverserSourceState finallyGlobalState; + private final GlobalTraverserSourceState candidateGlobalState; + private final TraverserGlobalCommonState commonGlobalState; + + private final Set> matchedInsns; + private final BlockCompletionMonitorMap finallyCompletionMonitor; + private final BlockCompletionMonitorMap candidateCompletionMonitor; + + /** + * Creates a new instance of a traversal active path. This constructor is used to create a new + * path to be used by the traverser controller to begin a new traversal. + * + * @param mth + * @param sameInstructionsStrategy + * @param finallyBlockTerminus + * @param candidateBlockTerminus + * @param finallyBlocks + * @param candidateBlocks + */ + public TraverserActivePathState(final MethodNode mth, final SameInstructionsStrategy sameInstructionsStrategy, + final BlockNode finallyBlockTerminus, final BlockNode candidateBlockTerminus, final List finallyBlocks, + final List candidateBlocks) { + final boolean shouldFinallyAllowFirstBlockSkip = !finallyBlockTerminus.getInstructions().isEmpty(); + final boolean shouldCandidateAllowFirstBlockSkip = !candidateBlockTerminus.getInstructions().isEmpty(); + final CentralityState finallyCentralityState = new CentralityState(sameInstructionsStrategy, shouldFinallyAllowFirstBlockSkip); + final CentralityState candidateCentralityState = new CentralityState(sameInstructionsStrategy, shouldCandidateAllowFirstBlockSkip); + + final TraverserBlockInfo finallyBlockInfo = new TraverserBlockInfo(finallyBlockTerminus); + final TraverserBlockInfo candidateBlockInfo = new TraverserBlockInfo(candidateBlockTerminus); + + final TraverserState finallyState = new NewBlockTraverserState(this, finallyCentralityState, finallyBlockInfo); + final TraverserState candidateState = new NewBlockTraverserState(this, candidateCentralityState, candidateBlockInfo); + + this.finallyGlobalState = new GlobalTraverserSourceState(new HashSet<>(finallyBlocks)); + this.candidateGlobalState = new GlobalTraverserSourceState(new HashSet<>(candidateBlocks)); + this.commonGlobalState = new TraverserGlobalCommonState(mth); + + this.finallyStateRef = new AtomicReference<>(finallyState); + this.candidateStateRef = new AtomicReference<>(candidateState); + this.matchedInsns = new HashSet<>(); + this.finallyCompletionMonitor = new BlockCompletionMonitorMap(); + this.candidateCompletionMonitor = new BlockCompletionMonitorMap(); + } + + /** + * Creates a new instance of a traversal active path. This constructor is used to duplicate a + * state between a previous traverser controller and is a liaison for initialising non-null + * final fields for the {@link TraverserActivePathState#produceFromFactories} function. + * + * @param matchedInsns + * @param finallyCompletionMonitor + * @param candidateCompletionMonitor + * @param commonGlobalState + * @param finallyGlobalState + * @param candidateGlobalState + */ + private TraverserActivePathState(final Set> matchedInsns, final BlockCompletionMonitorMap finallyCompletionMonitor, + final BlockCompletionMonitorMap candidateCompletionMonitor, final TraverserGlobalCommonState commonGlobalState, + final GlobalTraverserSourceState finallyGlobalState, final GlobalTraverserSourceState candidateGlobalState) { + this.finallyStateRef = new AtomicReference<>(); + this.candidateStateRef = new AtomicReference<>(); + this.matchedInsns = matchedInsns; + this.finallyGlobalState = finallyGlobalState; + this.candidateGlobalState = candidateGlobalState; + this.commonGlobalState = commonGlobalState; + this.finallyCompletionMonitor = finallyCompletionMonitor; + this.candidateCompletionMonitor = candidateCompletionMonitor; + } + + public final TraverserActivePathState duplicate() { + final Set> dMatchedInsns = new HashSet<>(matchedInsns); + final BlockCompletionMonitorMap dFinallyCompletionMonitor = finallyCompletionMonitor.duplicate(); + final BlockCompletionMonitorMap dCandidateCompletionMonitor = candidateCompletionMonitor.duplicate(); + final TraverserActivePathState dState = + new TraverserActivePathState(dMatchedInsns, dFinallyCompletionMonitor, dCandidateCompletionMonitor, + commonGlobalState, finallyGlobalState, candidateGlobalState); + + final TraverserState dFinallyState = getFinallyState().duplicate(dState); + final TraverserState dCandidateState = getCandidateState().duplicate(dState); + + dState.candidateStateRef.set(dCandidateState); + dState.finallyStateRef.set(dFinallyState); + + return dState; + } + + public final TraverserState getFinallyState() { + return finallyStateRef.get(); + } + + public final TraverserState getCandidateState() { + return candidateStateRef.get(); + } + + public final AtomicReference getFinallyStateRef() { + return finallyStateRef; + } + + public final AtomicReference getCandidateStateRef() { + return candidateStateRef; + } + + public final Set> getMatchedInsns() { + return matchedInsns; + } + + @Nullable + public final AtomicReference getReferenceForState(final TraverserState state) { + if (finallyStateRef.get() == state) { + return finallyStateRef; + } else if (candidateStateRef.get() == state) { + return candidateStateRef; + } else { + return null; + } + } + + public final GlobalTraverserSourceState getGlobalStateFor(final TraverserState state) { + if (finallyStateRef.get() == state) { + return finallyGlobalState; + } else if (candidateStateRef.get() == state) { + return candidateGlobalState; + } else { + throw new JadxRuntimeException("Orphaned TraverserState node"); + } + } + + public final GlobalTraverserSourceState getFinallyGlobalState() { + return finallyGlobalState; + } + + public final GlobalTraverserSourceState getCandidateGlobalState() { + return candidateGlobalState; + } + + public final TraverserGlobalCommonState getGlobalCommonState() { + return commonGlobalState; + } + + public final void mergeWith(final List otherStates) { + for (final TraverserActivePathState otherState : otherStates) { + matchedInsns.addAll(otherState.getMatchedInsns()); + + finallyCompletionMonitor.mergeMap(otherState.finallyCompletionMonitor); + candidateCompletionMonitor.mergeMap(otherState.candidateCompletionMonitor); + } + } + + public final void registerWithBlockInfo(final TraverserBlockInfo info, final int numberMatched) { + final BlockNode block = info.getBlock(); + final boolean isFinallyBlock = finallyGlobalState.isBlockContained(block); + final BlockCompletionMonitorMap monitorMap; + if (isFinallyBlock) { + monitorMap = finallyCompletionMonitor; + } else { + monitorMap = candidateCompletionMonitor; + } + monitorMap.registerWithBlockInfo(info, numberMatched); + } + + public final Set getAllFullyMatchedFinallyBlocks() { + return getAllFullyMatchedBlocks(finallyCompletionMonitor); + } + + public final Set getAllFullyMatchedCandidateBlocks() { + return getAllFullyMatchedBlocks(candidateCompletionMonitor); + } + + private Set getAllFullyMatchedBlocks(final BlockCompletionMonitorMap monitorMap) { + final Set matches = new HashSet<>(); + + for (final BlockCompletionMonitor monitor : monitorMap.values()) { + if (!monitor.isEntireBlock()) { + continue; + } + + matches.add(monitor.block); + } + + return matches; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserBlockInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserBlockInfo.java new file mode 100644 index 000000000..db0fd75d2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserBlockInfo.java @@ -0,0 +1,92 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; + +public final class TraverserBlockInfo { + + private final BlockNode block; + + // These offsets are the instruction indices NOT an instruction size. + private int bottomOffset; + private int topOffset; + private int bottomImplicitCount; + + public TraverserBlockInfo(final BlockNode block) { + this(block, 0, 0, 0); + } + + public TraverserBlockInfo(final BlockNode block, final int bottomOffset, final int topOffset, final int bottomImplicitCount) { + this.bottomOffset = bottomOffset; + this.topOffset = topOffset; + this.block = block; + this.bottomImplicitCount = bottomImplicitCount; + } + + @Override + public final String toString() { + return toString(""); + } + + public final String toString(final String indent) { + final StringBuilder sb = new StringBuilder("BlockInsnInfo - "); + + sb.append(block.toString()); + sb.append(" [↑ "); + sb.append(bottomOffset); + sb.append("] [↓ "); + sb.append(topOffset); + sb.append("] "); + + return sb.toString(); + } + + public final TraverserBlockInfo duplicate() { + return new TraverserBlockInfo(block, bottomOffset, topOffset, bottomImplicitCount); + } + + public final BlockNode getBlock() { + return block; + } + + public final int getTopOffset() { + return topOffset; + } + + public final void setTopOffset(final int topOffset) { + this.topOffset = topOffset; + } + + public final int getBottomOffset() { + return bottomOffset; + } + + public final void setBottomOffset(final int bottomOffset) { + this.bottomOffset = bottomOffset; + } + + public final int getBottomImplicitCount() { + return bottomImplicitCount; + } + + public final void setBottomImplicitOffset(final int bottomImplicitCount) { + this.bottomImplicitCount = bottomImplicitCount; + } + + public final List getInsnsSlice() { + final List insns = block.getInstructions(); + + final int totalSkippedCount = bottomOffset + topOffset; + if (totalSkippedCount > insns.size()) { + throw new IndexOutOfBoundsException("Attempted to get instructions slice of block " + block.toString() + " with " + + totalSkippedCount + " skipped instructions whilst only having " + insns.size() + " instructions in block."); + } + + final int startIndex = topOffset; + final int endIndex = insns.size() - bottomOffset; + + return insns.subList(startIndex, endIndex); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserGlobalCommonState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserGlobalCommonState.java new file mode 100644 index 000000000..803ea4c86 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserGlobalCommonState.java @@ -0,0 +1,43 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.Pair; + +public final class TraverserGlobalCommonState { + + private final MethodNode mth; + private final Map, List> searchedStates; + + public TraverserGlobalCommonState(final MethodNode mth) { + this.mth = mth; + this.searchedStates = new HashMap<>(); + } + + public final void addCachedStateFor(final BlockNode finallyBlock, final BlockNode candidateBlock, + final List state) { + final Pair blocks = new Pair<>(finallyBlock, candidateBlock); + searchedStates.put(blocks, state); + } + + @Nullable + public final List getCachedStateFor(final BlockNode finallyBlock, final BlockNode candidateBlock) { + final Pair blocks = new Pair<>(finallyBlock, candidateBlock); + return searchedStates.get(blocks); + } + + public final boolean hasBlocksBeenCached(final BlockNode finallyBlock, final BlockNode candidateBlock) { + final Pair blocks = new Pair<>(finallyBlock, candidateBlock); + return searchedStates.containsKey(blocks); + } + + public final MethodNode getMethodNode() { + return mth; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserState.java new file mode 100644 index 000000000..d8c3cc380 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/TraverserState.java @@ -0,0 +1,114 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractBlockTraverserHandler; + +public abstract class TraverserState { + + public static enum ComparisonState { + NOT_READY, + AWAITING_OPTIONAL_PREDECESSOR_MERGE, + READY_TO_COMPARE + } + + private final TraverserActivePathState comparatorState; + + public TraverserState(final TraverserActivePathState comparatorState) { + this.comparatorState = comparatorState; + } + + @Nullable + public abstract AbstractBlockTraverserHandler getNextHandler(); + + public abstract ComparisonState getCompareState(); + + public abstract boolean isTerminal(); + + protected abstract @Nullable CentralityState getUnderlyingCentralityState(); + + protected abstract @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo(); + + /** + * Performs a deep clone of this Traverser state. + * + * @return The deep cloned duplication of this Traverser state. + */ + protected abstract TraverserState duplicateInternalState(final TraverserActivePathState comparatorState); + + @Override + public final String toString() { + return toString(0); + } + + public final TraverserState duplicate(final TraverserActivePathState comparatorState) { + final TraverserState duplicatedState = duplicateInternalState(comparatorState); + return duplicatedState; + } + + public final TraverserActivePathState getComparatorState() { + return comparatorState; + } + + public final String toString(final int indentAmount) { + final String baseIndent = " ".repeat(indentAmount); + final String secondIndent = " ".repeat(indentAmount + 2); + + final StringBuilder sb = new StringBuilder(baseIndent); + sb.append(getClass().getSimpleName()); + sb.append(' '); + + if (isTerminal()) { + sb.append("TERMINAL "); + } + + sb.append(" {"); + sb.append(System.lineSeparator()); + + sb.append(secondIndent); + sb.append("centrality: "); + final CentralityState centralityState = getUnderlyingCentralityState(); + if (centralityState == null) { + sb.append("none"); + } else { + sb.append(getCentralityState()); + } + sb.append(System.lineSeparator()); + + sb.append(secondIndent); + sb.append(getCompareState()); + sb.append(System.lineSeparator()); + + sb.append(secondIndent); + final TraverserBlockInfo blockInsnInfo = getBlockInsnInfo(); + if (blockInsnInfo != null) { + sb.append(blockInsnInfo.toString(secondIndent)); + } else { + sb.append("NO ACTIVE BLOCK"); + } + sb.append(System.lineSeparator()); + + sb.append(baseIndent); + sb.append("}"); + + return sb.toString(); + } + + public final CentralityState getCentralityState() { + final CentralityState underlying = getUnderlyingCentralityState(); + if (underlying == null) { + throw new UnsupportedOperationException("Centrality state is not supported for " + getClass().getName()); + } + return underlying; + } + + public final @Nullable TraverserBlockInfo getBlockInsnInfo() { + return getUnderlyingBlockInsnInfo(); + } + + public final GlobalTraverserSourceState getGlobalState() { + return getComparatorState().getGlobalStateFor(this); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/UnknownAdvanceStrategyTraverserState.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/UnknownAdvanceStrategyTraverserState.java new file mode 100644 index 000000000..0f091ffdb --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/state/UnknownAdvanceStrategyTraverserState.java @@ -0,0 +1,64 @@ +package jadx.core.dex.visitors.finaly.traverser.state; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.handlers.AbstractActivePathTraverserHandler; +import jadx.core.dex.visitors.finaly.traverser.handlers.PredecessorMergeActivePathTraverserHandler; + +public final class UnknownAdvanceStrategyTraverserState extends TraverserState { + + private final CentralityState centralityState; + private final List nextBlocks; + + public UnknownAdvanceStrategyTraverserState(final TraverserActivePathState state, final CentralityState centralityState, + final List nextBlocks) { + super(state); + + this.centralityState = centralityState; + this.nextBlocks = nextBlocks; + } + + @Override + public final @Nullable AbstractActivePathTraverserHandler getNextHandler() { + return new PredecessorMergeActivePathTraverserHandler(getComparatorState()); + } + + @Override + public final ComparisonState getCompareState() { + return ComparisonState.READY_TO_COMPARE; + } + + @Override + public final boolean isTerminal() { + return false; + } + + @Override + protected final @Nullable CentralityState getUnderlyingCentralityState() { + return centralityState; + } + + @Override + protected final @Nullable TraverserBlockInfo getUnderlyingBlockInsnInfo() { + return null; + } + + @Override + protected final TraverserState duplicateInternalState(final TraverserActivePathState comparatorState) { + final CentralityState dCentralityState = centralityState.duplicate(); + final List dNextBlocks = new ArrayList<>(nextBlocks); + + final TraverserState duplicated = new UnknownAdvanceStrategyTraverserState(comparatorState, dCentralityState, dNextBlocks); + + return duplicated; + } + + public final List getNextBlocks() { + return nextBlocks; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/AbstractBlockTraverserVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/AbstractBlockTraverserVisitor.java new file mode 100644 index 000000000..88aec8f1a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/AbstractBlockTraverserVisitor.java @@ -0,0 +1,24 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; + +public abstract class AbstractBlockTraverserVisitor { + + private final TraverserState state; + + public AbstractBlockTraverserVisitor(TraverserState state) { + this.state = state; + } + + public abstract TraverserState visit(BlockNode block); + + public TraverserState getState() { + return state; + } + + public TraverserActivePathState getComparator() { + return state.getComparatorState(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/ImplicitInsnBlockTraverserVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/ImplicitInsnBlockTraverserVisitor.java new file mode 100644 index 000000000..f2c0ce1da --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/ImplicitInsnBlockTraverserVisitor.java @@ -0,0 +1,56 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors; + +import java.util.List; +import java.util.ListIterator; + +import jadx.core.dex.instructions.InsnType; +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; + +public final class ImplicitInsnBlockTraverserVisitor extends AbstractBlockTraverserVisitor { + + public static boolean isInstructionImplicit(final InsnNode node) { + // An instruction is implicit if it can be safely skipped for comparison when traversing in reverse + // order. + // The presence of a GOTO should be reflected in the structure of a block graph. + // i.e. a GOTO should be the last instruction of a block with a single successor. + // Another example might be NOP. + final InsnType type = node.getType(); + switch (type) { + case GOTO: + return true; + default: + return false; + } + } + + public ImplicitInsnBlockTraverserVisitor(final TraverserState state) { + super(state); + } + + @Override + public final TraverserState visit(final BlockNode block) { + final TraverserBlockInfo insnInfo = getState().getBlockInsnInfo(); + + final List insns = insnInfo.getInsnsSlice(); + final ListIterator insnsIterator = insns.listIterator(insns.size()); + + /** + * The number of instructions that have been identified as "implicit" instructions. + */ + int bottomDelta = 0; + while (insnsIterator.hasPrevious()) { + final InsnNode insn = insnsIterator.previous(); + if (!isInstructionImplicit(insn)) { + break; + } + bottomDelta++; + } + + insnInfo.setBottomOffset(insnInfo.getBottomOffset() + bottomDelta); + insnInfo.setBottomImplicitOffset(insnInfo.getBottomImplicitCount() + bottomDelta); + return getState(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PathEndBlockTraverserVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PathEndBlockTraverserVisitor.java new file mode 100644 index 000000000..0ce67ac97 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PathEndBlockTraverserVisitor.java @@ -0,0 +1,102 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors; + +import java.util.List; +import java.util.ListIterator; + +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.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.state.AwaitingInsnCompareTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.NoBlockTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; + +public final class PathEndBlockTraverserVisitor extends AbstractBlockTraverserVisitor { + + public static boolean isInstructionPathEnd(final InsnNode insn) { + final InsnType type = insn.getType(); + + switch (type) { + case RETURN: + case THROW: + return true; + default: + return false; + } + } + + public PathEndBlockTraverserVisitor(final TraverserState state) { + super(state); + } + + @Override + public final TraverserState visit(final BlockNode block) { + final CentralityState centralityState = getState().getCentralityState(); + + final TraverserBlockInfo insnInfo = getState().getBlockInsnInfo(); + + if (!centralityState.getAllowsCentral()) { + return new AwaitingInsnCompareTraverserState(getComparator(), centralityState, insnInfo); + } + + final List insns = insnInfo.getInsnsSlice(); + final ListIterator insnsIterator = insns.listIterator(insns.size()); + + /** + * The number of instructions that have been identified as "path end" instructions. + */ + int bottomDelta = 0; + while (insnsIterator.hasPrevious()) { + final InsnNode insn = insnsIterator.previous(); + + // Check if we should ignore the instruction due to it being a "path end" instruction. + if (isInstructionPathEnd(insn)) { + // This instruction is a path end instruction - this instruction causes the handler to exit. Here, + // we will check the argument + // of the path end instruction. If the instruction is a THROW or RETURN, this will indicate the + // argument, so long as it exists + // and is a register argument, which is operated upon before exiting this scope. Thus, we will mark + // this argument as an allowable + // path end instruction so long as an instruction returns this argument. + // + // Example: + // CONST_STR r2 = "return this string" <-- A path end instruction since it sets an arg which is used + // by path end insn + // RETURN r2 <-- A path end instruction + + if (insn.getArgsCount() != 0) { + final InsnArg handlerExitArg = insn.getArg(0); + // Returned values from instructions can only be register args so we check that the input to the + // path end insn is a register arg + if (handlerExitArg instanceof RegisterArg) { + centralityState.addAllowableOutput((RegisterArg) handlerExitArg); + } + } + + bottomDelta++; + // If this instruction is not a path end instruction, check if it sets or invokes a value which is + // used by a path end instruction. + } else if (centralityState.hasAllowableOutput(insn)) { + bottomDelta++; + centralityState.addAllowableOutputs(insn); + } else { + break; + } + } + + insnInfo.setBottomOffset(insnInfo.getBottomOffset() + bottomDelta); + + final BlockNode sourceBlock = insnInfo.getBlock(); + final boolean noInstructionsLeft = insnInfo.getBottomOffset() >= sourceBlock.getInstructions().size(); + if (noInstructionsLeft) { + // Mark the state to request finding predecessors to search for duplicate instructions for + return new NoBlockTraverserState(getComparator(), centralityState, sourceBlock); + } else { + // Mark the current state to await comparing of instructions + return new AwaitingInsnCompareTraverserState(getComparator(), centralityState, insnInfo); + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PredecessorBlockTraverserVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PredecessorBlockTraverserVisitor.java new file mode 100644 index 000000000..7a6161bd5 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/PredecessorBlockTraverserVisitor.java @@ -0,0 +1,44 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors; + +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.traverser.GlobalTraverserSourceState; +import jadx.core.dex.visitors.finaly.traverser.state.NewBlockTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.UnknownAdvanceStrategyTraverserState; +import jadx.core.utils.ListUtils; + +public final class PredecessorBlockTraverserVisitor extends AbstractBlockTraverserVisitor { + + public PredecessorBlockTraverserVisitor(final TraverserState state) { + super(state); + } + + @Override + public final TraverserState visit(final BlockNode block) { + + final TraverserState currentState = getState(); + final CentralityState centralityState = currentState.getCentralityState(); + final GlobalTraverserSourceState globalState = currentState.getGlobalState(); + + final List predecessors = block.getPredecessors(); + final List containedPredecessors = ListUtils.filter(predecessors, globalState::isBlockContained); + final int predecessorsCount = containedPredecessors.size(); + + switch (predecessorsCount) { + case 0: + return new TerminalTraverserState(getComparator(), TerminalTraverserState.TerminationReason.END_OF_PATH); + case 1: + final BlockNode nextBlock = containedPredecessors.get(0); + final TraverserBlockInfo blockInfo = new TraverserBlockInfo(nextBlock); + return new NewBlockTraverserState(getComparator(), centralityState, blockInfo); + default: + return new UnknownAdvanceStrategyTraverserState(getComparator(), centralityState, containedPredecessors); + } + } + +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/AbstractTraverserComparatorVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/AbstractTraverserComparatorVisitor.java new file mode 100644 index 000000000..b3b08293d --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/AbstractTraverserComparatorVisitor.java @@ -0,0 +1,8 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors.comparator; + +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; + +public abstract class AbstractTraverserComparatorVisitor { + + public abstract TraverserActivePathState visit(final TraverserActivePathState state); +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/InstructionBlockComparatorTraverserVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/InstructionBlockComparatorTraverserVisitor.java new file mode 100644 index 000000000..1c4feaa53 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/finaly/traverser/visitors/comparator/InstructionBlockComparatorTraverserVisitor.java @@ -0,0 +1,204 @@ +package jadx.core.dex.visitors.finaly.traverser.visitors.comparator; + +import java.util.ArrayList; +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.InsnNode; +import jadx.core.dex.visitors.finaly.CentralityState; +import jadx.core.dex.visitors.finaly.SameInstructionsStrategy; +import jadx.core.dex.visitors.finaly.SameInstructionsStrategyImpl; +import jadx.core.dex.visitors.finaly.traverser.factory.DuplicatedTraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.factory.TraverserStateFactory; +import jadx.core.dex.visitors.finaly.traverser.state.NoBlockTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TerminalTraverserState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserActivePathState; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserBlockInfo; +import jadx.core.dex.visitors.finaly.traverser.state.TraverserState; +import jadx.core.utils.Pair; + +public final class InstructionBlockComparatorTraverserVisitor extends AbstractTraverserComparatorVisitor { + + private static TraverserActivePathState createStateForPerfectMatch(final TraverserActivePathState previousState, + final BlockNode finallyBlock, + final BlockNode candidateBlock) { + final CentralityState finallyCentralityState = previousState.getFinallyState().getCentralityState().duplicate(); + final CentralityState candidateCentralityState = previousState.getCandidateState().getCentralityState().duplicate(); + + finallyCentralityState.setAllowsCentral(false); + candidateCentralityState.setAllowsCentral(false); + finallyCentralityState.setAllowsNonStartingNode(false); + candidateCentralityState.setAllowsNonStartingNode(false); + + final TraverserStateFactory finallyStateProducer = + NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); + final TraverserStateFactory candidateStateProducer = + NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); + + return TraverserActivePathState.produceFromFactories(previousState, finallyStateProducer, candidateStateProducer); + } + + private static TraverserActivePathState createStateForUnevenMatch(final TraverserActivePathState previousState, + final TraverserState finallyState, + final TraverserState candidateState, final BlockNode finallyBlock, final BlockNode candidateBlock, final int finallyInsnsSize, + final int candidateInsnsSize) { + final int maxIterateCount = Math.max(finallyInsnsSize, candidateInsnsSize); + final boolean finallyOverruns = finallyInsnsSize > candidateInsnsSize; + + final int insnsDelta; + final TraverserStateFactory newFinallyStateProducer; + final TraverserStateFactory newCandidateStateProducer; + final TraverserBlockInfo adjustedBlockInfo; + if (finallyOverruns) { + // More finally instructions than candidate instructions + final CentralityState candidateCentralityState = candidateState.getCentralityState().duplicate(); + candidateCentralityState.setAllowsCentral(false); + candidateCentralityState.setAllowsNonStartingNode(false); + final CentralityState finallyCentralityState = finallyState.getCentralityState(); + finallyCentralityState.setAllowsCentral(false); + finallyCentralityState.setAllowsNonStartingNode(false); + + insnsDelta = finallyInsnsSize - maxIterateCount; + newFinallyStateProducer = new DuplicatedTraverserStateFactory<>(finallyState); + adjustedBlockInfo = finallyState.getBlockInsnInfo(); + newCandidateStateProducer = NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); + } else { + // More candidate instructions than finally instructions + final CentralityState finallyCentralityState = finallyState.getCentralityState().duplicate(); + finallyCentralityState.setAllowsCentral(false); + finallyCentralityState.setAllowsNonStartingNode(false); + final CentralityState candidateCentralityState = candidateState.getCentralityState(); + candidateCentralityState.setAllowsCentral(false); + candidateCentralityState.setAllowsNonStartingNode(false); + + insnsDelta = candidateInsnsSize - maxIterateCount; + candidateState.getCentralityState().setAllowsCentral(false); + newCandidateStateProducer = new DuplicatedTraverserStateFactory<>(candidateState); + adjustedBlockInfo = candidateState.getBlockInsnInfo(); + newFinallyStateProducer = NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); + } + adjustedBlockInfo.setBottomOffset(adjustedBlockInfo.getBottomOffset() + insnsDelta); + + return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); + } + + private static TraverserActivePathState createStateForBlockSkip(final TraverserActivePathState previousState, + final TraverserState finallyState, + final TraverserState candidateState, final BlockNode finallyBlock, final BlockNode candidateBlock) { + final CentralityState finallyCentralityState = finallyState.getCentralityState(); + final CentralityState candidateCentralityState = candidateState.getCentralityState(); + + // TODO: Maybe replace this with controller logic so that we can determine if we need to use these + // as path ends and then merge above path? + + // Fix up finally path first. If this continues to fail, check if candidate can be fixed up in a + // later iteration. + if (finallyCentralityState.getAllowsNonStartingNode()) { + finallyCentralityState.setAllowsNonStartingNode(false); + final TraverserStateFactory newFinallyStateProducer = + NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock); + final TraverserStateFactory newCandidateStateProducer = new DuplicatedTraverserStateFactory<>(candidateState); + return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); + } else { + candidateCentralityState.setAllowsNonStartingNode(false); + final TraverserStateFactory newCandidateStateProducer = + NoBlockTraverserState.getFactory(candidateCentralityState, candidateBlock); + final TraverserStateFactory newFinallyStateProducer = new DuplicatedTraverserStateFactory<>(finallyState); + return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer); + } + } + + private static TraverserActivePathState createStateForTerminatorState(final TraverserActivePathState previousState) { + final TraverserStateFactory finallyStateProducer = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_INSTRUCTIONS); + final TraverserStateFactory candidateStateProducer = + TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_INSTRUCTIONS); + + return TraverserActivePathState.produceFromFactories(previousState, finallyStateProducer, candidateStateProducer); + } + + private final SameInstructionsStrategy sameInstructionsStrategy = new SameInstructionsStrategyImpl(); + + @Override + public final TraverserActivePathState visit(final TraverserActivePathState state) { + final TraverserState finallyState = state.getFinallyState(); + final TraverserState candidateState = state.getCandidateState(); + + final TraverserBlockInfo finallyBlockInfo = finallyState.getBlockInsnInfo(); + final TraverserBlockInfo candidateBlockInfo = candidateState.getBlockInsnInfo(); + + if (finallyBlockInfo == null || candidateBlockInfo == null) { + throw new UnsupportedOperationException( + "The instruction comparator handler has received a state which does not support block insn info"); + } + + final BlockNode finallyBlock = finallyBlockInfo.getBlock(); + final BlockNode candidateBlock = candidateBlockInfo.getBlock(); + + final List finallyInsns = finallyBlockInfo.getInsnsSlice(); + final List candidateInsns = candidateBlockInfo.getInsnsSlice(); + final int finallyInsnsSize = finallyInsns.size(); + final int candidateInsnsSize = candidateInsns.size(); + + final int maxIterateCount = Math.min(finallyInsnsSize, candidateInsnsSize); + + final List> matchingInsns = new ArrayList<>(maxIterateCount); + + // Search through each instruction in reverse and see how many match + for (int i = 0; i < maxIterateCount; i++) { + final InsnNode candidateInsn = candidateInsns.get(candidateInsnsSize - i - 1); + final InsnNode finallyInsn = finallyInsns.get(finallyInsnsSize - i - 1); + + if (!sameInstructionsStrategy.sameInsns(candidateInsn, finallyInsn)) { + break; + } + + final Pair match = new Pair<>(finallyInsn, candidateInsn); + matchingInsns.add(match); + } + + final int matchedInsnsCount = matchingInsns.size(); + + state.registerWithBlockInfo(finallyBlockInfo, matchedInsnsCount); + state.registerWithBlockInfo(candidateBlockInfo, matchedInsnsCount); + + final boolean finallyOverruns = finallyInsnsSize > candidateInsnsSize; + final boolean candidateOverruns = finallyInsnsSize < candidateInsnsSize; + final boolean sameSizedSlices = !finallyOverruns && !candidateOverruns; + final boolean allMatched = matchedInsnsCount == maxIterateCount; + final boolean noneMatched = matchedInsnsCount == 0; + + state.getMatchedInsns().addAll(matchingInsns); + + final TraverserActivePathState newState; + if (allMatched) { + if (sameSizedSlices) { + // All instructions matched and there are no more instructions to match in either + // block. Continue to the next set of blocks. + newState = createStateForPerfectMatch(state, finallyBlock, candidateBlock); + } else { + // All instructions matched, however one block contained more instructions than the + // other. Continue to next set of blocks for the handler whose instructions list was + // fully searched. + newState = createStateForUnevenMatch(state, finallyState, candidateState, finallyBlock, candidateBlock, finallyInsnsSize, + candidateInsnsSize); + } + } else if (noneMatched && eitherStateAllowsBlockSkip(finallyState, candidateState)) { + newState = createStateForBlockSkip(state, finallyState, candidateState, finallyBlock, candidateBlock); + } else { + // If any didn't match, this means that the first instructions of the block don't + // match. This therefore means that no future blocks should be marked as duplicate + // instructions and thus we should return a terminator state to stop the search. + newState = createStateForTerminatorState(state); + } + + return newState; + } + + private boolean eitherStateAllowsBlockSkip(final TraverserState finallyState, final TraverserState candidateState) { + final CentralityState finallyCentralityState = finallyState.getCentralityState(); + final CentralityState candidateCentralityState = candidateState.getCentralityState(); + + return finallyCentralityState.getAllowsNonStartingNode() || candidateCentralityState.getAllowsNonStartingNode(); + } +} 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 a4b5ed550..b1a7efd89 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 @@ -76,7 +76,8 @@ public class CheckRegions extends AbstractVisitor { if (region instanceof LoopRegion) { // check loop conditions BlockNode loopHeader = ((LoopRegion) region).getHeader(); - if (loopHeader != null && loopHeader.getInstructions().size() != 1) { + if (loopHeader != null && !loopHeader.contains(AFlag.ALLOW_MULTIPLE_INSNS_LOOP_COND) + && loopHeader.getInstructions().size() != 1) { mth.addWarn("Incorrect condition in loop: " + loopHeader); } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DebugRegionCounter.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DebugRegionCounter.java new file mode 100644 index 000000000..b77f1a41a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/DebugRegionCounter.java @@ -0,0 +1,73 @@ +package jadx.core.dex.visitors.regions; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.IBlock; +import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.visitors.AbstractVisitor; + +public class DebugRegionCounter extends AbstractVisitor { + + @Override + public void visit(MethodNode mth) { + RegionCounterVisitor visitor = new RegionCounterVisitor(); + DepthRegionTraversal.traverse(mth, visitor); + List sortedBlocks = visitor.getSortedEntries(); + for (BlockDepthEntry x : sortedBlocks) { + System.out.println(x.depth + " : " + x.block.toString() + " // " + x.block.getInstructions().toString()); + } + + System.out.println("nregions :: " + visitor.getNRegions()); + } + + private static class RegionCounterVisitor extends AbstractRegionVisitor { + private int depth = 0; + private int nregions = 0; + private List blockDepths = new ArrayList<>(); + + @Override + public boolean enterRegion(MethodNode mth, IRegion region) { + depth += 1; + nregions += 1; + return true; + } + + @Override + public void processBlock(MethodNode mth, IBlock container) { + if (container instanceof BlockNode) { + BlockNode b = (BlockNode) container; + blockDepths.add(new BlockDepthEntry(depth, b)); + } + } + + @Override + public void leaveRegion(MethodNode mth, IRegion region) { + depth -= 1; + } + + public List getSortedEntries() { + blockDepths.sort(Comparator.comparingInt(x -> x.depth)); + return blockDepths; + } + + public int getNRegions() { + return nregions; + } + + } + + private static class BlockDepthEntry { + public int depth; + public BlockNode block; + + public BlockDepthEntry(int depth, BlockNode block) { + this.depth = depth; + this.block = block; + } + } + +} 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 154d94259..10df6e2c9 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 @@ -80,10 +80,14 @@ public class ProcessTryCatchRegions extends AbstractRegionVisitor { Region tryRegion = new Region(replaceRegion); List subBlocks = replaceRegion.getSubBlocks(); + // traverse the enclosing region for blocks that have a path from the dominator but don't have a + // path from any of the exception handlers i.e. they are not before the end of the try block so + // should be inside the try block. for (IContainer cont : subBlocks) { if (RegionUtils.hasPathThroughBlock(dominator, cont)) { if (isHandlerPath(tb, cont)) { - break; + // this block/region has a path from an exception handler so is after the end of the try block + continue; } tryRegion.getSubBlocks().add(cont); } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/IfRegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/IfRegionMaker.java index b682084df..f84115ecb 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/IfRegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/IfRegionMaker.java @@ -1,6 +1,7 @@ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -21,6 +22,7 @@ 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.IRegion; +import jadx.core.dex.nodes.InsnContainer; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; @@ -33,9 +35,15 @@ import jadx.core.utils.BlockUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.utils.BlockUtils.bitSetToBlocks; +import static jadx.core.utils.BlockUtils.bitSetToOneBlock; +import static jadx.core.utils.BlockUtils.followEmptyPath; +import static jadx.core.utils.BlockUtils.getBottomBlock; +import static jadx.core.utils.BlockUtils.getPathCross; import static jadx.core.utils.BlockUtils.isEqualPaths; import static jadx.core.utils.BlockUtils.isEqualReturnBlocks; import static jadx.core.utils.BlockUtils.isPathExists; +import static jadx.core.utils.BlockUtils.newBlocksBitSet; final class IfRegionMaker { private static final Logger LOG = LoggerFactory.getLogger(IfRegionMaker.class); @@ -48,6 +56,7 @@ final class IfRegionMaker { } BlockNode process(IRegion currentRegion, BlockNode block, IfNode ifnode, RegionStack stack) { + if (block.contains(AFlag.ADDED_TO_REGION)) { // block already included in other 'if' region return ifnode.getThenBlock(); @@ -62,7 +71,14 @@ final class IfRegionMaker { currentIf = mergedIf; } else { // invert simple condition (compiler often do it) - currentIf = IfInfo.invert(currentIf); + // ensure that we only ever invert once, because if multiple regions contain this block + // we'll change the block after it's already been included in a region, which can cause + // other regions containing the block to believe the condition has been flipped when it + // has not, or vice versa. + if (!block.contains(AFlag.DONT_INVERT)) { + currentIf = IfInfo.invert(currentIf); + block.add(AFlag.DONT_INVERT); + } } IfInfo modifiedIf = restructureIf(mth, block, currentIf); if (modifiedIf != null) { @@ -103,17 +119,24 @@ final class IfRegionMaker { } // insert edge insns in new 'else' branch - // TODO: make more common algorithm if (ifRegion.getElseRegion() == null && outBlock != null) { List edgeInsnAttrs = outBlock.getAll(AType.EDGE_INSN); if (!edgeInsnAttrs.isEmpty()) { - Region elseRegion = new Region(ifRegion); + List instructions = new ArrayList<>(); for (EdgeInsnAttr edgeInsnAttr : edgeInsnAttrs) { if (edgeInsnAttr.getEnd().equals(outBlock)) { - addEdgeInsn(currentIf, elseRegion, edgeInsnAttr); + if (currentIf.getMergedBlocks().contains(followEmptyPath(edgeInsnAttr.getStart(), true))) { + instructions.add(edgeInsnAttr.getInsn()); + } } } - ifRegion.setElseRegion(elseRegion); + + if (!instructions.isEmpty()) { + Region elseRegion = new Region(ifRegion); + InsnContainer newBlock = new InsnContainer(instructions); + elseRegion.add(newBlock); + ifRegion.setElseRegion(elseRegion); + } } } @@ -129,21 +152,6 @@ final class IfRegionMaker { return condInfo; } - private void addEdgeInsn(IfInfo ifInfo, Region region, EdgeInsnAttr edgeInsnAttr) { - BlockNode start = edgeInsnAttr.getStart(); - boolean fromThisIf = false; - for (BlockNode ifBlock : ifInfo.getMergedBlocks()) { - if (ifBlock.getSuccessors().contains(start)) { - fromThisIf = true; - break; - } - } - if (!fromThisIf) { - return; - } - region.add(start); - } - @Nullable static IfInfo makeIfInfo(MethodNode mth, BlockNode ifBlock) { InsnNode lastInsn = BlockUtils.getLastInsn(ifBlock); @@ -181,7 +189,8 @@ final class IfRegionMaker { return info; } // init outblock, which will be used in isBadBranchBlock to compare with branch block - info.setOutBlock(BlockUtils.getPathCross(mth, thenBlock, elseBlock)); + info.setOutBlock(findOutBlock(mth, thenBlock, elseBlock)); + boolean badThen = isBadBranchBlock(info, thenBlock); boolean badElse = isBadBranchBlock(info, elseBlock); if (badThen && badElse) { @@ -219,6 +228,100 @@ final class IfRegionMaker { return info; } + static BlockNode findOutBlock(MethodNode mth, BlockNode thenBlock, BlockNode elseBlock) { + if (thenBlock == elseBlock) { + return thenBlock; + } + if (thenBlock == null || elseBlock == null) { + return null; + } + + BitSet thenDomFrontier = newBlocksBitSet(mth); + thenDomFrontier.or(thenBlock.getDomFrontier()); + thenDomFrontier.set(thenBlock.getPos()); + + BitSet elseDomFrontier = newBlocksBitSet(mth); + elseDomFrontier.or(elseBlock.getDomFrontier()); + elseDomFrontier.set(elseBlock.getPos()); + + BitSet intersection = newBlocksBitSet(mth); + intersection.or(thenDomFrontier); + intersection.and(elseDomFrontier); + + intersection.clear(mth.getExitBlock().getPos()); + BlockNode oneBlock = bitSetToOneBlock(mth, intersection); + + // Attempt one: there's a unique block in the intersection of dom frontiers, and no path from + // then->else or else->then + if (oneBlock != null) { + return oneBlock; + } + + BitSet union = newBlocksBitSet(mth); + union.or(thenBlock.getDomFrontier()); + union.or(elseBlock.getDomFrontier()); + union.clear(mth.getExitBlock().getPos()); + + // Attempt two: look for a suitable block in the union. + BitSet candidates = newBlocksBitSet(mth); + for (BlockNode candidate : bitSetToBlocks(mth, union)) { + if (isCandidateForOutBlock(mth, thenBlock, elseBlock, candidate)) { + candidates.set(candidate.getPos()); + } + } + + BlockNode bottom = getBottomBlock(bitSetToBlocks(mth, candidates), true); + if (bottom != null) { + return bottom; + } + + // Attempt three: fallback to path cross again + return getPathCross(mth, thenBlock, elseBlock); + } + + static boolean isCandidateForOutBlock(MethodNode mth, BlockNode thenBlock, BlockNode elseBlock, BlockNode candidate) { + // a candidate block requires: + // - >1 predecessor + // - each predecessor has a clean path from elseBlock or thenBlock, and there exist predecessors + // covering both cases + // - inside the union of the two dom frontiers + + if (candidate.getPredecessors().size() < 2) { + return false; // block has only one pred, and so can't be the outblock + } + + BitSet coverageThenPreds = newBlocksBitSet(mth); + BitSet coverageElsePreds = newBlocksBitSet(mth); + + if (candidate == elseBlock) { + coverageElsePreds.set(candidate.getPos()); + } + if (candidate == thenBlock) { + coverageThenPreds.set(candidate.getPos()); + } + + for (BlockNode pred : candidate.getPredecessors()) { + if (isPathExists(thenBlock, pred)) { + coverageThenPreds.set(pred.getPos()); + } + + if (isPathExists(elseBlock, pred)) { + coverageElsePreds.set(pred.getPos()); + } + } + if (coverageElsePreds.cardinality() == 0 || coverageThenPreds.cardinality() == 0) { + return false; // block has no path to both the then and else blocks + } + + BlockNode coverageElsePred = bitSetToOneBlock(mth, coverageElsePreds); + BlockNode coverageThenPred = bitSetToOneBlock(mth, coverageThenPreds); + if (coverageElsePred != null && coverageElsePred == coverageThenPred) { + return false; // the only paths from else and then go through the same block + } + + return true; + } + private static boolean isBadBranchBlock(IfInfo info, BlockNode block) { // check if block at end of loop edge if (block.contains(AFlag.LOOP_START) && block.getPredecessors().size() == 1) { @@ -508,12 +611,10 @@ final class IfRegionMaker { if (lastInsn != null && lastInsn.getType() == InsnType.IF) { return makeIfInfo(info.getMth(), block); } - // skip this block and search in successors chain - List successors = block.getSuccessors(); - if (successors.size() != 1) { + BlockNode next = getNextBlockInIfSuccessorChain(block); + if (next == null) { return null; } - BlockNode next = successors.get(0); if (next.getPredecessors().size() != 1 || next.contains(AFlag.ADDED_TO_REGION)) { return null; } @@ -529,6 +630,42 @@ final class IfRegionMaker { return nextInfo; } + /** + * Allow singular successor to block or 2 successors where one is a EXC_BOTTOM_SPLITTER + */ + private static @Nullable BlockNode getNextBlockInIfSuccessorChain(BlockNode block) { + + // skip this block and search in successors chain + List successors = block.getSuccessors(); + if (successors.size() > 2 || successors.size() == 0) { + return null; + } + // We might have the next IF and a EXC_BOTTOM_SPLITTER block to delimit a try region + BlockNode first = successors.get(0); + if (successors.size() == 1) { + return first; + } + BlockNode second = successors.get(1); + boolean firstIsHandlerPath = first.contains(AFlag.EXC_BOTTOM_SPLITTER); + boolean secondIsHandlerPath = second.contains(AFlag.EXC_BOTTOM_SPLITTER); + if (!firstIsHandlerPath && !secondIsHandlerPath) { + // unknown case + return null; + } + if (firstIsHandlerPath && secondIsHandlerPath) { + // unknown case + return null; + } + BlockNode candidate = firstIsHandlerPath ? second : first; + + // Continue to recurse through blocks as long as none of them have any instructions + if (candidate.getInstructions().isEmpty()) { + return getNextBlockInIfSuccessorChain(candidate); + } + + return candidate; + } + /** * Check that all instructions can be inlined */ diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/LoopRegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/LoopRegionMaker.java index 1812c0f60..31ef1e462 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/LoopRegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/LoopRegionMaker.java @@ -1,9 +1,11 @@ package jadx.core.dex.visitors.regions.maker; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Queue; import java.util.Set; import jadx.core.dex.attributes.AFlag; @@ -27,9 +29,24 @@ import jadx.core.utils.RegionUtils; import jadx.core.utils.blocks.BlockSet; import jadx.core.utils.exceptions.JadxRuntimeException; +import static jadx.core.utils.BlockUtils.followEmptyPath; import static jadx.core.utils.BlockUtils.getNextBlock; import static jadx.core.utils.BlockUtils.isPathExists; +/* + * Definitions: + * main loop body - the set of nodes that form a loop in the control flow graph e.g. they can all + * reach the loop start and are all reachable from the loop start + * loop exit edge - an edge with a source in the main loop body and a target outside the main loop + * body + * outblock - the first node after the entire loop has finished that should be regioned next + * header block - an IF node that implements the loop condition + * crossing - a block that is reachable from two different source nodes and represents where control + * flow paths from the two blocks cross + * exit block/node - an overloaded term. May mean the source or target of an exit edge, or a route + * to exit the overall region e.g. the outblock + */ + final class LoopRegionMaker { private final MethodNode mth; private final RegionMaker regionMaker; @@ -81,13 +98,32 @@ final class LoopRegionMaker { exitBlocks.removeAll(condInfo.getMergedBlocks().toList()); if (!exitBlocks.isEmpty()) { - BlockNode loopExit = condInfo.getElseBlock(); - if (loopExit != null) { - // add 'break' instruction before path cross between main loop exit and sub-exit - for (Edge exitEdge : loop.getExitEdges()) { - if (exitBlocks.contains(exitEdge.getSource())) { - insertLoopBreak(stack, loop, loopExit, exitEdge); + + // Blocks associated with the loop condition + List loopConditionBlocks = loopRegion.getConditionBlocks(); + + for (Edge exitEdge : loop.getExitEdges()) { + // An exit edge from the loop condition blocks + BlockNode exitSource = exitEdge.getSource(); + + if (loopConditionBlocks.contains(exitSource)) { + BlockNode outBlock = followEmptyPath(exitEdge.getTarget()); + + for (BlockNode pred : outBlock.getPredecessors()) { + + // Restarting search through exit edges from the beginning ("top") + for (Edge exitEdgeTop : loop.getExitEdges()) { + + if (!loopConditionBlocks.contains(exitEdgeTop.getSource())) { + if (isPathExists(exitEdgeTop.getTarget(), pred) || exitEdgeTop.getTarget() == outBlock) { + insertLoopBreak(stack, loop, outBlock, exitEdgeTop.getSource(), new Edge(pred, outBlock)); + } + } + } } + + // Exit edge found - no need to check further regardless of break outcome + break; } } } @@ -106,7 +142,8 @@ final class LoopRegionMaker { loopStart.addAttr(AType.LOOP, loop); loop.getEnd().remove(AFlag.ADDED_TO_REGION); } else { - out = condInfo.getElseBlock(); + out = condInfo.getElseBlock(); // Following Jadx convention, this must be the next synthetic block, not actual (theoretical) out + // block if (outerRegion != null && out != null && out.contains(AFlag.LOOP_START) @@ -149,23 +186,27 @@ final class LoopRegionMaker { */ private LoopRegion makeLoopRegion(IRegion curRegion, LoopInfo loop, List exitBlocks) { for (BlockNode block : exitBlocks) { + // Ignore blocks that lead to exception handlers if (block.contains(AType.EXC_HANDLER)) { continue; } + // Ignore blocks that do not branch based on an if statement InsnNode lastInsn = BlockUtils.getLastInsn(block); if (lastInsn == null || lastInsn.getType() != InsnType.IF) { continue; } + // Skip any nested if statements List loops = block.getAll(AType.LOOP); if (!loops.isEmpty() && loops.get(0) != loop) { // skip nested loop condition continue; } boolean exitAtLoopEnd = isExitAtLoopEnd(block, loop); + LoopRegion loopRegion = new LoopRegion(curRegion, loop, block, exitAtLoopEnd); + boolean found; - if (block == loop.getStart() || exitAtLoopEnd - || BlockUtils.isEmptySimplePath(loop.getStart(), block)) { + if (block == loop.getStart() || exitAtLoopEnd || BlockUtils.isEmptySimplePath(loop.getStart(), block)) { found = true; } else if (block.getPredecessors().contains(loop.getStart())) { loopRegion.setPreCondition(loop.getStart()); @@ -216,32 +257,273 @@ final class LoopRegionMaker { return loopEnd.getInstructions().isEmpty() && ListUtils.isSingleElement(loopEnd.getPredecessors(), exit); } + /* + * Check that the exits suggested by treating mainExitBlock as the header block + * are consistent with a loop condition + */ private boolean checkLoopExits(LoopInfo loop, BlockNode mainExitBlock) { List exitEdges = loop.getExitEdges(); if (exitEdges.size() < 2) { return true; } + // If the header selected does not have an exit edge, raise an exception Optional mainEdgeOpt = exitEdges.stream().filter(edge -> edge.getSource() == mainExitBlock).findFirst(); if (mainEdgeOpt.isEmpty()) { throw new JadxRuntimeException("Not found exit edge by exit block: " + mainExitBlock); } + Edge mainExitEdge = mainEdgeOpt.get(); BlockNode mainOutBlock = mainExitEdge.getTarget(); - for (Edge exitEdge : exitEdges) { - if (exitEdge != mainExitEdge) { - // all exit paths must be same or don't cross (will be inside loop) - BlockNode exitBlock = exitEdge.getTarget(); - if (!BlockUtils.isEqualPaths(mainOutBlock, exitBlock)) { - BlockNode crossBlock = BlockUtils.getPathCross(mth, mainOutBlock, exitBlock); - if (crossBlock != null) { - return false; - } + + BlockNode firstWorkAfterMainExitBlock = BlockUtils.followEmptyPath(mainOutBlock); + List firstInstructions = firstWorkAfterMainExitBlock.getInstructions(); + + // If there is a direct path to a return from the header, all exits are inside the loop + if (firstInstructions.size() == 1 && firstInstructions.get(0).getType() == InsnType.RETURN) { + return true; + } + + // Otherwise the exit must lead to a valid out block + return validOutBlock(firstWorkAfterMainExitBlock, loop); + } + + /* + * An out block is valid if every exit path passes through it or doesn't cross any other exit path + * (permitting one block of duplication) + * @param outblock The proposed region exit block + * @param exitEdges All edges leaving a section at the start of the region e.g. edges leaving a loop + * body + */ + private Boolean validOutBlock(BlockNode outBlock, LoopInfo loop) { + /* + * Not permitted: + * - An edge which cannot reach outblock, but does cross with another exit path + * --- This crossing could be on a path that never reaches outblock + * --- This crossing could be after outblock + * - An edge which can reach outblock, but has another crossing with an exit path + * --- This crossing could be before outblock + * --- This crossing could be after outblock + * --- This crossing could be on a branch that does not reach outblock + * Permitted: + * - If any of these inconsistent crossings occur at or near the method exit + * - If the node can reach the outblock but has no crossing there because it dominates the outnode + * - A number of other edge cases + */ + List exitEdges = loop.getExitEdges(); + Queue edgesToCheck = new LinkedList<>(exitEdges); + + while (!edgesToCheck.isEmpty()) { + Edge exitEdge = edgesToCheck.remove(); + BlockNode exitBlock = exitEdge.getTarget(); + + // Get the dominance frontier of exitEdge.getTarget() only along paths through exitEdge + + List dominanceFrontier; + if (!exitEdge.isSynthetic()) { + dominanceFrontier = BlockUtils.bitSetToBlocks(mth, BlockUtils.getDomFrontierThroughEdge(exitEdge)); + } else { + dominanceFrontier = BlockUtils.bitSetToBlocks(mth, exitEdge.getTarget().getDomFrontier()); + } + + if (outBlock.isDominator(exitBlock) || outBlock == exitBlock) { + // Accept if the loop exit block is a dominator of the suggested out block + continue; + } + + for (BlockNode crossing : dominanceFrontier) { + if (crossing == outBlock) { + // Accept if the crossing is at the outblock + continue; + } + if (BlockUtils.isExitBlock(mth, crossing)) { + // Accept if the crossing is at the method end + continue; + } + + // Find the first block after the crossing with instructions + BlockNode firstInstructionBlock = crossing; + List cInsns = crossing.getInstructions(); + if (cInsns.isEmpty()) { + firstInstructionBlock = BlockUtils.followEmptyPath(crossing); + } + + // Return false if the crossing doesn't satisfy any relevant edge case + if (!(viaValidUncleanSuccessor(exitBlock, crossing, loop) + || noWorkBeforeEnd(firstInstructionBlock, outBlock) + || oneBlockOfWorkBeforeEnd(firstInstructionBlock, outBlock) + || isNestedIfCross(crossing, edgesToCheck) + || isOuterOutblock(crossing, loop))) { + return false; } } } return true; } + /* + * @param exitBlock the target of an exit edge + * @param crossing the crossing block between exitBlock and a possible outblock + * @param loop the loop + */ + private boolean viaValidUncleanSuccessor(BlockNode exitBlock, BlockNode crossing, LoopInfo loop) { + // Return true if the path from exitBlock is to an exception handler or via a backwards loop edge + // with continue + + if (isPathExists(exitBlock, crossing)) { + // This case does not apply if there is a path via clean successors + return false; + } + + // If to a loop start check if the backwards edge has a branch without a valid continue + if (crossing.contains(AFlag.LOOP_START)) { + // Note: This loop start cannot be for the internal loop else exitEdge would not leave the loop + + // Find the outer loop containing the loop start + LoopInfo parent = loop.getParentLoop(); + LoopInfo outerLoop = null; + while (parent != null) { + if (parent.getStart() == crossing) { + outerLoop = parent; + break; + } + parent = parent.getParentLoop(); + } + + if (outerLoop != null) { + BlockNode loopEnd = outerLoop.getEnd(); + List predecessors = loopEnd.getPredecessors(); + if (predecessors.size() > 1) { + for (BlockNode predecessor : predecessors) { + // Do not accept if a predecessor to the loop end reachable from the exit would not have a + // continue inserted + if (BlockUtils.isPathExists(exitBlock, predecessor) + && !canInsertContinue(predecessor, predecessors, loopEnd, outerLoop.getExitNodes())) { + return false; + } + } + } else { + // Do not accept if no continues would be placed + return false; + } + } + + } + + // Accept if all branches have a valid continue or if not to a loop start (to an exception handler) + return true; + } + + /* + * @param firstInstructionBlock the first block containing instructions off the main loop body + * @param outBlock a possible outblock of the loop + */ + private boolean noWorkBeforeEnd(BlockNode firstInstructionBlock, BlockNode outBlock) { + // Return true if there is no work between the crossing and an exit block + return (BlockUtils.isExitBlock(mth, firstInstructionBlock) || firstInstructionBlock == outBlock); + } + + /* + * @param firstInstructionBlock the first block containing instructions off the main loop body + * @param outBlock a possible outblock of the loop + */ + private boolean oneBlockOfWorkBeforeEnd(BlockNode firstInstructionBlock, BlockNode outBlock) { + // Return true if down every path there is no more than one block of work between the crossing and + // an exit block + List cleanSuccessors = firstInstructionBlock.getCleanSuccessors(); + if (cleanSuccessors.isEmpty()) { + return false; + } + for (BlockNode cleanSuccessor : cleanSuccessors) { + BlockNode nextInstructionBlock = BlockUtils.followEmptyPath(cleanSuccessor); + if (!BlockUtils.isExitBlock(mth, nextInstructionBlock) && nextInstructionBlock != outBlock) { + return false; + } + } + return true; + } + + /* + * @param crossing the block that may be the joint block of a merged if + * @param edgesToCheck the list of edges that will be processed to add to + */ + private boolean isNestedIfCross(BlockNode crossing, Queue edgesToCheck) { + // Return true if the crossing is due to merged control flow after a nested if + // Add the edges out of the crossing to be investigated + + // If the crossing is the branch of a merged if, all predecessors will be synthetic up to the if + // statements, and the first if statement will dominate the crossing + List predecessors = crossing.getPredecessors(); + + // Find a predecessor that dominates all other predecessors + BlockNode possibleFirstIF = BlockUtils.followEmptyPath(predecessors.get(0), true); + for (BlockNode predecessor : predecessors) { + // Follow the predecessor up to the first node with instructions + BlockNode possibleIF = followEmptyPath(predecessor, true); + if (crossing.isDominator(possibleIF)) { + possibleFirstIF = possibleIF; + } + } + + // This case does not apply if a merged if cannot be made + IfInfo currentIf = IfRegionMaker.makeIfInfo(mth, possibleFirstIF); + if (currentIf == null) { + return false; + } + + IfInfo mergedIf = IfRegionMaker.mergeNestedIfNodes(currentIf); + if (mergedIf == null) { + return false; + } + + // Note: work will be repeated for large merged ifs. Results could be cached to improve performance + // Accept if following every predecessor path from the crossing reaches a merged if node + BlockSet mergedBlocks = mergedIf.getMergedBlocks(); + for (BlockNode predecessor : predecessors) { + BlockNode possibleIF = followEmptyPath(predecessor, true); + if (!mergedBlocks.contains(possibleIF)) { + return false; + } + } + + // If this crossing is the result of merged ifs, check the next crossing after this one + Edge placeHolderEdge = new Edge(crossing, crossing, true); + if (!edgesToCheck.contains(placeHolderEdge)) { + edgesToCheck.add(placeHolderEdge); + } + + return true; + } + + /* + * @param crossing the block that may be an outblock for a parent loop + * @param loop the inner loop currently being considered + */ + private boolean isOuterOutblock(BlockNode crossing, LoopInfo loop) { + // Return true if the crossing is the outblock for an outer loop and is jumped to using a labelled + // break + + List edgeInsns = crossing.getAll(AType.EDGE_INSN); + for (EdgeInsnAttr edgeInsn : edgeInsns) { + InsnNode insn = edgeInsn.getInsn(); + // If there is a break edge instruction + if (insn.getType() == InsnType.BREAK) { + List loopsBrokenFrom = insn.get(AType.LOOP).getList(); + for (LoopInfo loopBrokenFrom : loopsBrokenFrom) { + // If it is for a parent of the current loop + if (loop.hasParent(loopBrokenFrom)) { + BlockNode target = edgeInsn.getEnd(); + // If it points at the crossing + if (target == crossing) { + // Accept if the crossing block is already the target of a break instruction from a parent loop + return true; + } + } + } + } + } + return false; + } + private BlockNode makeEndlessLoop(IRegion curRegion, RegionStack stack, LoopInfo loop, BlockNode loopStart) { LoopRegion loopRegion = new LoopRegion(curRegion, loop, null, false); curRegion.getSubBlocks().add(loopRegion); @@ -256,7 +538,7 @@ final class LoopRegionMaker { if (exitEdges.size() == 1) { Edge exitEdge = exitEdges.get(0); BlockNode exit = exitEdge.getTarget(); - if (insertLoopBreak(stack, loop, exit, exitEdge)) { + if (insertLoopBreak(stack, loop, exit, exitEdge.getSource(), exitEdge)) { BlockNode nextBlock = getNextBlock(exit); if (nextBlock != null) { stack.addExit(nextBlock); @@ -264,16 +546,42 @@ final class LoopRegionMaker { } } } else { - for (Edge exitEdge : exitEdges) { + loop0: for (Edge exitEdge : exitEdges) { BlockNode exit = exitEdge.getTarget(); - List blocks = BlockUtils.bitSetToBlocks(mth, exit.getDomFrontier()); + List blocks = BlockUtils.bitSetToBlocks(mth, BlockUtils.getDomFrontierThroughEdge(exitEdge)); + + // Only select the method exit if there is no other valid outblock + BlockNode methodExit = mth.getExitBlock(); + if (blocks.contains(methodExit)) { + blocks.remove(methodExit); + blocks.add(methodExit); + } for (BlockNode block : blocks) { if (BlockUtils.isPathExists(exit, block)) { - stack.addExit(block); - insertLoopBreak(stack, loop, block, exitEdge); - out = block; - } else { - insertLoopBreak(stack, loop, exit, exitEdge); + if (validOutBlock(block, loop)) { + out = block; + break loop0; + } + } else if (block.contains(AFlag.LOOP_START)) { + // Special case if there is no joining control flow before an outer loop back edge + if (validOutBlock(exit, loop)) { + out = exit; + break loop0; + } + } + } + } + + // Add breaks + stack.addExit(out); + if (out != null && out != mth.getExitBlock()) { + // Add a break on every incoming edge where the predecessor is reachable from the loop + for (BlockNode predecessor : out.getPredecessors()) { + for (Edge exitEdge : loop.getExitEdges()) { + BlockNode target = exitEdge.getTarget(); + if (BlockUtils.isPathExists(exitEdge.getTarget(), predecessor) || target == out) { + insertLoopBreak(stack, loop, out, exitEdge.getSource(), new Edge(predecessor, out)); + } } } } @@ -332,7 +640,16 @@ final class LoopRegionMaker { return true; } - private boolean insertLoopBreak(RegionStack stack, LoopInfo loop, BlockNode loopExit, Edge exitEdge) { + /* + * Insert a break instruction where exitEdge meets loopExit + * @param stack the region stack + * @param loop the loop being broken out of + * @param loopExit the outblock for loop + * @param blockOnLoop an exit block on loop through which exitEdge is reachable + * @param exitEdge an edge on the path between blockOnLoop and loopExit indicative of the breaking + * path + */ + private boolean insertLoopBreak(RegionStack stack, LoopInfo loop, BlockNode loopExit, BlockNode blockOnLoop, Edge exitEdge) { BlockNode exit = exitEdge.getTarget(); Edge insertEdge = null; boolean confirm = false; @@ -349,7 +666,10 @@ final class LoopRegionMaker { } if (!confirm) { - BlockNode insertBlock = null; + // Start search from the next edge if the target is simple (e.g. first + // node after loop exit) + Boolean isSimple = BlockUtils.followEmptyPath(exit) != exit; + BlockNode insertBlock = isSimple ? null : exitEdge.getSource(); BlockSet visited = new BlockSet(mth); while (true) { if (exit == null || visited.contains(exit)) { @@ -359,7 +679,7 @@ final class LoopRegionMaker { if (insertBlock != null && isPathExists(loopExit, exit)) { // found cross if (canInsertBreak(insertBlock)) { - insertEdge = new Edge(insertBlock, insertBlock.getSuccessors().get(0)); + insertEdge = new Edge(insertBlock, exit); confirm = true; break; } @@ -378,20 +698,23 @@ final class LoopRegionMaker { EdgeInsnAttr.addEdgeInsn(insertEdge, breakInsn); stack.addExit(exit); // add label to 'break' if needed - addBreakLabel(exitEdge, exit, breakInsn); + addBreakLabel(blockOnLoop, exit, breakInsn); return true; } - private void addBreakLabel(Edge exitEdge, BlockNode exit, InsnNode breakInsn) { - BlockNode outBlock = BlockUtils.getNextBlock(exitEdge.getTarget()); - if (outBlock == null) { - return; - } - List exitLoop = mth.getAllLoopsForBlock(outBlock); + /* + * Adds a label to a break instruction if reaching the exit from the loop involves leaving multiple + * loops + * @param blockOnLoop the exit block on the loop to which breakInsn is currently associated + * @param exit the out block of the loop to which breakInsn is currently associated + * @param breakInsn a break instruction + */ + private void addBreakLabel(BlockNode blockOnLoop, BlockNode exit, InsnNode breakInsn) { + List exitLoop = mth.getAllLoopsForBlock(exit); if (!exitLoop.isEmpty()) { return; } - List inLoops = mth.getAllLoopsForBlock(exitEdge.getSource()); + List inLoops = mth.getAllLoopsForBlock(blockOnLoop); if (inLoops.size() < 2) { return; } @@ -449,6 +772,15 @@ final class LoopRegionMaker { if (isDominatedOnBlocks(codePred, predecessors)) { return false; } + if (!pred.getAll(AType.EDGE_INSN).isEmpty()) { + // if we've already inserted a break, don't also insert a continue in the same spot + List insns = pred.getAll(AType.EDGE_INSN); + for (EdgeInsnAttr insn : insns) { + if (insn.getInsn().getType() == InsnType.BREAK) { + return false; + } + } + } boolean gotoExit = false; for (BlockNode exit : loopExitNodes) { if (BlockUtils.isPathExists(codePred, exit)) { diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionMaker.java b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionMaker.java index 3fda1bae5..bf9e00c24 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionMaker.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/regions/maker/RegionMaker.java @@ -41,7 +41,7 @@ public class RegionMaker { this.ifMaker = new IfRegionMaker(mth, this); this.loopMaker = new LoopRegionMaker(mth, this, ifMaker); this.processedBlocks = BlockSet.empty(mth); - this.regionsLimit = mth.getBasicBlocks().size() * 100; + this.regionsLimit = mth.getBasicBlocks().size() * 400; } public Region makeMthRegion() { @@ -57,16 +57,18 @@ public class RegionMaker { } if (processedBlocks.addChecked(startBlock)) { - mth.addWarn("Removed duplicated region for block: " + startBlock + ' ' + startBlock.getAttributesString()); - return region; + mth.addWarnComment("Found duplicated region for block: " + startBlock + ' ' + startBlock.getAttributesString()); + // Add block to multiple regions (duplicate the instructions in decompiled code) and allow + // processing to continue } BlockNode next = startBlock; + while (next != null) { next = traverse(region, next); regionsCount++; if (regionsCount > regionsLimit) { - throw new JadxOverflowException("Regions count limit reached"); + throw new JadxOverflowException("Regions count limit reached at block " + startBlock.toString()); } } return region; 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 800b58f38..3099853ae 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 @@ -9,6 +9,8 @@ import java.util.Map; import java.util.Set; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LoopInfo; @@ -26,6 +28,7 @@ import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.regions.Region; import jadx.core.dex.regions.SwitchRegion; +import jadx.core.dex.regions.SwitchRegion.CaseInfo; import jadx.core.dex.visitors.regions.AbstractRegionVisitor; import jadx.core.dex.visitors.regions.DepthRegionTraversal; import jadx.core.dex.visitors.regions.SwitchBreakVisitor; @@ -34,12 +37,13 @@ import jadx.core.utils.ListUtils; import jadx.core.utils.RegionUtils; import jadx.core.utils.Utils; import jadx.core.utils.blocks.BlockSet; -import jadx.core.utils.exceptions.JadxRuntimeException; public final class SwitchRegionMaker { private final MethodNode mth; private final RegionMaker regionMaker; + private static final Logger LOG = LoggerFactory.getLogger(SwitchRegionMaker.class); + SwitchRegionMaker(MethodNode mth, RegionMaker regionMaker) { this.mth = mth; this.regionMaker = regionMaker; @@ -69,7 +73,7 @@ public final class SwitchRegionMaker { stack.addExit(out); addCases(sw, out, stack, blocksMap); - removeEmptyCases(insn, sw, defCase); + removeEmptyCases(insn, sw, defCase, out); stack.pop(); return out; @@ -214,7 +218,10 @@ public final class SwitchRegionMaker { } if (out != null && regionMaker.isProcessed(out)) { // 'out' block already processed, prevent endless loop - throw new JadxRuntimeException("Failed to find switch 'out' block (already processed)"); + // 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(); } return out; } @@ -246,14 +253,14 @@ public final class SwitchRegionMaker { if (firstArg.isRegister()) { RegisterArg reg = (RegisterArg) firstArg; for (int i = 1; i < count; i++) { - InsnArg arg = returnArgs.get(1); + InsnArg arg = returnArgs.get(i); if (!arg.isRegister() || !((RegisterArg) arg).sameCodeVar(reg)) { return exitBlock; } } } else { for (int i = 1; i < count; i++) { - InsnArg arg = returnArgs.get(1); + InsnArg arg = returnArgs.get(i); if (!arg.equals(firstArg)) { return exitBlock; } @@ -276,31 +283,53 @@ public final class SwitchRegionMaker { * 1. single 'default' case * 2. filler cases if switch is 'packed' and 'default' case is empty */ - private void removeEmptyCases(SwitchInsn insn, SwitchRegion sw, BlockNode defCase) { + private void removeEmptyCases(SwitchInsn insn, SwitchRegion sw, BlockNode defCase, BlockNode outBlock) { boolean defaultCaseIsEmpty; if (defCase == null) { defaultCaseIsEmpty = true; } else { defaultCaseIsEmpty = sw.getCases().stream() .anyMatch(c -> c.getKeys().contains(SwitchRegion.DEFAULT_CASE_KEY) - && RegionUtils.isEmpty(c.getContainer())); + && canRemove(c.getContainer(), outBlock)); } if (defaultCaseIsEmpty) { - sw.getCases().removeIf(caseInfo -> { - if (RegionUtils.isEmpty(caseInfo.getContainer())) { + List cases = new ArrayList<>(sw.getCases()); + for (CaseInfo caseInfo : cases) { + if (canRemove(caseInfo.getContainer(), outBlock)) { List keys = caseInfo.getKeys(); - if (keys.contains(SwitchRegion.DEFAULT_CASE_KEY)) { - return true; - } - if (insn.isPacked()) { - return true; + if (keys.contains(SwitchRegion.DEFAULT_CASE_KEY) || insn.isPacked()) { + // Remove case and mark all blocks as don't generate + RegionUtils.addToAll(mth, caseInfo.getContainer(), AFlag.DONT_GENERATE); + sw.getCases().remove(caseInfo); } } - return false; - }); + } } } + /* + * Check container is empty and all paths through container are empty up until outBlock + */ + private boolean canRemove(IContainer container, BlockNode outBlock) { + if (RegionUtils.isEmpty(container)) { + if (container instanceof BlockNode) { + // Base case - empty path from block node to outBlock + return BlockUtils.followEmptyPath((BlockNode) container) == outBlock; + } else if (container instanceof IRegion) { + // Recursive case - every subBlock can be removed + List subBlocks = ((IRegion) container).getSubBlocks(); + for (IContainer subBlock : subBlocks) { + if (!canRemove(subBlock, outBlock)) { + return false; + } + } + return true; + } + LOG.debug("Unexpected container type in switch"); + } + return false; + } + private boolean isBadCasesOrder(Map> blocksMap, Map fallThroughCases) { BlockNode nextCaseBlock = null; for (BlockNode caseBlock : blocksMap.keySet()) { @@ -345,7 +374,7 @@ public final class SwitchRegionMaker { // 'continue' not needed } else { for (BlockNode p : loopEnd.getPredecessors()) { - if (list.contains(p)) { + if (list.contains(p) || p == caseBlock) { if (p.isSynthetic()) { p.getInstructions().add(new InsnNode(InsnType.CONTINUE, 0)); inserted = true; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java index 9a4e54197..2acd8bc8c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfo.java @@ -2,10 +2,14 @@ package jadx.core.dex.visitors.usage; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; +import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoData; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.clsp.ClspClass; @@ -17,6 +21,7 @@ import jadx.core.dex.nodes.FieldNode; import jadx.core.dex.nodes.ICodeNode; import jadx.core.dex.nodes.MethodNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import static jadx.core.utils.Utils.notEmpty; @@ -28,7 +33,13 @@ public class UsageInfo implements IUsageInfoData { private final UseSet clsUsage = new UseSet<>(); private final UseSet clsUseInMth = new UseSet<>(); private final UseSet fieldUsage = new UseSet<>(); + // MethodNodeA -> Set of MethodNodes that MethodNodeA is called from private final UseSet mthUsage = new UseSet<>(); + // MethodNodeA -> Set of MethodNodes that MethodNodeA calls + private final UseSet mthUses = new UseSet<>(); + // MethodNodeA -> Set of IMethodRefs for methods that MethodNodeA calls that cannot be resolved + private final UseSet unresolvedMthUsage = new UseSet<>(); + private final Map selfCalls = new HashMap<>(); public UsageInfo(RootNode root) { this.root = root; @@ -38,21 +49,27 @@ public class UsageInfo implements IUsageInfoData { public void apply() { clsDeps.visit((cls, deps) -> cls.setDependencies(sortedList(deps))); clsUsage.visit((cls, deps) -> cls.setUseIn(sortedList(deps))); - clsUseInMth.visit((cls, methods) -> cls.setUseInMth(sortedList(methods))); - fieldUsage.visit((field, methods) -> field.setUseIn(sortedList(methods))); - mthUsage.visit((mth, methods) -> mth.setUseIn(sortedList(methods))); + clsUseInMth.visit((cls, methods) -> cls.setUseInMth(resolveMthList(sortedList(methods)))); + fieldUsage.visit((field, methods) -> field.setUseIn(resolveMthList(sortedList(methods)))); + mthUsage.visit((mth, methods) -> mth.setUseIn(resolveMthList(sortedList(methods)))); + mthUses.visit((mth, methods) -> mth.setUsed(resolveMthList(sortedList(methods)))); + unresolvedMthUsage.visit((mth, unresolvedMethods) -> mth.setUnresolvedUsed(new ArrayList<>(unresolvedMethods))); + selfCalls.forEach((mth, selfCall) -> mth.setCallsSelf(selfCall)); } @Override public void applyForClass(ClassNode cls) { - cls.setDependencies(sortedList(clsDeps.get(cls))); - cls.setUseIn(sortedList(clsUsage.get(cls))); - cls.setUseInMth(sortedList(clsUseInMth.get(cls))); + cls.setDependencies(sortedList(clsDeps.getOrDefault(cls, Collections.emptySet()))); + cls.setUseIn(sortedList(clsUsage.getOrDefault(cls, Collections.emptySet()))); + cls.setUseInMth(resolveMthList(sortedList(clsUseInMth.getOrDefault(cls, Collections.emptySet())))); for (FieldNode fld : cls.getFields()) { - fld.setUseIn(sortedList(fieldUsage.get(fld))); + fld.setUseIn(resolveMthList(sortedList(fieldUsage.getOrDefault(fld, Collections.emptySet())))); } for (MethodNode mth : cls.getMethods()) { - mth.setUseIn(sortedList(mthUsage.get(mth))); + mth.setUseIn(resolveMthList(sortedList(mthUsage.getOrDefault(mth, Collections.emptySet())))); + mth.setUsed(resolveMthList(sortedList(mthUses.getOrDefault(mth, Collections.emptySet())))); + mth.setUnresolvedUsed(new ArrayList<>(unresolvedMthUsage.getOrDefault(mth, Collections.emptySet()))); + mth.setCallsSelf(selfCalls.getOrDefault(mth, false)); } } @@ -60,9 +77,16 @@ public class UsageInfo implements IUsageInfoData { public void visitUsageData(IUsageInfoVisitor visitor) { clsDeps.visit((cls, deps) -> visitor.visitClassDeps(cls, sortedList(deps))); clsUsage.visit((cls, deps) -> visitor.visitClassUsage(cls, sortedList(deps))); - clsUseInMth.visit((cls, methods) -> visitor.visitClassUseInMethods(cls, sortedList(methods))); - fieldUsage.visit((field, methods) -> visitor.visitFieldsUsage(field, sortedList(methods))); - mthUsage.visit((mth, methods) -> visitor.visitMethodsUsage(mth, sortedList(methods))); + clsUseInMth.visit((cls, methods) -> visitor.visitClassUseInMethods(cls, resolveMthList(sortedList(methods)))); + fieldUsage.visit((field, methods) -> visitor.visitFieldsUsage(field, resolveMthList(sortedList(methods)))); + mthUsage.visit((mth, methods) -> visitor.visitMethodsUsage(mth, resolveMthList(sortedList(methods)))); + mthUses.visit((mth, methods) -> visitor.visitMethodsUses(mth, resolveMthList(sortedList(methods)))); + unresolvedMthUsage.visit((mth, unresolvedMethods) -> visitor.visitUnresolvedMethodsUsage(mth, new ArrayList<>(unresolvedMethods))); + for (Entry entry : selfCalls.entrySet()) { + MethodNode mth = entry.getKey(); + Boolean selfCall = entry.getValue(); + visitor.visitIsSelfCall(mth, selfCall); + } visitor.visitComplete(); } @@ -118,12 +142,23 @@ public class UsageInfo implements IUsageInfoData { */ public void methodUse(MethodNode mth, MethodNode useMth) { clsUse(mth, useMth.getParentClass()); - mthUsage.add(useMth, mth); + mthUsage.add(useMth, mth); // useMth is used in mth + mthUses.add(mth, useMth); // mth uses useMth + if (mth == useMth) { + selfCalls.put(mth, true); + } // implicit usage clsUse(mth, useMth.getReturnType()); useMth.getMethodInfo().getArgumentsTypes().forEach(argType -> clsUse(mth, argType)); } + /** + * Add method usage: {@code useMth} occurrence found in {@code mth} code + */ + public void unresolvedMethodUse(MethodNode mth, IMethodRef useMth) { + unresolvedMthUsage.add(mth, useMth); + } + public void fieldUse(MethodNode mth, FieldNode useFld) { clsUse(mth, useFld.getParentClass()); fieldUsage.add(useFld, mth); @@ -197,4 +232,9 @@ public class UsageInfo implements IUsageInfoData { Collections.sort(list); return list; } + + private List resolveMthList(List mthNodeList) { + return Utils.collectionMap(mthNodeList, + m -> root.resolveDirectMethod(m.getParentClass().getRawName(), m.getMethodInfo().getShortId())); + } } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java index 6e2e3dd1f..0015e2fb0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UsageInfoVisitor.java @@ -166,6 +166,11 @@ public class UsageInfoVisitor extends AbstractVisitor { MethodNode methodNode = root.resolveMethod(MethodInfo.fromRef(root, mthRef)); if (methodNode != null) { usageInfo.methodUse(mth, methodNode); + } else { + mthRef.load(); + if (mthRef.getName() != null || mthRef.getParentClassType() != null) { + usageInfo.unresolvedMethodUse(mth, mthRef); + } } break; } @@ -179,6 +184,8 @@ public class UsageInfoVisitor extends AbstractVisitor { MethodNode mthNode = root.resolveMethod(MethodInfo.fromRef(root, mthRef)); if (mthNode != null) { usageInfo.methodUse(mth, mthNode); + } else if (mthRef.getName() != null || mthRef.getParentClassType() != null) { + usageInfo.unresolvedMethodUse(mth, mthRef); } } break; diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java index 8a6cee12e..ba1ea6e90 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/usage/UseSet.java @@ -22,6 +22,10 @@ public class UseSet { return useMap.get(obj); } + public Set getOrDefault(K obj, Set defaultValue) { + return useMap.getOrDefault(obj, defaultValue); + } + public void visit(BiConsumer> consumer) { for (Map.Entry> entry : useMap.entrySet()) { consumer.accept(entry.getKey(), entry.getValue()); 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 789f6a69f..5ba00807b 100644 --- a/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/BlockUtils.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Queue; @@ -31,6 +32,7 @@ import jadx.core.dex.instructions.args.InsnWrapArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.instructions.mods.TernaryInsn; import jadx.core.dex.nodes.BlockNode; +import jadx.core.dex.nodes.Edge; import jadx.core.dex.nodes.IBlock; import jadx.core.dex.nodes.InsnNode; import jadx.core.dex.nodes.MethodNode; @@ -506,6 +508,13 @@ public class BlockUtils { } } + public static List collectAllPredecessors(MethodNode mth, BlockNode startBlock) { + List list = new ArrayList<>(mth.getBasicBlocks().size()); + Function> nextFunc = BlockNode::getPredecessors; + visitDFS(mth, startBlock, nextFunc, list::add); + return list; + } + public static List collectAllSuccessors(MethodNode mth, BlockNode startBlock, boolean clean) { List list = new ArrayList<>(mth.getBasicBlocks().size()); Function> nextFunc = clean ? BlockNode::getCleanSuccessors : BlockNode::getSuccessors; @@ -513,6 +522,96 @@ public class BlockUtils { return list; } + public static List collectAllSuccessorsUntil(MethodNode mth, BlockNode startBlock, boolean clean, + Predicate stopCondition) { + List blocks = new ArrayList<>(); + collectAllSuccessorsUntil(mth, blocks, startBlock, clean, stopCondition); + return blocks; + } + + private static void collectAllSuccessorsUntil(MethodNode mth, List blocks, BlockNode currentBlock, boolean clean, + Predicate stopCondition) { + if (blocks.contains(currentBlock)) { + return; + } + + blocks.add(currentBlock); + + if (stopCondition.test(currentBlock)) { + return; + } + + List successors = clean ? currentBlock.getCleanSuccessors() : currentBlock.getSuccessors(); + for (BlockNode successor : successors) { + collectAllSuccessorsUntil(mth, blocks, successor, clean, stopCondition); + } + } + + @Nullable + public static BlockNode getBottomCommonPredecessor(MethodNode mth, List blocks, Set containedBlocks) { + return getBottomCommonPredecessor(mth, blocks, containedBlocks, false); + } + + @Nullable + public static BlockNode getBottomCommonPredecessor(MethodNode mth, List blocks, Set containedBlocks, + boolean addTopBlock) { + if (blocks.isEmpty()) { + return null; + } + + Set visitedPredecessorsByAll = new HashSet<>(collectAllPredecessors(mth, blocks.get(0))); + + if (addTopBlock) { + BlockNode topBlock = BlockUtils.getBottomBlock(blocks); + if (topBlock == null) { + // TODO: These nodes are not connected so there will be no common successor ???? + // return null; + } else { + visitedPredecessorsByAll.add(topBlock); + } + } + + for (int i = 1; i < blocks.size(); i++) { + BlockNode nextBlock = blocks.get(i); + List predecessors = collectAllPredecessors(mth, nextBlock); + visitedPredecessorsByAll.retainAll(predecessors); + } + + return BlockUtils.getBottomBlock(new ArrayList<>(visitedPredecessorsByAll)); + } + + @Nullable + public static BlockNode getTopCommonSuccessor(MethodNode mth, List blocks, boolean cleanOnly) { + return getTopCommonSuccessor(mth, blocks, cleanOnly, false); + } + + @Nullable + public static BlockNode getTopCommonSuccessor(MethodNode mth, List blocks, boolean cleanOnly, boolean addTopBlock) { + if (blocks.isEmpty()) { + return null; + } + + Set visitedSuccessorsByAll = new HashSet<>(collectAllSuccessors(mth, blocks.get(0), cleanOnly)); + + if (addTopBlock) { + BlockNode topBlock = BlockUtils.getTopBlock(blocks); + if (topBlock == null) { + // TODO: These nodes are not connected so there will be no common successor ???? + // return null; + } else { + visitedSuccessorsByAll.add(topBlock); + } + } + + for (int i = 1; i < blocks.size(); i++) { + BlockNode nextBlock = blocks.get(i); + List successors = collectAllSuccessors(mth, nextBlock, cleanOnly); + visitedSuccessorsByAll.retainAll(successors); + } + + return BlockUtils.getTopBlock(new ArrayList<>(visitedSuccessorsByAll)); + } + public static void visitDFS(MethodNode mth, Consumer visitor) { visitDFS(mth, mth.getEnterBlock(), BlockNode::getSuccessors, visitor); } @@ -584,6 +683,22 @@ public class BlockUtils { return set; } + /** + * Collect blocks from one possible execution path from 'start' to 'end' containing no instructions + */ + public static List getOneEmptyPath(BlockNode start, BlockNode end) { + return collectPathUntil(start, end, false, b -> { + return b.getInstructions().isEmpty() || b.equals(end); + }); + } + + /** + * Collect blocks from one possible execution path from 'start' to 'end' + */ + public static List getOnePath(BlockNode start, BlockNode end) { + return collectPathUntil(start, end, false, b -> true); + } + private static void addPredecessors(Set set, BlockNode from, BlockNode until) { set.add(from); for (BlockNode pred : from.getPredecessors()) { @@ -594,8 +709,30 @@ public class BlockUtils { } private static boolean traverseSuccessorsUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean) { + return traverseSuccessorsUntil(from, until, visited, clean, b -> true); + } + + /** + * + * Traverse succcessors until a node is found + * + * @param from the source node to begin traversing + * @param until the destination node to halt traversing + * @param visited the set of visited blocks so far + * @param clean use only clean successors + * @param pred a predicate that must be true to traverse a block (until or a reachable dominator + * of until must satisfy pred) + * @return true if there is a path from `from` to `until` or a dominator of `until` through blocks + * that satisfy `pred`, false otherwise + */ + private static boolean traverseSuccessorsUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean, + Predicate pred) { List nodes = clean ? from.getCleanSuccessors() : from.getSuccessors(); for (BlockNode s : nodes) { + if (!pred.test(s)) { + // Only explore blocks such that the predicate holds + continue; + } if (s == until) { return true; } @@ -609,7 +746,7 @@ public class BlockUtils { if (until.isDominator(s)) { return true; } - if (traverseSuccessorsUntil(s, until, visited, clean)) { + if (traverseSuccessorsUntil(s, until, visited, clean, pred)) { return true; } } @@ -617,6 +754,65 @@ public class BlockUtils { return false; } + /** + * + * Traverse succcessors until a node is found, collecting the path to the node + * + * @param from the source node to begin traversing + * @param until the destination node to halt traversing + * @param clean use only clean successors + * @param pred a predicate that must be true to traverse a block (until must satisfy pred) + * @return the list of blocks satisfying pred on a path between from and until (inclusive), or null + * if no such path exists + */ + public static List collectPathUntil(BlockNode from, BlockNode until, boolean clean, Predicate pred) { + List path = internalCollectPathUntil(from, until, new BitSet(), clean, pred); + if (path == null) { + return path; + } + path.add(from); + Collections.reverse(path); + return path; + } + + /** + * + * Traverse succcessors until a node is found, collecting the path to the node + * + * @param from the source node to begin traversing + * @param until the destination node to halt traversing + * @param visited the set of visited blocks so far + * @param clean use only clean successors + * @param pred a predicate that must be true to traverse a block (until must satisfy pred) + * @return the list of blocks satisfying pred on a path between from (exclusive) and until + * (inclusive) in reverse order, or null if no such path exists + */ + private static List internalCollectPathUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean, + Predicate pred) { + List nodes = clean ? from.getCleanSuccessors() : from.getSuccessors(); + for (BlockNode s : nodes) { + if (!pred.test(s)) { + // Only explore blocks such that the predicate holds + continue; + } + if (s == until) { + List path = new ArrayList<>(); + path.add(s); + return path; + } + int id = s.getPos(); + if (!visited.get(id)) { + visited.set(id); + List path = internalCollectPathUntil(s, until, visited, clean, pred); + if (path != null) { + path.add(s); + return path; + } + } + } + return null; + } + /** * Search at least one path from startBlocks to end */ @@ -643,13 +839,9 @@ public class BlockUtils { public static boolean isPathExists(BlockNode start, BlockNode end) { if (start == end - || end.isDominator(start) || start.getCleanSuccessors().contains(end)) { return true; } - if (start.getPredecessors().contains(end)) { - return false; - } return traverseSuccessorsUntil(start, end, new BitSet(), true); } @@ -659,12 +851,16 @@ public class BlockUtils { || start.getSuccessors().contains(end)) { return true; } - if (start.getPredecessors().contains(end)) { - return false; - } return traverseSuccessorsUntil(start, end, new BitSet(), false); } + public static boolean isPathExists(BlockNode start, BlockNode end, Predicate pred) { + if (start == end) { + return true; + } + return traverseSuccessorsUntil(start, end, new BitSet(), false, pred); + } + public static BlockNode getTopBlock(List blocks) { if (blocks.size() == 1) { return blocks.get(0); @@ -689,16 +885,46 @@ public class BlockUtils { */ @Nullable public static BlockNode getBottomBlock(List blocks) { + return getBottomBlock(blocks, false); + } + + public static BlockNode getBottomBlock(List blocks, boolean clean) { if (blocks.size() == 1) { return blocks.get(0); } + // attempt 1: look for a block dominated by every other block + // don't do this if clean, since dominators always consider all successors + if (!clean) { + for (BlockNode bottomCandidate : blocks) { + boolean bottom = true; + for (BlockNode from : blocks) { + if (bottomCandidate != from && !bottomCandidate.isDominator(from)) { + bottom = false; + break; + } + } + if (bottom) { + return bottomCandidate; + } + } + } + + // attempt 2: look for a block with a path from every other block for (BlockNode bottomCandidate : blocks) { boolean bottom = true; for (BlockNode from : blocks) { - if (bottomCandidate != from && !isAnyPathExists(from, bottomCandidate)) { - bottom = false; - break; + if (clean) { + if (bottomCandidate != from && !isPathExists(from, bottomCandidate)) { + bottom = false; + break; + } + } else { + if (bottomCandidate != from && !isAnyPathExists(from, bottomCandidate)) { + bottom = false; + break; + } } + } if (bottom) { return bottomCandidate; @@ -763,6 +989,24 @@ public class BlockUtils { return bitSetToOneBlock(mth, combine); } + /** + * Return the dominace frontier of an edge - the blocks for which any path to the block must pass + * through this edge + */ + public static BitSet getDomFrontierThroughEdge(Edge edge) { + BlockNode target = edge.getTarget(); + + if (target.getPredecessors().size() > 1) { + // If the target node has other incoming edges, the dominance frontier is a single block + BitSet dominanceFrontier = new BitSet(); + dominanceFrontier.set(target.getPos()); + return dominanceFrontier; + } else { + // Otherwise the dominance frontier is equivalent to the domiance frontier of the target + return target.getDomFrontier(); + } + } + /** * Return common cross block for input set. * @@ -951,8 +1195,16 @@ public class BlockUtils { * Return start block if no such path. */ public static BlockNode followEmptyPath(BlockNode start) { + return followEmptyPath(start, false); + } + + public static BlockNode followEmptyPath(BlockNode start, Boolean reverse) { + return followEmptyPath(start, reverse, true); + } + + public static BlockNode followEmptyPath(BlockNode start, Boolean reverse, boolean cleanOnly) { while (true) { - BlockNode next = getNextBlockOnEmptyPath(start); + BlockNode next = getNextBlockOnEmptyPath(start, reverse, cleanOnly); if (next == null) { return start; } @@ -960,9 +1212,36 @@ public class BlockUtils { } } + public static List followEmptyUpPathWithinSet(BlockNode start, Collection traversableBlocks) { + List results = new LinkedList<>(); + followEmptyUpPathWithinSet(results, start, traversableBlocks, new HashSet<>()); + return results; + } + + public static void followEmptyUpPathWithinSet(List results, BlockNode start, Collection traversableBlocks, + Collection traversedBlocks) { + List predecessors = ListUtils.filter(start.getPredecessors(), traversableBlocks::contains); + for (BlockNode predecessor : predecessors) { + if (!traversableBlocks.contains(predecessor) || traversedBlocks.contains(predecessor)) { + continue; + } + traversedBlocks.add(predecessor); + if (predecessor.getInstructions().isEmpty()) { + followEmptyUpPathWithinSet(results, start, traversableBlocks, traversedBlocks); + } else { + results.add(predecessor); + } + start = predecessor; + } + } + public static void visitBlocksOnEmptyPath(BlockNode start, Consumer visitor) { + visitBlocksOnEmptyPath(start, visitor, false); + } + + public static void visitBlocksOnEmptyPath(BlockNode start, Consumer visitor, boolean reverse) { while (true) { - BlockNode next = getNextBlockOnEmptyPath(start); + BlockNode next = getNextBlockOnEmptyPath(start, reverse); if (next == null) { return; } @@ -973,14 +1252,25 @@ public class BlockUtils { @Nullable private static BlockNode getNextBlockOnEmptyPath(BlockNode block) { - if (!block.getInstructions().isEmpty() || block.getPredecessors().size() > 1) { + return getNextBlockOnEmptyPath(block, false); + } + + @Nullable + private static BlockNode getNextBlockOnEmptyPath(BlockNode block, Boolean reverse) { + return getNextBlockOnEmptyPath(block, reverse, true); + } + + @Nullable + private static BlockNode getNextBlockOnEmptyPath(BlockNode block, Boolean reverse, boolean cleanOnly) { + if (!block.getInstructions().isEmpty() || (!reverse && block.getPredecessors().size() > 1) + || (reverse && block.getCleanSuccessors().size() > 1)) { return null; } - List successors = block.getCleanSuccessors(); - if (successors.size() != 1) { + List nextBlocks = reverse ? block.getPredecessors() : (cleanOnly ? block.getCleanSuccessors() : block.getSuccessors()); + if (nextBlocks.size() != 1) { return null; } - return successors.get(0); + return nextBlocks.get(0); } /** @@ -1138,6 +1428,12 @@ public class BlockUtils { return false; } + public static void removeInstructions(List blocks) { + for (IBlock block : blocks) { + block.getInstructions().clear(); + } + } + public static boolean insertBeforeInsn(BlockNode block, InsnNode insn, InsnNode newInsn) { int index = getInsnIndexInBlock(block, insn); if (index == -1) { diff --git a/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java new file mode 100644 index 000000000..4574cf9c4 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/DotGraphUtils.java @@ -0,0 +1,514 @@ +package jadx.core.utils; + +import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import jadx.api.ICodeWriter; +import jadx.api.JavaMethod; +import jadx.api.impl.SimpleCodeWriter; +import jadx.api.plugins.input.data.IMethodRef; +import jadx.core.codegen.MethodGen; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.IAttributeNode; +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.nodes.BlockNode; +import jadx.core.dex.nodes.ClassNode; +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.SwitchRegion; +import jadx.core.dex.regions.SynchronizedRegion; +import jadx.core.dex.regions.TryCatchRegion; +import jadx.core.dex.regions.conditions.IfRegion; +import jadx.core.dex.regions.loops.LoopRegion; +import jadx.core.dex.trycatch.ExceptionHandler; +import jadx.core.dex.visitors.SaveCode; +import jadx.core.utils.files.FileUtils; + +import static jadx.core.codegen.MethodGen.FallbackOption.BLOCK_DUMP; + +public class DotGraphUtils { + private static final String NL = "\\l"; + private static final String NLQR = Matcher.quoteReplacement(NL); + private static final boolean PRINT_DOMINATORS = false; + private static final boolean PRINT_DOMINATORS_INFO = false; + private static final int MAX_REGION_NAME_LENGTH = 2000; + + private final ICodeWriter dot = new SimpleCodeWriter(); + private final ICodeWriter conn = new SimpleCodeWriter(); + + private final boolean useRegions; + private final boolean rawInsn; + + // if present, this region and it's children will still be drawn when not in regions mode. + private Optional highlightRegion; + + // flag set when the highlighted region has been processed once, to avoid processing it's children + // more than once + private boolean processedHighlightRegion = false; + + public DotGraphUtils(boolean useRegions, boolean rawInsn) { + this(useRegions, rawInsn, Optional.empty()); + } + + public DotGraphUtils(boolean useRegions, boolean rawInsn, Optional highlightRegion) { + this.useRegions = useRegions; + this.rawInsn = rawInsn; + this.highlightRegion = highlightRegion; + } + + // The default out directory for the method + public static File getOutDir(MethodNode mth) { + return mth.root().getArgs().getOutDir(); + } + + // The filename the method cfg would be stored in under the default out directory + public File getFullFile(MethodNode mth) { + return getFullFile(mth, getOutDir(mth)); + } + + // The filename the method cfg would be stored in under the given out directory + public File getFullFile(MethodNode mth, File outDir) { + String fileName = StringUtils.escape(mth.getMethodInfo().getShortId()) + + (useRegions ? ".regions" : "") + + (rawInsn ? ".raw" : "") + + ".dot"; + File file = outDir.toPath() + .resolve(mth.getParentClass().getClassInfo().getAliasFullPath() + "_graphs") + .resolve(fileName) + .toFile(); + file = FileUtils.cutFileName(file); + return file; + } + + public void dumpToFile(MethodNode mth) { + File dir = getOutDir(mth); + dumpToFile(mth, dir); + } + + public void dumpToFile(MethodNode mth, File dir) { + String graph = dumpToString(mth); + + if (graph == null) { + return; + } + + File file = getFullFile(mth, dir); + SaveCode.save(graph, file); + } + + public String dumpToString(MethodNode mth) { + dot.startLine("digraph \"CFG for"); + dot.add(escape(mth.getMethodInfo().getFullId())); + dot.add("\" {"); + + BlockNode enterBlock = mth.getEnterBlock(); + if (useRegions) { + if (mth.getRegion() == null) { + return null; + } + processMethodRegion(mth); + } else { + List blocks = mth.getBasicBlocks(); + if (blocks == null) { + InsnNode[] insnArr = mth.getInstructions(); + if (insnArr == null) { + return null; + } + BlockNode block = new BlockNode(0, 0, 0); + List insnList = block.getInstructions(); + for (InsnNode insn : insnArr) { + if (insn != null) { + insnList.add(insn); + } + } + enterBlock = block; + blocks = Collections.singletonList(block); + } + for (BlockNode block : blocks) { + if (processedHighlightRegion && highlightRegion.isPresent() + && RegionUtils.isRegionContainsBlock(highlightRegion.get(), block)) { + // Don't process blocks in the highlight region if it's already been processed, since processing the + // region will already process all it's containing blocks. + continue; + } + + processBlock(mth, block); + + } + } + + dot.startLine("MethodNode[shape=record,label=\"{"); + dot.add(escape(mth.getAccessFlags().makeString(true))); + dot.add(escape(mth.getReturnType() + " " + + mth.getParentClass() + '.' + mth.getName() + + '(' + Utils.listToString(mth.getAllArgRegs()) + ") ")); + + String attrs = attributesString(mth); + if (!attrs.isEmpty()) { + dot.add(" | ").add(attrs); + } + dot.add("}\"];"); + + dot.startLine("MethodNode -> ").add(makeName(enterBlock)).add(';'); + + dot.add(conn.toString()); + + dot.startLine('}'); + dot.startLine(); + + return dot.finish().getCodeStr(); + } + + private void processMethodRegion(MethodNode mth) { + Set regionsBlocks = new HashSet<>(mth.getBasicBlocks().size()); + RegionUtils.getAllRegionBlocks(mth.getRegion(), regionsBlocks); + for (ExceptionHandler handler : mth.getExceptionHandlers()) { + IContainer handlerRegion = handler.getHandlerRegion(); + if (handlerRegion != null) { + RegionUtils.getAllRegionBlocks(handlerRegion, regionsBlocks); + } + } + + processRegion(mth, mth.getRegion(), regionsBlocks); + for (ExceptionHandler h : mth.getExceptionHandlers()) { + if (h.getHandlerRegion() != null) { + processRegion(mth, h.getHandlerRegion(), regionsBlocks); + } + } + + for (BlockNode block : mth.getBasicBlocks()) { + if (!regionsBlocks.contains(block)) { + processBlock(mth, block, true, false); + } + } + } + + private void processRegion(MethodNode mth, IContainer region, Set regionsBlocks) { + if (region instanceof IRegion) { + IRegion r = (IRegion) region; + dot.startLine("subgraph " + makeName(region) + " {"); + dot.startLine("color = " + getColorForRegion(r)); + dot.startLine("label = \"").add(truncateRegionName(r)); + dot.add("\";"); + dot.startLine("node [shape=record,color=blue];"); + + for (IContainer c : r.getSubBlocks()) { + processRegion(mth, c, regionsBlocks); + } + + dot.startLine('}'); + } else if (region instanceof BlockNode) { + checkAndFixFloatingBlocks(mth, (BlockNode) region, regionsBlocks); + processBlock(mth, (BlockNode) region); + } else if (region instanceof IBlock) { + processIBlock(mth, (IBlock) region); + } + } + + private String getColorForRegion(IRegion region) { + if (region instanceof IfRegion) { + return "lightgoldenrod3"; + } else if (region instanceof LoopRegion) { + return "lightpink2"; + } else if (region instanceof SwitchRegion) { + return "lightsteelblue3"; + } else if (region instanceof SynchronizedRegion) { + return "mediumpurple3"; + } else if (region instanceof TryCatchRegion) { + return "olivedrab4"; + } else if (region.contains(AType.EXC_HANDLER)) { + return "orangered4"; + } + return "gray"; + } + + private String truncateRegionName(IRegion r) { + String regionName = r.toString(); + String attrs = attributesString(r); + if (!attrs.isEmpty()) { + regionName += " | " + attrs; + } + if (regionName.length() > MAX_REGION_NAME_LENGTH) { + regionName = regionName.substring(0, MAX_REGION_NAME_LENGTH); + regionName += "..."; + } + return regionName; + } + + /** + * A block is floating if it exists in no regions at all. These are placed in a region that makes + * sense for generation of this graph only, because otherwise the generated graph is unreadable. + */ + private void checkAndFixFloatingBlocks(MethodNode mth, BlockNode block, Set regionBlocks) { + if (regionBlocks == null || regionBlocks.isEmpty()) { + return; + } + + // Heuristic: place the floating block in the same region as either it's predecessor or successor, + // depending on which it has less of. This results in a more readable graph as a block with a single + // predecessor will be placed near it. + for (BlockNode floating : block.getSuccessors()) { + if (!regionBlocks.contains(floating) && floating.getPredecessors().size() <= floating.getSuccessors().size()) { + // Set true on the pseudoInRegion to draw the block with a dotted outline and apply a marker to it + // to notify that it isn't actually in this region. + processBlock(mth, floating, true, true); + regionBlocks.add(floating); + } + } + + for (BlockNode floating : block.getPredecessors()) { + if (!regionBlocks.contains(floating) && floating.getPredecessors().size() > floating.getSuccessors().size()) { + processBlock(mth, floating, true, true); + regionBlocks.add(floating); + } + } + } + + private void processBlock(MethodNode mth, BlockNode block) { + processBlock(mth, block, false, false); + } + + private void processBlock(MethodNode mth, BlockNode block, boolean error, boolean pseudoInRegion) { + if (!processedHighlightRegion && highlightRegion.isPresent() + && RegionUtils.isRegionContainsBlock(highlightRegion.get(), block)) { + processedHighlightRegion = true; + processRegion(mth, highlightRegion.get(), null); + return; + } + + boolean isMthStart = block.contains(AFlag.MTH_ENTER_BLOCK); + boolean isMthEnd = block.contains(AFlag.MTH_EXIT_BLOCK); + + if (isMthEnd) { + dot.startLine("subgraph { rank = sink; "); + } + + dot.startLine(makeName(block)); + dot.add(" [shape=record,"); + if (error) { + dot.add("color=red,"); + } + if (pseudoInRegion) { + dot.add("style = \"filled,dashed\""); + } else { + dot.add("style = filled,"); + } + if (isMthStart || isMthEnd) { + dot.add("fillcolor = \"#def3fd\","); + } else { + dot.add("fillcolor = \"#f8fafb\","); + } + dot.add("label=\"{"); + dot.add(String.valueOf(block.getCId())).add("\\:\\ "); + dot.add(InsnUtils.formatOffset(block.getStartOffset())); + if (pseudoInRegion) { + dot.add("\\nNOT IN ANY REGION"); + } + + String attrs = attributesString(block); + if (!attrs.isEmpty()) { + dot.add('|').add(attrs); + } + + if (PRINT_DOMINATORS_INFO) { + dot.add('|'); + dot.startLine("doms: ").add(escape(block.getDoms())); + dot.startLine("\\lidom: ").add(escape(block.getIDom())); + dot.startLine("\\lpost-doms: ").add(escape(block.getPostDoms())); + dot.startLine("\\lpost-idom: ").add(escape(block.getIPostDom())); + dot.startLine("\\ldom-f: ").add(escape(block.getDomFrontier())); + dot.startLine("\\ldoms-on: ").add(escape(Utils.listToString(block.getDominatesOn()))); + dot.startLine("\\l"); + } + String insns = insertInsns(mth, block); + if (!insns.isEmpty()) { + dot.add('|').add(insns); + } + dot.add("}\"];"); + + if (isMthEnd) { + dot.add("};"); + } + + BlockNode falsePath = null; + 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]" : ""; + addEdge(block, next, style); + } + + if (PRINT_DOMINATORS) { + for (BlockNode c : block.getDominatesOn()) { + conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];"); + } + for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) { + conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];"); + } + } + } + + private void processIBlock(MethodNode mth, IBlock block) { + processIBlock(mth, block, false); + } + + private void processIBlock(MethodNode mth, IBlock block, boolean error) { + String attrs = attributesString(block); + dot.startLine(makeName(block)); + dot.add(" [shape=record,"); + if (error) { + dot.add("color=red,"); + } + dot.add("label=\"{"); + if (!attrs.isEmpty()) { + dot.add(attrs); + } + String insns = insertInsns(mth, block); + if (!insns.isEmpty()) { + dot.add('|').add(insns); + } + dot.add("}\"];"); + } + + private void addEdge(BlockNode from, BlockNode to, String style) { + conn.startLine(makeName(from)).add(" -> ").add(makeName(to)); + conn.add(style); + conn.add(';'); + } + + private String attributesString(IAttributeNode block) { + StringBuilder attrs = new StringBuilder(); + for (String attr : block.getAttributesStringsList()) { + attrs.append(escape(attr)).append(NL); + } + return attrs.toString(); + } + + private String makeName(IContainer c) { + String name; + if (c instanceof BlockNode) { + name = "Node_" + ((BlockNode) c).getCId(); + } else if (c instanceof IBlock) { + name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode(); + } else { + name = "cluster_" + c.getClass().getSimpleName() + '_' + c.hashCode(); + } + return name; + } + + private String insertInsns(MethodNode mth, IBlock block) { + if (rawInsn) { + StringBuilder sb = new StringBuilder(); + for (InsnNode insn : block.getInstructions()) { + sb.append(escape(insn)).append(NL); + } + return sb.toString(); + } else { + ICodeWriter code = new SimpleCodeWriter(); + List instructions = block.getInstructions(); + MethodGen.addFallbackInsns(code, mth, instructions.toArray(new InsnNode[0]), BLOCK_DUMP); + // For some reason, instructions here get put through an additional step of unescaping + String str = escape(code.newLine().toString()); + if (str.startsWith(NL)) { + str = str.substring(NL.length()); + } + return str; + } + } + + private String escape(Object obj) { + if (obj == null) { + return "null"; + } + return escape(obj.toString()); + } + + private String escape(String string) { + return escape(string, NLQR); + } + + private String escape(String string, String newline) { + return string + .replace("\\", "") // TODO replace \" + .replace("/", "\\/") + .replace(">", "\\>").replace("<", "\\<") + .replace("{", "\\{").replace("}", "\\}") + .replace("\"", "\\\"") + .replace("-", "\\-") + .replace("|", "\\|") + .replaceAll("\\R", newline); + } + + // Consistently format names for graphs + + public static String classFormatName(ClassNode cls, boolean longName) { + return classFormatName(cls.getClassInfo(), longName); + } + + public static String classFormatName(ClassInfo cls, boolean longName) { + return longName ? cls.getAliasFullName() : cls.getAliasShortName(); + } + + public static String methodFormatName(JavaMethod javaMethod, boolean longName) { + return methodFormatName(javaMethod.getMethodNode(), longName); + } + + public static String methodFormatName(MethodNode methodNode, boolean longName) { + if (longName) { + ClassNode parentClass = methodNode.getParentClass(); + List argTypes = methodNode.getArgTypes(); + ArgType retType = methodNode.getReturnType(); + return classFormatName(parentClass, true) + "." + methodFormatName(methodNode, false) + + '(' + Utils.listToString(argTypes, ", ", e -> argTypeFormatName(e, parentClass, true)) + "):" + + argTypeFormatName(retType, parentClass, true); + } + return methodNode.getAlias(); + } + + public static String unresolvedMethodFormatName(IMethodRef methodRef, boolean longName) { + String name = methodRef.getName(); + if (longName) { + String className = methodRef.getParentClassType(); + className = Utils.cleanObjectName(className); + + String returnName = methodRef.getReturnType(); + returnName = Utils.smaliNameToJavaName(returnName); + + List argTypes = methodRef.getArgTypes(); + argTypes = argTypes.stream().map(c -> Utils.smaliNameToJavaName(c)).collect(Collectors.toList()); + + return String.format("%s.%s(%s):%s", className, name, Utils.listToString(argTypes), returnName); + } + return name; + } + + public static String interfaceFormatName(ArgType iface, ClassNode cls, boolean longName) { + ClassInfo ifaceInfo = ClassInfo.fromType(cls.root(), iface); + return longName ? ifaceInfo.getAliasFullName() : ifaceInfo.getAliasShortName(); + } + + public static String argTypeFormatName(ArgType arg, ClassNode cls, boolean longName) { + if (arg.isObject() && !arg.isGenericType()) { + ClassNode superCls = cls.root().resolveClass(arg); + if (superCls != null) { + return DotGraphUtils.classFormatName(superCls, longName); + } + } + return arg.toString(); + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/Pair.java b/jadx-core/src/main/java/jadx/core/utils/Pair.java new file mode 100644 index 000000000..2a5f38242 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/Pair.java @@ -0,0 +1,42 @@ +package jadx.core.utils; + +public class Pair { + + private final T first; + private final T second; + + public Pair(T first, T second) { + this.first = first; + this.second = second; + } + + public T getFirst() { + return first; + } + + public T getSecond() { + return second; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Pair)) { + return false; + } + Pair other = (Pair) o; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return first.hashCode() + 31 * second.hashCode(); + } + + @Override + public String toString() { + return "(" + first + ", " + second + ')'; + } +} diff --git a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java index 9d16e8040..267bcb9cf 100644 --- a/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/RegionUtils.java @@ -81,6 +81,27 @@ public class RegionUtils { } } + @Nullable + public static IBlock getFirstBlock(IContainer container) { + if (container == null) { + return null; + } + if (container instanceof IBlock) { + return (IBlock) container; + } else if (container instanceof IBranchRegion) { + return null; + } else if (container instanceof IRegion) { + IRegion region = (IRegion) container; + List blocks = region.getSubBlocks(); + if (blocks.isEmpty()) { + return null; + } + return getFirstBlock(blocks.get(0)); + } else { + throw new JadxRuntimeException(unknownContainerType(container)); + } + } + public static @Nullable BlockNode getFirstBlockNode(IContainer container) { if (container instanceof IBlock) { if (container instanceof BlockNode) { @@ -421,6 +442,16 @@ public class RegionUtils { return Collections.emptyList(); } + public static List getLoopsStartInRegion(MethodNode mth, IRegion r) { + List loops = new ArrayList<>(); + visitBlocks(mth, r, b -> { + if (b.contains(AFlag.LOOP_START)) { + loops.addAll(b.getAll(AType.LOOP)); + } + }); + return loops; + } + private static boolean isRegionContainsExcHandlerRegion(IContainer container, IRegion region) { if (container == region) { return true; @@ -600,4 +631,14 @@ public class RegionUtils { } return subBlocks.get(index + 1); } + + // Add a flag to all blocks in a region + public static void addToAll(MethodNode mth, IContainer container, AFlag flag) { + RegionUtils.visitBlocks(mth, container, new Consumer() { + @Override + public void accept(IBlock t) { + t.add(flag); + } + }); + } } 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 229b43053..c142b2bd3 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -60,6 +60,104 @@ public class Utils { return 'L' + obj.replace('.', '/') + ';'; } + public static String smaliNameToJavaName(String descString) { + if (descString.isEmpty()) { + return descString; + } + + String javaName; + switch (descString.charAt(0)) { + case 'V': + javaName = "void"; + break; + case 'Z': + javaName = "boolean"; + break; + case 'C': + javaName = "char"; + break; + case 'B': + javaName = "byte"; + break; + case 'S': + javaName = "short"; + break; + case 'I': + javaName = "int"; + break; + case 'F': + javaName = "float"; + break; + case 'J': + javaName = "long"; + break; + case 'D': + javaName = "double"; + break; + case 'L': + javaName = cleanObjectNameWithInnerClass(descString); + break; + case '[': + javaName = String.format("%s[]", smaliNameToJavaName(descString.substring(1, descString.length()))); + break; + default: + javaName = descString; + break; + } + return javaName; + } + + private static String cleanObjectNameWithInnerClass(String obj) { + // Probably can just update the Utils.cleanObjectName method? + String result = Utils.cleanObjectName(obj); + return result.replace('$', '.'); + } + + public static String javaNameToSmaliName(String descString) { + if (descString.isEmpty()) { + return descString; + } + + if (descString.endsWith("[]")) { + return String.format("[%s", javaNameToSmaliName(descString.substring(0, descString.length() - 2))); + } + + String javaName; + switch (descString) { + case "void": + javaName = "V"; + break; + case "boolean": + javaName = "Z"; + break; + case "char": + javaName = "C"; + break; + case "byte": + javaName = "B"; + break; + case "short": + javaName = "S"; + break; + case "int": + javaName = "I"; + break; + case "float": + javaName = "F"; + break; + case "long": + javaName = "J"; + break; + case "double": + javaName = "D"; + break; + default: + javaName = Utils.makeQualifiedObjectName(descString); + break; + } + return javaName; + } + @SuppressWarnings("StringRepeatCanBeUsed") public static String strRepeat(String str, int count) { if (count < 1) { diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index 85940ff50..cbd2265f9 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -47,6 +47,7 @@ public class FileUtils { public static final int READ_BUFFER_SIZE = 8 * 1024; private static final int MAX_FILENAME_LENGTH = 128; + private static final int MAX_UNIQUE_ID_LENGTH = 3; public static final String JADX_TMP_INSTANCE_PREFIX = "jadx-instance-"; public static final String JADX_TMP_PREFIX = "jadx-tmp-"; @@ -366,17 +367,23 @@ public class FileUtils { return saveFile; } - private static File cutFileName(File file) { + public static File cutFileName(File file) { String name = file.getName(); if (name.length() <= MAX_FILENAME_LENGTH) { return file; } + + String uniqueID = String.valueOf(name.hashCode()); + if (uniqueID.length() > MAX_UNIQUE_ID_LENGTH) { + uniqueID = uniqueID.substring(0, MAX_UNIQUE_ID_LENGTH); + } int dotIndex = name.indexOf('.'); - int cutAt = MAX_FILENAME_LENGTH - name.length() + dotIndex - 1; + int lengthOfSuffix = name.length() - dotIndex; + int cutAt = MAX_FILENAME_LENGTH - lengthOfSuffix - uniqueID.length() - 1; if (cutAt <= 0) { name = name.substring(0, MAX_FILENAME_LENGTH - 1); } else { - name = name.substring(0, cutAt) + name.substring(dotIndex); + name = name.substring(0, cutAt) + uniqueID + name.substring(dotIndex); } return new File(file.getParentFile(), name); } diff --git a/jadx-core/src/test/java/jadx/core/dex/trycatch/TryCatchBlockAttrTest.java b/jadx-core/src/test/java/jadx/core/dex/trycatch/TryCatchBlockAttrTest.java new file mode 100644 index 000000000..f4d779102 --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/dex/trycatch/TryCatchBlockAttrTest.java @@ -0,0 +1,10 @@ +package jadx.core.dex.trycatch; + +import org.junit.jupiter.api.Nested; + +public class TryCatchBlockAttrTest { + @Nested + public class TryCatchBlockAttrIntegration { + + } +} 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 1bdd1461c..64fa8a1a0 100644 --- a/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/SmaliTest.java @@ -14,7 +14,6 @@ import jadx.api.JadxInternalAccess; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; -import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; import static org.assertj.core.api.Assertions.assertThat; public abstract class SmaliTest extends IntegrationTest { diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf4.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf4.java new file mode 100644 index 000000000..b4df6045e --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestComplexIf4.java @@ -0,0 +1,16 @@ +package jadx.tests.integration.conditions; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestComplexIf4 extends SmaliTest { + @Test + void test() { + disableCompilation(); + allowWarnInCode(); // this is just to allow a harmless duplicated region warning + assertThat(getClassNodeFromSmali()).code().contains("if (0 >= 0) {"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfAndSwitch.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfAndSwitch.java new file mode 100644 index 000000000..97a2f71e4 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfAndSwitch.java @@ -0,0 +1,44 @@ +package jadx.tests.integration.conditions; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; +import jadx.tests.api.utils.assertj.JadxAssertions; + +public class TestIfAndSwitch extends SmaliTest { + + /* @formatter:off + private final static int C = 0; + + private static int i; + private static final Random rd = new Random(); + private static final int ACTION_MOVE = 2; + + public static boolean ifAndSwitch() { + boolean update = false; + if (rd.nextInt() == ACTION_MOVE) { + switch (i) { + case C: + update = true; + break; + } + } + if (update) { + return true; + } + return false; + } + @formatter:on */ + + @Test + public void test() { + allowWarnInCode(); + JadxAssertions.assertThat(getClassNodeFromSmali()) + .code() + .countString(1, "if (rd.nextInt() == ACTION_MOVE) {") + .countString(1, "switch (") + .countString(1, "else {"); + + } + +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfElseAndConditionIntermediateInstruction.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfElseAndConditionIntermediateInstruction.java new file mode 100644 index 000000000..7f07e53e3 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestIfElseAndConditionIntermediateInstruction.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.conditions; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; +import jadx.tests.api.utils.assertj.JadxAssertions; + +// Here there are two IF blocks for each part of the IF predicate. +// In some cases with optimised dex, two IF blocks cannot merged into the same IF region. +// This happens where there are intermediate instructions between the two blocks which cannot be +// inlined. +// Both IF blocks share the same ELSE block which is then added to both resultant regions. +// The resultant code does not reproduce the single if-else statement but it is better than failing +// to decompile. +public class TestIfElseAndConditionIntermediateInstruction extends SmaliTest { + + /* @formatter:off + private boolean bool; + private float num; + private static final float CONST = 342; + + public void function() { + if (bool && num < 1) { + num += CONST; + } else { + nothing2(); + } + nothing1(); + } + + private void nothing1() { + + } + + private void nothing2() { + + } + @formatter:on */ + + @Test + public void test() { + allowWarnInCode(); + JadxAssertions.assertThat(getClassNodeFromSmali()) + .code() + .countString(2, "else") + .countString(2, "nothing2();") + .countString(1, "nothing1();") + .countString(1, "if (this.bool)") + .countString(1, "if (f < 1.0f)"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop4.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop4.java new file mode 100644 index 000000000..d7953058f --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop4.java @@ -0,0 +1,51 @@ +package jadx.tests.integration.loops; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; +import jadx.tests.api.utils.assertj.JadxAssertions; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestBreakInLoop4 extends IntegrationTest { + + public static class TestCls { + + public double test(char c) { + double m = 1.0; + for (int i = 0; i < 5; i++) { + if (c != '.') { + if (c == 'a' || c == 'b') { + m = 1024.0; + } + break; + } + } + return m; + } + + public void check() { + assertThat(test('.')).isEqualTo(1.0); + assertThat(test('a')).isEqualTo(1024.0); + assertThat(test('b')).isEqualTo(1024.0); + assertThat(test('c')).isEqualTo(1.0); + } + } + + @Test + public void test() { + JadxAssertions.assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("while") + .containsOne("for"); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + JadxAssertions.assertThat(getClassNode(TestCls.class)) + .code() + .doesNotContain("while") + .containsOne("for"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop5.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop5.java new file mode 100644 index 000000000..4caeb4649 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop5.java @@ -0,0 +1,58 @@ +package jadx.tests.integration.loops; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; +import jadx.tests.api.utils.assertj.JadxAssertions; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestBreakInLoop5 extends IntegrationTest { + + public static class TestCls { + + public long test(String spaceStr) { + try { + double multiplier = 1.0; // Often the compiler will move this line to each of the loop exits creating complexity + char c; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < spaceStr.length(); i++) { + c = spaceStr.charAt(i); + if (!Character.isDigit(c) && c != '.') { + if (c == 'm' || c == 'M') { + multiplier = 1024.0; + } else if (c == 'g' || c == 'G') { + multiplier = 1024.0 * 1024.0; + } + break; + } + sb.append(spaceStr.charAt(i)); + } + return (long) Math.ceil(Double.valueOf(sb.toString()) * multiplier); + } catch (Exception e) { + return -1; + } + } + + public void check() { + assertThat(test("1.2")).isEqualTo(2); + assertThat(test("12am")).isEqualTo(12); + assertThat(test("13m")).isEqualTo(13 * 1024); + assertThat(test("1G4")).isEqualTo(1 * 1024 * 1024); + assertThat(test("")).isEqualTo(-1); + } + } + + @Test + public void test() { + JadxAssertions.assertThat(getClassNode(TestCls.class)) + .code(); + } + + @Test + public void testNoDebug() { + noDebugInfo(); + JadxAssertions.assertThat(getClassNode(TestCls.class)) + .code(); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop6.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop6.java new file mode 100644 index 000000000..4847cb9cb --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakInLoop6.java @@ -0,0 +1,20 @@ +package jadx.tests.integration.loops; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +/** + * Test that a continue is not erroneously inserted where a break edge instruction already exists, + * leading to the continue being lost and causing a decompile failure. + */ +public class TestBreakInLoop6 extends SmaliTest { + @Test + public void test() { + disableCompilation(); + assertThat(getClassNodeFromSmali()).code().containsOne("break"); + } + +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop4.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop4.java new file mode 100644 index 000000000..ccbbe68f5 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestIfInLoop4.java @@ -0,0 +1,24 @@ +package jadx.tests.integration.loops; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.SmaliTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestIfInLoop4 extends SmaliTest { + + /* + * Test handling of edge instructions when generated from a loop near an if statement with no else. + * They should not be added to an else region, since the if statement has no else. + * The actual condition here is less important than if decompilation succeeds at all. + */ + @Test + public void test() { + disableCompilation(); + assertThat(getClassNodeFromSmaliWithPath("loops", "TestIfInLoop4")) + .code() + .containsOne("return true;"); + } + +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops4.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops4.java index d6efaff3b..d00ecf052 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops4.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNestedLoops4.java @@ -2,7 +2,6 @@ package jadx.tests.integration.loops; import org.junit.jupiter.api.Test; -import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @@ -37,7 +36,6 @@ public class TestNestedLoops4 extends IntegrationTest { } @Test - @NotYetImplemented public void test() { assertThat(getClassNode(TestCls.class)) .code() diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNotIndexedLoop.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNotIndexedLoop.java index b24a0c2f8..5e974502e 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestNotIndexedLoop.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestNotIndexedLoop.java @@ -23,7 +23,7 @@ public class TestNotIndexedLoop extends IntegrationTest { int i = 0; while (true) { if (i >= length) { - file = null; + file = new File("h"); break; } file = files[i]; @@ -48,6 +48,8 @@ public class TestNotIndexedLoop extends IntegrationTest { File file = new File("f"); assertThat(test(new File[] { new File("a"), file })).isEqualTo(file); + + assertThat(test(new File[] { new File("a") }).getName()).isEqualTo("h"); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop6.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop6.java new file mode 100644 index 000000000..5a7085e6d --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop6.java @@ -0,0 +1,105 @@ +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 TestSwitchInLoop6 extends IntegrationTest { + + public static class TestCls { + private void test() throws Exception { + while (true) { + int n = getN(); + switch (n) { + case 1: + n1(); + return; + case 2: + n2(); + if (getN() == 3) { + return; + } + break; + case 3: + n3(); + return; + case 4: + n4(); + return; + default: + throw new Exception(); + } + } + } + // Output below: + // @formatter:off + /* + public void function() throws Exception { + do { + switch (getN()) { + case 1: + n1(); + return; + case 2: + n2(); + break; + case 3: + n3(); + return; + case 4: + n4(); + return; + default: + throw new Exception(); + } + } while (getN() != 3); + } + */ + // @formatter:on + + void n1() { + } + + void n2() { + } + + void n3() { + } + + void n4() { + } + + private int getN() { + double i = Math.random(); + if (i < 0.25) { + return 1; + } + if (i < 0.5) { + return 2; + } + if (i < 0.75) { + return 3; + } + if (i < 1.0) { + return 4; + } + return -1; + } + } + + @Test + public void test() { + allowWarnInCode(); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("switch (n) {") + .containsOne("case 1:") + .containsOne("case 2:") + .containsOne("case 3:") + .containsOne("case 4:") + .containsOne("do {") + .containsOne("while (getN() != 3)"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop7.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop7.java new file mode 100644 index 000000000..ff611092d --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop7.java @@ -0,0 +1,93 @@ +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 TestSwitchInLoop7 extends IntegrationTest { + + public static class TestCls { + private void test() { + int i = 0; + int n = getN(); + while (true) { + if (i > n) { + break; + } + i++; + if (n == 5) { + continue; + } + switch (n) { + case 0: { + if (n != 1) { + return; + } + break; + } + case 1: + i++; + break; + } + } + return; + } + // Output below: + // @formatter:off + /* + public void function() { + int i = 0; + int n = getN(); + while (i <= n) { + i++; + if (n != 5) { + switch (n) { + case 0: + if (n == 1) { + break; + } else { + return; + } + case 1: + i++; + break; + } + } + } + } + */ + // @formatter:on + + private int getN() { + double i = Math.random(); + if (i < 0.25) { + return 1; + } + if (i < 0.5) { + return 2; + } + if (i < 0.75) { + return 3; + } + if (i < 1.0) { + return 4; + } + return -1; + } + } + + @Test + public void test() { + // Checks that the redundant default continue case is not recovered + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("switch (n) {") + .containsOne("case 0:") + .containsOne("case 1:") + .containsOne("while (") + .doesNotContain("default") + .doesNotContain("contine"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop8.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop8.java new file mode 100644 index 000000000..aa9e87592 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop8.java @@ -0,0 +1,72 @@ +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 TestSwitchInLoop8 extends IntegrationTest { + + public static class TestCls { + private void test() { + int i = 0; + int n = getN(); + while (true) { + if (i > n) { + break; + } + i++; + if (n == 5) { + continue; + } + switch (n) { + case 0: { + if (n != 1) { + return; + } + break; + } + case 1: + i++; + break; + default: + continue; + } + if (i < 2) { + i++; + } + } + return; + } + + private int getN() { + double i = Math.random(); + if (i < 0.25) { + return 1; + } + if (i < 0.5) { + return 2; + } + if (i < 0.75) { + return 3; + } + if (i < 1.0) { + return 4; + } + return -1; + } + } + + @Test + public void test() { + // Checks that the default continue case is not removed + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("switch (n) {") + .containsOne("case 0:") + .containsOne("case 1:") + .containsOne("while (") + .containsOne("default"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop9.java b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop9.java new file mode 100644 index 000000000..1f93e45d6 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/switches/TestSwitchInLoop9.java @@ -0,0 +1,73 @@ +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 TestSwitchInLoop9 extends IntegrationTest { + + public static class TestCls { + private void test() { + int i = 0; + int n = getN(); + while (true) { + if (i > n) { + break; + } + i++; + if (n == 5) { + continue; + } + switch (n) { + case 0: { + if (n != 1) { + return; + } + break; + } + case 1: + i++; + break; + default: + continue; + } + if (i < 2) { + i += 327; + } + } + return; + } + + private int getN() { + double i = Math.random(); + if (i < 0.25) { + return 1; + } + if (i < 0.5) { + return 2; + } + if (i < 0.75) { + return 3; + } + if (i < 1.0) { + return 4; + } + return -1; + } + } + + @Test + public void test() { + // Checks that the work after the switch is recovered only once + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("switch (n) {") + .containsOne("case 0:") + .containsOne("case 1:") + .containsOne("while (") + .containsOne("default") + .containsOne("327"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized5.java b/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized5.java index f2b8b1025..7758a9f66 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/synchronize/TestSynchronized5.java @@ -9,6 +9,7 @@ import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSynchronized5 extends SmaliTest { @Test public void test() { + allowWarnInCode(); assertThat(getClassNodeFromSmali()) .code() .contains("1 != 0") diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch5.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch5.java index 83acd3f3d..5cdf73560 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch5.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestNestedTryCatch5.java @@ -2,6 +2,7 @@ package jadx.tests.integration.trycatch; import org.junit.jupiter.api.Test; +import jadx.NotYetImplemented; import jadx.tests.api.SmaliTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @@ -9,7 +10,20 @@ import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestNestedTryCatch5 extends SmaliTest { @Test + @NotYetImplemented("Extracting finally on loop advancement") public void test() { + disableCompilation(); + assertThat(getClassNodeFromSmali()) + .code() + .doesNotContain("?? ") + .containsOne("} finally") + .containsOne("endTransaction") + .countString(1, "throw "); // 1 real throws, 1 implicit throw on finally handler and 1 implicit throw on empty ALL handler + } + + @Test + public void testNoFinally() { + args.setExtractFinally(false); disableCompilation(); assertThat(getClassNodeFromSmali()) .code() diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch11.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch11.java new file mode 100644 index 000000000..605c797ad --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch11.java @@ -0,0 +1,45 @@ +package jadx.tests.integration.trycatch; + +import jadx.tests.api.IntegrationTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +// Ensure that we can still merge if conditions even in the case where +// the BOTTOM_SPLITTER occurs before the final IF block. +public class TestTryCatch11 extends IntegrationTest { + + public static class TestCls { + public static class Cursor implements AutoCloseable { + @Override + public void close() { + System.out.println("Closed AutoCloseableResources_First"); + } + + public String getString() { + return "jfdkelapgfureiqop[]"; + } + } + + public static String test() { + try (Cursor cursor = new Cursor()) { + String value = cursor.getString(); + if (value.startsWith("content://") || !value.startsWith("/") && !value.startsWith("file://")) { + return null; + } + return value; + } catch (Exception ignore) { + System.out.println("catch"); + } + return null; + } + } + + @TestWithProfiles({ TestProfile.DX_J8, TestProfile.JAVA8 }) + public void test() { + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("value.startsWith(\"content://\") || (!value.startsWith(\"/\") && !value.startsWith(\"file://\"))"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch9.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch9.java index e4ce1b18c..464557793 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch9.java +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatch9.java @@ -39,6 +39,7 @@ public class TestTryCatch9 extends IntegrationTest { assertThat(getClassNode(TestCls.class)) .code() .containsOne("logError(ex);") - .containsOne("Integer res = null;"); + .containsOne("Integer res") + .contains("res = null;"); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally16.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally16.java new file mode 100644 index 000000000..1e7474aab --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally16.java @@ -0,0 +1,45 @@ +package jadx.tests.integration.trycatch; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTryCatchFinally16 extends SmaliTest { + + @SuppressWarnings("unused") + public static class TestCls { + public void test() { + try { + TCls.doSomething(); + } catch (Exception e) { + // do nothing + } finally { + TCls.doFinally(); + } + } + + private static class TCls { + public static void doSomething() { + } + + public static void doFinally() { + } + } + } + + @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) + public void test() { + disableCompilation(); + ClassNode node = getClassNode(TestCls.class); + assertThat(node) + .code() + .containsOne("TCls.doSomething()") + .containsOne("TCls.doFinally()") + .containsOne("finally") + .containsOne("} catch") + .contains("catch (Exception e)"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally17.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally17.java new file mode 100644 index 000000000..18e2c495a --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally17.java @@ -0,0 +1,49 @@ +package jadx.tests.integration.trycatch; + +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTryCatchFinally17 extends SmaliTest { + + @SuppressWarnings("unused") + public static class TestCls { + public int test() { + try { + TCls.doSomething(); + } catch (UnsupportedOperationException e) { + // do nothing + } catch (NullPointerException e) { + return 1; + } finally { + TCls.doFinally(); + } + return 0; + } + + private static class TCls { + public static void doSomething() { + } + + public static void doFinally() { + } + } + } + + @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11, TestProfile.JAVA8 }) + public void test() { + disableCompilation(); + ClassNode node = getClassNode(TestCls.class); + assertThat(node) + .code() + .containsOne("TCls.doSomething()") + .containsOne("TCls.doFinally()") + .containsOne("} finally") + .containsOne("catch (NullPointerException ") + .containsOne("catch (UnsupportedOperationException ") + .doesNotContain("catch (Throwable"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally18.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally18.java new file mode 100644 index 000000000..54664daa9 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally18.java @@ -0,0 +1,75 @@ +package jadx.tests.integration.trycatch; + +import jadx.NotYetImplemented; +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestTryCatchFinally18 extends SmaliTest { + + @SuppressWarnings("unused") + public static class TestCls { + public int test3() { + int val; + try { + val = TCls.doSomething(); + } catch (UnsupportedOperationException e) { + return -1; + } catch (NullPointerException e) { + val = 0; + } finally { + TCls.dispose(); + } + val += 4; + if (val < 10) { + TCls.log("less than 10"); + } + return val; + } + + private static class TCls { + public static int doSomething() { + return 14; + } + + public static void dispose() { + } + + public static void log(String msg) { + + } + } + } + + @TestWithProfiles({ TestProfile.DX_J8, TestProfile.D8_J11 }) + public void test() { + disableCompilation(); + ClassNode node = getClassNode(TestCls.class); + assertThat(node) + .code() + .containsOne("TCls.doSomething()") + .containsOne("TCls.dispose()") + .containsOne("} finally") + .containsOne("catch (NullPointerException ") + .containsOne("catch (UnsupportedOperationException ") + .doesNotContain("catch (Throwable"); + } + + @NotYetImplemented("To be investigated why J8 does not work") + @TestWithProfiles({ TestProfile.JAVA8 }) + public void testJ8() { + disableCompilation(); + ClassNode node = getClassNode(TestCls.class); + assertThat(node) + .code() + .containsOne("TCls.doSomething()") + .containsOne("TCls.dispose()") + .containsOne("} finally") + .containsOne("catch (NullPointerException ") + .containsOne("catch (UnsupportedOperationException ") + .doesNotContain("catch (Throwable"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally19.java b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally19.java new file mode 100644 index 000000000..1a2396943 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/trycatch/TestTryCatchFinally19.java @@ -0,0 +1,57 @@ +package jadx.tests.integration.trycatch; + +import jadx.NotYetImplemented; +import jadx.core.dex.nodes.ClassNode; +import jadx.tests.api.SmaliTest; +import jadx.tests.api.extensions.profiles.TestProfile; +import jadx.tests.api.extensions.profiles.TestWithProfiles; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +/** + * A class which tests finally extraction for cases where the all handler does not rethrow an + * exception. + */ +public class TestTryCatchFinally19 extends SmaliTest { + + @SuppressWarnings("unused") + public static class TestCls { + public Integer test() { + Integer val; + try { + return TCls.doSomething(); + } catch (Throwable t) { + return null; + } finally { + TCls.dispose(); + } + } + + private static class TCls { + public static int doSomething() { + return 14; + } + + public static void dispose() { + } + + public static void log(String msg) { + + } + } + } + + @TestWithProfiles({ TestProfile.D8_J11 }) + @NotYetImplemented("Currently only processing finally blocks if the all handler throws.") + public void testDXJ8() { + disableCompilation(); + ClassNode node = getClassNode(TestCls.class); + assertThat(node) + .code() + .containsOne("TCls.doSomething()") + .containsOne("TCls.dispose()") + .containsOne("} finally") + .containsOne("catch (Throwable ") + .containsOne("return null"); + } +} diff --git a/jadx-core/src/test/smali/conditions/TestComplexIf4.smali b/jadx-core/src/test/smali/conditions/TestComplexIf4.smali new file mode 100644 index 000000000..fea288090 --- /dev/null +++ b/jadx-core/src/test/smali/conditions/TestComplexIf4.smali @@ -0,0 +1,59 @@ +.class public final Lconditions/TestComplexIf4; +.super Ljava/lang/Object; + +# virtual methods +.method public final test()V + .registers 14 + + const/4 v1, 0x0 + const/16 v6, 0x0 + + :loop_0 + if-ge v1, v1, :cond_0 + goto :loop_0 + + :cond_0 + + if-lt v1, v6, :cond_1 + goto/16 :goto_0 + :cond_1 + + cmp-long v2, v6, v6 + if-nez v2, :cond_2 + + goto/16 :near_end + + :cond_2 + + if-le v2, v1, :cond_3 + if-lez v1, :cond_4 + goto :near_end + + :cond_4 + const/4 v5, 0x0 + + if-ge v5, v1, :cond_5 + :cond_5 + cmp-long v5, v6, v6 + + if-ltz v5, :cond_3 + goto :near_end + + :cond_3 + cmp-long v5, v6, v6 + + :goto_0 + if-eqz v1, :near_end + + const/4 v1, 0x0 + + goto :cond_6 + + :near_end + const/4 v1, 0x0 + + :cond_6 + if-ne v1, v1, :cond_magic + :cond_magic + const/4 v1, 0x0 +.end method \ No newline at end of file diff --git a/jadx-core/src/test/smali/conditions/TestIfAndSwitch.smali b/jadx-core/src/test/smali/conditions/TestIfAndSwitch.smali new file mode 100644 index 000000000..59895b00d --- /dev/null +++ b/jadx-core/src/test/smali/conditions/TestIfAndSwitch.smali @@ -0,0 +1,95 @@ +###### Class conditions.TestIfAndSwitch (conditions.TestIfAndSwitch) +.class Lconditions/TestIfAndSwitch; +.super Ljava/lang/Object; +.source "TestIfAndSwitch.java" + + +# static fields +.field private static final ACTION_MOVE:I = 0x2 + +.field private static final C:I + +.field private static i:I + +.field private static final rd:Ljava/util/Random; + + +# direct methods +.method static constructor ()V + .registers 1 + + .line 8 + new-instance v0, Ljava/util/Random; + + invoke-direct {v0}, Ljava/util/Random;->()V + + sput-object v0, Lconditions/TestIfAndSwitch;->rd:Ljava/util/Random; + + return-void +.end method + +.method constructor ()V + .registers 1 + + .line 3 + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method public static ifAndSwitch()Z + .registers 4 + + .line 12 + nop + + .line 13 + sget-object v0, Lconditions/TestIfAndSwitch;->rd:Ljava/util/Random; + + invoke-virtual {v0}, Ljava/util/Random;->nextInt()I + + move-result v0 + + const/4 v1, 0x2 + + const/4 v2, 0x1 + + const/4 v3, 0x0 + + if-ne v0, v1, :cond_14 + + .line 14 + sget v0, Lconditions/TestIfAndSwitch;->i:I + + packed-switch v0, :pswitch_data_1a + + goto :goto_14 + + .line 16 + :pswitch_12 + const/4 v0, 0x1 + + goto :goto_15 + + .line 20 + :cond_14 + :goto_14 + const/4 v0, 0x0 + + :goto_15 + if-eqz v0, :cond_18 + + .line 21 + return v2 + + .line 23 + :cond_18 + return v3 + + nop + + :pswitch_data_1a + .packed-switch 0x0 + :pswitch_12 + .end packed-switch +.end method diff --git a/jadx-core/src/test/smali/conditions/TestIfElseAndConditionIntermediateInstruction.smali b/jadx-core/src/test/smali/conditions/TestIfElseAndConditionIntermediateInstruction.smali new file mode 100644 index 000000000..ccc51c778 --- /dev/null +++ b/jadx-core/src/test/smali/conditions/TestIfElseAndConditionIntermediateInstruction.smali @@ -0,0 +1,78 @@ +###### Class conditions.TestIfElseAndConditionIntermediateInstruction (conditions.TestIfElseAndConditionIntermediateInstruction) +.class public Lconditions/TestIfElseAndConditionIntermediateInstruction; +.super Ljava/lang/Object; +.source "TestIfElseAndConditionIntermediateInstruction.java" + + +# static fields +.field private static final CONST:F = 342.0f + + +# instance fields +.field private bool:Z + +.field private num:F + + +# direct methods +.method public constructor ()V + .registers 1 + + .line 3 + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method private nothing1()V + .registers 1 + + .line 19 + return-void +.end method + +.method private nothing2()V + .registers 1 + + .line 23 + return-void +.end method + + +# virtual methods +.method public function()V + .registers 3 + + .line 9 + iget-boolean v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->bool:Z + + if-eqz v0, :cond_12 + + iget v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->num:F + + const/high16 v1, 0x3f800000 # 1.0f + + cmpg-float v1, v0, v1 + + if-gez v1, :cond_12 + + .line 10 + const/high16 v1, 0x43ab0000 # 342.0f + + add-float/2addr v0, v1 + + iput v0, p0, Lconditions/TestIfElseAndConditionIntermediateInstruction;->num:F + + goto :goto_15 + + .line 12 + :cond_12 + invoke-direct {p0}, Lconditions/TestIfElseAndConditionIntermediateInstruction;->nothing2()V + + .line 14 + :goto_15 + invoke-direct {p0}, Lconditions/TestIfElseAndConditionIntermediateInstruction;->nothing1()V + + .line 15 + return-void +.end method diff --git a/jadx-core/src/test/smali/loops/TestBreakInLoop6.smali b/jadx-core/src/test/smali/loops/TestBreakInLoop6.smali new file mode 100644 index 000000000..c7f1a8b59 --- /dev/null +++ b/jadx-core/src/test/smali/loops/TestBreakInLoop6.smali @@ -0,0 +1,59 @@ +.class public Lloops/TestBreakInLoop6; +.super Ljava/lang/Object; + + +.method public test()J + .registers 19 + move-object/from16 v0, p0 + const-wide v1, 0x1L + iget-object v3, v0, Ltest/Test;->j:[Ltest/Test; + array-length v4, v3 + const/4 v6, 0x0 + :goto_c + if-ge v6, v4, :cond_64 + + aget-object v7, v3, v6 + invoke-interface {v7}, Ltest/Test;->h()J + + move-result-wide v8 + const-wide v11, 0x2L + + cmp-long v13, v8, v11 + + if-eqz v13, :cond_4e + cmp-long v13, v1, v11 + + if-nez v13, :cond_4e + move-wide v1, v8 + iget-object v11, v0, Ltest/Test;->i:[Ltest/Test; + + array-length v12, v11 + + const/4 v13, 0x0 + + :goto_28 + if-ge v13, v12, :cond_4e + + aget-object v14, v11, v13 + if-ne v14, v7, :cond_2f + goto :cond_4e + :cond_2f + invoke-interface {v14, v1, v2}, Ltest/Test;->f(J)J + move-result-wide v15 + cmp-long v17, v15, v1 + if-nez v17, :cond_3a + add-int/lit8 v13, v13, 0x1 + goto :goto_28 + + :cond_3a + new-instance v3, Ljava/lang/IllegalStateException; + invoke-direct {v3}, Ljava/lang/IllegalStateException;->()V + throw v3 + + :cond_4e + add-int/lit8 v6, v6, 0x1 + goto :goto_c + + :cond_64 + return-wide v1 +.end method \ No newline at end of file diff --git a/jadx-core/src/test/smali/loops/TestIfInLoop4.smali b/jadx-core/src/test/smali/loops/TestIfInLoop4.smali new file mode 100644 index 000000000..0ebafc6b8 --- /dev/null +++ b/jadx-core/src/test/smali/loops/TestIfInLoop4.smali @@ -0,0 +1,30 @@ +.class public LTestIfInLoop4; +.super Ljava/lang/Object; + +.method public test()Z + .registers 5 + + move-object/from16 v0, p0 + + const/4 v2, 0x0 + const/4 v3, 0x1 + + :goto_0 + iget v1, v0, LTestIfInLoop4;->x:I + + if-ge v2, v1, :goto_1 + if-gtz v2, :cond + if-gez v2, :cond + + if-ltz v2, :goto_1 + if-ge v2, v1, :goto_1 + + goto :goto_1 + + :cond + goto :goto_0 + + :goto_1 + return v3 +.end method + diff --git a/jadx-core/src/test/smali/switches/TestSwitchOverStrings3.smali b/jadx-core/src/test/smali/switches/TestSwitchOverStrings3.smali new file mode 100644 index 000000000..fa3587d14 --- /dev/null +++ b/jadx-core/src/test/smali/switches/TestSwitchOverStrings3.smali @@ -0,0 +1,99 @@ +.class public LTestSwitchOverStrings3; +.super Ljava/lang/Object; + +.method public test3(Ljava/lang/String;)I + .registers 5 + + .line 87 + invoke-virtual {p1}, Ljava/lang/String;->hashCode()I + + move-result v0 + + const/4 v1, 0x0 + + const/4 v2, 0x1 + + packed-switch v0, :pswitch_data_38 + + :cond_9 + goto :goto_32 + + :pswitch_a + const-string v0, "branch4" + + invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result p1 + + if-eqz p1, :cond_9 + + const/4 p1, 0x3 + + goto :goto_33 + + :pswitch_14 + const-string v0, "branch3" + + invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result p1 + + if-eqz p1, :cond_9 + + const/4 p1, 0x2 + + goto :goto_33 + + :pswitch_1e + const-string v0, "branch2" + + invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result p1 + + if-eqz p1, :cond_9 + + const/4 p1, 0x1 + + goto :goto_33 + + :pswitch_28 + const-string v0, "branch1" + + invoke-virtual {p1, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result p1 + + if-eqz p1, :cond_9 + + const/4 p1, 0x0 + + goto :goto_33 + + :goto_32 + const/4 p1, -0x1 + + :goto_33 + packed-switch p1, :pswitch_data_44 + + .line 94 + return v1 + + .line 90 + :pswitch_37 + return v2 + + :pswitch_data_38 + .packed-switch 0x8358ecf + :pswitch_28 + :pswitch_1e + :pswitch_14 + :pswitch_a + .end packed-switch + + :pswitch_data_44 + .packed-switch 0x0 + :pswitch_37 + :pswitch_37 + .end packed-switch +.end method diff --git a/jadx-core/src/test/smali/switches/TestSwitchOverStrings4.smali b/jadx-core/src/test/smali/switches/TestSwitchOverStrings4.smali new file mode 100644 index 000000000..3ee7abc8a --- /dev/null +++ b/jadx-core/src/test/smali/switches/TestSwitchOverStrings4.smali @@ -0,0 +1,89 @@ +.class public LTestSwitchOverStrings4; +.super Ljava/lang/Object; + +.method public static test4(Ljava/lang/String;)I + .registers 10 + + const/16 v3, 0x0 + + const/16 v4, -0x1 + + if-nez p0, :cond_26 + + return v4 + + :cond_26 + .line 202 + + invoke-virtual {p0}, Ljava/lang/String;->hashCode()I + + move-result v2 + + sparse-switch v2, :sswitch_data_222 + + const/4 v0, -0x1 + + const/16 v2, 0x13 + + goto/16 :goto_20a + + :sswitch_3b + const/16 v2, 0x13 + + const-string/jumbo v1, "video/x-matroska" + + invoke-virtual {p0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result v0 + + if-nez v0, :cond_48 + + goto/16 :goto_207 + + :cond_48 + const/16 v0, 0x1 + + goto/16 :goto_20a + + :sswitch_1fd + const/16 v2, 0x13 + + const-string v1, "audio/eac3-joc" + + invoke-virtual {p0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result v0 + + if-nez v0, :cond_209 + + :goto_207 + const/4 v0, -0x1 + + goto :goto_20a + + :cond_209 + const/4 v0, 0x0 + + :goto_20a + packed-switch v0, :pswitch_data_29c + + return v4 + + :pswitch_216 + return v2 + + :pswitch_221 + return v3 + + :sswitch_data_222 + .sparse-switch + 0xb269699 -> :sswitch_1fd + 0x79909c15 -> :sswitch_3b + .end sparse-switch + + :pswitch_data_29c + .packed-switch 0x0 + :pswitch_221 + :pswitch_216 + .end packed-switch +.end method diff --git a/jadx-gui/build.gradle.kts b/jadx-gui/build.gradle.kts index 2677881f6..73cd322b2 100644 --- a/jadx-gui/build.gradle.kts +++ b/jadx-gui/build.gradle.kts @@ -50,6 +50,11 @@ dependencies { implementation("org.exbin.auxiliary:binary_data:$bined") implementation("org.exbin.auxiliary:binary_data-array:$bined") + // Library for rendering GraphViz DOT files + implementation("guru.nidi:graphviz-java:0.18.1") + implementation("com.eclipsesource.j2v8:j2v8_linux_x86_64:4.6.0") + implementation("com.eclipsesource.j2v8:j2v8_win32_x86_64:4.6.0") + testImplementation(project.project(":jadx-core").sourceSets.getByName("test").output) } @@ -160,7 +165,7 @@ fun escapeJVMOptions(): List { } runtime { - addOptions("--strip-debug", "--compress", "zip-9", "--no-header-files", "--no-man-pages") + addOptions("--strip-debug", "--no-header-files", "--no-man-pages") addModules( "java.desktop", "java.naming", diff --git a/jadx-gui/src/main/java/jadx/gui/cache/usage/CachedMethodRef.java b/jadx-gui/src/main/java/jadx/gui/cache/usage/CachedMethodRef.java new file mode 100644 index 000000000..13ca8ddcb --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/cache/usage/CachedMethodRef.java @@ -0,0 +1,67 @@ +package jadx.gui.cache.usage; + +import java.util.List; + +import jadx.api.plugins.input.data.IMethodRef; + +public class CachedMethodRef implements IMethodRef { + + private String parentClassType; + private String name; + private String returnType; + private List argTypes; + + public CachedMethodRef(String parentClassType, String name, String returnType, List argTypes) { + this.parentClassType = parentClassType; + this.name = name; + this.returnType = returnType; + this.argTypes = argTypes; + } + + @Override + public String getParentClassType() { + return parentClassType; + } + + public void setParentClassType(String parentClassType) { + this.parentClassType = parentClassType; + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getReturnType() { + return returnType; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + @Override + public List getArgTypes() { + return argTypes; + } + + public void setArgTypes(List argTypes) { + this.argTypes = argTypes; + } + + @Override + public int getUniqId() { + throw new UnsupportedOperationException("Unimplemented method 'getUniqId'"); + } + + @Override + public void load() { + throw new UnsupportedOperationException("Unimplemented method 'load'"); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/cache/usage/CollectUsageData.java b/jadx-gui/src/main/java/jadx/gui/cache/usage/CollectUsageData.java index b9a8c7ed2..49ba47f6d 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/usage/CollectUsageData.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/usage/CollectUsageData.java @@ -2,6 +2,7 @@ package jadx.gui.cache.usage; import java.util.List; +import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoVisitor; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.FieldNode; @@ -40,6 +41,21 @@ final class CollectUsageData implements IUsageInfoVisitor { data.getMethodData(mth).setUsage(mthNodesRef(methods)); } + @Override + public void visitMethodsUses(MethodNode mth, List methods) { + data.getMethodData(mth).setUses(mthNodesRef(methods)); + } + + @Override + public void visitUnresolvedMethodsUsage(MethodNode mth, List methods) { + data.getMethodData(mth).setUnresolvedUsage(methods); + } + + @Override + public void visitIsSelfCall(MethodNode mth, boolean isSelfCall) { + data.getMethodData(mth).setCallsSelf(isSelfCall); + } + @Override public void visitComplete() { data.collectClassesWithoutData(); diff --git a/jadx-gui/src/main/java/jadx/gui/cache/usage/MthUsageData.java b/jadx-gui/src/main/java/jadx/gui/cache/usage/MthUsageData.java index 10dbaf85f..b5da1b76b 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/usage/MthUsageData.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/usage/MthUsageData.java @@ -2,9 +2,14 @@ package jadx.gui.cache.usage; import java.util.List; +import jadx.api.plugins.input.data.IMethodRef; + final class MthUsageData { private final MthRef mthRef; private List usage; + private List uses; + private List unresolvedUsage; + private boolean callsSelf; public MthUsageData(MthRef mthRef) { this.mthRef = mthRef; @@ -21,4 +26,28 @@ final class MthUsageData { public void setUsage(List usage) { this.usage = usage; } + + public List getUses() { + return uses; + } + + public void setUses(List uses) { + this.uses = uses; + } + + public List getUnresolvedUsage() { + return unresolvedUsage; + } + + public void setUnresolvedUsage(List unresolvedUsage) { + this.unresolvedUsage = unresolvedUsage; + } + + public boolean callsSelf() { + return callsSelf; + } + + public void setCallsSelf(boolean callsSelf) { + this.callsSelf = callsSelf; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageData.java b/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageData.java index e34be77bf..61645b43b 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageData.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageData.java @@ -59,6 +59,9 @@ class UsageData implements IUsageInfoData { MthUsageData mthUsageData = mthUsage.get(mth.getMethodInfo().getShortId()); if (mthUsageData != null) { mth.setUseIn(resolveMthList(mthUsageData.getUsage())); + mth.setUsed(resolveMthList(mthUsageData.getUses())); + mth.setUnresolvedUsed(mthUsageData.getUnresolvedUsage()); + mth.setCallsSelf(mthUsageData.callsSelf()); } } Map fldUsage = clsUsageData.getFldUsage(); diff --git a/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageFileAdapter.java b/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageFileAdapter.java index c0ead18c3..4ca8fa2b4 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageFileAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/usage/UsageFileAdapter.java @@ -11,16 +11,20 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.plugins.input.data.IMethodRef; import jadx.api.usage.IUsageInfoData; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; @@ -104,6 +108,7 @@ public class UsageFileAdapter extends DataAdapterHelper { int clsCount = readUVInt(in); int clsWithoutDataCount = readUVInt(in); + // Class information String[] clsNames = new String[clsCount + clsWithoutDataCount]; ClsUsageData[] classes = new ClsUsageData[clsCount]; int c = 0; @@ -115,6 +120,8 @@ public class UsageFileAdapter extends DataAdapterHelper { for (int i = 0; i < clsWithoutDataCount; i++) { clsNames[c++] = in.readUTF(); } + + // Method information int mthCount = readUVInt(in); MthRef[] methods = new MthRef[mthCount]; for (int i = 0; i < mthCount; i++) { @@ -125,6 +132,24 @@ public class UsageFileAdapter extends DataAdapterHelper { cls.getMthUsage().put(mthShortId, new MthUsageData(mthRef)); methods[i] = mthRef; } + + // Unresolved method information + int uMthCount = readUVInt(in); + IMethodRef[] unresolvedMethods = new IMethodRef[uMthCount]; + for (int i = 0; i < uMthCount; i++) { + String name = in.readUTF(); + String parentClassType = in.readUTF(); + String returnType = in.readUTF(); + int argCount = in.readInt(); + String[] args = new String[argCount]; + for (int j = 0; j < argCount; j++) { + args[j] = in.readUTF(); + } + IMethodRef iMethodRef = new CachedMethodRef(parentClassType, name, returnType, Arrays.asList(args)); + unresolvedMethods[i] = iMethodRef; + } + + // Usage data for (int i = 0; i < clsCount; i++) { ClsUsageData cls = data.getClassData(clsNames[i]); cls.setClsDeps(readClsList(in, clsNames)); @@ -134,8 +159,11 @@ public class UsageFileAdapter extends DataAdapterHelper { int mCount = readUVInt(in); for (int m = 0; m < mCount; m++) { MthRef mthRef = methods[readUVInt(in)]; - cls.getMthUsage().get(mthRef.getShortId()) - .setUsage(readMthList(in, methods)); + MthUsageData mthUsageData = cls.getMthUsage().get(mthRef.getShortId()); + mthUsageData.setUsage(readMthList(in, methods)); + mthUsageData.setUses(readMthList(in, methods)); + mthUsageData.setUnresolvedUsage(readUnresolvedMthList(in, unresolvedMethods)); + mthUsageData.setCallsSelf(in.readBoolean()); } int fCount = readUVInt(in); for (int f = 0; f < fCount; f++) { @@ -151,11 +179,13 @@ public class UsageFileAdapter extends DataAdapterHelper { private static void writeData(DataOutputStream out, RawUsageData usageData) throws IOException { Map clsMap = new HashMap<>(); Map mthMap = new HashMap<>(); + Map uMthMap = new HashMap<>(); Map clsDataMap = usageData.getClsMap(); List classes = new ArrayList<>(clsDataMap.keySet()); Collections.sort(classes); List classesWithoutData = usageData.getClassesWithoutData(); + // Class information writeUVInt(out, classes.size()); writeUVInt(out, classesWithoutData.size()); int i = 0; @@ -167,6 +197,8 @@ public class UsageFileAdapter extends DataAdapterHelper { out.writeUTF(cls); clsMap.put(cls, i++); } + + // Method information List methods = clsDataMap.values().stream() .flatMap(c -> c.getMthUsage().values().stream()) .map(MthUsageData::getMthRef) @@ -178,6 +210,38 @@ public class UsageFileAdapter extends DataAdapterHelper { out.writeUTF(mth.getShortId()); mthMap.put(mth, j++); } + + // Unresolved method information + Set unresolvedMethods = clsDataMap.values().stream() + .flatMap(classUsageData -> classUsageData.getMthUsage().values().stream()) + .flatMap(methodUsageData -> { + List unresolvedUsageList = methodUsageData.getUnresolvedUsage(); + return (unresolvedUsageList == null) ? null : unresolvedUsageList.stream(); + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + writeUVInt(out, unresolvedMethods.size()); + int k = 0; + for (IMethodRef uMth : unresolvedMethods) { + String name = uMth.getName(); + out.writeUTF((name == null) ? "" : name); + String parentClassType = uMth.getParentClassType(); + out.writeUTF((parentClassType == null) ? "" : parentClassType); + String returnType = uMth.getReturnType(); + out.writeUTF((returnType == null) ? "" : returnType); + List argTypes = uMth.getArgTypes(); + if (argTypes == null) { + out.writeInt(0); + } else { + out.writeInt(argTypes.size()); + for (String arg : argTypes) { + out.writeUTF(arg); + } + } + uMthMap.put(uMth, k++); + } + + // Usage data for (String cls : classes) { ClsUsageData clsData = clsDataMap.get(cls); writeClsList(out, clsMap, clsData.getClsDeps()); @@ -188,6 +252,9 @@ public class UsageFileAdapter extends DataAdapterHelper { for (MthUsageData mthData : clsData.getMthUsage().values()) { writeUVInt(out, mthMap.get(mthData.getMthRef())); writeMthList(out, mthMap, mthData.getUsage()); + writeMthList(out, mthMap, mthData.getUses()); + writeUnresolvedMthList(out, uMthMap, mthData.getUnresolvedUsage()); + out.writeBoolean(mthData.callsSelf()); } writeUVInt(out, clsData.getFldUsage().size()); @@ -248,6 +315,30 @@ public class UsageFileAdapter extends DataAdapterHelper { } } + private static List readUnresolvedMthList(DataInputStream in, IMethodRef[] methods) throws IOException { + int count = readUVInt(in); + if (count == 0) { + return Collections.emptyList(); + } + List list = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + list.add(methods[readUVInt(in)]); + } + return list; + } + + private static void writeUnresolvedMthList(DataOutputStream out, Map uMthMap, List mthList) + throws IOException { + if (Utils.isEmpty(mthList)) { + writeUVInt(out, 0); + return; + } + writeUVInt(out, mthList.size()); + for (IMethodRef mth : mthList) { + writeUVInt(out, uMthMap.get(mth)); + } + } + private static String buildInputsHash(List inputs) { List paths = inputs.stream() .filter(f -> !f.getName().endsWith(".jadx.kts")) diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java index 9639e61a5..859aac4e9 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/DbgUtils.java @@ -19,6 +19,7 @@ import jadx.core.utils.android.AndroidManifestParser; import jadx.core.utils.android.AppAttribute; import jadx.core.utils.android.ApplicationParams; import jadx.gui.device.debugger.smali.Smali; +import jadx.gui.device.debugger.smali.SmaliMethodNode; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; @@ -53,6 +54,21 @@ public class DbgUtils { return null; } + @Nullable + public static SmaliMethodNode getSmaliMethodNode(JClass cls, String mthRawFullID) { + Smali smali = getSmali(cls.getCls().getClassNode().getTopParentClass()); + if (smali != null) { + return smali.getMethodNode(mthRawFullID); + } + return null; + } + + public static void printSmaliLineMapping(SmaliMethodNode smn) { + for (Map.Entry lineToCodeOffset : smn.getLineMapping().entrySet()) { + LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue()); + } + } + public static String[] sepClassAndMthSig(String fullSig) { int pos = fullSig.indexOf('('); if (pos != -1) { diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java index 4d4df33ed..6da54e347 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/Smali.java @@ -108,6 +108,11 @@ public class Smali { return -1; } + @Nullable + public SmaliMethodNode getMethodNode(String mthFullRawID) { + return insnMap.get(mthFullRawID); + } + public int getRegCount(String mthFullRawID) { SmaliMethodNode info = insnMap.get(mthFullRawID); if (info != null) { diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java index 4a9f4c618..3c37d170b 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/smali/SmaliMethodNode.java @@ -10,7 +10,7 @@ import jadx.core.dex.instructions.args.InsnArg; import jadx.core.dex.instructions.args.RegisterArg; import jadx.core.dex.nodes.InsnNode; -class SmaliMethodNode { +public class SmaliMethodNode { private Map nodes; // codeOffset: InsnNode private List regList; private int[] insnPos; @@ -27,6 +27,13 @@ class SmaliMethodNode { return this.regCount; } + /** + * Returns the line mapping of the: + * 'output disassembled smali file line index' to 'dex instruction position code offset' + * The value is the same as {@link InsnNode#getOffset()} + * + * @return the line mapping + */ public Map getLineMapping() { return lineMapping; } diff --git a/jadx-gui/src/main/java/jadx/gui/events/services/RenameService.java b/jadx-gui/src/main/java/jadx/gui/events/services/RenameService.java index 5da12d818..317bc5d6e 100644 --- a/jadx-gui/src/main/java/jadx/gui/events/services/RenameService.java +++ b/jadx-gui/src/main/java/jadx/gui/events/services/RenameService.java @@ -60,9 +60,10 @@ public class RenameService { private void process(NodeRenamedByUser event) { try { LOG.debug("Applying rename event: {}", event); + long timeStarted = System.nanoTime(); JRenameNode node = getRenameNode(event); updateCodeRenames(set -> processRename(node, event, set)); - refreshState(node); + refreshState(node, timeStarted); } catch (Exception e) { LOG.error("Rename failed", e); UiUtils.errorMessage(mainWindow, "Rename failed:\n" + Utils.getStackTrace(e)); @@ -109,7 +110,7 @@ public class RenameService { project.setCodeData(codeData); } - private void refreshState(JRenameNode node) { + private void refreshState(JRenameNode node, long timeStarted) { List toUpdate = new ArrayList<>(); node.addUpdateNodes(toUpdate); @@ -128,8 +129,18 @@ public class RenameService { mainWindow.getBackgroundExecutor().execute("Refreshing", () -> { mainWindow.getWrapper().reloadCodeData(); + // Reload all the classes in the background process, rather than using the UI thread for + // decompilation. We don't just use codeArea.backgroundRefreshClass because it would spawn a + // separate background process, whereas we would like it to happen in this one. + for (ContentPanel tab : mainWindow.getTabbedPane().getTabs()) { + JClass rootClass = tab.getNode().getRootClass(); + if (updatedTopClasses.contains(rootClass)) { + rootClass.reload(mainWindow.getCacheObject()); + } + } UiUtils.uiRunAndWait(() -> refreshTabs(mainWindow.getTabbedPane(), updatedTopClasses)); refreshClasses(updatedTopClasses); + LOG.debug("Finished rename, took " + (System.nanoTime() - timeStarted) + " ns"); }, (status) -> { if (status == TaskStatus.CANCEL_BY_MEMORY) { @@ -171,7 +182,7 @@ public class RenameService { if (updatedClasses.remove(rootClass)) { ClassCodeContentPanel contentPanel = (ClassCodeContentPanel) tab; CodeArea codeArea = (CodeArea) contentPanel.getJavaCodePanel().getCodeArea(); - codeArea.refreshClass(); + codeArea.refreshClass(true); } } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index c70b84cf5..8903da2a8 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -896,4 +896,5 @@ public class JadxSettings { public void setSmaliAreaShowBytecode(boolean smaliAreaShowBytecode) { settingsData.setSmaliAreaShowBytecode(smaliAreaShowBytecode); } + } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsData.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsData.java index c0c5877e4..9fe5077d3 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsData.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsData.java @@ -501,4 +501,5 @@ public class JadxSettingsData extends JadxGUIArgs { public void setXposedCodegenLanguage(XposedCodegenLanguage xposedCodegenLanguage) { this.xposedCodegenLanguage = xposedCodegenLanguage; } + } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java index 6cef6e49a..e8aae906c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -35,6 +35,7 @@ public class HeapUsageBar extends JProgressBar { private final double maxGB; private final long limit; + private long peakUsed; private final String labelTemplate; private transient Disposable timer; @@ -45,6 +46,7 @@ public class HeapUsageBar extends JProgressBar { setStringPainted(true); long maxMemory = runtime.maxMemory(); + peakUsed = 0; maxGB = maxMemory / GB; limit = maxMemory - UiUtils.MIN_FREE_MEMORY; labelTemplate = NLS.str("heapUsage.text"); @@ -102,8 +104,11 @@ public class HeapUsageBar extends JProgressBar { } UpdateData updateData = new UpdateData(); long used = runtime.totalMemory() - runtime.freeMemory(); + if (used > peakUsed) { + peakUsed = used; + } updateData.value = (int) (used / 1024); - updateData.label = String.format(labelTemplate, used / GB, maxGB); + updateData.label = String.format(labelTemplate, used / GB, maxGB, peakUsed / GB); updateData.color = used > limit ? RED : GREEN; return updateData; } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java index f71733819..1da55fce1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ActionModel.java @@ -45,6 +45,7 @@ public enum ActionModel { Shortcut.keyboard(KeyEvent.VK_T, UiUtils.ctrlButton())), TEXT_SEARCH(MENU_TOOLBAR, "menu.text_search", "menu.text_search", "ui/find", Shortcut.keyboard(KeyEvent.VK_F, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), + CLASS_SEARCH(MENU_TOOLBAR, "menu.class_search", "menu.class_search", "ui/ejbFinderMethod", Shortcut.keyboard(KeyEvent.VK_N, UiUtils.ctrlButton())), COMMENT_SEARCH(MENU_TOOLBAR, "menu.comment_search", "menu.comment_search", "ui/usagesFinder", @@ -53,7 +54,8 @@ public enum ActionModel { Shortcut.keyboard(KeyEvent.VK_M, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), GO_TO_APPLICATION(MENU_TOOLBAR, "menu.go_to_application", "menu.go_to_application", "ui/application", Shortcut.keyboard(KeyEvent.VK_A, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), - GO_TO_ANDROID_MANIFEST(MENU_TOOLBAR, "menu.go_to_android_manifest", "menu.go_to_android_manifest", "ui/androidManifest", + GO_TO_ANDROID_MANIFEST(MENU_TOOLBAR, "menu.go_to_android_manifest", "menu.go_to_android_manifest", + "ui/androidManifest", Shortcut.none()), PREVIEW_TAB(MENU_TOOLBAR, "menu.enable_preview_tab", "menu.enable_preview_tab", "ui/editorPreview", Shortcut.none()), @@ -85,6 +87,16 @@ public enum ActionModel { Shortcut.keyboard(KeyEvent.VK_C)), GOTO_DECLARATION(CODE_AREA, "popup.go_to_declaration", "popup.go_to_declaration", null, Shortcut.keyboard(KeyEvent.VK_D)), + CONVERT_NUMBER(CODE_AREA, "popup.convert_number", "popup.convert_number", null, Shortcut.none()), + VIEW_CLASS_INHERITANCE_GRAPH(CODE_AREA, "popup.view_class_graph", "popup.view_class_graph_description", null, + Shortcut.none()), + VIEW_CLASS_METHOD_GRAPH(CODE_AREA, "popup.view_class_method_graph", "popup.view_class_method_graph_description", + null, Shortcut.none()), + VIEW_CALL_GRAPH(CODE_AREA, "popup.view_call_graph", "popup.view_call_graph_description", null, Shortcut.none()), + VIEW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_cfg", "popup.view_cfg_description", null, Shortcut.none()), + VIEW_RAW_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_raw_cfg", "popup.view_raw_cfg_description", null, Shortcut.none()), + VIEW_REGION_CONTROL_FLOW_GRAPH(CODE_AREA, "popup.view_region_cfg", "popup.view_region_cfg_description", null, Shortcut.none()), + CODE_COMMENT(CODE_AREA, "popup.add_comment", "popup.add_comment", null, Shortcut.keyboard(KeyEvent.VK_SEMICOLON)), CODE_COMMENT_SEARCH(CODE_AREA, "popup.search_comment", "popup.search_comment", null, diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java new file mode 100644 index 000000000..8369d85ad --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewCallGraphAction.java @@ -0,0 +1,48 @@ +package jadx.gui.ui.action; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.CallGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewCallGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewCallGraphAction.class); + private static final long serialVersionUID = -11122327621269039L; + + public ViewCallGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_CALL_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JMethod methodNode; + + if (node instanceof JMethod) { + methodNode = (JMethod) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + CallGraphDialog.open(getCodeArea().getMainWindow(), methodNode); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + return node instanceof JMethod; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java new file mode 100644 index 000000000..a48a64d8f --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassInheritanceGraphAction.java @@ -0,0 +1,54 @@ +package jadx.gui.ui.action; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JField; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.ClassInheritanceGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewClassInheritanceGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewClassInheritanceGraphAction.class); + private static final long serialVersionUID = -331826691076655264L; + + public ViewClassInheritanceGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_CLASS_INHERITANCE_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JClass classNode; + + if (node instanceof JMethod) { + classNode = node.getJParent(); + } else if (node instanceof JField) { + classNode = node.getJParent(); + } else if (node instanceof JClass) { + classNode = (JClass) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + ClassInheritanceGraphDialog.open(getCodeArea().getMainWindow(), classNode); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + return node instanceof JMethod || node instanceof JClass || node instanceof JField; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java new file mode 100644 index 000000000..29252a610 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewClassMethodGraphAction.java @@ -0,0 +1,54 @@ +package jadx.gui.ui.action; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JClass; +import jadx.gui.treemodel.JField; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.ClassMethodGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewClassMethodGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewClassMethodGraphAction.class); + private static final long serialVersionUID = -331826691076655264L; + + public ViewClassMethodGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_CLASS_METHOD_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JClass classNode; + + if (node instanceof JMethod) { + classNode = node.getJParent(); + } else if (node instanceof JField) { + classNode = node.getJParent(); + } else if (node instanceof JClass) { + classNode = (JClass) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + ClassMethodGraphDialog.open(getCodeArea().getMainWindow(), classNode); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + return node instanceof JMethod || node instanceof JClass || node instanceof JField; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java new file mode 100644 index 000000000..4df8f9987 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewControlFlowGraphAction.java @@ -0,0 +1,58 @@ +package jadx.gui.ui.action; + +import java.io.File; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.ControlFlowGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewControlFlowGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewControlFlowGraphAction.class); + private static final long serialVersionUID = -490213655L; + + public ViewControlFlowGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_CONTROL_FLOW_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JMethod methodNode; + + if (node instanceof JMethod) { + methodNode = (JMethod) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, false); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + if (!(node instanceof JMethod)) { + return false; + } + MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); + File file = new DotGraphUtils(false, false).getFullFile(mth); + + return file.exists() && !file.isDirectory(); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java new file mode 100644 index 000000000..7b481b725 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRawControlFlowGraphAction.java @@ -0,0 +1,58 @@ +package jadx.gui.ui.action; + +import java.io.File; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.ControlFlowGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewRawControlFlowGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewRawControlFlowGraphAction.class); + private static final long serialVersionUID = -535703386523657L; + + public ViewRawControlFlowGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_RAW_CONTROL_FLOW_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JMethod methodNode; + + if (node instanceof JMethod) { + methodNode = (JMethod) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, false, true); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + if (!(node instanceof JMethod)) { + return false; + } + MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); + File file = new DotGraphUtils(false, true).getFullFile(mth); + + return file.exists() && !file.isDirectory(); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java new file mode 100644 index 000000000..291993b41 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/action/ViewRegionControlFlowGraphAction.java @@ -0,0 +1,58 @@ +package jadx.gui.ui.action; + +import java.io.File; + +import javax.swing.JOptionPane; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.gui.treemodel.JMethod; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.dialog.ControlFlowGraphDialog; +import jadx.gui.utils.NLS; + +public final class ViewRegionControlFlowGraphAction extends JNodeAction { + private static final Logger LOG = LoggerFactory.getLogger(ViewRegionControlFlowGraphAction.class); + private static final long serialVersionUID = -14970352087936L; + + public ViewRegionControlFlowGraphAction(CodeArea codeArea) { + super(ActionModel.VIEW_REGION_CONTROL_FLOW_GRAPH, codeArea); + } + + @Override + public void runAction(JNode node) { + try { + + JMethod methodNode; + + if (node instanceof JMethod) { + methodNode = (JMethod) node; + } else { + throw new JadxRuntimeException("Unsupported node type: " + (node != null ? node.getClass() : "null")); + } + + ControlFlowGraphDialog.open(getCodeArea().getMainWindow(), methodNode, true, false); + } catch (Exception e) { + LOG.error("Failed to view graph", e); + JOptionPane.showMessageDialog(getCodeArea().getMainWindow(), e.getLocalizedMessage(), NLS.str("error_dialog.title"), + JOptionPane.ERROR_MESSAGE); + } + } + + @Override + public boolean isActionEnabled(JNode node) { + if (!(node instanceof JMethod)) { + return false; + } + MethodNode mth = ((JMethod) node).getJavaMethod().getMethodNode(); + File file = new DotGraphUtils(true, false).getFullFile(mth); + + return file.exists() && !file.isDirectory(); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java index bf1e13681..3b6acbcfa 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/AbstractCodeArea.java @@ -234,14 +234,31 @@ public abstract class AbstractCodeArea extends RSyntaxTextArea { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_C && UiUtils.isCtrlDown(e)) { - if (StringUtils.isEmpty(getSelectedText())) { - UiUtils.copyToClipboard(getWordUnderCaret()); - } + UiUtils.copyToClipboard(getSelectedTokenOrWord()); } } }); } + /** + * If the user has selected an individual word, for example by clicking and dragging + * the mouse, then get that. Otherwise get the token underneath the cursor. + * This is useful when the token is a string or comment and we want to control or copy + * the word rather than the whole thing. + * + * @return The word or the token text + */ + public @Nullable String getSelectedTokenOrWord() { + final String rc = getSelectedText(); + if (rc == null) { + return getWordUnderCaret(); + } + if (StringUtils.isEmpty(rc)) { + return getWordUnderCaret(); + } + return rc; + } + private void addSaveActions(JEditableNode node) { addKeyListener(new KeyAdapter() { @Override diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java index 2dc9e5608..5df5e6deb 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ClassCodeContentPanel.java @@ -4,20 +4,25 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JCheckBox; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JToolBar; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.DecompilationMode; -import jadx.gui.jobs.BackgroundExecutor; import jadx.gui.treemodel.JClass; import jadx.gui.ui.codearea.mode.JCodeMode; +import jadx.gui.ui.codearea.sync.CodePanelSyncee; +import jadx.gui.ui.codearea.sync.CodePanelSyncer; +import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; +import jadx.gui.ui.codearea.sync.fallback.FallbackSyncer; import jadx.gui.ui.panel.IViewStateSupport; import jadx.gui.ui.tab.TabbedPane; import jadx.gui.utils.NLS; @@ -25,7 +30,7 @@ import jadx.gui.utils.NLS; import static com.formdev.flatlaf.FlatClientProperties.TABBED_PANE_TRAILING_COMPONENT; /** - * Displays one class with two different view: + * Displays one class with two different views: * *
    *
  • Java source code of the selected class (default)
  • @@ -39,6 +44,7 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem private final transient CodePanel javaCodePanel; private final transient CodePanel smaliCodePanel; private final transient JTabbedPane areaTabbedPane; + private final AtomicBoolean syncInProgress = new AtomicBoolean(false); private boolean splitView = false; @@ -46,12 +52,12 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem super(panel, jCls); javaCodePanel = new CodePanel(new CodeArea(this, jCls)); - smaliCodePanel = new CodePanel(new SmaliArea(this, jCls)); - areaTabbedPane = buildTabbedPane(jCls, false); + smaliCodePanel = new CodePanel(new SmaliArea(this, jCls, false)); + areaTabbedPane = buildTabbedPane(jCls); addCustomControls(areaTabbedPane); - initView(); javaCodePanel.load(); + initView(); } private void initView() { @@ -59,28 +65,108 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem setLayout(new BorderLayout()); setBorder(new EmptyBorder(0, 0, 0, 0)); if (splitView) { - JTabbedPane splitPaneView = buildTabbedPane(((JClass) node), true); - JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, areaTabbedPane, splitPaneView); - add(splitPane); - splitPane.setDividerLocation(0.5); - splitPaneView.setSelectedIndex(1); + setupSplitPane(); } else { + javaCodePanel.load(); + smaliCodePanel.load(); + attachSyncListeners(javaCodePanel, smaliCodePanel); + areaTabbedPane.setSelectedIndex(0); // default to Java add(areaTabbedPane); } - invalidate(); + revalidate(); + repaint(); } - private JTabbedPane buildTabbedPane(JClass jCls, boolean split) { + private void attachSyncListeners(CodePanel javaPanel, CodePanel smaliPanel) { + javaPanel.getCodeArea().addCaretListener(e -> { + if (syncInProgress.get()) { + return; + } + syncInProgress.set(true); + syncToMethod(javaPanel, smaliPanel); + syncInProgress.set(false); + }); + + smaliPanel.getCodeArea().addCaretListener(e -> { + if (syncInProgress.get()) { + return; + } + syncInProgress.set(true); + syncToMethod(smaliPanel, javaPanel); + syncInProgress.set(false); + }); + } + + private void setupSplitPane() { + JTabbedPane leftTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); + JTabbedPane rightTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); + + CodePanel[] leftPanels = { + new CodePanel(new CodeArea(this, (JClass) node)), // Java + new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali + new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik + new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple + new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback + }; + + CodePanel[] rightPanels = { + new CodePanel(new SmaliArea(this, (JClass) node, false)), // Smali + new CodePanel(new SmaliArea(this, (JClass) node, true)), // Smali with Dalvik + new CodePanel(new CodeArea(this, (JClass) node)), // Java + new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.SIMPLE))), // Simple + new CodePanel(new CodeArea(this, new JCodeMode((JClass) node, DecompilationMode.FALLBACK))) // Fallback + }; + + leftTabbedPane.add(leftPanels[0], NLS.str("tabs.code")); + leftTabbedPane.add(leftPanels[1], NLS.str("tabs.smali")); + leftTabbedPane.add(leftPanels[2], NLS.str("tabs.smali_bytecode")); + leftTabbedPane.add(leftPanels[3], "Simple"); + leftTabbedPane.add(leftPanels[4], "Fallback"); + + rightTabbedPane.add(rightPanels[0], NLS.str("tabs.smali")); + rightTabbedPane.add(rightPanels[1], NLS.str("tabs.smali_bytecode")); + rightTabbedPane.add(rightPanels[2], NLS.str("tabs.code")); + rightTabbedPane.add(rightPanels[3], "Simple"); + rightTabbedPane.add(rightPanels[4], "Fallback"); + + for (CodePanel p : leftPanels) { + p.load(); + } + for (CodePanel p : rightPanels) { + p.load(); + } + + leftTabbedPane.addChangeListener(e -> ((CodePanel) leftTabbedPane.getSelectedComponent()).load()); + rightTabbedPane.addChangeListener(e -> ((CodePanel) rightTabbedPane.getSelectedComponent()).load()); + + // Attach caret sync between all combinations + for (CodePanel leftPanel : leftPanels) { + for (CodePanel rightPanel : rightPanels) { + attachSyncListeners(leftPanel, rightPanel); + } + } + + // Create and configure split pane + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabbedPane, rightTabbedPane); + splitPane.setResizeWeight(0.5); + leftTabbedPane.setMinimumSize(new Dimension(200, 200)); + rightTabbedPane.setMinimumSize(new Dimension(200, 200)); + add(splitPane); + + // Set divider location after layout + SwingUtilities.invokeLater(() -> splitPane.setDividerLocation(0.5)); + + rightTabbedPane.setSelectedIndex(0); + addCustomControls(leftTabbedPane); + } + + private JTabbedPane buildTabbedPane(JClass jCls) { JTabbedPane areaTabbedPane = new JTabbedPane(JTabbedPane.BOTTOM); areaTabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0)); areaTabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); - if (split) { - areaTabbedPane.add(new CodePanel(new CodeArea(this, jCls)), NLS.str("tabs.code")); - areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls)), NLS.str("tabs.smali")); - } else { - areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code")); - areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali")); - } + areaTabbedPane.add(javaCodePanel, NLS.str("tabs.code")); + areaTabbedPane.add(smaliCodePanel, NLS.str("tabs.smali")); + areaTabbedPane.add(new CodePanel(new SmaliArea(this, jCls, true)), NLS.str("tabs.smali_bytecode")); areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.SIMPLE))), "Simple"); areaTabbedPane.add(new CodePanel(new CodeArea(this, new JCodeMode(jCls, DecompilationMode.FALLBACK))), "Fallback"); areaTabbedPane.addChangeListener(e -> { @@ -108,11 +194,6 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem tabbedPane.putClientProperty(TABBED_PANE_TRAILING_COMPONENT, trailing); } - private void execInBackground(Runnable runnable) { - BackgroundExecutor bgExec = this.tabbedPane.getMainWindow().getBackgroundExecutor(); - bgExec.execute("Loading", runnable); - } - @Override public void loadSettings() { javaCodePanel.loadSettings(); @@ -195,4 +276,27 @@ public final class ClassCodeContentPanel extends AbstractCodeContentPanel implem } super.dispose(); } + + private void syncToMethod(CodePanel fromPanel, CodePanel toPanel) { + if (!fromPanel.isShowing() || !toPanel.isShowing()) { + return; + } + try { + AbstractCodeArea from = fromPanel.getCodeArea(); + AbstractCodeArea to = toPanel.getCodeArea(); + toPanel.load(); + + if (from instanceof CodePanelSyncerAbstractFactory && to instanceof CodePanelSyncee) { + CodePanelSyncer syncer = ((CodePanelSyncerAbstractFactory) from).createCodePanelSyncer(); + if (((CodePanelSyncee) to).sync(syncer)) { + return; + } + } + if (!FallbackSyncer.sync(fromPanel, toPanel)) { + LOG.warn("Code pane area sync not possible"); + } + } catch (Exception ex) { + LOG.warn("Failed to sync method/class across views: {}", ex.getLocalizedMessage()); + } + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java index b7636ffec..d07d2f414 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CodeArea.java @@ -4,7 +4,10 @@ import java.awt.Point; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuEvent; @@ -20,6 +23,7 @@ import jadx.api.ICodeInfo; import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; import jadx.gui.JadxWrapper; import jadx.gui.jobs.IBackgroundTask; import jadx.gui.jobs.LoadTask; @@ -30,13 +34,31 @@ import jadx.gui.treemodel.JLoadableNode; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JResource; import jadx.gui.ui.MainWindow; -import jadx.gui.ui.action.*; +import jadx.gui.ui.action.CommentSearchAction; +import jadx.gui.ui.action.FindUsageAction; +import jadx.gui.ui.action.FridaAction; +import jadx.gui.ui.action.GoToDeclarationAction; +import jadx.gui.ui.action.JNodeAction; +import jadx.gui.ui.action.JsonPrettifyAction; +import jadx.gui.ui.action.RenameAction; +import jadx.gui.ui.action.ViewCallGraphAction; +import jadx.gui.ui.action.ViewClassInheritanceGraphAction; +import jadx.gui.ui.action.ViewClassMethodGraphAction; +import jadx.gui.ui.action.ViewControlFlowGraphAction; +import jadx.gui.ui.action.ViewRawControlFlowGraphAction; +import jadx.gui.ui.action.ViewRegionControlFlowGraphAction; +import jadx.gui.ui.action.XposedAction; import jadx.gui.ui.codearea.mode.JCodeMode; +import jadx.gui.ui.codearea.sync.CodePanelSyncee; +import jadx.gui.ui.codearea.sync.CodePanelSyncer; +import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; +import jadx.gui.ui.codearea.sync.JavaSyncer; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.CaretPositionFix; import jadx.gui.utils.DefaultPopupMenuListener; import jadx.gui.utils.JNodeCache; import jadx.gui.utils.JumpPosition; +import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; import jadx.gui.utils.shortcut.ShortcutsController; @@ -44,7 +66,7 @@ import jadx.gui.utils.shortcut.ShortcutsController; * The {@link AbstractCodeArea} implementation used for displaying Java code and text based * resources (e.g. AndroidManifest.xml) */ -public final class CodeArea extends AbstractCodeArea { +public final class CodeArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee { private static final Logger LOG = LoggerFactory.getLogger(CodeArea.class); private static final long serialVersionUID = 6312736869579635796L; @@ -172,6 +194,18 @@ public final class CodeArea extends AbstractCodeArea { popup.addSeparator(); popup.add(new FridaAction(this)); popup.add(new XposedAction(this)); + popup.addSeparator(); + popup.add(new ViewClassInheritanceGraphAction(this)); + popup.add(new ViewClassMethodGraphAction(this)); + popup.add(new ViewCallGraphAction(this)); + popup.addSubmenu(new JNodeAction[] { + new ViewControlFlowGraphAction(this), + new ViewRawControlFlowGraphAction(this), + new ViewRegionControlFlowGraphAction(this), + }, NLS.str("popup.cfg_submenu")); + popup.addSeparator(); + popup.add(new ConvertNumberAction(this)); + getMainWindow().getWrapper().getGuiPluginsContext().appendPopupMenus(this, popup); // move caret on mouse right button click @@ -362,13 +396,22 @@ public final class CodeArea extends AbstractCodeArea { } public void refreshClass() { + refreshClass(false); + } + + public void refreshClass(boolean alreadyReloaded) { if (node instanceof JClass) { JClass cls = node.getRootClass(); try { CaretPositionFix caretFix = new CaretPositionFix(this); caretFix.save(); - cachedCodeInfo = cls.reload(getMainWindow().getCacheObject()); + if (alreadyReloaded) { + cachedCodeInfo = cls.getCodeInfo(); + } else { + // bad. blocks the UI thread for a potentially expensive decomp + cachedCodeInfo = cls.reload(getMainWindow().getCacheObject()); + } ClassCodeContentPanel codeContentPanel = (ClassCodeContentPanel) this.contentPanel; codeContentPanel.getTabbedPane().refresh(cls); @@ -379,6 +422,20 @@ public final class CodeArea extends AbstractCodeArea { } } + /** + * Refresh the class in the background, updating the UI once the potential decomp is complete. + * Should be called from the UI thread. + */ + public void backgroundRefreshClass() { + UiUtils.uiThreadGuard(); + this.getMainWindow().getBackgroundExecutor().execute("Refreshing...", () -> { + this.getNode().getRootClass().reload(this.getMainWindow().getCacheObject()); + UiUtils.uiRunAndWait(() -> { + this.refreshClass(true); + }); + }); + } + public MainWindow getMainWindow() { return contentPanel.getMainWindow(); } @@ -398,4 +455,65 @@ public final class CodeArea extends AbstractCodeArea { super.dispose(); cachedCodeInfo = null; } + + @Override + public CodePanelSyncer createCodePanelSyncer() { + return new JavaSyncer(this); + } + + @Override + public boolean sync(CodePanelSyncer codePanelSyncer) { + return codePanelSyncer.syncTo(this); + } + + @Nullable + public ICodeMetadata getCodeMetadata() { + ICodeInfo codeInfo = getCodeInfo(); + if (!codeInfo.hasMetadata()) { + LOG.warn("No code info metadata for {}", codeInfo.toString()); + return null; + } + return codeInfo.getCodeMetadata(); + } + + /** + * Returns a mapping of 'decompilation output line number' to 'dex debug line number' + * These are 1-indexed line numbers not the line indices of the CodeArea + * + * @return the line mapping + */ + public Map getLineMappings() { + ICodeInfo codeInfo = getCodeInfo(); + if (!codeInfo.hasMetadata()) { + LOG.debug("No code info metadata for {}", codeInfo.toString()); + return Map.of(); + } + Map lineMapping = codeInfo.getCodeMetadata().getLineMapping(); + if (lineMapping.isEmpty()) { + LOG.debug("Line mappings are empty for {}", codeInfo.toString()); + return Map.of(); + } + return lineMapping; + } + + /** + * Returns the same as {@link #getLineMappings()} but only if each value (dex debug line number) + * appears only once. + * If a value appears more than once then it suggests that methods might share dex debug line + * numbers. + * If this is the case then the line mapping cannot be used for code sync correlation. + * + * @return the line mapping + */ + public Map getFunctionUniqueLineMappings() { + final var lineMappings = getLineMappings(); + final boolean isAnyRepeated = + lineMappings.values().stream().collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).values().stream() + .filter(v -> v > 1).findAny().isPresent(); + if (isAnyRepeated) { + LOG.debug("Dex debug line mappings are not unique"); + return Map.of(); + } + return lineMappings; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java index d182803e0..edfe339c5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/CommentAction.java @@ -41,7 +41,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis private static final Logger LOG = LoggerFactory.getLogger(CommentAction.class); - private final boolean enabled; + protected final boolean enabled; private @Nullable ICodeComment actionComment; private boolean updateComment; @@ -50,6 +50,11 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis this.enabled = codeArea.getNode() instanceof JClass; } + public CommentAction(ActionModel actionModel, CodeArea codeArea) { + super(actionModel, codeArea); + this.enabled = codeArea.getNode() instanceof JClass; + } + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { if (enabled && updateCommentAction(UiUtils.getOffsetAtMousePosition(codeArea))) { @@ -98,7 +103,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis CommentDialog.show(codeArea, actionComment, updateComment); } - private @Nullable ICodeComment searchForExistComment(ICodeComment blankComment) { + protected @Nullable ICodeComment searchForExistComment(ICodeComment blankComment) { try { JadxProject project = codeArea.getProject(); JadxCodeData codeData = project.getCodeData(); @@ -123,7 +128,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis * @return blank code comment object (comment string empty) */ @Nullable - private ICodeComment getCommentRef(int pos) { + protected ICodeComment getCommentRef(int pos) { if (pos == -1) { return null; } @@ -181,7 +186,7 @@ public class CommentAction extends CodeAreaAction implements DefaultPopupMenuLis /** * Check if all tokens are 'comment' in line at 'pos' */ - private boolean isCommentLine(int pos) { + protected boolean isCommentLine(int pos) { try { int line = codeArea.getLineOfOffset(pos); Token lineTokens = codeArea.getTokenListForLine(line); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java new file mode 100644 index 000000000..4f4581865 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/ConvertNumberAction.java @@ -0,0 +1,227 @@ +package jadx.gui.ui.codearea; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.event.PopupMenuEvent; +import javax.swing.text.BadLocationException; + +import org.fife.ui.rsyntaxtextarea.Token; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.data.CommentStyle; +import jadx.api.data.ICodeComment; +import jadx.api.data.impl.JadxCodeComment; +import jadx.api.data.impl.JadxCodeData; +import jadx.gui.settings.JadxProject; +import jadx.gui.treemodel.JClass; +import jadx.gui.ui.action.ActionModel; +import jadx.gui.utils.NLS; + +public class ConvertNumberAction extends CommentAction { + + private static final Logger LOG = LoggerFactory.getLogger(ConvertNumberAction.class); + + private static final String DEFAULT_TEXT = ""; + private final String tooltipText; + + public ConvertNumberAction(CodeArea codeArea) { + + super(ActionModel.CONVERT_NUMBER, codeArea); + + tooltipText = NLS.str("popup.convert_number"); + + // default menu item to disabled + setEnabled(false); + setNameAndDesc(DEFAULT_TEXT); + } + + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + + if (codeArea.getNode() instanceof JClass) { + // try parse number from word under caret + // and set text of popup menu dynamically + String word = getWordByPosition(codeArea.getCaretPosition()); + List conversions = getConversionsFromWord(word); + if (conversions != null && !conversions.isEmpty()) { + String joined = String.join(" | ", conversions); + setName(joined); + setShortDescription(tooltipText); + setEnabled(true); + } + } + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + // reset menu to disabled on cancel + setEnabled(false); + setNameAndDesc(DEFAULT_TEXT); + } + + @Override + public void actionPerformed(ActionEvent e) { + + if (!super.enabled) { + return; + } + + String newText = e.getActionCommand(); + if (newText == null) { + return; + } + + ICodeComment comment = getCommentRef(codeArea.getCaretPosition()); + if (comment == null) { + return; + } + + ICodeComment newComment = new JadxCodeComment(comment.getNodeRef(), comment.getCodeRef(), newText, CommentStyle.LINE); + updateCommentsData(codeArea, list -> list.add(newComment)); + + } + + /** + * Adds comments to project file and code area + */ + private static void updateCommentsData(CodeArea codeArea, Consumer> updater) { + try { + JadxProject project = codeArea.getProject(); + JadxCodeData codeData = project.getCodeData(); + if (codeData == null) { + codeData = new JadxCodeData(); + } + List list = new ArrayList<>(codeData.getComments()); + updater.accept(list); + Collections.sort(list); + codeData.setComments(list); + project.setCodeData(codeData); + codeArea.getMainWindow().getWrapper().reloadCodeData(); + } catch (Exception e) { + LOG.error("Comment action failed", e); + } + try { + // refresh code + codeArea.backgroundRefreshClass(); + } catch (Exception e) { + LOG.error("Failed to reload code", e); + } + } + + /** + * similar to AbstractCodeArea::getWordByPosition + * but includes "-" for negative numbers + */ + public @Nullable String getWordByPosition(int offset) { + Token token = codeArea.getWordTokenAtOffset(offset); + if (token == null) { + return null; + } + + String str = token.getLexeme(); + + try { + String prev = codeArea.getText(token.getOffset() - 1, 1); + if (prev.equals("-")) { + str = "-" + str; + } + + } catch (BadLocationException e) { + // ignore + } + + int len = str.length(); + if (len > 2 && str.startsWith("\"") && str.endsWith("\"")) { + return str.substring(1, len - 1); + } + return str; + } + + /** + * Tries to parse a number from input string, + * returns list of strings of the number converted to different formats. + * e.g. if input number is in hex, converts to decimal and binary. + */ + static @Nullable List getConversionsFromWord(String word) { + + List conversions = new ArrayList<>(); + + if (word == null || word.isEmpty()) { + return null; + } + + int i32 = 0; + long i64 = 0; + int radix = 10; + boolean parsedLong = false; + + // handle hex + if (word.startsWith("0x")) { + word = word.substring(2); + radix = 16; + } + + // handle long int syntax like "12345L" + if (word.endsWith("L")) { + word = word.substring(0, word.length() - 1); + parsedLong = true; + } + + // try parse int + try { + i32 = Integer.parseInt(word, radix); + i64 = i32; + + } catch (NumberFormatException e) { + + // try parse long + try { + i64 = Long.parseLong(word, radix); + parsedLong = true; + + } catch (NumberFormatException ignore) { + return null; + } + } + + // if we parsed decimal, output hex and vice versa + if (radix == 10) { + if (parsedLong) { + conversions.add(String.format("0x%x", i64)); + } else { + conversions.add(String.format("0x%x", i32)); + } + + } else if (radix == 16) { + conversions.add(String.format("%d", i32)); + } + + // pad binary in 8-bit groups + // int leadingZeros = parsed_long ? : Integer.numberOfLeadingZeros(i32); + int padBits = (int) Math.ceil((64 - Long.numberOfLeadingZeros(i64)) / 8.0) * 8; + if (padBits < 8) { + padBits = 8; + } + if (!parsedLong && padBits > 32) { + padBits = 32; + } + + // format padded binary + String binaryString = parsedLong ? Long.toBinaryString(i64) : Integer.toBinaryString(i32); + String fmt = String.format("0b%%%ds", padBits); + conversions.add(String.format(fmt, binaryString).replace(' ', '0')); + + // format printable ascii chars + if (i32 >= ' ' && i32 <= '~') { + conversions.add(String.format("'%c'", (int) i32)); + } + + return conversions; // no match + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java index 780acdf98..ff361763f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/JNodePopupBuilder.java @@ -1,5 +1,6 @@ package jadx.gui.ui.codearea; +import javax.swing.JMenu; import javax.swing.JPopupMenu; import javax.swing.event.PopupMenuListener; @@ -37,6 +38,21 @@ public class JNodePopupBuilder { popupListener.addActions(nodeAction); } + public void addSubmenu(JNodeAction[] nodeActions, String name) { + JMenu submenu = new JMenu(name); + + for (JNodeAction nodeAction : nodeActions) { + if (nodeAction.getActionModel() != null) { + shortcutsController.bindImmediate(nodeAction); + } + + submenu.add(nodeAction); + popupListener.addActions(nodeAction); + } + + menu.add(submenu); + } + public void add(JadxGuiAction action) { if (action.getActionModel() != null) { shortcutsController.bindImmediate(action); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java index 069846503..204709924 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/SmaliArea.java @@ -43,11 +43,15 @@ import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.TextNode; +import jadx.gui.ui.codearea.sync.CodePanelSyncee; +import jadx.gui.ui.codearea.sync.CodePanelSyncer; +import jadx.gui.ui.codearea.sync.CodePanelSyncerAbstractFactory; +import jadx.gui.ui.codearea.sync.SmaliSyncer; import jadx.gui.ui.panel.ContentPanel; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; -public final class SmaliArea extends AbstractCodeArea { +public final class SmaliArea extends AbstractCodeArea implements CodePanelSyncerAbstractFactory, CodePanelSyncee { private static final Logger LOG = LoggerFactory.getLogger(SmaliArea.class); private static final long serialVersionUID = 1334485631870306494L; @@ -59,12 +63,16 @@ public final class SmaliArea extends AbstractCodeArea { private final JNode textNode; private final JCheckBoxMenuItem cbUseSmaliV2; + private final boolean allowToggleV2 = false; // add to constructor args to change back + private final boolean initialDisplayV2; + private boolean curVersion = false; private SmaliModel model; - SmaliArea(ContentPanel contentPanel, JClass node) { + SmaliArea(ContentPanel contentPanel, JClass node, boolean initialDisplayV2) { super(contentPanel, node); this.textNode = new TextNode(node.getName()); + this.initialDisplayV2 = initialDisplayV2; setCodeFoldingEnabled(true); @@ -85,7 +93,9 @@ public final class SmaliArea extends AbstractCodeArea { settings.sync(); } }); - getPopupMenu().add(cbUseSmaliV2); + if (allowToggleV2) { + getPopupMenu().add(cbUseSmaliV2); + } switchModel(); } @@ -117,6 +127,10 @@ public final class SmaliArea extends AbstractCodeArea { return textNode; } + public boolean isShowingDalvikBytecode() { + return model instanceof DebugModel; + } + public JClass getJClass() { return (JClass) node; } @@ -454,4 +468,14 @@ public final class SmaliArea extends AbstractCodeArea { } }; } + + @Override + public CodePanelSyncer createCodePanelSyncer() { + return new SmaliSyncer(this); + } + + @Override + public boolean sync(CodePanelSyncer codePanelSyncer) { + return codePanelSyncer.syncTo(this); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeMetadataRange.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeMetadataRange.java new file mode 100644 index 000000000..eb036d3b9 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeMetadataRange.java @@ -0,0 +1,42 @@ +package jadx.gui.ui.codearea.sync; + +import java.util.Map; + +import jadx.api.metadata.ICodeAnnotation; + +/** + * Marks the start and end of annotation within a CodeMetadataStorage + */ +public class CodeMetadataRange { + // Use Map.Entry here because Java has no built in tuple/pair utility + private final Map.Entry start; + private final Map.Entry end; + + CodeMetadataRange( + Map.Entry start, + Map.Entry end) { + this.start = start; + this.end = end; + } + + Map.Entry getStart() { + return start; + } + + Map.Entry getEnd() { + return end; + } + + @Override + public String toString() { + return "CodeMetadataRange{start=" + + start.getKey() + + "->" + + start.getValue() + + ",end=" + + end.getKey() + + "->" + + end.getValue() + + "}"; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncee.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncee.java new file mode 100644 index 000000000..b0e855987 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncee.java @@ -0,0 +1,8 @@ +package jadx.gui.ui.codearea.sync; + +/** + * Accepts a code panel syncer for syncing code areas + */ +public interface CodePanelSyncee { + boolean sync(CodePanelSyncer syncer); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncer.java new file mode 100644 index 000000000..63ac218b5 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncer.java @@ -0,0 +1,4 @@ +package jadx.gui.ui.codearea.sync; + +public interface CodePanelSyncer extends IToJavaSyncStrategy, IToSmaliSyncStrategy { +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncerAbstractFactory.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncerAbstractFactory.java new file mode 100644 index 000000000..a4fb0c2d2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodePanelSyncerAbstractFactory.java @@ -0,0 +1,5 @@ +package jadx.gui.ui.codearea.sync; + +public interface CodePanelSyncerAbstractFactory { + CodePanelSyncer createCodePanelSyncer(); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeSyncHighlighter.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeSyncHighlighter.java new file mode 100644 index 000000000..cc5769665 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/CodeSyncHighlighter.java @@ -0,0 +1,47 @@ +package jadx.gui.ui.codearea.sync; + +import java.awt.Color; + +import javax.swing.Timer; +import javax.swing.UIManager; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.Highlighter; +import javax.swing.text.Highlighter.HighlightPainter; + +import jadx.gui.ui.codearea.AbstractCodeArea; + +/** + * Highlighting and scrolling utility into a CodeArea for a given color + */ +public class CodeSyncHighlighter { + private final Color color; + + public CodeSyncHighlighter(Color color) { + this.color = color; + } + + public void highlightAndScrollToLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { + highlightLine(area, lineIndex); + area.scrollToPos(area.getLineStartOffset(lineIndex)); + } + + public void highlightLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { + int startOffset = area.getLineStartOffset(lineIndex); + int endOffset = area.getLineEndOffset(lineIndex); + highlightRange(area, startOffset, endOffset); + } + + // Highlight range in code area with a temporary yellow highlight + public void highlightRange(AbstractCodeArea area, int startOffset, int endOffset) throws BadLocationException { + Highlighter hl = area.getHighlighter(); + HighlightPainter painter = + new DefaultHighlighter.DefaultHighlightPainter(this.color); + Object tag = hl.addHighlight(startOffset, endOffset, painter); + new Timer(1000, e -> hl.removeHighlight(tag)).start(); + } + + public static CodeSyncHighlighter defaultHighlighter() { + return new CodeSyncHighlighter(UIManager.getColor("TabbedPane.hoverColor")); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineJavaSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineJavaSyncer.java new file mode 100644 index 000000000..b9334d4b2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineJavaSyncer.java @@ -0,0 +1,115 @@ +package jadx.gui.ui.codearea.sync; + +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/** + * Use debug line info from dex to correlate from java to java/smali + */ +public class DebugLineJavaSyncer implements IToSmaliSyncStrategy, IToJavaSyncStrategy { + private static final Logger LOG = LoggerFactory.getLogger(DebugLineJavaSyncer.class); + + private final CodeArea from; + + public DebugLineJavaSyncer(CodeArea area) { + this.from = area; + } + + @Override + public boolean syncTo(CodeArea to) { + // This might be any combination between java/simple/fallback + // We cannot just rely on the current line. + // Instead try to correlate with line mappings. + try { + int lineIndex = from.getCaretLineNumber(); + Map toLineMapping = to.getFunctionUniqueLineMappings(); + // lineIndex is 0-indexed whereas the line mappings are based off a 1-index. + Integer sourceLine = getClosestSourceLine(lineIndex + 1); + if (sourceLine == null) { + return false; + } + // find the equivalent linenumber in the 'to' by a reverse lookup from the source line + for (Map.Entry entry : toLineMapping.entrySet()) { + int toLine = entry.getKey(); + int candidateSourceLine = entry.getValue(); + if (sourceLine == candidateSourceLine) { + // we have the mapped line we target the lineIndex which is a 0-index + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, toLine - 1); + LOG.info("{} - successful sync of code to code", LOG.getName()); + return true; + } + } + } catch (Exception e) { + LOG.error("{} - Failed to sync from CodeArea to CodeArea: {}", LOG.getName(), e.getLocalizedMessage()); + } + return false; + } + + @Override + public boolean syncTo(SmaliArea to) { + try { + int lineIndex = from.getCaretLineNumber(); + + // lineIndex is 0-indexed but the line mappings are based of 1-indexed line numbers. + int lineNum = lineIndex + 1; + Integer sourceLine = getClosestSourceLine(lineNum); + if (sourceLine == null) { + to.removeAllLineHighlights(); + LOG.debug("decompiled line {} not mapped to source line", lineNum); + return false; + } + + // find the smali line where ".line " is + LOG.debug("Finding \".line {}\" in smali", sourceLine); + int smaliLine = findSmaliLineIndex(to, sourceLine); + if (smaliLine < 0) { + LOG.warn("{} - Source line {} not annotated in Smali", LOG.getName(), sourceLine); + return false; + } + + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLine); + LOG.info("{} - successful sync of code to smali", LOG.getName()); + return true; + } catch (Exception ex) { + LOG.error("{} - Failed to sync CodeArea to SmaliArea: {}", LOG.getName(), ex.getLocalizedMessage()); + } + return false; + } + + private @Nullable Integer getClosestSourceLine(int lineNum) { + // get the line mappings of the Java/Simple/Fallback code + Map lineMapping = from.getFunctionUniqueLineMappings(); + if (lineMapping == null || lineMapping.isEmpty()) { + return null; + } + // get the source line from the decomp line + Integer sourceLine = null; + // Some of the intermediate lines are not mapped so keep going back until we find one + // e.g. multiple instruction lines in the 'Simple' view belong to a single source line + while (lineNum >= 0 && (sourceLine = lineMapping.get(lineNum)) == null) { + --lineNum; + } + return sourceLine; + } + + /** + * find the ".line \d+" line in the smali + */ + private static int findSmaliLineIndex(SmaliArea smaliArea, int sourceLine) { + String line = ".line " + Integer.toString(sourceLine); + String[] smaliLines = smaliArea.getText().split("\\R"); + for (int i = 0; i < smaliLines.length; ++i) { + String l = smaliLines[i]; + if (l.trim().equals(line)) { + return i; + } + } + return -1; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineSmaliSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineSmaliSyncer.java new file mode 100644 index 000000000..3f7c5cfc2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/DebugLineSmaliSyncer.java @@ -0,0 +1,140 @@ +package jadx.gui.ui.codearea.sync; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/** + * Use Debug lines in smali from dex debug info to correlate with code + */ +public class DebugLineSmaliSyncer implements IToJavaSyncStrategy { + private static final Logger LOG = LoggerFactory.getLogger(DebugLineSmaliSyncer.class); + + private final SmaliArea from; + + public DebugLineSmaliSyncer(SmaliArea area) { + this.from = area; + } + + @Override + public boolean syncTo(CodeArea to) { + try { + // Get the from lines and currentline index + int lineIndex = from.getCaretLineNumber(); + String[] fromLines = from.getText().split("\\R"); + if (lineIndex >= fromLines.length) { + return false; + } + + // find an Anchor to guide what to look for and highlight in the CodeArea + Anchor anchor = findNearestAnchor(lineIndex, fromLines); + if (anchor == null) { + LOG.error("{} - No Smali Anchor found", LOG.getName()); + return false; + } + + if (anchor.getType() == Anchor.Type.SOURCE_LINE) { + LOG.debug(anchor.toString()); + Map toDecompToSourceMapping = to.getFunctionUniqueLineMappings(); + for (Map.Entry entry : toDecompToSourceMapping.entrySet()) { + int decompLine = entry.getKey(); + int sourceLine = entry.getValue(); + if (anchor.getCodeMappedLineNumber() == sourceLine) { + int decompLineIndex = decompLine - 1; + LOG.debug("Highlighting {} on {}", decompLine, to); + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, decompLineIndex); + LOG.info("{} - successful sync of smali to code", LOG.getName()); + return true; + } + } + } + to.removeAllLineHighlights(); + } catch (Exception ex) { + LOG.error("{} - Failed to sync from Smali to Code", LOG.getName(), ex); + } + return false; + } + + @Nullable + private Anchor findNearestAnchor(int smaliLineNumber, String[] lines) { + for (int i = smaliLineNumber; i >= 0; i--) { + String trimmedLine = lines[i].trim(); + if (trimmedLine.startsWith(".line")) { + return new Anchor(Anchor.Type.SOURCE_LINE, trimmedLine, i); + } + if (trimmedLine.startsWith(".method")) { + return new Anchor(Anchor.Type.METHOD_START, trimmedLine, i); + } + if (trimmedLine.startsWith(".end")) { + return new Anchor(Anchor.Type.METHOD_END, trimmedLine, i); + } + if (trimmedLine.startsWith(".field")) { + return new Anchor(Anchor.Type.FIELD, trimmedLine, i); + } + if (trimmedLine.startsWith(".class")) { + return new Anchor(Anchor.Type.CLASS, trimmedLine, smaliLineNumber); + } + } + return null; + } + + /** + * Line in the smali that can be used to find a section to highlight in the code area + */ + private static class Anchor { + public enum Type { + SOURCE_LINE, + METHOD_START, + METHOD_END, + FIELD, + CLASS + } + + private final Type type; + private final String line; + private final int smaliLineNumber; + private int codeMappedLineNumber = -1; + + public Anchor(Type type, String line, int smaliLineNumber) { + this.type = type; + this.line = line; + this.smaliLineNumber = smaliLineNumber; + this.map(); + } + + public Type getType() { + return type; + } + + public int getCodeMappedLineNumber() { + return codeMappedLineNumber; + } + + private void map() { + switch (type) { + case SOURCE_LINE: + Pattern p = Pattern.compile("(\\.line\\s)(\\d+)"); + Matcher m = p.matcher(line); + if (m.find()) { + codeMappedLineNumber = Integer.parseInt(m.group(2)); + } + break; + default: + codeMappedLineNumber = -1; + break; + } + } + + @Override + public String toString() { + return String.format("Anchor %s, %d, %d", type.name(), smaliLineNumber, codeMappedLineNumber); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToJavaSyncStrategy.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToJavaSyncStrategy.java new file mode 100644 index 000000000..1cc56b8a2 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToJavaSyncStrategy.java @@ -0,0 +1,7 @@ +package jadx.gui.ui.codearea.sync; + +import jadx.gui.ui.codearea.CodeArea; + +public interface IToJavaSyncStrategy { + boolean syncTo(CodeArea area); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToSmaliSyncStrategy.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToSmaliSyncStrategy.java new file mode 100644 index 000000000..59db33ebb --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/IToSmaliSyncStrategy.java @@ -0,0 +1,7 @@ +package jadx.gui.ui.codearea.sync; + +import jadx.gui.ui.codearea.SmaliArea; + +public interface IToSmaliSyncStrategy { + boolean syncTo(SmaliArea area); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetJavaSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetJavaSyncer.java new file mode 100644 index 000000000..cd5abf904 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetJavaSyncer.java @@ -0,0 +1,323 @@ +package jadx.gui.ui.codearea.sync; + +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeNodeRef; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.core.dex.nodes.MethodNode; +import jadx.gui.device.debugger.DbgUtils; +import jadx.gui.device.debugger.smali.SmaliMethodNode; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/** + * Use insn code offsets to sync code panel area to code/smali + * This only works for Smali when SmaliArea is showing the dalvik bytecode. + */ +public class InsnOffsetJavaSyncer implements IToJavaSyncStrategy, IToSmaliSyncStrategy { + private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetJavaSyncer.class); + + private final CodeArea from; + + public InsnOffsetJavaSyncer(CodeArea area) { + this.from = area; + } + + @Override + public boolean syncTo(SmaliArea to) { + if (!to.isShowingDalvikBytecode()) { + return false; + } + + // 1. Find the Method start and end boundaries enclosing the caret position in the code metadata + // 2. Find the closest InsnCodeOffset range within the method boundary corresponding to the caret + // position + // 3. Get all of the smali lines which fall within the InsnCodeOffset range. + // 4. Highlight those found in 3. and scroll to the first one. + int caretPos = from.getCaretPosition(); + CodeMetadataRange mthRange = findEnclosingMethodRange(caretPos); + if (mthRange == null) { + return false; + } + + Integer mthDefPos = mthRange.getStart().getKey(); + Integer mthEndPos = mthRange.getEnd().getKey(); + + LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos); + LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos); + LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos); + + CodeMetadataRange insnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos); + if (insnOffsetRange == null) { + return false; + } + + String mthID = getMthRawFullID(mthDefPos); + SmaliMethodNode smaliMthNode = DbgUtils.getSmaliMethodNode(to.getJClass(), mthID); + if (smaliMthNode == null) { + LOG.error("{} - mth ID {} not mapped to a SmaliMethodNode", LOG.getName(), mthID); + return false; + } + + List smaliLines = getMappedSmaliLines(smaliMthNode, insnOffsetRange); + if (smaliLines.size() < 2) { + return false; + } + + try { + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, smaliLines.get(0)); + for (int i = 1; i < smaliLines.size(); ++i) { + CodeSyncHighlighter.defaultHighlighter().highlightLine(to, smaliLines.get(i)); + } + LOG.info("{} - successful sync of code to smali", LOG.getName()); + return true; + } catch (Exception ex) { + LOG.error("{} - Failed to sync code to smali with instruction offsets ", LOG.getName(), ex); + } + + return false; + } + + @Override + public boolean syncTo(CodeArea to) { + int caretPos = from.getCaretPosition(); + CodeMetadataRange fromMthRange = findEnclosingMethodRange(caretPos); + if (fromMthRange == null) { + return false; + } + + Integer mthDefPos = fromMthRange.getStart().getKey(); + Integer mthEndPos = fromMthRange.getEnd().getKey(); + + LOG.debug("InsnOffsetJavaSyncer caretPos = {}", caretPos); + LOG.debug("InsnOffsetJavaSyncer mthDefPos = {}", mthDefPos); + LOG.debug("InsnOffsetJavaSyncer mthEndPos = {}", mthEndPos); + + CodeMetadataRange fromInsnOffsetRange = findOffsetRange(caretPos, mthDefPos, mthEndPos); + if (fromInsnOffsetRange == null) { + return false; + } + + String mthID = getMthRawFullID(mthDefPos); + + // now search for this range within the target area + CodeMetadataRange toMthRange = findMethodRange(mthID, to); + if (toMthRange == null) { + return false; + } + + // search for the first insn offset + int firstInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getStart().getValue()).getOffset(); + Integer highlightPosStart = to.getCodeMetadata().searchDown(toMthRange.getStart().getKey(), (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { + return null; + } + int pos = ((InsnCodeOffset) ann).getOffset(); + if (pos != firstInsnOffset) { + return null; + } + return offset; + }); + + if (highlightPosStart == null) { + return false; + } + + // search for the second insn offset + int secondInsnOffset = ((InsnCodeOffset) fromInsnOffsetRange.getEnd().getValue()).getOffset(); + Integer highlightPosEnd = to.getCodeMetadata().searchDown(highlightPosStart, (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { + return null; + } + int pos = ((InsnCodeOffset) ann).getOffset(); + if (pos != secondInsnOffset) { + return null; + } + return offset; + }); + + if (highlightPosEnd == null) { + return false; + } + + to.scrollToPos(highlightPosStart); + try { + CodeSyncHighlighter.defaultHighlighter().highlightRange(to, highlightPosStart, highlightPosEnd); + LOG.info("{} - successful sync of code to code", LOG.getName()); + return true; + } catch (Exception ex) { + LOG.error("{} - Unable to highlight code area from insn offset mappings {} -> {}", LOG.getName(), highlightPosStart, + highlightPosEnd); + } + return false; + } + + @Nullable + private static CodeMetadataRange findMethodRange(String mthFullRawID, CodeArea area) { + Map.Entry toMthDecl = area.getCodeMetadata().searchDown(0, (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) { + return null; + } + NodeDeclareRef decl = (NodeDeclareRef) ann; + ICodeNodeRef node = decl.getNode(); + if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) { + return null; + } + MethodNode mth = (MethodNode) node; + if (!mth.getMethodInfo().getRawFullId().equals(mthFullRawID)) { + return null; + } + return new SimpleEntry<>(offset, ann); + }); + + if (toMthDecl == null) { + return null; + } + + Map.Entry toMthEnd = area.getCodeMetadata().searchDown(toMthDecl.getKey(), (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.END) { + return null; + } + return new SimpleEntry<>(offset, ann); + }); + + if (toMthEnd == null) { + return null; + } + + return new CodeMetadataRange(toMthDecl, toMthEnd); + } + + @Nullable + private CodeMetadataRange findEnclosingMethodRange(Integer startPos) { + Map.Entry mthDef = from.getCodeMetadata().searchUp(startPos, (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.DECLARATION) { + return null; + } + NodeDeclareRef decl = (NodeDeclareRef) ann; + ICodeNodeRef node = decl.getNode(); + if (node.getAnnType() != ICodeAnnotation.AnnType.METHOD) { + return null; + } + return new SimpleEntry<>(offset, ann); + }); + + if (mthDef == null) { + return null; + } + + Map.Entry mthEnd = from.getCodeMetadata().searchDown(startPos, (offset, ann) -> { + if (ann.getAnnType() != ICodeAnnotation.AnnType.END) { + return null; + } + return new SimpleEntry<>(offset, ann); + }); + + if (mthEnd == null) { + return null; + } + + return new CodeMetadataRange(mthDef, mthEnd); + } + + /** + * Gets a CodeMetadataRange for the from CodeArea where start and end + * are InsnCodeOffsets whose offsets are monotonically increasing. + * + * @param - startPos the starting position to start searching from + * @param - mthDefPos the method node decl position enclosing the range + * @param - mthEndPos the method end position enclosing the range + */ + @Nullable + private CodeMetadataRange findOffsetRange(Integer startPos, Integer mthDefPos, Integer mthEndPos) { + Map.Entry first = findInsnOffsetBeforePos(startPos, mthDefPos); + Map.Entry second = findInsnOffsetAfterPos(startPos, mthEndPos); + if (first == null || second == null) { + LOG.warn("{} - Unable to find InsnCodeOffsets between {} -> {}", LOG.getName(), mthDefPos, mthEndPos); + return null; + } + int startOffset = ((InsnCodeOffset) first.getValue()).getOffset(); + int endOffset = ((InsnCodeOffset) second.getValue()).getOffset(); + if (startOffset > endOffset) { + LOG.warn("{} - insn startOffset={} is greater than insn endOffset={} - cannot construct range", LOG.getName(), startOffset, + endOffset); + return null; + } + return new CodeMetadataRange(first, second); + } + + @Nullable + private Map.Entry findInsnOffsetBeforePos(Integer startPos, Integer limit) { + return from.getCodeMetadata().searchUp(startPos, (offset, ann) -> { + if (offset <= limit) { + return null; + } + if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { + return null; + } + return new SimpleEntry(offset, ann); + }); + } + + @Nullable + private Map.Entry findInsnOffsetAfterPos(Integer startPos, Integer limit) { + return from.getCodeMetadata().searchDown(startPos, (offset, ann) -> { + if (offset >= limit) { + return null; + } + if (ann.getAnnType() != ICodeAnnotation.AnnType.OFFSET) { + return null; + } + return new SimpleEntry(offset, ann); + }); + } + + /** + * Assumes that there is a NodeDeclareRef{MethodNode{}} annotation at mthDefPos in the `from` + * CodeInfoMetadata + */ + private String getMthRawFullID(Integer mthDefPos) { + ICodeAnnotation ann = from.getCodeMetadata().getAt(mthDefPos); + NodeDeclareRef ref = (NodeDeclareRef) ann; + MethodNode mth = (MethodNode) ref.getNode(); + return mth.getMethodInfo().getRawFullId(); + } + + /** + * Gets the mapped smali line indices for the code offsets of interest + * + * @param smaliMethodNode - method of interest + * @param insnCodeOffsetRange - code offset range from the caret pos + * @return + */ + private static List getMappedSmaliLines( + SmaliMethodNode smaliMethodNode, + CodeMetadataRange insnCodeOffsetRange) { + List lines = new ArrayList<>(); + int startInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getStart().getValue()).getOffset(); + int endInsnCodeOffset = ((InsnCodeOffset) insnCodeOffsetRange.getEnd().getValue()).getOffset(); + // Line mappings are Line index -> Code offset + Map smaliLineMapping = smaliMethodNode.getLineMapping(); + LOG.debug("startInsnPos={}, endInsnPos={}", startInsnCodeOffset, endInsnCodeOffset); + for (Map.Entry lineToCodeOffset : smaliLineMapping.entrySet()) { + LOG.debug("line={} -> codeOffset={}", lineToCodeOffset.getKey(), lineToCodeOffset.getValue()); + // Asume code offsets from smali debug utils are the same as those in the code metadata + if (lineToCodeOffset.getValue() == startInsnCodeOffset || lineToCodeOffset.getValue() == endInsnCodeOffset) { + lines.add(lineToCodeOffset.getKey()); + } + } + Collections.sort(lines); // only two elements + return lines; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetSmaliSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetSmaliSyncer.java new file mode 100644 index 000000000..61486b683 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/InsnOffsetSmaliSyncer.java @@ -0,0 +1,135 @@ +package jadx.gui.ui.codearea.sync; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.metadata.ICodeAnnotation; +import jadx.api.metadata.ICodeMetadata; +import jadx.api.metadata.annotations.InsnCodeOffset; +import jadx.api.metadata.annotations.NodeDeclareRef; +import jadx.core.dex.nodes.MethodNode; +import jadx.gui.device.debugger.DbgUtils; +import jadx.gui.treemodel.JClass; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/* + * Use insn code offsets to sync smali to code panel area + * This only works for Smali when the SmaliArea is showing the dalvik bytecode + */ +public class InsnOffsetSmaliSyncer implements IToJavaSyncStrategy { + private static final Logger LOG = LoggerFactory.getLogger(InsnOffsetSmaliSyncer.class); + + private final SmaliArea from; + + public InsnOffsetSmaliSyncer(SmaliArea area) { + this.from = area; + } + + @Override + public boolean syncTo(CodeArea to) { + if (!from.isShowingDalvikBytecode()) { + // This strategy can only be used when the debug model has been used to generate the smali. + // This populates the code offsets by line as opposed to just text. + return false; + } + // 1. Get the code offset from the Smali caret line number + // 2. Find the appropriate NodeDeclareRef for the method enclosed in the CodeArea annotations + // 3. Find all code offset range intervals in the map which contain the code offset + // 4. Get the CodeArea positions of these intervals and hightlight them in the code area + // 5. Scroll to the first one. + JClass jclass = from.getJClass(); + Map.Entry lineInfo = DbgUtils.getCodeOffsetInfoByLine(jclass, from.getCaretLineNumber()); + if (lineInfo == null) { + return false; + } + Integer lineInfoPos = lineInfo.getValue(); + LOG.debug("lineInfo key {}, lineInfo value {}, caretLineNumber {}", lineInfo.getKey(), lineInfo.getValue(), + from.getCaretLineNumber()); + ICodeMetadata toMetadata = to.getCodeMetadata(); + NavigableMap codeAreaAnnotationMap = + (NavigableMap) toMetadata.getAsMap(); + Iterator> methodDecl = + findMethodDeclAnnotation(codeAreaAnnotationMap, lineInfo.getKey()); + if (methodDecl == null) { + LOG.warn("{} - No NodeDeclareRef exists for {}", LOG.getName(), lineInfo.getKey()); + return false; + } + // Looking through the annotations in order from the Method declaration to its end + // compare every adjacent pair of instruction offsets where the second is greater than the first. + // Highlight if the smali offset falls between the second and the first. + Iterator> it = methodDecl; + NavigableMap.Entry prev = null; + List offsetBoundariesToHighlight = new ArrayList<>(); + while (it.hasNext()) { + NavigableMap.Entry entry = it.next(); + if (entry.getValue().getAnnType() == ICodeAnnotation.AnnType.END) { + break; + } + if (entry.getValue().getAnnType() != ICodeAnnotation.AnnType.OFFSET) { + continue; + } + if (prev != null) { + InsnCodeOffset currentInsnOffset = (InsnCodeOffset) entry.getValue(); + InsnCodeOffset prevInsnOffset = (InsnCodeOffset) prev.getValue(); + if (prevInsnOffset.getOffset() <= lineInfoPos && lineInfoPos <= currentInsnOffset.getOffset()) { + offsetBoundariesToHighlight.add(new CodeMetadataRange(prev, entry)); + } + } + prev = entry; + } + + if (offsetBoundariesToHighlight.isEmpty()) { + return false; + } + + to.scrollToPos(offsetBoundariesToHighlight.get(0).getStart().getKey()); + + try { + for (CodeMetadataRange cmr : offsetBoundariesToHighlight) { + LOG.debug("Highlighting {}", cmr); + CodeSyncHighlighter.defaultHighlighter().highlightRange(to, cmr.getStart().getKey(), cmr.getEnd().getKey()); + } + LOG.info("{} - successful sync of smali to code", LOG.getName()); + return true; + } catch (Exception ex) { + LOG.error("{} - Unable to highlight smali -> code insn offset range: {}", LOG.getName(), ex.getLocalizedMessage()); + } + return false; + } + + /** + * Find the NodeDeclareRef annotation of the method identified by smaliLineMthFullID + * + * @param map the annotation map from the CodeArea + * @param smaliLineMthFullID the raw full method ID to look for + * @return iterator to the entry in the annotation map + */ + @Nullable + private static Iterator> findMethodDeclAnnotation( + NavigableMap map, + String smaliLineMthFullID) { + // Ensure we use NavigableMap here to get ordering guarantee from iterator call + Iterator> it = map.descendingMap().entrySet().iterator(); + while (it.hasNext()) { + NavigableMap.Entry entry = it.next(); + if (entry.getValue() instanceof NodeDeclareRef) { + NodeDeclareRef nodeDeclareRef = (NodeDeclareRef) entry.getValue(); + if (nodeDeclareRef.getNode() instanceof MethodNode) { + MethodNode mth = (MethodNode) nodeDeclareRef.getNode(); + if (mth.getMethodInfo().getRawFullId().equals(smaliLineMthFullID)) { + return it; + } + } + } + } + return null; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/JavaSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/JavaSyncer.java new file mode 100644 index 000000000..415f8cc00 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/JavaSyncer.java @@ -0,0 +1,32 @@ +package jadx.gui.ui.codearea.sync; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/** + * Syncs a Java code panel area (Java/Simple/Fallback) to another area + */ +public class JavaSyncer implements CodePanelSyncer { + private static final Logger LOG = LoggerFactory.getLogger(JavaSyncer.class); + + private final DebugLineJavaSyncer debugLineSyncer; + private final InsnOffsetJavaSyncer insnOffsetSyncer; + + public JavaSyncer(CodeArea area) { + this.debugLineSyncer = new DebugLineJavaSyncer(area); + this.insnOffsetSyncer = new InsnOffsetJavaSyncer(area); + } + + @Override + public boolean syncTo(CodeArea to) { + return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); + } + + @Override + public boolean syncTo(SmaliArea to) { + return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/SmaliSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/SmaliSyncer.java new file mode 100644 index 000000000..ac450c05d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/SmaliSyncer.java @@ -0,0 +1,39 @@ +package jadx.gui.ui.codearea.sync; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.SmaliArea; + +/** + * Syncs a Smali code panel area to another area + */ +public class SmaliSyncer implements CodePanelSyncer { + private static final Logger LOG = LoggerFactory.getLogger(SmaliSyncer.class); + + private final SmaliArea from; + private final InsnOffsetSmaliSyncer insnOffsetSyncer; + private final DebugLineSmaliSyncer debugLineSyncer; + + public SmaliSyncer(SmaliArea area) { + this.from = area; + this.insnOffsetSyncer = new InsnOffsetSmaliSyncer(area); + this.debugLineSyncer = new DebugLineSmaliSyncer(area); + } + + @Override + public boolean syncTo(CodeArea to) { + // first try debug lines then insn offsets + return debugLineSyncer.syncTo(to) || insnOffsetSyncer.syncTo(to); + } + + @Override + public boolean syncTo(SmaliArea to) { + if (from.isShowingDalvikBytecode() == to.isShowingDalvikBytecode()) { + // smali -> smali just highlight the current line but only if content is the same + to.scrollToPos(from.getLineStartOffsetOfCurrentLine()); + } + return true; // Prevent fallback syncing + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaLine.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaLine.java new file mode 100644 index 000000000..507672677 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaLine.java @@ -0,0 +1,95 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import javax.swing.text.BadLocationException; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.ui.codearea.AbstractCodeArea; + +abstract class AbstractCodeAreaLine { + private final AbstractCodeArea area; + private final int lineIndex; + private final String line; + + protected AbstractCodeAreaLine(AbstractCodeArea area, int lineIndex) throws BadLocationException { + this.area = area; + this.lineIndex = lineIndex; + this.line = this.area.getText().split("\\R")[lineIndex]; + } + + public AbstractCodeArea getArea() { + return area; + } + + public int getLineIndex() { + return lineIndex; + } + + public String getStr() { + return line; + } + + public String getTrimmedStr() { + return line.trim(); + } + + public abstract AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException; + + public abstract boolean isClassDeclaration(); + + public abstract boolean isMethodOrConstructorDeclaration(); + + public abstract boolean isFieldDeclaration(); + + @Nullable + public abstract String extractDeclaredMethodName(); + + @Nullable + public abstract String extractDeclaredClassName(); + + protected abstract MethodDeclaration createMethodDeclaration() throws FallbackSyncException; + + /** + * This could be itself or: + * - the enclosing method delcaration if line is in a method + * - the enclosing class declaration if line is a field declaration + */ + public IDeclaration getEnclosingScopeDeclaration() throws BadLocationException, FallbackSyncException { + IDeclaration decl = this.getDeclaration(); + if (decl != null) { + return decl; + } + for (int i = lineIndex - 1; i >= 0; i--) { + AbstractCodeAreaLine line = getLineAt(i); + boolean enclosingDecl = line.isScopeDeclarationLine(); + if (enclosingDecl) { + return line.getDeclaration(); + } + } + throw new FallbackSyncException("No enclosing declaration found for " + this); + } + + public boolean isScopeDeclarationLine() { + return isClassDeclaration() || isMethodOrConstructorDeclaration(); + } + + public boolean isDeclarationLine() { + return isScopeDeclarationLine() || isFieldDeclaration(); + } + + @Nullable + public IDeclaration getDeclaration() throws FallbackSyncException { + if (isClassDeclaration()) { + return new ClassDeclaration(this); + } + if (isMethodOrConstructorDeclaration()) { + return createMethodDeclaration(); + } + return null; + } + + @Override + public String toString() { + return line; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaToken.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaToken.java new file mode 100644 index 000000000..6a94605ee --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/AbstractCodeAreaToken.java @@ -0,0 +1,78 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.text.BadLocationException; + +import jadx.gui.ui.codearea.AbstractCodeArea; + +public abstract class AbstractCodeAreaToken { + protected final AbstractCodeArea area; + private final int atPos; + protected int startPos; + protected int length; + + protected AbstractCodeAreaToken(AbstractCodeArea area, int at) throws BadLocationException, FallbackSyncException { + this.area = area; + this.atPos = at; + this.extractTokenAt(); + } + + public int getAtPos() { + return atPos; + } + + public String getStr() throws BadLocationException { + return area.getText(this.startPos, this.length); + } + + public boolean isMethodConstructorDeclarationOrCall() throws BadLocationException { + return area.getText(this.startPos + this.length, 1).equals("("); + } + + // Class field reference within a method + public abstract boolean isFieldReference() throws BadLocationException; + + // Class field token in class field declaration + public abstract boolean isClassField() throws BadLocationException; + + public abstract AbstractCodeAreaLine getLine() throws BadLocationException; + + // Helper to extract token under caret (at pos) + private void extractTokenAt() throws FallbackSyncException, BadLocationException { + String text = area.getText(); + if (text == null || text.isEmpty()) { + throw new FallbackSyncException("text area is null or empty"); + } + // Find word boundaries around caretPos + int start = atPos; + int end = atPos; + + while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - 1))) { + start--; + } + while (end < text.length() && Character.isJavaIdentifierPart(text.charAt(end))) { + end++; + } + if (start == end) { + // No identifier found, try string literal at caret line + int line = area.getLineOfOffset(atPos); + String lineText = area.getText(area.getLineStartOffset(line), area.getLineEndOffset(line) - area.getLineStartOffset(line)); + Pattern p = Pattern.compile("\"([^\"]*)\""); + Matcher m = p.matcher(lineText); + while (m.find()) { + int litStart = area.getLineStartOffset(line) + m.start(1); + int litEnd = area.getLineStartOffset(line) + m.end(1); + if (atPos >= litStart && atPos <= litEnd) { + this.startPos = m.start(1); + this.length = m.end(1) - m.start(1); + return; + } + } + throw new FallbackSyncException("Unable to extract token at position " + atPos); + } + this.startPos = start; + this.length = end - start; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/ClassDeclaration.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/ClassDeclaration.java new file mode 100644 index 000000000..a97b3cf47 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/ClassDeclaration.java @@ -0,0 +1,41 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import java.util.Objects; + +public class ClassDeclaration implements IDeclaration { + private final AbstractCodeAreaLine line; + private final String name; + + public ClassDeclaration(AbstractCodeAreaLine line) throws FallbackSyncException { + this.name = line.extractDeclaredClassName(); + if (this.name == null) { + throw new FallbackSyncException("line does not declare a class: " + toString()); + } + this.line = line; + } + + @Override + public String getIdentifyingName() { + return name; + } + + @Override + public AbstractCodeAreaLine getLine() { + return line; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ClassDeclaration) { + ClassDeclaration cd = (ClassDeclaration) o; + return this.getIdentifyingName().equals(cd.getIdentifyingName()); + } + return false; + } + + // Not necessary but removes checkstyle warning + @Override + public int hashCode() { + return Objects.hash(line, name); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncException.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncException.java new file mode 100644 index 000000000..4c0f56aba --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncException.java @@ -0,0 +1,7 @@ +package jadx.gui.ui.codearea.sync.fallback; + +public class FallbackSyncException extends Exception { + public FallbackSyncException(String msg) { + super(msg); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncer.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncer.java new file mode 100644 index 000000000..59a5551a3 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/FallbackSyncer.java @@ -0,0 +1,256 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.text.BadLocationException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.AbstractCodeArea; +import jadx.gui.ui.codearea.CodeArea; +import jadx.gui.ui.codearea.CodePanel; +import jadx.gui.ui.codearea.SmaliArea; +import jadx.gui.ui.codearea.sync.CodeSyncHighlighter; + +/** + * Regex/String based sync strategy of toPanel when clicking in fromPanel + * Summary of syncing strategy: + * 1) Look for an identifying class member token under the caret position. + * 2) If found look for the enclosing method or class declaration. + * 3) If the line is a declaration line, find the equivalent line in the other code panel. + * 4) Otherwise find the nth occurence of the token in the enclosing method/class in the other code + * panel. + * The following are not yet supported: + * - generic classes/methods + * - anonymous classes + * - lambda functions + * - constructors + */ +public class FallbackSyncer { + private static final Logger LOG = LoggerFactory.getLogger(FallbackSyncer.class); + + public static boolean sync(CodePanel fromPanel, CodePanel toPanel) throws BadLocationException, Exception { + LOG.debug("FALLBACK SYNC START"); + try { + AbstractCodeArea from = fromPanel.getCodeArea(); + AbstractCodeArea to = toPanel.getCodeArea(); + + int caretPos = from.getCaretPosition(); + int lineIndex = from.getLineOfOffset(caretPos); + String[] fromLines = from.getText().split("\\R"); + if (lineIndex >= fromLines.length) { + return false; + } + + String caretLine = fromLines[lineIndex]; + LOG.debug("Caret line [{}]: {}", caretPos, caretLine); + + // Extract token under caret (string literal or identifier) + AbstractCodeAreaToken areaToken = FallbackSyncer.getToken(from, caretPos); + String token = areaToken.getStr(); + LOG.debug("Token at caret: '{}'", token); + if (token == null || token.isEmpty()) { + return false; + } + + if (!allowSync(areaToken)) { + LOG.debug("Fallback matching only applicable for variable, classname, field or method tokens"); + return false; + } + + return syncToIdentifyingNthOccurence(areaToken, to); + } finally { + LOG.debug("FALLBACK SYNC END"); + } + } + + // This function just serves as a way to create the correct Token type + // FallbackSyncer should be refactored to use CodePanelSyncer + private static AbstractCodeAreaToken getToken(AbstractCodeArea from, int caretPos) throws BadLocationException, FallbackSyncException { + if (from instanceof SmaliArea) { + return new SmaliAreaToken((SmaliArea) from, caretPos); + } + if (from instanceof CodeArea) { + return new JavaCodeAreaToken((CodeArea) from, caretPos); + } + throw new FallbackSyncException("Unknown AbstractCodeArea type for " + from); + } + + /** + * Looks for the nth occurence of the token in the enclosing class/method scope in the `to` area. + * If found, sync to it in the `to` area. + */ + private static boolean syncToIdentifyingNthOccurence(AbstractCodeAreaToken sourceToken, AbstractCodeArea to) + throws BadLocationException, FallbackSyncException { + AbstractCodeAreaLine tokenLine = sourceToken.getLine(); + + // Locate the method/class declaration line for context + IDeclaration fromDeclaration = tokenLine.getEnclosingScopeDeclaration(); + if (fromDeclaration == null) { + LOG.warn("Unable to find declaration line above {}", tokenLine); + return false; + } + AbstractCodeAreaLine fromDeclaringLine = fromDeclaration.getLine(); + + AbstractCodeArea from = fromDeclaringLine.getArea(); + String declarationLineStr = fromDeclaringLine.getStr(); + LOG.debug("Found declaration line: {}", declarationLineStr); + String nameToFind = fromDeclaration.getIdentifyingName(); + if (nameToFind == null || nameToFind.isEmpty()) { + return false; + } + + // Determine whether we're matching a class or method + boolean isClass = fromDeclaringLine.isClassDeclaration(); + String regex = isClass + ? generateClassRegex(nameToFind) + : generateMethodRegex(nameToFind); + + // Find the declaration in target text + Matcher matcher = Pattern.compile(regex).matcher(to.getText()); + LOG.debug("Searching for {} in targetText, isClass {}", nameToFind, isClass); + AbstractCodeAreaLine targetDeclLine = findTargetDeclaringLine(to, matcher, fromDeclaration); + if (targetDeclLine == null) { + LOG.debug("Cannot find target declaration line"); + return false; + } + int targetDeclarationLineIndex = targetDeclLine.getLineIndex(); + LOG.debug("Target declaration line {}", targetDeclLine.getStr()); + if (tokenLine.isScopeDeclarationLine()) { + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, targetDeclarationLineIndex); + LOG.info("{} - Highlighted target declaration line", LOG.getName(), targetDeclLine.getStr()); + return true; + } + + // Extract the method/class body from target + String methodBody = extractMethodBody(to, matcher.start()); + + // Find nth occurrence of token in source method + // Extract method body from source (to count occurrences) + Matcher fromMatcher = Pattern.compile(regex).matcher(from.getText()); + if (!fromMatcher.find()) { + LOG.debug("No method/class match found in source for regex: {}", regex); + return false; + } + String sourceMethodBody = extractMethodBody(from, fromMatcher.start()); + + // Count which occurrence of token the caret corresponds to in the source method body + String tokenStr = sourceToken.getStr(); + int caretPos = sourceToken.getAtPos(); + int caretOffsetInMethod = caretPos - fromMatcher.start(); + int nthOccurrence = 0; + Pattern tokenPattern = Pattern.compile("\"" + Pattern.quote(tokenStr) + "\"|\\b" + Pattern.quote(tokenStr) + "\\b"); + Matcher tokenMatcher = tokenPattern.matcher(sourceMethodBody); + + while (tokenMatcher.find()) { + if (tokenMatcher.start() > caretOffsetInMethod) { + break; + } + nthOccurrence++; + } + + LOG.debug("Caret is at occurrence number: {}", nthOccurrence); + + // Now find nth occurrence of token in target method body + tokenMatcher = tokenPattern.matcher(methodBody); + int occurrenceCount = 0; + while (tokenMatcher.find()) { + occurrenceCount++; + if (occurrenceCount == nthOccurrence) { + // Find absolute offset of this line in targetText + int tokenPosInMethod = tokenMatcher.start(); + int absoluteOffset = matcher.start() + tokenPosInMethod; + + // Find line start and end offset in target + int tokenLineIndex = to.getLineOfOffset(absoluteOffset); + CodeSyncHighlighter.defaultHighlighter().highlightAndScrollToLine(to, tokenLineIndex); + LOG.info("{} - Highlighted token '{}' at nth occurrence: {}", LOG.getName(), tokenStr, nthOccurrence); + return true; + } + } + + LOG.debug("No matching token or instruction found in method: {}", nameToFind); + return false; + } + + private static AbstractCodeAreaLine findTargetDeclaringLine( + AbstractCodeArea to, // target area + Matcher matcher, // matcher to search for method/ctor name + IDeclaration sourceDecl // source decl to match against + ) throws BadLocationException, FallbackSyncException { + // Find the declaration in target text + while (matcher.find()) { + LOG.debug("Match found at offset: {}", matcher.start()); + int targetDeclarationLineIndex = to.getLineOfOffset(matcher.start()); + AbstractCodeAreaLine toDeclCandidate = getLine(to, targetDeclarationLineIndex); + if (!toDeclCandidate.isScopeDeclarationLine()) { + continue; + } + IDeclaration targetDecl = toDeclCandidate.getDeclaration(); + if (sourceDecl.equals(targetDecl)) { + return toDeclCandidate; + } + } + return null; + } + + // Similar with the function above if refactored to use the CodePanelSyncer Abstraction we can + // remove this. + private static AbstractCodeAreaLine getLine(AbstractCodeArea area, int lineIndex) throws BadLocationException, FallbackSyncException { + if (area instanceof SmaliArea) { + return new SmaliAreaLine((SmaliArea) area, lineIndex); + } + if (area instanceof CodeArea) { + return new JavaCodeAreaLine((CodeArea) area, lineIndex); + } + throw new FallbackSyncException("Unknown AbstractCodeArea type for " + area); + } + + private static boolean allowSync(AbstractCodeAreaToken areaToken) throws BadLocationException { + boolean isOnDeclarationLine = areaToken.getLine().isDeclarationLine(); + return isOnDeclarationLine + || areaToken.isClassField() + || areaToken.isFieldReference() + || areaToken.isMethodConstructorDeclarationOrCall(); + } + + private static String generateClassRegex(String name) { + return "\\b(class|interface|enum)\\s+" + Pattern.quote(name) + "\\b" // java + + "|" + + "\\.class.*L.*" + Pattern.quote(name) + ";" // smali text + + "|" + + "Class:\\sL.*" + Pattern.quote(name) + ";"; // smali + dalvik + } + + private static String generateMethodRegex(String name) { + return "\\b" + Pattern.quote(name) + "\\s*\\(" // java like + + "|" + + "\\.method.*" + Pattern.quote(name) + "\\s*\\("; // smali + } + + private static String extractMethodBody(AbstractCodeArea area, int startIndex) { + String text = area.getText(); + if (area instanceof SmaliArea) { + int end = text.indexOf(".end method", startIndex); + return end != -1 ? text.substring(startIndex, end + ".end method".length()) : text.substring(startIndex); + } else { + int brace = 0; + boolean inMethod = false; + for (int i = startIndex; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '{') { + brace++; + inMethod = true; + } else if (c == '}') { + brace--; + if (brace == 0 && inMethod) { + return text.substring(startIndex, i + 1); + } + } + } + return text.substring(startIndex); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/IDeclaration.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/IDeclaration.java new file mode 100644 index 000000000..9c69734f4 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/IDeclaration.java @@ -0,0 +1,7 @@ +package jadx.gui.ui.codearea.sync.fallback; + +interface IDeclaration { + String getIdentifyingName(); + + AbstractCodeAreaLine getLine(); +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaLine.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaLine.java new file mode 100644 index 000000000..57c2648dd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaLine.java @@ -0,0 +1,104 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import javax.swing.text.BadLocationException; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.ui.codearea.CodeArea; + +public class JavaCodeAreaLine extends AbstractCodeAreaLine { + private static final Logger LOG = LoggerFactory.getLogger(JavaCodeAreaLine.class); + + public JavaCodeAreaLine(CodeArea area, int lineIndex) throws BadLocationException { + super(area, lineIndex); + } + + @Override + public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException { + return new JavaCodeAreaLine((CodeArea) getArea(), lineIndex); + } + + @Override + public boolean isClassDeclaration() { + return getTrimmedStr().matches(".*\\b(class|interface|enum)\\b.*\\{"); + } + + @Override + public boolean isMethodOrConstructorDeclaration() { + String l = getTrimmedStr(); + // Skip control-flow constructs (to avoid matching 'if', 'for', etc.) + // WARNING - we are relying on the code gen format output of jadx here and that it is trimmed. + // it also assumes that jadx will never output two statements on the same line separated by ';' + if (l.startsWith("if ") + || l.startsWith("for ") + || l.startsWith("while ") + || l.startsWith("switch ") + || l.startsWith("case ") + || l.startsWith("break ") + || l.startsWith("default ") + || l.startsWith("} else if ") + || l.startsWith("} else ") + || l.startsWith("try ") + || l.startsWith("} catch ") + || l.startsWith("} finally ") + || l.startsWith("throw ") + || l.startsWith("do ") + || l.startsWith("synchronized ")) { + return false; + } + boolean hasParens = l.contains("(") && l.contains(")"); + boolean isDefined = l.endsWith("{"); + boolean isAbstract = l.contains("abstract") && l.endsWith(";"); + return hasParens && (isDefined || isAbstract); + } + + @Override + public boolean isFieldDeclaration() { + try { + IDeclaration enclosingDeclaration = getEnclosingScopeDeclaration(); + if (!(enclosingDeclaration instanceof ClassDeclaration)) { + return false; + } + String line = getTrimmedStr(); + // This may also include fields which are anonymous classes or lambdas + return line.endsWith(";") || line.contains(" = "); + } catch (Exception ex) { + LOG.error("{} - Unable to determine if line is a field declaration", LOG.getName(), ex); + } + return false; + } + + @Override + public final @Nullable String extractDeclaredClassName() { + if (!isClassDeclaration()) { + return null; + } + String[] tokens = getTrimmedStr().split("\\s+"); + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].equals("class") || tokens[i].equals("interface") || tokens[i].equals("enum")) { + if (i + 1 < tokens.length) { + return tokens[i + 1]; + } + } + } + return null; + } + + @Override + public @Nullable String extractDeclaredMethodName() { + if (!isMethodOrConstructorDeclaration()) { + return null; + } + int paren = getTrimmedStr().indexOf('('); + String before = getTrimmedStr().substring(0, paren).trim(); + String[] parts = before.split("\\s+"); + return parts[parts.length - 1]; // last token + } + + @Override + protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException { + return MethodDeclaration.create(this); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaToken.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaToken.java new file mode 100644 index 000000000..a7e40c3b7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/JavaCodeAreaToken.java @@ -0,0 +1,35 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import javax.swing.text.BadLocationException; + +import jadx.gui.ui.codearea.CodeArea; + +public class JavaCodeAreaToken extends AbstractCodeAreaToken { + public JavaCodeAreaToken(CodeArea area, int at) throws BadLocationException, FallbackSyncException { + super(area, at); + } + + @Override + public boolean isClassField() throws BadLocationException { + AbstractCodeAreaLine line = getLine(); + if (!line.isFieldDeclaration()) { + return false; + } + // assignment immediately follows the token + if (line.getStr().contains("=")) { + return area.getText(this.startPos + this.length, 2).equals(" ="); + } + // ends with ';' + return area.getText(this.startPos + this.length, 1).equals(";"); + } + + @Override + public boolean isFieldReference() throws BadLocationException { + return area.getText(this.startPos - 5, 5).equals("this."); + } + + @Override + public AbstractCodeAreaLine getLine() throws BadLocationException { + return new JavaCodeAreaLine((CodeArea) area, area.getLineOfOffset(getAtPos())); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/MethodDeclaration.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/MethodDeclaration.java new file mode 100644 index 000000000..bf61e65d5 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/MethodDeclaration.java @@ -0,0 +1,205 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import jadx.core.utils.Utils; + +class MethodDeclaration implements IDeclaration { + private final AbstractCodeAreaLine line; + private final Type returnType; + private final List argTypes; + private final String name; + + boolean isStatic; + + public static MethodDeclaration create(JavaCodeAreaLine line) throws FallbackSyncException { + String methodName = line.extractDeclaredMethodName(); + if (methodName == null) { + throw new FallbackSyncException("no method name found in java declaration"); + } + + // Get the return string + String trimmed = line.getTrimmedStr(); + int methodNameStartPos = trimmed.indexOf(methodName); + // -2 to jump to last char of return type + // +1 to get to first char of return type + int returnTypeStartPos = trimmed.lastIndexOf(' ', methodNameStartPos - 2) + 1; + returnTypeStartPos = returnTypeStartPos > -1 ? returnTypeStartPos : 0; + String returnStr = trimmed.substring(returnTypeStartPos, methodNameStartPos - 1); + + // Get the arg types + String argString = trimmed.substring(trimmed.indexOf('(') + 1, trimmed.indexOf(')')); + String[] argStringParts = argString.split(", "); + List argTypeStrings = new ArrayList<>(); + for (int i = 0; i < argStringParts.length; i++) { + String part = argStringParts[i]; + if (part.isEmpty()) { + break; + } + argTypeStrings.add(part.substring(0, part.indexOf(" "))); + } + + boolean isStatic = trimmed.contains("static "); + + List argTypes = argTypeStrings.stream().map(s -> Type.fromJavaName(s)).collect(Collectors.toList()); + return new MethodDeclaration(line, Type.fromJavaName(returnStr), argTypes, isStatic, methodName); + } + + public static MethodDeclaration create(SmaliAreaLine line) throws FallbackSyncException { + String methodName = line.extractDeclaredMethodName(); + if (methodName == null) { + throw new FallbackSyncException("no method name found in smali declaration"); + } + + // Get the return string + String trimmed = line.getTrimmedStr(); + String returnStr = trimmed.substring(trimmed.indexOf(')') + 1); + returnStr = returnStr.endsWith(";") ? returnStr.substring(0, returnStr.length() - 1) : returnStr; + + boolean isStatic = trimmed.contains("static "); + + return new MethodDeclaration(line, Type.fromSmaliName(returnStr), parseSmaliArgs(trimmed), isStatic, methodName); + } + + private MethodDeclaration(AbstractCodeAreaLine line, Type returnType, List argTypes, boolean isStatic, String name) { + this.line = line; + this.returnType = returnType; + this.argTypes = argTypes; + this.isStatic = isStatic; + this.name = name; + } + + @Override + public String getIdentifyingName() { + return name; + } + + @Override + public AbstractCodeAreaLine getLine() { + return line; + } + + private static List parseSmaliArgs(String lineStr) { + List argTypeStrings = new ArrayList<>(); + String argString = lineStr.substring(lineStr.indexOf('(') + 1, lineStr.indexOf(')')); + for (int i = 0; i < argString.length();) { + char c = argString.charAt(i); + if (c == 'L') { + int j = i; + for (; j < argString.length(); ++j) { + if (argString.charAt(j) == ';') { + argTypeStrings.add(argString.substring(i, j + 1)); + break; + } + } + i = j + 1; + } else if (c == '[') { + argTypeStrings.add(argString.substring(i, i + 2)); + i += 2; + } else if (c != ' ') { + argTypeStrings.add(argString.substring(i, i + 1)); + ++i; + } else { + ++i; + } + } + return argTypeStrings.stream().map(s -> Type.fromSmaliName(s)).collect(Collectors.toList()); + } + + @Override + public boolean equals(Object o) { + if (o instanceof MethodDeclaration) { + MethodDeclaration decl = (MethodDeclaration) o; + if (!decl.name.equals(this.name)) { + return false; + } + if (decl.isStatic != this.isStatic) { + return false; + } + if (!decl.returnType.equals(this.returnType)) { + return false; + } + if (decl.argTypes.size() != this.argTypes.size()) { + return false; + } + for (int i = 0; i < decl.argTypes.size(); ++i) { + if (!decl.argTypes.get(i).equals(this.argTypes.get(i))) { + return false; + } + } + return true; + } + return false; + } + + // Not necessary but removes checkstyle warning + @Override + public int hashCode() { + return Objects.hash(name, isStatic, returnType, argTypes); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("NAME=").append(name).append("+++") + .append("RETURN=").append(returnType).append("+++") + .append("ARGS="); + for (final var a : argTypes) { + sb.append(a).append(","); + } + return sb.toString(); + } + + private static class Type { + private String smaliName; + private String javaName; + + public static Type fromJavaName(String name) { + return new Type(Utils.javaNameToSmaliName(name), name); + } + + public static Type fromSmaliName(String name) { + return new Type(name, Utils.smaliNameToJavaName(name)); + } + + private Type(String smaliName, String javaName) { + this.smaliName = smaliName; + this.javaName = javaName; + } + + private boolean isNonPrimitive() { + return smaliName.startsWith("L"); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Type) { + Type t = (Type) o; + if (t.isNonPrimitive() || this.isNonPrimitive()) { + // One of them might be missing the package prefix + return t.javaName.endsWith(this.javaName) + || this.javaName.endsWith(t.javaName); + } + return t.javaName.equals(this.javaName) + || t.smaliName.equals(this.smaliName); + // Slightly less strict - should think about this more + // && t.smaliName.equals(this.smaliName); + } + return false; + } + + // Not necessary but removes checkstyle warning + @Override + public int hashCode() { + return Objects.hash(this, javaName, smaliName); + } + + @Override + public String toString() { + return "@" + smaliName + "-OR-" + javaName + "@"; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaLine.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaLine.java new file mode 100644 index 000000000..79c1be0a8 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaLine.java @@ -0,0 +1,75 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import javax.swing.text.BadLocationException; + +import org.jetbrains.annotations.Nullable; + +import jadx.gui.ui.codearea.SmaliArea; + +public class SmaliAreaLine extends AbstractCodeAreaLine { + public SmaliAreaLine(SmaliArea area, int lineIndex) throws BadLocationException { + super(area, lineIndex); + } + + @Override + public AbstractCodeAreaLine getLineAt(int lineIndex) throws BadLocationException { + return new SmaliAreaLine((SmaliArea) getArea(), lineIndex); + } + + @Override + public boolean isClassDeclaration() { + return getTrimmedStr().startsWith("Class: ") || getTrimmedStr().startsWith(".class "); + } + + @Override + public boolean isMethodOrConstructorDeclaration() { + return getTrimmedStr().startsWith(".method"); + } + + @Override + public boolean isFieldDeclaration() { + return getTrimmedStr().startsWith(".field"); + } + + @Override + public final @Nullable String extractDeclaredClassName() { + if (!isClassDeclaration()) { + return null; + } + String[] parts = getTrimmedStr().split("\\s+"); + for (String part : parts) { + if (part.startsWith("L") && part.endsWith(";")) { + String fileClassName; + if (part.contains("/")) { + fileClassName = part.substring(part.lastIndexOf('/') + 1, part.length() - 1); + } else { + fileClassName = part.substring(1, part.length() - 1); // remove leading 'L' and trailing ';' + } + if (fileClassName.contains("$")) { // inner class + return fileClassName.substring(fileClassName.lastIndexOf('$') + 1); + } + return fileClassName; + } + } + return null; + } + + @Override + public final @Nullable String extractDeclaredMethodName() { + if (!isMethodOrConstructorDeclaration()) { + return null; + } + int parenIndex = getTrimmedStr().indexOf('('); + if (parenIndex > 0) { + String beforeParen = getTrimmedStr().substring(0, parenIndex).trim(); + String[] tokens = beforeParen.split("\\s+"); + return tokens[tokens.length - 1]; + } + return null; + } + + @Override + protected MethodDeclaration createMethodDeclaration() throws FallbackSyncException { + return MethodDeclaration.create(this); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaToken.java b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaToken.java new file mode 100644 index 000000000..71a21067e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/codearea/sync/fallback/SmaliAreaToken.java @@ -0,0 +1,45 @@ +package jadx.gui.ui.codearea.sync.fallback; + +import javax.swing.text.BadLocationException; + +import jadx.gui.ui.codearea.SmaliArea; + +public class SmaliAreaToken extends AbstractCodeAreaToken { + public SmaliAreaToken(SmaliArea area, int at) throws BadLocationException, FallbackSyncException { + super(area, at); + } + + @Override + public boolean isFieldReference() throws BadLocationException { + return area.getText(this.startPos - 2, 2).equals("->"); + } + + @Override + public boolean isClassField() throws BadLocationException { + AbstractCodeAreaLine line = this.getLine(); + boolean startsWithField = line.isFieldDeclaration(); + if (startsWithField) { + String tokenStr = getStr(); + String trimmedLine = line.getTrimmedStr(); + int lineTokenStartPos = trimmedLine.indexOf(tokenStr); + int lineTokenAfterPos = lineTokenStartPos + this.length; + for (int i = lineTokenAfterPos; i < trimmedLine.length(); ++i) { + char c = trimmedLine.charAt(i); + switch (c) { + case ' ': + break; + case ':': + return true; + default: + return false; + } + } + } + return false; + } + + @Override + public AbstractCodeAreaLine getLine() throws BadLocationException { + return new SmaliAreaLine((SmaliArea) area, area.getLineOfOffset(getAtPos())); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java new file mode 100644 index 000000000..92d292dfe --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CallGraphDialog.java @@ -0,0 +1,342 @@ +package jadx.gui.ui.dialog; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.util.Formatter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JMenuBar; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.JavaMethod; +import jadx.api.JavaNode; +import jadx.api.plugins.input.data.IMethodRef; +import jadx.core.utils.DotGraphUtils; +import jadx.gui.treemodel.JMethod; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.layout.WrapLayout; + +public class CallGraphDialog extends GraphDialog { + + private static final long serialVersionUID = -850803763322590708L; + + private static final Logger LOG = LoggerFactory.getLogger(CallGraphDialog.class); + + private static final String FONT = "fontname=\"Courier\" fontsize=12"; + private int callerDepthLimit = 3; + private int calleeDepthLimit = 3; + private int nextNodeID; + private Map methodToNodeID; + private Map unresolvedMethodToNodeID; + private Set edges; + private JavaMethod javaMethod; + private boolean longNames = false; + + public CallGraphDialog(MainWindow mainWindow, JavaMethod javaMethod) { + super(mainWindow, + String.format("%s: %s", NLS.str("graph_viewer.call_graph.title"), DotGraphUtils.methodFormatName(javaMethod, false))); + this.javaMethod = javaMethod; + } + + public JMenuBar addMenuBar() { + JMenuBar menuBar = super.addMenuBar(); + + // Long names checkbox + JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); + showLongNames.setSelected(false); + showLongNames.addItemListener(e -> { + longNames = showLongNames.isSelected(); + reload(); + }); + + // Calee spinner + SpinnerNumberModel calleeDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1); + JSpinner calleeDepthSpinner = new JSpinner(calleeDepthSpinnerModel); + calleeDepthSpinner.addChangeListener(e -> { + calleeDepthLimit = (int) calleeDepthSpinner.getValue(); + reload(); + }); + + // Callee label + JLabel calleeLbl = new JLabel(NLS.str("graph_viewer.callee_depth")); + calleeLbl.setLabelFor(calleeDepthSpinner); + calleeLbl.setHorizontalAlignment(SwingConstants.LEFT); + + // Assemble callee panel + JPanel calleePanel = new JPanel(); + calleePanel.setOpaque(false); + calleePanel.setLayout(new BoxLayout(calleePanel, BoxLayout.LINE_AXIS)); + calleePanel.add(calleeLbl); + calleePanel.add(Box.createRigidArea(new Dimension(3, 0))); + calleePanel.add(calleeDepthSpinner); + + // Caller spinner + SpinnerNumberModel callerDepthSpinnerModel = new SpinnerNumberModel(3, 0, 100, 1); + JSpinner callerDepthSpinner = new JSpinner(callerDepthSpinnerModel); + callerDepthSpinner.addChangeListener(e -> { + callerDepthLimit = (int) callerDepthSpinner.getValue(); + reload(); + }); + + // Caller label + JLabel callerLbl = new JLabel(NLS.str("graph_viewer.caller_depth")); + callerLbl.setLabelFor(callerDepthSpinner); + callerLbl.setHorizontalAlignment(SwingConstants.LEFT); + + // Assemble caller panel + JPanel callerPanel = new JPanel(); + callerPanel.setOpaque(false); + callerPanel.setLayout(new BoxLayout(callerPanel, BoxLayout.LINE_AXIS)); + callerPanel.add(callerLbl); + callerPanel.add(Box.createRigidArea(new Dimension(3, 0))); + callerPanel.add(callerDepthSpinner); + + // Assemble menubar panel + JPanel menuBarPanel = new JPanel(); + menuBarPanel.setOpaque(false); + menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); + menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); + menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0))); + menuBarPanel.add(calleePanel); + menuBarPanel.add(Box.createRigidArea(new Dimension(10, 0))); + menuBarPanel.add(callerPanel); + + // Add menubar panel to menuBar + menuBar.add(menuBarPanel); + return menuBar; + } + + public static void open(MainWindow window, JMethod method) { + + JavaMethod javaMethod = method.getJavaMethod(); + CallGraphDialog graphDialog = new CallGraphDialog(window, javaMethod); + graphDialog.addMenuBar(); + + graphDialog.setVisible(true); + graphDialog.reload(); + } + + public void reload() { + SwingUtilities.invokeLater(() -> { + String graph = generateGraph(javaMethod); + getPanel().setGraph(graph); + + }); + } + + private String generateGraph(JavaMethod javaMethod) { + StringBuilder sb = new StringBuilder(); + + Color themeBackground = UIManager.getColor("Panel.background"); + Color themeForeground = UIManager.getColor("Label.foreground"); + Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); + Color themeShade = UIManager.getColor("TextArea.background"); + + String bgColor = + String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), + themeBackground.getBlue()); + String lineColor = + String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); + String fontColor = + String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), + themeForeground.getBlue()); + String highlightColor = + String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(), + themeHighlight.getBlue()); + String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); + + try (Formatter f = new Formatter(sb)) { + + // graph header + f.format("digraph G {\n"); + f.format("%s\n", bgColor); + f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); + f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); + + nextNodeID = 0; + methodToNodeID = new HashMap<>(); + unresolvedMethodToNodeID = new HashMap<>(); + edges = new HashSet<>(); + + addNode(f, javaMethod, highlightColor); + + // add caller relationships + addCallers(0, f, javaMethod); + + // add calee relationships + addCallees(0, f, javaMethod); + + // close graph + f.format("}"); + + return f.toString(); + } + } + + private void addCallers(int depth, Formatter f, JavaMethod javaMethod) { + if (depth >= callerDepthLimit) { + return; + } + + List uses = javaMethod.getUseIn(); + + // add "calls" relationships + for (JavaNode node : uses) { + if (!(node instanceof JavaMethod)) { + continue; + } + JavaMethod caller = (JavaMethod) node; + + int nodeID = addNode(f, caller); + addEdge(f, nodeID, methodToNodeID.get(javaMethod)); + + addCallers(depth + 1, f, caller); + } + } + + private void addCallees(int depth, Formatter f, JavaMethod javaMethod) { + if (depth >= calleeDepthLimit) { + return; + } + + List used = javaMethod.getUsed(); + + // add "calls" relationships + for (JavaNode node : used) { + if (!(node instanceof JavaMethod)) { + continue; + } + JavaMethod callee = (JavaMethod) node; + + int nodeID = addNode(f, callee); + addEdge(f, methodToNodeID.get(javaMethod), nodeID); + + addCallees(depth + 1, f, callee); + } + addUnresolvedCallees(depth, f, javaMethod); + } + + private void addUnresolvedCallees(int depth, Formatter f, JavaMethod javaMethod) { + if (depth >= calleeDepthLimit) { + return; + } + + List used = javaMethod.getUnresolvedUsed(); + + // add "calls" relationships + for (IMethodRef callee : used) { + String name = callee.getName(); + if (name == null) { + continue; + } + + int nodeID = addNode(f, callee); + addEdge(f, methodToNodeID.get(javaMethod), nodeID); + } + } + + private int addNode(Formatter f, JavaMethod method) { + return addNode(f, method, ""); + } + + // Add a node representing method to the graph in f. Returns the ID of the new node + private int addNode(Formatter f, JavaMethod method, String extra) { + int nodeID; + if (methodToNodeID.containsKey(method)) { + nodeID = methodToNodeID.get(method); + } else { + nodeID = nextNodeID; + nextNodeID++; + methodToNodeID.put(method, nodeID); + } + + String name = DotGraphUtils.methodFormatName(method, longNames); + f.format("Node_%d [ label=\"{%s}\" %s]\n", nodeID, UiUtils.toDotNodeName(name), extra); + + if (javaMethod.callsSelf()) { + addEdge(f, nodeID, nodeID); + } + + return nodeID; + } + + private int addNode(Formatter f, IMethodRef method) { + return addNode(f, method, ""); + } + + // Add a node representing an unresolved method to the graph in f. Returns the ID of the new node + private int addNode(Formatter f, IMethodRef method, String extra) { + int nodeID; + if (unresolvedMethodToNodeID.containsKey(method)) { + nodeID = unresolvedMethodToNodeID.get(method); + } else { + nodeID = nextNodeID; + nextNodeID++; + unresolvedMethodToNodeID.put(method, nodeID); + } + + String name = DotGraphUtils.unresolvedMethodFormatName(method, longNames); + + Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); + String outOfFocus = + String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(), + themeOutOfFocus.getBlue()); + + f.format("Node_%d [ label=\"{%s}\" style=dashed %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra); + return nodeID; + } + + // Add an edge between sourceID and destID to the graph in f + private void addEdge(Formatter f, int sourceID, int destID) { + Edge edge = new Edge(sourceID, destID); + if (!edges.contains(edge)) { + f.format("Node_%d -> Node_%d\n", sourceID, destID); + edges.add(edge); + } + } + + private static class Edge { + public int source; + public int dest; + + public Edge(int source, int dest) { + this.source = source; + this.dest = dest; + } + + @Override + public boolean equals(Object otherObject) { + if (!(otherObject instanceof Edge)) { + return false; + } + Edge other = (Edge) otherObject; + return (this.source == other.source) && (this.dest == other.dest); + } + + @Override + public int hashCode() { + return Objects.hash(source, dest); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java new file mode 100644 index 000000000..08446eaee --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassInheritanceGraphDialog.java @@ -0,0 +1,282 @@ +package jadx.gui.ui.dialog; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JCheckBox; +import javax.swing.JMenuBar; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.android.apksig.internal.util.Pair; + +import jadx.core.dex.attributes.AType; +import jadx.core.dex.attributes.nodes.MethodOverrideAttr; +import jadx.core.dex.instructions.args.ArgType; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.IMethodDetails; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.gui.treemodel.JClass; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.layout.WrapLayout; + +public class ClassInheritanceGraphDialog extends GraphDialog { + + private static final long serialVersionUID = 938883901412562913L; + + private static final Logger LOG = LoggerFactory.getLogger(ClassInheritanceGraphDialog.class); + + private static final String FONT = "fontname=\"Courier\" fontsize=12"; + private ClassNode cls; + private boolean longNames = false; + private boolean overrides = false; + + private Map objectToNodeID = new HashMap<>(); + private int nextNodeID = 0; + + public ClassInheritanceGraphDialog(MainWindow mainWindow, ClassNode cls) { + super(mainWindow, + String.format("%s: %s", NLS.str("graph_viewer.inheritance_graph.title"), DotGraphUtils.classFormatName(cls, false))); + this.cls = cls; + } + + public JMenuBar addMenuBar() { + JMenuBar menuBar = super.addMenuBar(); + + // Long names checkbox + JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); + showLongNames.setSelected(false); + showLongNames.addItemListener(e -> { + longNames = showLongNames.isSelected(); + reload(); + }); + + // Overrides checkbox + JCheckBox showOverrides = new JCheckBox(NLS.str("graph_viewer.overrides")); + showOverrides.setSelected(false); + showOverrides.addItemListener(e -> { + overrides = showOverrides.isSelected(); + reload(); + }); + + // Assemble menubar panel + JPanel menuBarPanel = new JPanel(); + menuBarPanel.setOpaque(false); + menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); + menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); + menuBarPanel.add(showOverrides, BorderLayout.PAGE_START); + + // Add menubar panel to menuBar + menuBar.add(menuBarPanel); + return menuBar; + } + + public static void open(MainWindow window, JClass node) { + + ClassNode cls = node.getCls().getClassNode(); + + ClassInheritanceGraphDialog graphDialog = new ClassInheritanceGraphDialog(window, cls); + graphDialog.addMenuBar(); + + graphDialog.setVisible(true); + graphDialog.reload(); + } + + public void reload() { + SwingUtilities.invokeLater(() -> { + String graph = generateGraph(cls); + getPanel().setGraph(graph); + }); + } + + private String generateGraph(ClassNode rootClass) { + StringBuilder sb = new StringBuilder(); + + ClassNode cls = rootClass; + + objectToNodeID = new HashMap<>(); + + Color themeBackground = UIManager.getColor("Panel.background"); + Color themeForeground = UIManager.getColor("Label.foreground"); + Color themeHighlight = UIManager.getColor("Component.focusedBorderColor"); + Color themeShade = UIManager.getColor("TextArea.background"); + + String bgColor = + String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), themeBackground.getBlue()); + String lineColor = + String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); + String fontColor = + String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), + themeForeground.getBlue()); + String highlightColor = + String.format("color=\"#%02x%02x%02x\"", themeHighlight.getRed(), themeHighlight.getGreen(), + themeHighlight.getBlue()); + String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); + + try (Formatter f = new Formatter(sb)) { + + // graph header + f.format("digraph G {\n"); + f.format("%s\n", bgColor); + f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); + f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); + + // add nodes + processClass(f, cls, highlightColor); + + // close graph + f.format("}"); + + return f.toString(); + } + } + + private int processClass(Formatter f, ClassNode cls) { + return processClass(f, cls, ""); + } + + private int processClass(Formatter f, ClassNode cls, String extra) { + if (objectToNodeID.containsKey(cls)) { + // Don't process a class that has been processed before + return objectToNodeID.get(cls); + } + int classID = addNode(f, cls, extra); + + // add interface relationships + List ifaces = cls.getInterfaces(); + for (int i = 0; i < ifaces.size(); i++) { + ArgType iface = ifaces.get(i); + + int ifaceID; + ClassNode ifaceNode = cls.root().resolveClass(iface); + if (ifaceNode != null) { + ifaceID = processClass(f, ifaceNode); + objectToNodeID.put(iface, ifaceID); + } else { + ifaceID = addNode(f, iface); + } + // Classes implement interfaces, interfaces extend interfaces + String edgeLabel = cls.getAccessFlags().isInterface() ? "extends" : "implements"; + f.format("Node_%d -> Node_%d [label=\"%s\" style=\"dashed\" ]\n", classID, ifaceID, edgeLabel); + } + + // add superclass relationship + ArgType superClass = cls.getSuperClass(); + + if (superClass != ArgType.OBJECT) { + int superClsID; + cls = cls.root().resolveClass(superClass); + if (cls != null) { + superClsID = processClass(f, cls); + objectToNodeID.put(superClass, superClsID); + } else { + superClsID = addNode(f, superClass); + } + + f.format("Node_%d -> Node_%d [label=\"extends\" ]\n", classID, superClsID); + } + return classID; + } + + // Add a node for a class + private int addNode(Formatter f, ClassNode cls) { + return addNode(f, cls, ""); + } + + private int addNode(Formatter f, ClassNode cls, String extra) { + int nodeID; + if (objectToNodeID.containsKey(cls)) { + nodeID = objectToNodeID.get(cls); + } else { + nodeID = nextNodeID; + nextNodeID++; + objectToNodeID.put(cls, nodeID); + } + + if (cls.getAccessFlags().isInterface()) { + extra += " style=\"dashed, filled\""; + } + + String name = DotGraphUtils.classFormatName(cls, longNames); + f.format("Node_%d [ label=\"{%s\\ ", nodeID, UiUtils.toDotNodeName(name)); + + if (overrides) { + f.format("|"); + List> table = new ArrayList<>(); + for (MethodNode method : cls.getMethods()) { + MethodOverrideAttr ovrdAttr = method.get(AType.METHOD_OVERRIDE); + if (ovrdAttr != null) { + if (!ovrdAttr.getOverrideList().isEmpty()) { + String methodName = DotGraphUtils.methodFormatName(method, longNames); + Formatter details = new Formatter(); + details.format(" overrides "); + for (IMethodDetails baseMthDetails : ovrdAttr.getOverrideList()) { + String baseClassName = DotGraphUtils.classFormatName(baseMthDetails.getMethodInfo().getDeclClass(), longNames); + details.format("%s, ", baseClassName); + } + + String detailsString = details.toString(); + + // Remove trailing ', ' + detailsString = detailsString.substring(0, detailsString.length() - 2); + + table.add(Pair.of(methodName, detailsString)); + details.close(); + } + } + } + + if (!table.isEmpty()) { + int longestLength = table.stream().map(Pair::getFirst).map(String::length).max((a, b) -> a - b).get(); + for (Pair entry : table) { + f.format("%-" + longestLength + "s %s\\l", entry.getFirst(), entry.getSecond()); + } + + } else { + f.format("No overrides."); + } + } + f.format("}\" %s]\n", extra); + + return nodeID; + } + + // Add a node for an unresolved argtype + private int addNode(Formatter f, ArgType argType) { + return addNode(f, argType, ""); + } + + private int addNode(Formatter f, ArgType argType, String extra) { + int nodeID; + if (objectToNodeID.containsKey(argType)) { + nodeID = objectToNodeID.get(argType); + } else { + nodeID = nextNodeID; + nextNodeID++; + objectToNodeID.put(argType, nodeID); + } + + Color themeOutOfFocus = UIManager.getColor("Component.disabledBorderColor"); + String outOfFocus = + String.format("color=\"#%02x%02x%02x\"", themeOutOfFocus.getRed(), themeOutOfFocus.getGreen(), themeOutOfFocus.getBlue()); + + String name = DotGraphUtils.interfaceFormatName(argType, cls, longNames); + f.format("Node_%d [ label=\"{%s}\" %s %s]\n", nodeID, UiUtils.toDotNodeName(name), outOfFocus, extra); + + return nodeID; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java new file mode 100644 index 000000000..ce37d5bda --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ClassMethodGraphDialog.java @@ -0,0 +1,216 @@ +package jadx.gui.ui.dialog; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.FlowLayout; +import java.util.Collections; +import java.util.Formatter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.swing.JCheckBox; +import javax.swing.JMenuBar; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import jadx.api.JavaMethod; +import jadx.api.JavaNode; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.gui.treemodel.JClass; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.layout.WrapLayout; + +public class ClassMethodGraphDialog extends GraphDialog { + + private static final long serialVersionUID = -850803763322590708L; + + private static final String FONT = "fontname=\"Courier\" fontsize=12"; + private int callerDepthLimit = 10; + private int nextNodeID = 0; + private Map methodToNodeID; + private Set edges; + private List javaMethods = Collections.emptyList(); + private ClassNode cls; + private boolean longNames = false; + + public ClassMethodGraphDialog(MainWindow mainWindow, ClassNode cls) { + super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.method_graph.title"), DotGraphUtils.classFormatName(cls, false))); + this.cls = cls; + } + + public JMenuBar addMenuBar() { + JMenuBar menuBar = super.addMenuBar(); + + // Long names checkbox + JCheckBox showLongNames = new JCheckBox(NLS.str("graph_viewer.long_names")); + showLongNames.setSelected(false); + showLongNames.addItemListener(e -> { + longNames = showLongNames.isSelected(); + reload(); + }); + + // Assemble menubar panel + JPanel menuBarPanel = new JPanel(); + menuBarPanel.setOpaque(false); + menuBarPanel.setLayout(new WrapLayout(FlowLayout.LEFT)); + menuBarPanel.add(showLongNames, BorderLayout.PAGE_START); + + // Add menubar panel to menuBar + menuBar.add(menuBarPanel); + return menuBar; + } + + public static void open(MainWindow window, JClass node) { + + ClassNode cls = node.getCls().getClassNode(); + ClassMethodGraphDialog graphDialog = new ClassMethodGraphDialog(window, cls); + graphDialog.addMenuBar(); + + graphDialog.setVisible(true); + graphDialog.reload(); + } + + public void reload() { + SwingUtilities.invokeLater(() -> { + String graph = generateGraph(cls); + getPanel().setGraph(graph); + }); + } + + private String generateGraph(ClassNode classNode) { + StringBuilder sb = new StringBuilder(); + + Color themeBackground = UIManager.getColor("Panel.background"); + Color themeForeground = UIManager.getColor("Label.foreground"); + Color themeShade = UIManager.getColor("TextArea.background"); + + String bgColor = + String.format("bgcolor=\"#%02x%02x%02x\"", themeBackground.getRed(), themeBackground.getGreen(), + themeBackground.getBlue()); + String lineColor = + String.format("color=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), themeForeground.getBlue()); + String fontColor = + String.format("fontcolor=\"#%02x%02x%02x\"", themeForeground.getRed(), themeForeground.getGreen(), + themeForeground.getBlue()); + String shadeColor = String.format("fillcolor=\"#%02x%02x%02x\"", themeShade.getRed(), themeShade.getGreen(), themeShade.getBlue()); + + try (Formatter f = new Formatter(sb)) { + + // graph header + f.format("digraph G {\n"); + f.format("%s\n", bgColor); + f.format("node[shape=\"record\" style=\"filled\" %s %s %s %s]\n", FONT, fontColor, lineColor, shadeColor); + f.format("edge[arrowtail=\"onormal\" arrowhead=\"onormal\" %s %s %s]\n", FONT, fontColor, lineColor); + + nextNodeID = 0; + methodToNodeID = new HashMap<>(); + edges = new HashSet<>(); + + List methods = classNode.getMethods(); + javaMethods = methods.stream().map(method -> method.getJavaNode()).collect(Collectors.toList()); + + for (JavaMethod javaMethod : javaMethods) { + + addNode(f, javaMethod); + + // add caller relationships + addCallers(0, f, javaMethod); + } + + // close graph + f.format("}"); + + return f.toString(); + } + } + + private void addCallers(int depth, Formatter f, JavaMethod javaMethod) { + if (depth >= callerDepthLimit) { + return; + } + + List uses = javaMethod.getUseIn(); + + // add "calls" relationships + for (JavaNode node : uses) { + if (!(node instanceof JavaMethod)) { + continue; + } + JavaMethod caller = (JavaMethod) node; + + // Do not process callers that are not methods from the class + if (!javaMethods.contains(node)) { + continue; + } + + int nodeID = addNode(f, caller); + addEdge(f, nodeID, methodToNodeID.get(javaMethod)); + + addCallers(depth + 1, f, caller); + } + } + + // Add a node representing method to the graph in f. Returns the ID of the new node + private int addNode(Formatter f, JavaMethod method) { + int nodeID; + if (methodToNodeID.containsKey(method)) { + nodeID = methodToNodeID.get(method); + } else { + nodeID = nextNodeID; + nextNodeID++; + methodToNodeID.put(method, nodeID); + } + + String name = DotGraphUtils.methodFormatName(method, longNames); + f.format("Node_%d [ label=\"{%s}\"]\n", nodeID, UiUtils.toDotNodeName(name)); + + if (method.callsSelf()) { + addEdge(f, nodeID, nodeID); + } + + return nodeID; + } + + // Add an edge between sourceID and destID to the graph in f + private void addEdge(Formatter f, int sourceID, int destID) { + Edge edge = new Edge(sourceID, destID); + if (!edges.contains(edge)) { + f.format("Node_%d -> Node_%d\n", sourceID, destID); + edges.add(edge); + } + } + + private static class Edge { + public int source; + public int dest; + + public Edge(int source, int dest) { + this.source = source; + this.dest = dest; + } + + @Override + public boolean equals(Object otherObject) { + if (!(otherObject instanceof Edge)) { + return false; + } + Edge other = (Edge) otherObject; + return (this.source == other.source) && (this.dest == other.dest); + } + + @Override + public int hashCode() { + return Objects.hash(source, dest); + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java index e47e9efbb..2acf05f94 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/CommentDialog.java @@ -61,8 +61,8 @@ public class CommentDialog extends CommonDialog { LOG.error("Comment action failed", e); } try { - // refresh code - codeArea.refreshClass(); + // refresh code in a background thread to avoid blocking the ui + codeArea.backgroundRefreshClass(); } catch (Exception e) { LOG.error("Failed to reload code", e); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java new file mode 100644 index 000000000..a83880d0d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ControlFlowGraphDialog.java @@ -0,0 +1,52 @@ +package jadx.gui.ui.dialog; + +import java.io.File; +import java.util.Scanner; + +import javax.swing.SwingUtilities; + +import jadx.api.JavaMethod; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.utils.DotGraphUtils; +import jadx.gui.treemodel.JMethod; +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; + +public class ControlFlowGraphDialog extends GraphDialog { + + private static final long serialVersionUID = -68749445239697710L; + + public ControlFlowGraphDialog(MainWindow mainWindow, String method) { + super(mainWindow, String.format("%s: %s", NLS.str("graph_viewer.cfg.title"), method)); + } + + public static void open(MainWindow window, JMethod method, boolean useRegions, boolean rawInsn) { + + JavaMethod javaMethod = method.getJavaMethod(); + + GraphDialog graphDialog = new ControlFlowGraphDialog(window, DotGraphUtils.methodFormatName(javaMethod, false)); + graphDialog.addMenuBar(); + graphDialog.setVisible(true); + + SwingUtilities.invokeLater(() -> { + String graph = generateGraph(javaMethod, useRegions, rawInsn); + if (graph != null) { + graphDialog.getPanel().setGraph(graph); + } else { + graphDialog.getPanel().invalidateImage(graphError(NLS.str("graph_viewer.file_not_found_error"))); + } + }); + } + + private static String generateGraph(JavaMethod javaMethod, boolean useRegions, boolean rawInsn) { + MethodNode mth = javaMethod.getMethodNode(); + File file = new DotGraphUtils(useRegions, rawInsn).getFullFile(mth); + + try (Scanner reader = new Scanner(file)) { + String contents = reader.useDelimiter("\\Z").next(); + return contents; + } catch (Exception e) { + return null; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java new file mode 100644 index 000000000..8e335852c --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/GraphDialog.java @@ -0,0 +1,417 @@ +package jadx.gui.ui.dialog; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseWheelEvent; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import guru.nidi.graphviz.engine.Format; +import guru.nidi.graphviz.engine.Graphviz; +import guru.nidi.graphviz.model.MutableGraph; +import guru.nidi.graphviz.parse.Parser; + +import jadx.gui.ui.MainWindow; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; +import jadx.gui.utils.layout.WrapLayout; + +public abstract class GraphDialog extends JFrame { + + private static final long serialVersionUID = 5840390965763493590L; + + private static final Logger LOG = LoggerFactory.getLogger(GraphDialog.class); + + private final MainWindow mainWindow; + private GraphPanel panel; + + private static final Dimension MIN_WINDOW_SIZE = new Dimension(800, 500); + + private JMenuBar menuBar = null; + + public static JTextArea graphError() { + return graphError(NLS.str("graph_viewer.default_error")); + } + + public static JTextArea graphError(String errorMessage) { + JTextArea errorText = new JTextArea(); + errorText.setText(errorMessage); + errorText.setVisible(true); + errorText.setEditable(false); + errorText.setLineWrap(false); + return errorText; + } + + public static JTextArea graphError(Exception error) { + JTextArea errorText = new JTextArea(); + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + stringWriter.write(NLS.str("graph_viewer.default_error")); + stringWriter.write(": "); + error.printStackTrace(printWriter); + errorText.setText(stringWriter.toString()); + errorText.setVisible(true); + errorText.setEditable(false); + errorText.setLineWrap(false); + return errorText; + } + + public GraphDialog(MainWindow mainWindow) { + this(mainWindow, NLS.str("graph_viewer.default_title")); + } + + public JMenuBar addMenuBar() { + JMenuBar menuBar = new JMenuBar(); + menuBar.setLayout(new WrapLayout(FlowLayout.LEFT)); + add(menuBar, BorderLayout.PAGE_START); + this.menuBar = menuBar; + + JFileChooser fileChooser = new JFileChooser(); + + JButton saveButton = new JButton(NLS.str("graph_viewer.save_graph")); + saveButton.setEnabled(false); + saveButton.addActionListener(e -> { + try { + int option = fileChooser.showSaveDialog(this); + + if (option == JFileChooser.APPROVE_OPTION) { + File file = fileChooser.getSelectedFile(); + getPanel().renderer.render(Format.SVG).toFile(file); + + } + } catch (Exception ex) { + LOG.error("Failed to save file: ", ex); + JOptionPane.showMessageDialog(this, NLS.str("graph_viewer.file_failure"), + NLS.str("graph_viewer.file_failure"), + JOptionPane.INFORMATION_MESSAGE); + } + }); + + // Assemble menubar panel + JPanel menuBarPanel = new JPanel(); + menuBarPanel.setOpaque(false); + menuBarPanel.add(saveButton); + + // Add menubar panel to menuBar + menuBar.add(menuBarPanel); + + return menuBar; + } + + private void enableMenu() { + JMenuBar menu = this.menuBar; + setAllEnabled(true, menu); + } + + private void disableMenu() { + JMenuBar menu = this.menuBar; + setAllEnabled(false, menu); + } + + private void setAllEnabled(boolean isEnabled, JComponent component) { + component.setEnabled(isEnabled); + + Component[] components = component.getComponents(); + for (Component subComponent : components) { + if (subComponent instanceof JComponent) { + setAllEnabled(isEnabled, (JComponent) subComponent); + } else { + subComponent.setEnabled(isEnabled); + } + } + } + + public GraphDialog(MainWindow mainWindow, String title) { + super(title); + this.mainWindow = mainWindow; + + setMinimumSize(MIN_WINDOW_SIZE); + + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + UiUtils.addEscapeShortCutToDispose(this); + setLocationRelativeTo(null); + + loadWindowPos(); + + LOG.debug("Dialog w: {} h: {}", getWidth(), getHeight()); + + LOG.debug("cwd: {}", System.getProperty("user.dir")); + + panel = new GraphPanel(this); + panel.setFocusable(true); + panel.addMouseListener(new MouseListener() { + public void mouseClicked(MouseEvent e) { + requestFocusInWindow(); + } + + public void mouseEntered(MouseEvent e) { + } + + public void mouseExited(MouseEvent e) { + } + + public void mousePressed(MouseEvent e) { + } + + public void mouseReleased(MouseEvent e) { + } + }); + + setLayout(new BorderLayout()); + add(panel, BorderLayout.CENTER); + + } + + public void loadWindowPos() { + if (!mainWindow.getSettings().loadWindowPos(this)) { + setPreferredSize(MIN_WINDOW_SIZE); + } + } + + @Override + public void dispose() { + try { + mainWindow.getSettings().saveWindowPos(this); + } catch (Exception e) { + LOG.warn("Failed to save window size and position", e); + } + super.dispose(); + } + + class GraphPanel extends JPanel { + + private Dimension fullImageSize = new Dimension(); + private double scale = 1.0; + private double minimumScale = 0.01; + private double maximumScale = 7.0; + private double translateX = 0; + private double translateY = 0; + private Point lastDragPoint = null; + + private BufferedImage image; + + private Graphviz renderer; + + private final GraphDialog parentDialog; + + public GraphPanel(GraphDialog parentDialog) { + + this.parentDialog = parentDialog; + + MouseAdapter ma = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + lastDragPoint = e.getPoint(); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (image != null) { + Point p = e.getPoint(); + translateX += (p.x - lastDragPoint.x) / scale; + translateY += (p.y - lastDragPoint.y) / scale; + lastDragPoint = p; + repaint(); + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + if (image != null) { + double prevScale = scale; + scale *= Math.pow(1.1, -e.getWheelRotation()); + + if (scale > maximumScale) { + scale = maximumScale; + } + + if (scale < minimumScale) { + scale = minimumScale; + } + + if (scale != prevScale) { + Point p = e.getPoint(); + double px = (p.x - translateX * prevScale) / prevScale; + double py = (p.y - translateY * prevScale) / prevScale; + translateX = (p.x / scale) - px; + translateY = (p.y / scale) - py; + LOG.debug("Rescaling {}%", scale * 100); + renderGraphScaled(); + if (image == null) { + return; + } + repaint(); + + } + + } + } + + }; + + addMouseListener(ma); + addMouseMotionListener(ma); + addMouseWheelListener(ma); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + if (image != null) { + Graphics2D g2d = (Graphics2D) g; + AffineTransform transform = new AffineTransform(); + transform.translate(translateX * scale, translateY * scale); + g2d.drawImage(image, transform, null); + } + } + + public void setGraph(File dotString) { + try { + LOG.debug("Parsing DOT file: {} ", dotString.getAbsolutePath()); + setGraph(new Parser().read(dotString)); + } catch (Exception e) { + LOG.error("Error parsing DOT file", e); + invalidateImage(graphError(e)); + } + } + + public void setGraph(String dotString) { + try { + setGraph(new Parser().read(dotString)); + } catch (Exception e) { + LOG.error("Error parsing DOT string", e); + invalidateImage(graphError(e)); + } + } + + public void setGraph(MutableGraph g) { + + renderer = Graphviz.fromGraph(g); + parentDialog.enableMenu(); + + scale = 1.0; + + // set initial image scale and posiition + Runnable doCenter = new Runnable() { + public void run() { + + renderGraphFullSize(); + if (image == null) { + return; + } + + LOG.debug("full image w {} h {}", fullImageSize.width, fullImageSize.height); + + // scale required to fit image to window width or height + double heightScale = (double) getHeight() / (double) fullImageSize.height; + double widthScale = (double) getWidth() / (double) fullImageSize.width; + if (widthScale < heightScale) { + scale = widthScale; + LOG.debug("scaling to fit width {}/{} {}", getWidth(), fullImageSize.width, scale); + + } else { + scale = heightScale; + LOG.debug("scaling to fit height {}/{} {}", getHeight(), fullImageSize.height, scale); + } + + scale = scale * 0.95; + maximumScale = Math.sqrt(Integer.MAX_VALUE / (fullImageSize.width * fullImageSize.height)) / 8; + minimumScale = Math.min(scale, maximumScale); + + renderGraphScaled(); + if (image == null) { + return; + } + + // center image in window + translateY = (getHeight() / 2 - (fullImageSize.height * scale) / 2) / scale; + translateX = (getWidth() / 2 - (fullImageSize.width * scale) / 2) / scale; + + repaint(); + } + }; + + SwingUtilities.invokeLater(doCenter); + + } + + private void renderGraphFullSize() { + try { + image = null; + image = renderer.render(Format.SVG).toImage(); + if (image.getWidth() == 0 || image.getHeight() == 0) { + // If rendered image is too small, calculating the scale would later cause a + // division by zero + LOG.error("Graph render failed, image too small"); + invalidateImage(graphError(NLS.str("graph_viewer.image_too_small"))); + return; + } + + fullImageSize.setSize(image.getWidth(), image.getHeight()); + + } catch (IllegalArgumentException illegalArgumentException) { + // If rendered image is too large, a Dimension object is passed invalid arguments + LOG.error("Graph render failed, illegal arguments: ", illegalArgumentException); + invalidateImage(graphError(NLS.str("graph_viewer.image_too_large"))); + + } catch (Exception e) { + // A large image may cause a number of other other exception types caught here along with other + // failure cases + LOG.error("Graph render failed: ", e); + invalidateImage(graphError(e)); + } + + } + + private void renderGraphScaled() { + try { + if (fullImageSize.width * scale * fullImageSize.height * scale >= Integer.MAX_VALUE) { + scale = maximumScale; + } + image = renderer.width((int) (fullImageSize.width * scale)).render(Format.SVG).toImage(); + } catch (Exception e) { + LOG.error("Graph render failed: ", e); + invalidateImage(graphError(e)); + } + + } + + public void invalidateImage(JTextArea errorMsg) { + this.add(errorMsg); + image = null; + this.parentDialog.disableMenu(); + this.revalidate(); + repaint(); + } + + } + + protected GraphPanel getPanel() { + return this.panel; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java index 6c97d50b9..04f235c65 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/tab/TabsController.java @@ -78,6 +78,10 @@ public class TabsController { blueprint = newBlueprint; } setTabHiddenInternal(blueprint, hidden); + if (!blueprint.isCreated()) { + LOG.warn("No content panel for node: {}", node); + closeTabForce(blueprint); + } return blueprint; } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java index aa0e96545..620861b60 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/JNodeCache.java @@ -82,10 +82,20 @@ public class JNodeCache { public void removeWholeClass(JavaClass javaCls) { remove(javaCls); - javaCls.getMethods().forEach(this::remove); - javaCls.getFields().forEach(this::remove); - javaCls.getInnerClasses().forEach(this::remove); - javaCls.getInlinedClasses().forEach(this::remove); + /* + * These javaCls.get...() calls require the class to be loaded, or will force it to load, generating + * a potentially large decompilation task if needed, before throwing away that work when the class + * is unloaded. To avoid this, which is very slow, we only bother to remove things from the cache if + * the class is already loaded. If it's not then there either isn't going to be anything relevant in + * the node cache or decompilation would regenerate the cache anyway. + */ + // if (true) { + if (!javaCls.loadingWouldRequireDecompilation()) { + javaCls.getMethods().forEach(this::remove); + javaCls.getFields().forEach(this::remove); + javaCls.getInnerClasses().forEach(this::remove); + javaCls.getInlinedClasses().forEach(this::remove); + } } public void reset() { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/LafManager.java b/jadx-gui/src/main/java/jadx/gui/utils/LafManager.java index 6f81906e8..18833310b 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/LafManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/LafManager.java @@ -21,17 +21,24 @@ import jadx.gui.settings.JadxSettings; public class LafManager { private static final Logger LOG = LoggerFactory.getLogger(LafManager.class); - public static final String SYSTEM_THEME_NAME = "default"; public static final String INITIAL_THEME_NAME = FlatLightLaf.NAME; private static final Map THEMES_MAP = initThemesMap(); public static void init(JadxSettings settings) { - if (setupLaf(getThemeClass(settings))) { + String preferredThemeClass = getThemeClass(settings); + + // reset if settings refers to missing theme + if (preferredThemeClass == null) { + settings.setLafTheme(INITIAL_THEME_NAME); + preferredThemeClass = getThemeClass(settings); + } + + if (setupLaf(preferredThemeClass)) { return; } - setupLaf(SYSTEM_THEME_NAME); - settings.setLafTheme(SYSTEM_THEME_NAME); + setupLaf(INITIAL_THEME_NAME); + settings.setLafTheme(INITIAL_THEME_NAME); settings.sync(); } @@ -48,9 +55,7 @@ public class LafManager { } private static boolean setupLaf(String themeClass) { - if (SYSTEM_THEME_NAME.equals(themeClass)) { - return applyLaf(UIManager.getSystemLookAndFeelClassName()); - } + if (themeClass != null && !themeClass.isEmpty()) { return applyLaf(themeClass); } @@ -59,7 +64,6 @@ public class LafManager { private static Map initThemesMap() { Map map = new LinkedHashMap<>(); - map.put(SYSTEM_THEME_NAME, SYSTEM_THEME_NAME); // default flatlaf themes map.put(FlatLightLaf.NAME, FlatLightLaf.class.getName()); diff --git a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java index 8fd9f195b..de51d18a5 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/UiUtils.java @@ -562,4 +562,11 @@ public class UiUtils { public static boolean nearlyEqual(float a, float b) { return Math.abs(a - b) < 1E-6f; } + + // Formats a string to be in a .DOT node + public static String toDotNodeName(String fullName) { + String newName = fullName.replace("<", "\\<"); + newName = newName.replace(">", "\\>"); + return newName; + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index a07b42929..c1ff472e4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Alles rechts schließen #tabs.closeAllLeft=Close All Left tabs.code=Code tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=Zurück nav.forward=Vorwärts @@ -136,7 +137,7 @@ message.desktop_entry_creation_success=Desktop-Eintrag erfolgreich erstellt! message.success_title=Erfolg #message.unable_preview_font=Unable preview font -heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB +heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB (%.2f GB Höhepunkt) common_dialog.ok=OK common_dialog.cancel=Abbrechen @@ -370,6 +371,20 @@ popup.find_usage=Verwendung suchen popup.go_to_declaration=Zur Erklärung gehen popup.exclude=Ausschließen popup.exclude_packages=Pakete ausschließen +popup.convert_number=Conversion als Kommentar hinzufügen +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Kommentar popup.update_comment=Kommentar aktualisieren popup.search_comment=Kommentar suchen @@ -522,3 +537,20 @@ action_category.plugin_script=Plugin-Skript #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index cd4ac9391..944111a9e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Close All Right tabs.closeAllLeft=Close All Left tabs.code=Code tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=Back nav.forward=Forward @@ -136,7 +137,7 @@ message.desktop_entry_creation_success=Desktop entry created successfully! message.success_title=Success message.unable_preview_font=Unable preview font -heapUsage.text=JADX memory usage: %.2f GB of %.2f GB +heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak) common_dialog.ok=OK common_dialog.cancel=Cancel @@ -370,6 +371,20 @@ popup.find_usage=Find Usage popup.go_to_declaration=Go to declaration popup.exclude=Exclude popup.exclude_packages=Exclude packages +popup.convert_number=Add conversion as comment +popup.view_call_graph=View call graph +popup.view_call_graph_description=Show call chains to this function +popup.view_class_graph=View inheritance graph +popup.view_class_graph_description=Show inheritance tree for this class +popup.view_class_method_graph=View methods graph +popup.view_class_method_graph_description=Show all methods for this class +popup.cfg_submenu=View control flow graph +popup.view_cfg=Regular +popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +popup.view_raw_cfg=Raw +popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +popup.view_region_cfg=Region +popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Comment popup.update_comment=Update comment popup.search_comment=Search comments @@ -522,3 +537,20 @@ hex_viewer.change_encoding=Change Encoding hex_viewer.goto_address=Go To Address hex_viewer.enter_address=Enter address range: hex_viewer.find=Find + +graph_viewer.long_names=Show full names +graph_viewer.overrides=Show overrides +graph_viewer.callee_depth=Down depth +graph_viewer.caller_depth=Up depth +graph_viewer.default_error=Failed to view graph +graph_viewer.file_not_found_error=Failed to load graph file +graph_viewer.image_too_large=Failed to render graph: graph too large +graph_viewer.image_too_small=Failed to render graph: graph too small +graph_viewer.file_failure=Error in File Operation +graph_viewer.save_graph=Save graph + +graph_viewer.default_title=Graph Viewer +graph_viewer.method_graph.title=Methods Graph +graph_viewer.call_graph.title=Call Graph +graph_viewer.inheritance_graph.title=Inheritance Graph +graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index cddd03108..4c5daf94f 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Cierra todo a la derecha #tabs.closeAllLeft=Close All Left #tabs.code=Code #tabs.smali=Smali +#tabs.smali_bytecode=Smali+Bytecode nav.back=Atrás nav.forward=Adelante @@ -136,7 +137,7 @@ nav.forward=Adelante #message.success_title=Success #message.unable_preview_font=Unable preview font -#heapUsage.text=JADX memory usage: %.2f GB of %.2f GB +#heapUsage.text=JADX memory usage: %.2f GB of %.2f GB (%.2f GB peak) #common_dialog.ok=OK #common_dialog.cancel=Cancel @@ -370,6 +371,20 @@ popup.xposed=Copiar como fragmento de xposed #popup.go_to_declaration=Go to declaration #popup.exclude=Exclude #popup.exclude_packages=Exclude packages +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) #popup.add_comment=Comment #popup.update_comment=Update comment #popup.search_comment=Search comments @@ -522,3 +537,20 @@ certificate.serialPubKeyY=Y #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index 23e7e1a18..2f260fcd5 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Tutup Semua yang Kanan #tabs.closeAllLeft=Close All Left tabs.code=Kode tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=Kembali nav.forward=Maju @@ -136,7 +137,7 @@ message.indexingClassesSkipped=JADX kekurangan memori. Oleh karena itu %d #message.success_title=Success #message.unable_preview_font=Unable preview font -heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB +heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB (%.2f GB tertinggi) common_dialog.ok=OK common_dialog.cancel=Batal @@ -370,6 +371,20 @@ popup.find_usage=Cari Penggunaan popup.go_to_declaration=Pergi ke Deklarasi popup.exclude=Kecualikan popup.exclude_packages=Kecualikan paket +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Komentar #popup.update_comment=Update comment popup.search_comment=Cari komentar @@ -522,3 +537,20 @@ action_category.plugin_script=Plugin Script #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 01090790f..674de8588 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=오른쪽의 모든 것을 닫으십시오 #tabs.closeAllLeft=Close All Left tabs.code=코드 tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=뒤로 nav.forward=앞으로 @@ -136,7 +137,7 @@ message.indexingClassesSkipped=Jadx의 메모리가 부족합니다. 따 #message.success_title=Success #message.unable_preview_font=Unable preview font -heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB +heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB (%.2f GB 첨단) common_dialog.ok=확인 common_dialog.cancel=취소 @@ -370,6 +371,20 @@ popup.find_usage=사용 찾기 popup.go_to_declaration=선언문으로 이동 popup.exclude=제외 popup.exclude_packages=패키지 제외 +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=주석 #popup.update_comment=Update comment popup.search_comment=주석 검색 @@ -522,3 +537,20 @@ adb_dialog.starting_debugger=디버거 시작 중 ... #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index c236657fb..4ab789a9b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Feche tudo à direita #tabs.closeAllLeft=Close All Left tabs.code=Código tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=Voltar nav.forward=Avançar @@ -136,7 +137,7 @@ message.indexingClassesSkipped=Jadx está rodando com pouca memória. Por #message.success_title=Success #message.unable_preview_font=Unable preview font -heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB +heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB (%.2f GB pico) common_dialog.ok=Ok common_dialog.cancel=Cancelar @@ -370,6 +371,20 @@ popup.find_usage=Buscar uso popup.go_to_declaration=Ir para declaração popup.exclude=Ignorar popup.exclude_packages=Pacotes ignorados +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Comentar #popup.update_comment=Update comment popup.search_comment=Buscar comentários @@ -522,3 +537,20 @@ adb_dialog.starting_debugger=Iniciando depurador... #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 3e2bf3403..58c1157f7 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=Закройте все справа #tabs.closeAllLeft=Close All Left tabs.code=Код tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=Назад nav.forward=Вперед @@ -136,7 +137,7 @@ message.indexingClassesSkipped=JaDX запущен с малым коли #message.success_title=Success #message.unable_preview_font=Unable preview font -heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ +heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ (%.2f GB пикпик) common_dialog.ok=Ok common_dialog.cancel=Отмена @@ -370,6 +371,20 @@ popup.find_usage=Найти использования popup.go_to_declaration=Перейти к объявлению popup.exclude=Исключить popup.exclude_packages=Исключить пакеты +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=Комментарий #popup.update_comment=Update comment popup.search_comment=Поиск комментариев @@ -522,3 +537,20 @@ action_category.plugin_script=Скрипты и плагины #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index a8e8c2f41..70d307ab4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=关闭右边的所有 tabs.closeAllLeft=关闭左边的所有 tabs.code=代码 tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=后退 nav.forward=前进 @@ -136,7 +137,7 @@ message.desktop_entry_creation_success=创建桌面入口创建成功! message.success_title=成功 message.unable_preview_font=无法预览字体 -heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB +heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB (%.2f GB 顶点) common_dialog.ok=确定 common_dialog.cancel=取消 @@ -370,6 +371,20 @@ popup.find_usage=查找用例 popup.go_to_declaration=跳到声明 popup.exclude=排除此包 popup.exclude_packages=排除包 +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=添加注释 popup.update_comment=更新注释 popup.search_comment=搜索注释 @@ -522,3 +537,20 @@ hex_viewer.change_encoding=更改编码 hex_viewer.goto_address=跳转到地址 hex_viewer.enter_address=输入地址范围: hex_viewer.find=查找 + +graph_viewer.long_names=Show full names +graph_viewer.overrides=Show overrides +graph_viewer.callee_depth=Down depth +graph_viewer.caller_depth=Up depth +graph_viewer.default_error=Failed to view graph +graph_viewer.file_not_found_error=Failed to load graph file +graph_viewer.image_too_large=Failed to render graph: graph too large +graph_viewer.image_too_small=Failed to render graph: graph too small +graph_viewer.file_failure=Error in File Operation +graph_viewer.save_graph=Save graph + +graph_viewer.default_title=Graph Viewer +graph_viewer.method_graph.title=Methods Graph +graph_viewer.call_graph.title=Call Graph +graph_viewer.inheritance_graph.title=Inheritance Graph +graph_viewer.cfg.title=Control Flow Graph \ No newline at end of file diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 86bbd9e0f..cdb0d4ca0 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -112,6 +112,7 @@ tabs.closeAllRight=關閉右邊的所有 #tabs.closeAllLeft=Close All Left tabs.code=程式碼 tabs.smali=Smali +tabs.smali_bytecode=Smali+Bytecode nav.back=返回 nav.forward=向前 @@ -136,7 +137,7 @@ message.desktop_entry_creation_success=成功建立桌面項目! message.success_title=成功 #message.unable_preview_font=Unable preview font -heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB +heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB (%.2f GB 潼) common_dialog.ok=Ok common_dialog.cancel=取消 @@ -370,6 +371,20 @@ popup.find_usage=尋找使用情況 popup.go_to_declaration=前往宣告 popup.exclude=排除 popup.exclude_packages=排除套件 +#popup.convert_number=Add conversion as comment +#popup.view_call_graph=View call graph +#popup.view_call_graph_description=Show call chains to this function +#popup.view_class_graph=View inheritance graph +#popup.view_class_graph_description=Show inheritance tree for this class +#popup.view_class_method_graph=View methods graph +#popup.view_class_method_graph_description=Show all methods for this class +#popup.cfg_submenu=View control flow graph +#popup.view_cfg=Regular +#popup.view_cfg_description=Show regular control flow graph for this function (enable in File->Preferences->Other) +#popup.view_raw_cfg=Raw +#popup.view_raw_cfg_description=Show control flow graph with raw instructions for this function (enable in File->Preferences->Other) +#popup.view_region_cfg=Region +#popup.view_region_cfg_description=Show regioned control flow graph for this function (enable in File->Preferences->Other) popup.add_comment=註解 popup.update_comment=更新註解 popup.search_comment=搜尋註解 @@ -522,3 +537,20 @@ action_category.plugin_script=外掛程式腳本 #hex_viewer.goto_address=Go To Address #hex_viewer.enter_address=Enter address range: #hex_viewer.find=Find + +#graph_viewer.long_names=Show full names +#graph_viewer.overrides=Show overrides +#graph_viewer.callee_depth=Down depth +#graph_viewer.caller_depth=Up depth +#graph_viewer.default_error=Failed to view graph +#graph_viewer.file_not_found_error=Failed to load graph file +#graph_viewer.image_too_large=Failed to render graph: graph too large +#graph_viewer.image_too_small=Failed to render graph: graph too small +#graph_viewer.file_failure=Error in File Operation +#graph_viewer.save_graph=Save graph + +#graph_viewer.default_title=Graph Viewer +#graph_viewer.method_graph.title=Methods Graph +#graph_viewer.call_graph.title=Call Graph +#graph_viewer.inheritance_graph.title=Inheritance Graph +#graph_viewer.cfg.title=Control Flow Graph diff --git a/jadx-gui/src/main/resources/icons/ui/strings.svg b/jadx-gui/src/main/resources/icons/ui/strings.svg new file mode 100644 index 000000000..37e0ae51c --- /dev/null +++ b/jadx-gui/src/main/resources/icons/ui/strings.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + diff --git a/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java b/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java new file mode 100644 index 000000000..8abb0ca0e --- /dev/null +++ b/jadx-gui/src/test/java/jadx/gui/ui/codearea/ConvertNumberActionTest.java @@ -0,0 +1,212 @@ +package jadx.gui.ui.codearea; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConvertNumberActionTest { + + @Test + public void nonNumeric() { + assertThat(ConvertNumberAction.getConversionsFromWord("non-numeric")).isNullOrEmpty(); + assertThat(ConvertNumberAction.getConversionsFromWord("0xnon-numeric")).isNullOrEmpty(); + assertThat(ConvertNumberAction.getConversionsFromWord("non-numericL")).isNullOrEmpty(); + assertThat(ConvertNumberAction.getConversionsFromWord("-non-numeric")).isNullOrEmpty(); + assertThat(ConvertNumberAction.getConversionsFromWord("ABCD")).isNullOrEmpty(); + } + + @Test + public void simpleDecimalToHex() { + + List expected = new ArrayList(); + expected.add("0x7b"); + expected.add("0b01111011"); + expected.add("'{'"); + + List result = ConvertNumberAction.getConversionsFromWord("123"); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + } + + @Test + public void negativeDecimalToHex() { + + List expected = new ArrayList(); + expected.add("0xffffff85"); + expected.add("0b11111111111111111111111110000101"); + + List result = ConvertNumberAction.getConversionsFromWord("-123"); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + } + + @Test + public void negativeLongDecimalToHex() { + + List expected = new ArrayList(); + expected.add("0xFFFFFFE8B7891800".toLowerCase()); + expected.add("0b1111111111111111111111111110100010110111100010010001100000000000"); + + List result = ConvertNumberAction.getConversionsFromWord("-100000000000"); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + } + + @Test + public void simpleHexToDecimal() { + + List expected = new ArrayList(); + expected.add("123"); + expected.add("0b01111011"); + expected.add("'{'"); + + List result = ConvertNumberAction.getConversionsFromWord("0x7b"); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + } + + @Test + public void zeroToHex() { + + List expected = new ArrayList(); + expected.add("0x0"); + expected.add("0b00000000"); + + List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(0)); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void minIntToHex() { + + List expected = new ArrayList(); + expected.add("0x80000000"); + expected.add("0b10000000000000000000000000000000"); + + List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MIN_VALUE)); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void maxIntToHex() { + + List expected = new ArrayList(); + expected.add("0x7fffffff"); + expected.add("0b01111111111111111111111111111111"); + + List result = ConvertNumberAction.getConversionsFromWord(Integer.toString(Integer.MAX_VALUE)); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void minLongToHex() { + + List expected = new ArrayList(); + expected.add("0x8000000000000000"); + expected.add("0b1000000000000000000000000000000000000000000000000000000000000000"); + + List result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MIN_VALUE)); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void maxLongToHex() { + + List expected = new ArrayList(); + expected.add("0x7fffffffffffffff"); + expected.add("0b0111111111111111111111111111111111111111111111111111111111111111"); + + List result = ConvertNumberAction.getConversionsFromWord(Long.toString(Long.MAX_VALUE)); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void simpleLongSuffix() { + + List expected = new ArrayList(); + expected.add("0x7b"); + expected.add("0b01111011"); + expected.add("'{'"); + + List result = ConvertNumberAction.getConversionsFromWord("123L"); + + assertThat(result).isNotEmpty(); + + expected.removeAll(result); + assertThat(expected).isEmpty(); + + } + + @Test + public void binaryPadding() { + + assertThat(ConvertNumberAction.getConversionsFromWord("0")).containsOnlyOnce("0b00000000"); + assertThat(ConvertNumberAction.getConversionsFromWord("1")).containsOnlyOnce("0b00000001"); + assertThat(ConvertNumberAction.getConversionsFromWord("127")).containsOnlyOnce("0b01111111"); + assertThat(ConvertNumberAction.getConversionsFromWord("0xff")).containsOnlyOnce("0b11111111"); + assertThat(ConvertNumberAction.getConversionsFromWord("0x7fff")).containsOnlyOnce("0b0111111111111111"); + assertThat(ConvertNumberAction.getConversionsFromWord("0xffff")).containsOnlyOnce("0b1111111111111111"); + assertThat(ConvertNumberAction.getConversionsFromWord("0x10000")).containsOnlyOnce("0b000000010000000000000000"); + assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffff")).containsOnlyOnce("0b11111111111111111111111111111111"); + + assertThat(ConvertNumberAction.getConversionsFromWord("0xffffffffffff")) + .containsOnlyOnce("0b111111111111111111111111111111111111111111111111"); + + assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffff")) + .containsOnlyOnce("0b011111111111111111111111111111111111111111111111"); + + assertThat(ConvertNumberAction.getConversionsFromWord("0x7fffffffffffffff")) + .containsOnlyOnce("0b0111111111111111111111111111111111111111111111111111111111111111"); + + } + + @Test + public void printableAscii() { + + for (int i = 32; i < 127; i++) { + String printed = String.format("'%c'", i); + assertThat(ConvertNumberAction.getConversionsFromWord(Integer.toString(i))).containsOnlyOnce(printed); + } + } + +} diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java index 9e38e99d0..a60182fbc 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/sections/DexClassData.java @@ -27,10 +27,17 @@ public class DexClassData implements IClassData { private final SectionReader in; private final AnnotationsParser annotationsParser; + private final int inputFileOffset; public DexClassData(SectionReader sectionReader, AnnotationsParser annotationsParser) { this.in = sectionReader; this.annotationsParser = annotationsParser; + this.inputFileOffset = this.in.getOffset(); + } + + @Override + public int getInputFileOffset() { + return this.inputFileOffset; } @Override diff --git a/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IClassData.java b/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IClassData.java index 228656da6..ae715b901 100644 --- a/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IClassData.java +++ b/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/IClassData.java @@ -15,6 +15,8 @@ public interface IClassData { int getAccessFlags(); + int getInputFileOffset(); + @Nullable String getSuperType(); diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java index becb16fba..a1c51b654 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/data/JavaClassData.java @@ -36,6 +36,11 @@ public class JavaClassData implements IClassData { this.attributesReader = new AttributesReader(this, this.constPoolReader); } + @Override + public int getInputFileOffset() { + return offsets.getAccessFlagsOffset(); + } + @Override public IClassData copy() { return this;