feat: graph views, code pane sync, and more (PR #2784)

* snapshot 219

* revert non-working string searcher

* fix(gui): fix illegal ':' character in path when exporting resources.arsc/res

* fix(gui): use resource short name when exporting a folder via context menu

* fix(gui): use new resource class for files in arsc (#2771)

* fix(gui): limit tabs title length, fix tooltips (#2771)

* resolve issues with script code area after merge

---------

Co-authored-by: Jan S. <jpstotz@users.noreply.github.com>
Co-authored-by: Skylot <118523+skylot@users.noreply.github.com>
This commit is contained in:
gordon-f0
2026-02-13 19:02:36 +00:00
committed by GitHub
parent 2a2806ebd7
commit be8b96280e
184 changed files with 12718 additions and 1011 deletions
+6 -4
View File
@@ -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 {
@@ -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;
}
}
@@ -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<JavaNode> getUsed() {
return getDeclaringClass().getRootDecompiler().convertNodes(mth.getUsed());
}
public List<IMethodRef> getUnresolvedUsed() {
return mth.getUnresolvedUsed();
}
public boolean callsSelf() {
return mth.callsSelf();
}
public List<JavaMethod> getOverrideRelatedMethods() {
MethodOverrideAttr ovrdAttr = mth.get(AType.METHOD_OVERRIDE);
if (ovrdAttr == null) {
@@ -32,8 +32,11 @@ public class CodeMetadataStorage implements ICodeMetadata {
return new CodeMetadataStorage(Collections.emptyMap(), Collections.emptyNavigableMap());
}
// <decomp file line number> -> <dex debug line number>
private final Map<Integer, Integer> lines;
// <character index into the file> -> <code annotation>
// the key is what is returned by AbstractCodeArea#getCaretPos() when clicking in a code panel.
private final NavigableMap<Integer, ICodeAnnotation> navMap;
private CodeMetadataStorage(Map<Integer, Integer> lines, NavigableMap<Integer, ICodeAnnotation> navMap) {
@@ -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<MethodNode> methods);
void visitMethodsUses(MethodNode mth, List<MethodNode> methods);
void visitUnresolvedMethodsUsage(MethodNode mth, List<IMethodRef> methods);
void visitIsSelfCall(MethodNode mth, boolean isSelfCall);
void visitComplete();
}
@@ -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());
}
@@ -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,
@@ -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<T extends IJadxAttribute> implements IJadxAttrType<T> {
public static final AType<AttrList<SpecialEdgeAttr>> SPECIAL_EDGE = new AType<>();
public static final AType<TmpEdgeAttr> TMP_EDGE = new AType<>();
public static final AType<TryCatchBlockAttr> TRY_BLOCK = new AType<>();
public static final AType<ExcSplitCrossAttr> EXC_SPLIT_CROSS = new AType<>();
// block or insn
public static final AType<ExcHandlerAttr> EXC_HANDLER = new AType<>();
@@ -9,6 +9,8 @@ import jadx.core.utils.Utils;
public class AttrList<T> implements IJadxAttribute {
private static final int MAX_ATTRLIST_LENGTH = 300;
private final IJadxAttrType<AttrList<T>> type;
private final List<T> list = new ArrayList<>();
@@ -27,6 +29,11 @@ public class AttrList<T> 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;
}
}
@@ -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<? extends IJadxAttribute> getAttrType() {
return AType.EXC_SPLIT_CROSS;
}
@Override
public String toString() {
return "ExcSplitCross -> " + originalPathCross.toString();
}
}
@@ -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:
@@ -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)) {
@@ -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) {
@@ -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<LoopInfo> loops;
private Region region;
// Methods that use this method
private List<MethodNode> useIn = Collections.emptyList();
// Unresolved methods that use this method
private List<IMethodRef> unresolvedUsed = Collections.emptyList();
// Methods that this method uses
private Set<MethodNode> 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<MethodNode> getUseIn() {
return useIn;
return Collections.unmodifiableList(useIn);
}
// Do not modify passed list after setting
public void setUseIn(List<MethodNode> 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<MethodNode> methodsUsed) {
this.methodsUsed = new HashSet<>(methodsUsed);
}
public Set<MethodNode> getUsed() {
this.removeInavlidMethodsUsed();
return methodsUsed;
}
public List<IMethodRef> getUnresolvedUsed() {
return unresolvedUsed;
}
public void setUnresolvedUsed(List<IMethodRef> 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() {
@@ -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;
}
@@ -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<ClassInfo> 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));
@@ -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<ExceptionHandler> handlers;
private List<BlockNode> blocks;
@@ -134,6 +151,244 @@ public class TryCatchBlockAttr implements IJadxAttribute {
return id;
}
public List<TryEdge> getHandlerTryEdges() {
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
List<TryEdge> 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<BlockNode> 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<TryEdge> getFallthroughTryEdges() {
List<TryEdge> edges = new LinkedList<>();
List<BlockNode> exploredBlocks = new ArrayList<>();
List<TryCatchBlockAttr> exploredTrys = new LinkedList<>();
getFallthroughTryEdges(edges, exploredBlocks, exploredTrys);
return edges;
}
public void getFallthroughTryEdges(List<TryEdge> edges, List<BlockNode> exploredBlocks, List<TryCatchBlockAttr> exploredTrys) {
List<ExceptionHandler> mergedHandlers = getMergedHandlers();
Set<BlockNode> 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<TryEdge> getTryEdges() {
List<TryEdge> handlerEdges = getHandlerTryEdges();
List<TryEdge> fallthroughEdges = getFallthroughTryEdges();
List<TryEdge> edges = new ArrayList<>(handlerEdges.size() + fallthroughEdges.size());
edges.addAll(handlerEdges);
edges.addAll(fallthroughEdges);
return Collections.unmodifiableList(edges);
}
private void exploreTryPath(List<TryEdge> edges, BlockNode blk, Set<BlockNode> searchBlocks, List<BlockNode> exploredBlocks,
List<TryCatchBlockAttr> 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<LoopInfo> loops = loopsAttrList.getList();
final List<BlockNode> loopStartBlocks = new LinkedList<>();
for (final LoopInfo loop : loops) {
loopStartBlocks.add(loop.getStart());
final List<Edge> 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<BlockNode> 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<TryCatchBlockAttr> nestedTrys = new HashSet<>();
List<BlockNode> 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<ExceptionHandler> getMergedHandlers() {
boolean hasInnerBlocks = !getInnerTryBlocks().isEmpty();
final List<ExceptionHandler> 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<TryEdge, BlockNode> getEdgeBlockMap(MethodNode mth) {
List<TryEdge> edges = getTryEdges();
Map<TryEdge, BlockNode> blockMap = new HashMap<>();
for (TryEdge edge : edges) {
blockMap.put(edge, edge.getTarget());
}
return blockMap;
}
public TryEdgeScopeGroupMap getExecutionScopeGroups(MethodNode mth) {
Map<TryEdge, BlockNode> handlerBlocks = getEdgeBlockMap(mth);
TryEdgeScopeGroupMap scopeGroups = new TryEdgeScopeGroupMap(mth, this, handlerBlocks.size());
scopeGroups.populateFromEdges(handlerBlocks);
return scopeGroups;
}
public Map<BlockNode, List<TryEdge>> getHandlerFallthroughGroups(MethodNode mth, TryEdgeScopeGroupMap scopeGroups) {
return scopeGroups.getScopeEnds(mth);
}
public List<BlockNode> getSearchBlocksFromFallthroughGroups(MethodNode mth, ExceptionHandler finallyHandler,
Map<BlockNode, List<TryEdge>> fallthroughGroups) {
List<BlockNode> searchBlocks = new LinkedList<>();
for (Map.Entry<BlockNode, List<TryEdge>> entry : fallthroughGroups.entrySet()) {
BlockNode scopeEndBlock = entry.getKey();
List<TryEdge> 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<TryEdge> 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<? extends IJadxAttribute> getAttrType() {
return AType.TRY_BLOCK;
@@ -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<ExceptionHandler> 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<ExceptionHandler> 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();
}
}
@@ -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<TryEdge, Map<TryEdge, BlockNode>> {
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<Pair<TryEdge>> mergedEdges = new ArrayList<>();
private final TryCatchBlockAttr tryCatch;
private final Map<TryEdge, Map<TryEdge, BlockNode>> 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<Entry<TryEdge, Map<TryEdge, BlockNode>>> entrySet() {
return underlyingMap.entrySet();
}
@Override
public final Map<TryEdge, BlockNode> get(Object key) {
return underlyingMap.get(key);
}
@Override
public final boolean isEmpty() {
return underlyingMap.isEmpty();
}
@Override
public final Set<TryEdge> keySet() {
return underlyingMap.keySet();
}
@Override
public final Map<TryEdge, BlockNode> put(TryEdge key, Map<TryEdge, BlockNode> value) {
return underlyingMap.put(key, value);
}
@Override
public final void putAll(Map<? extends TryEdge, ? extends Map<TryEdge, BlockNode>> otherMap) {
underlyingMap.putAll(otherMap);
}
@Override
public final Map<TryEdge, BlockNode> remove(Object key) {
return underlyingMap.remove(key);
}
@Override
public final int size() {
return underlyingMap.size();
}
@Override
public final Collection<Map<TryEdge, BlockNode>> values() {
return underlyingMap.values();
}
public final boolean hasMergedEdges() {
return !mergedEdges.isEmpty();
}
public final List<Pair<TryEdge>> getMergedScopes() {
return mergedEdges;
}
public final void populateFromEdges(final Map<TryEdge, BlockNode> edges) {
mergeSameScopes(edges);
for (final TryEdge edge : edges.keySet()) {
final BlockNode edgeBlock = edges.get(edge);
final Map<TryEdge, BlockNode> 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<BlockNode, List<TryEdge>> getScopeEnds(final MethodNode mth) {
final Map<BlockNode, List<TryEdge>> 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<TryEdge> isolatedEdgePairs = new LinkedList<>();
for (final TryEdge mergeEdgeA : keySet()) {
final Pair<TryEdge> edgeMergedPair = getMergedNodeFromEdge(mergeEdgeA);
if (edgeMergedPair != null) {
continue;
}
final Map<TryEdge, BlockNode> handlerRelations = get(mergeEdgeA);
final List<BlockNode> scopeEnds = new ArrayList<>(handlerRelations.size());
for (final TryEdge mergeEdgeB : handlerRelations.keySet()) {
final Pair<TryEdge> 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<TryEdge> groupingHandlers = new LinkedList<>();
groupingHandlers.add(mergeEdgeA);
groups.put(topGrouping, groupingHandlers);
}
}
for (final TryEdge isolatedEdge : isolatedEdgePairs) {
boolean isInList = false;
for (final List<TryEdge> 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<BlockNode> 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<TryEdge> 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<TryEdge> 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<TryEdge> edgesWithTerminus : groups.values()) {
if (edgesWithTerminus.contains(keptEdge)) {
edgesWithTerminus.remove(keptEdge);
}
}
final BlockNode terminus = get(keptEdge).get(removedEdge);
final List<TryEdge> 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<TryEdge> getMergedNodeFromEdge(final TryEdge edge) {
for (Pair<TryEdge> pair : mergedEdges) {
if (pair.getSecond() == edge) {
return pair;
}
}
return null;
}
private Map<TryEdge, BlockNode> createEdgeTerminusMap(final Map<TryEdge, BlockNode> edgeStartMap, final TryEdge edge,
final BlockNode edgeStart) {
final Map<TryEdge, BlockNode> 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<BlockNode> 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<TryEdge, BlockNode> mergeSameScopes(final Map<TryEdge, BlockNode> handlers) {
final List<Entry<TryEdge, BlockNode>> exceptionHandlers = new ArrayList<>(handlers.entrySet());
final List<Pair<TryEdgeScope>> 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<TryEdge, BlockNode> simplifiedScopes = new HashMap<>(handlers);
int i = 0;
while (i < handlerPairs.size()) {
final Pair<TryEdgeScope> 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;
}
}
@@ -0,0 +1,8 @@
package jadx.core.dex.trycatch;
public enum TryEdgeType {
TRUE_FALLTHROUGH,
PREMATURE_EXIT,
LOOP_EXIT,
HANDLER
}
@@ -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<BlockNode> 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<InsnNode> 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<SpecialEdgeAttr> 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<InsnNode> 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<InsnNode> 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<RegisterArg> 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<InsnNode> 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);
}
}
}
}
}
@@ -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<IRegion> 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<IRegion> 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<BlockNode> blocks = mth.getBasicBlocks();
if (blocks == null) {
InsnNode[] insnArr = mth.getInstructions();
if (insnArr == null) {
return;
}
BlockNode block = new BlockNode(0, 0, 0);
List<InsnNode> 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<IBlock> 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<InsnNode> 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);
}
}
@@ -158,7 +158,9 @@ public class InlineMethods extends AbstractVisitor {
}
private void updateUsageInfo(MethodNode mth, MethodNode inlinedMth, InsnNode insn) {
inlinedMth.getUseIn().remove(mth);
List<MethodNode> 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;
@@ -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);
}
}
@@ -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<BlockNode> 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<BlockNode> 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);
@@ -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<BlockNode> 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
@@ -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);
@@ -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<RegisterArg> 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<RegisterArg> 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<RegisterArg> 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<RegisterArg> 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<RegisterArg> getAllowableOutputArguments() {
return allowableOutputArguments;
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
@@ -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));
}
}
@@ -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<TryEdge, List<BlockNode>> {
public static boolean anyBlockHasNonImplicitTry(final List<BlockNode> blocks) {
final List<BlockNode> 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<BlockNode, List<TryEdge>> scopeTerminusGroups) {
final Map<TryEdge, BlockNode> edgeBlocks = tryCatch.getEdgeBlockMap(mth);
final TryCatchEdgeBlockMap result = new TryCatchEdgeBlockMap();
for (final BlockNode scopeTerminus : scopeTerminusGroups.keySet()) {
final List<TryEdge> 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<BlockNode> 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<BlockNode> finallyBlocks = result.getBlocksForHandler(finallyHandler);
for (final TryEdge edge : result.keySet()) {
if (edge.isHandlerExit() && edge.getExceptionHandler() == finallyHandler) {
continue;
}
final List<BlockNode> blocks = result.get(edge);
blocks.removeAll(finallyBlocks);
}
return result;
}
private final Map<TryEdge, List<BlockNode>> 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<Entry<TryEdge, List<BlockNode>>> entrySet() {
return underlying.entrySet();
}
@Override
public final List<BlockNode> get(Object key) {
return underlying.get(key);
}
@Override
public final boolean isEmpty() {
return underlying.isEmpty();
}
@Override
public final Set<TryEdge> keySet() {
return underlying.keySet();
}
@Override
public final List<BlockNode> put(TryEdge key, List<BlockNode> value) {
return underlying.put(key, value);
}
@Override
public final void putAll(Map<? extends TryEdge, ? extends List<BlockNode>> otherMap) {
underlying.putAll(otherMap);
}
@Override
public final List<BlockNode> remove(Object key) {
return underlying.remove(key);
}
@Override
public final int size() {
return underlying.size();
}
@Override
public final Collection<List<BlockNode>> values() {
return underlying.values();
}
@Nullable
public final List<BlockNode> 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<BlockNode> getBlocksForAllFallthroughs() {
final List<BlockNode> blks = new ArrayList<>();
for (final TryEdge edge : keySet()) {
if (edge.isHandlerExit()) {
continue;
}
blks.addAll(get(edge));
}
return blks;
}
}
@@ -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<BlockNode> containedBlocks;
public GlobalTraverserSourceState(final Set<BlockNode> containedBlocks) {
this.containedBlocks = containedBlocks;
}
public final boolean isBlockContained(final BlockNode block) {
return containedBlocks.contains(block);
}
public final Set<BlockNode> getContainedBlocks() {
return containedBlocks;
}
}
@@ -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<TraverserActivePathState> 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<TraverserState, Boolean> stateAbortCondition;
public TraverserController() {
this(null);
}
public TraverserController(final @Nullable Function<TraverserState, Boolean> stateAbortCondition) {
this.stateAbortCondition = stateAbortCondition;
}
/**
* Processes a traverser path state using from a {@link TraverserActivePathState}. This
* function will continue evaluating an active path until either:
* <ul>
* <li>The state abort condition is met by both "finally" and "candidate" path, if there is
* one.</li>
* <li>The path state of either the "finally" or "candidate" path has terminated.</li>
* <li>The path has began a comparison of two blocks which have already been compared.</li>
* <li>The "finally" and "candidate" states, on two different executions of
* {@link TraverserController#advance}, did not change.
* </ul>
* 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<TraverserActivePathState> process(final TraverserActivePathState state) throws TraverserException {
TraverserActivePathState nextState = state;
final AtomicReference<TraverserState> previousFinallyState = new AtomicReference<>(null);
final AtomicReference<TraverserState> previousCandidateState = new AtomicReference<>(null);
while (true) {
final List<TraverserActivePathState> advancedStates = advance(nextState, previousFinallyState, previousCandidateState);
if (advancedStates == null || advancedStates.isEmpty()) {
break;
}
if (advancedStates.size() != 1) {
final TraverserController nextController = new TraverserController(stateAbortCondition);
final List<TraverserActivePathState> returnStates = new ArrayList<>();
for (final TraverserActivePathState advancedState : advancedStates) {
final List<TraverserActivePathState> 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<TraverserActivePathState> advance(final TraverserActivePathState state,
final AtomicReference<TraverserState> previousFinallyState,
final AtomicReference<TraverserState> 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<TerminalTraverserState> finallyStateProducer =
TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNRESOLVABLE_STATES);
final TraverserStateFactory<TerminalTraverserState> 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<TraverserActivePathState> dupStates = commonState.getCachedStateFor(finallyBlock, candidateBlock);
final List<TraverserActivePathState> 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<TraverserActivePathState> 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<TraverserActivePathState> 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;
}
}
@@ -0,0 +1,8 @@
package jadx.core.dex.visitors.finaly.traverser;
public class TraverserException extends Exception {
public TraverserException(final String msg) {
super(msg);
}
}
@@ -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<T extends TraverserState> extends TraverserStateFactory<T> {
private final T baseState;
public DuplicatedTraverserStateFactory(final T baseState) {
this.baseState = baseState;
}
@Override
public final T generateInternalState(final TraverserActivePathState state) {
final Class<? extends T> baseStateClass = (Class<? extends T>) 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);
}
}
@@ -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<T extends TraverserState> {
protected abstract T generateInternalState(final TraverserActivePathState state);
public final T generateState(final TraverserActivePathState state) {
final T generatedState = generateInternalState(state);
return generatedState;
}
}
@@ -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<TraverserActivePathState> handle() throws TraverserException;
public final List<TraverserActivePathState> process() throws TraverserException {
return handle();
}
public final TraverserActivePathState getComparator() {
return comparatorState;
}
}
@@ -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<? extends TraverserState> stateRef;
public AbstractBlockPathTraverserHandler(final TraverserState initialState) {
this.stateRef = new AtomicReference<>(initialState);
}
public AbstractBlockPathTraverserHandler(final AtomicReference<? extends TraverserState> 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<? extends TraverserState> getStateReference() {
return stateRef;
}
}
@@ -0,0 +1,7 @@
package jadx.core.dex.visitors.finaly.traverser.handlers;
/**
* Sealed class
*/
public abstract class AbstractBlockTraverserHandler {
}
@@ -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<TraverserState> 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<TraverserState> 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);
}
}
@@ -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<TraverserActivePathState> 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);
}
}
@@ -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<TerminalTraverserState> finallyStateFactory =
TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_PATHS);
final TraverserStateFactory<TerminalTraverserState> 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<TraverserState, Boolean> 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<TraverserActivePathState> 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<RegisterArg> finallyAllowableOutputs = new HashSet<>();
public final Set<RegisterArg> candidateAllowableOutputs = new HashSet<>();
public boolean finallyAllowsCentral;
public boolean candidateAllowsCentral;
public boolean perfectMatch = true;
}
public MergePathActivePathTraverserHandler(final TraverserActivePathState comparatorState) {
super(comparatorState);
}
@Override
protected final List<TraverserActivePathState> 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<TraverserState, Boolean> abortFunction = getStateAbortOnTerminusFunction(finallyState, candidateState);
final List<BlockNode[]> allPermutationsPaths = getAllPermutationsOfCollection(candidateState.getRoots());
List<TraverserActivePathState> paths = null;
PostMergeStatus postMerge = null;
for (final BlockNode[] candidateRootsPermutation : allPermutationsPaths) {
final List<TraverserActivePathState> 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<TraverserActivePathState> currentPaths = new ArrayList<>();
boolean errorOccurred = false;
for (final TraverserActivePathState pathState : traversalPaths) {
final TraverserController branchController = new TraverserController(abortFunction);
final List<TraverserActivePathState> 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<NewBlockTraverserState> finallyStateFactory =
NewBlockTraverserState.getFactory(newFinallyCentralityState, finallyTerminusBlockInfo);
final TraverserStateFactory<NewBlockTraverserState> candidateStateFactory =
NewBlockTraverserState.getFactory(newCandidateCentralityState, candidateTerminusBlockInfo);
final TraverserActivePathState nextState =
TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory);
nextState.mergeWith(paths);
return List.of(nextState);
}
public static List<BlockNode[]> getAllPermutationsOfCollection(final Collection<BlockNode> elements) {
final Stack<BlockNode> permutationStack = new Stack<>();
final List<BlockNode[]> permutations = new ArrayList<>();
permutations(permutations, elements, permutationStack, elements.size());
return permutations;
}
public static void permutations(final List<BlockNode[]> permutations, final Collection<BlockNode> elements,
final Stack<BlockNode> 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());
}
}
}
@@ -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<T extends TraverserState & ISourceBlockState>
extends AbstractBlockPathTraverserHandler {
private final ISourceBlockState sourceBlockState;
public PredecessorBlockPathTraverserHandler(final T initialState) {
super(initialState);
this.sourceBlockState = initialState;
}
public PredecessorBlockPathTraverserHandler(final AtomicReference<T> initialStateRef) {
super(initialStateRef);
this.sourceBlockState = initialStateRef.get();
}
@Override
protected final void handle() {
final TraverserState baseState = getState();
final TraverserActivePathState comparator = baseState.getComparatorState();
final AtomicReference<TraverserState> 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);
}
}
@@ -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<BlockNode> orderBlocks(final List<BlockNode> blocks) {
final List<BlockNode> 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<TraverserActivePathState> 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<TraverserActivePathState> mergeScopes(final UnknownAdvanceStrategyTraverserState finallyState,
final UnknownAdvanceStrategyTraverserState candidateState) throws TraverserException {
final List<BlockNode> finallyBlocks = finallyState.getNextBlocks();
final List<BlockNode> candidateBlocks = candidateState.getNextBlocks();
final int finallyBlocksSize = finallyBlocks.size();
final int candidateBlocksSize = candidateBlocks.size();
final List<TraverserActivePathState> states;
if (candidateBlocksSize % finallyBlocksSize == 0 && candidateBlocksSize == finallyBlocksSize) {
final List<BlockNode> finallyBlocksOrdered = orderBlocks(finallyBlocks);
final List<BlockNode> candidateBlocksOrdered = orderBlocks(candidateBlocks);
final int duplicationCount = candidateBlocksSize / finallyBlocksSize;
states = new ArrayList<>(duplicationCount);
for (int i = 0; i < duplicationCount; i++) {
final List<BlockNode> 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<TerminalTraverserState> finallyStateFactory =
TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE);
final TraverserStateFactory<TerminalTraverserState> candidateStateFactory =
TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.UNMERGEABLE_STATE);
final TraverserActivePathState newState =
TraverserActivePathState.produceFromFactories(getComparator(), finallyStateFactory, candidateStateFactory);
states = List.of(newState);
}
return states;
}
private List<TraverserActivePathState> duplicateForPaths(final TraverserActivePathState comparator,
final UnknownAdvanceStrategyTraverserState advancingState, final TraverserState otherState,
final boolean duplicateIsFromFinally) {
final List<BlockNode> nextPredecessors = advancingState.getNextBlocks();
final List<TraverserActivePathState> newPaths = new ArrayList<>(nextPredecessors.size());
for (final BlockNode predecessor : nextPredecessors) {
final CentralityState centralityState = advancingState.getCentralityState();
final TraverserBlockInfo duplicatePathBlockInfo = new TraverserBlockInfo(predecessor);
final TraverserStateFactory<NewBlockTraverserState> 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<BlockNode> finallyBlocks, final List<BlockNode> 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<IdentifiedScopeWithTerminatorTraverserState> 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<IdentifiedScopeWithTerminatorTraverserState> candidateStateFactory =
IdentifiedScopeWithTerminatorTraverserState.getFactory(candidateCentralityState, candidateBlocks, candidateTerminator);
return TraverserActivePathState.produceFromFactories(comparator, finallyStateFactory, candidateStateFactory);
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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<IdentifiedScopeWithTerminatorTraverserState> getFactory(final CentralityState centralityState,
final List<BlockNode> roots, final BlockNode scopeTerminator) {
return new IdentifiedScopeWithTerminatorStateFactory(centralityState, roots, scopeTerminator);
}
private static final class IdentifiedScopeWithTerminatorStateFactory
extends TraverserStateFactory<IdentifiedScopeWithTerminatorTraverserState> {
private final CentralityState centralityState;
private final List<BlockNode> roots;
private final BlockNode scopeTerminator;
public IdentifiedScopeWithTerminatorStateFactory(final CentralityState centralityState, final List<BlockNode> 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<BlockNode> roots;
private final BlockNode scopeTerminator;
public IdentifiedScopeWithTerminatorTraverserState(final TraverserActivePathState state, final CentralityState centralityState,
final List<BlockNode> 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<BlockNode> getRoots() {
return roots;
}
}
@@ -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<NewBlockTraverserState> getFactory(final CentralityState centralityState,
final TraverserBlockInfo blockInsnInfo) {
return new NewBlockStateFactory(centralityState, blockInsnInfo);
}
private static class NewBlockStateFactory extends TraverserStateFactory<NewBlockTraverserState> {
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;
}
}
@@ -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<NoBlockTraverserState> getFactory(final CentralityState centralityState,
final BlockNode sourceBlock) {
return new NoBlockStateFactory(centralityState, sourceBlock);
}
private static class NoBlockStateFactory extends TraverserStateFactory<NoBlockTraverserState> {
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);
}
}
@@ -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<RecoveredFromCacheTraverserState> getFactory(final TraverserState underlying) {
return new RecoveredFromCacheStateFactory(underlying);
}
private static final class RecoveredFromCacheStateFactory extends TraverserStateFactory<RecoveredFromCacheTraverserState> {
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();
}
}
@@ -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<TerminalTraverserState> 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<TerminalTraverserState> {
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;
}
}
@@ -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<Integer> 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<BlockNode, BlockCompletionMonitor> {
private final Map<BlockNode, BlockCompletionMonitor> 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<Entry<BlockNode, BlockCompletionMonitor>> 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<BlockNode> keySet() {
return underlying.keySet();
}
@Override
public final BlockCompletionMonitor put(BlockNode key, BlockCompletionMonitor value) {
return underlying.put(key, value);
}
@Override
public final void putAll(Map<? extends BlockNode, ? extends BlockCompletionMonitor> 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<BlockCompletionMonitor> 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<TraverserState> finallyStateRef;
private final AtomicReference<TraverserState> candidateStateRef;
private final GlobalTraverserSourceState finallyGlobalState;
private final GlobalTraverserSourceState candidateGlobalState;
private final TraverserGlobalCommonState commonGlobalState;
private final Set<Pair<InsnNode>> 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<BlockNode> finallyBlocks,
final List<BlockNode> 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<Pair<InsnNode>> 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<Pair<InsnNode>> 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<TraverserState> getFinallyStateRef() {
return finallyStateRef;
}
public final AtomicReference<TraverserState> getCandidateStateRef() {
return candidateStateRef;
}
public final Set<Pair<InsnNode>> getMatchedInsns() {
return matchedInsns;
}
@Nullable
public final AtomicReference<TraverserState> 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<TraverserActivePathState> 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<BlockNode> getAllFullyMatchedFinallyBlocks() {
return getAllFullyMatchedBlocks(finallyCompletionMonitor);
}
public final Set<BlockNode> getAllFullyMatchedCandidateBlocks() {
return getAllFullyMatchedBlocks(candidateCompletionMonitor);
}
private Set<BlockNode> getAllFullyMatchedBlocks(final BlockCompletionMonitorMap monitorMap) {
final Set<BlockNode> matches = new HashSet<>();
for (final BlockCompletionMonitor monitor : monitorMap.values()) {
if (!monitor.isEntireBlock()) {
continue;
}
matches.add(monitor.block);
}
return matches;
}
}
@@ -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<InsnNode> getInsnsSlice() {
final List<InsnNode> 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);
}
}
@@ -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<Pair<BlockNode>, List<TraverserActivePathState>> 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<TraverserActivePathState> state) {
final Pair<BlockNode> blocks = new Pair<>(finallyBlock, candidateBlock);
searchedStates.put(blocks, state);
}
@Nullable
public final List<TraverserActivePathState> getCachedStateFor(final BlockNode finallyBlock, final BlockNode candidateBlock) {
final Pair<BlockNode> blocks = new Pair<>(finallyBlock, candidateBlock);
return searchedStates.get(blocks);
}
public final boolean hasBlocksBeenCached(final BlockNode finallyBlock, final BlockNode candidateBlock) {
final Pair<BlockNode> blocks = new Pair<>(finallyBlock, candidateBlock);
return searchedStates.containsKey(blocks);
}
public final MethodNode getMethodNode() {
return mth;
}
}
@@ -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);
}
}
@@ -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<BlockNode> nextBlocks;
public UnknownAdvanceStrategyTraverserState(final TraverserActivePathState state, final CentralityState centralityState,
final List<BlockNode> 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<BlockNode> dNextBlocks = new ArrayList<>(nextBlocks);
final TraverserState duplicated = new UnknownAdvanceStrategyTraverserState(comparatorState, dCentralityState, dNextBlocks);
return duplicated;
}
public final List<BlockNode> getNextBlocks() {
return nextBlocks;
}
}
@@ -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();
}
}
@@ -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<InsnNode> insns = insnInfo.getInsnsSlice();
final ListIterator<InsnNode> 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();
}
}
@@ -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<InsnNode> insns = insnInfo.getInsnsSlice();
final ListIterator<InsnNode> 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);
}
}
}
@@ -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<BlockNode> predecessors = block.getPredecessors();
final List<BlockNode> 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);
}
}
}
@@ -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);
}
@@ -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<NoBlockTraverserState> finallyStateProducer =
NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock);
final TraverserStateFactory<NoBlockTraverserState> 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<NoBlockTraverserState> newFinallyStateProducer =
NoBlockTraverserState.getFactory(finallyCentralityState, finallyBlock);
final TraverserStateFactory<?> newCandidateStateProducer = new DuplicatedTraverserStateFactory<>(candidateState);
return TraverserActivePathState.produceFromFactories(previousState, newFinallyStateProducer, newCandidateStateProducer);
} else {
candidateCentralityState.setAllowsNonStartingNode(false);
final TraverserStateFactory<NoBlockTraverserState> 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<TerminalTraverserState> finallyStateProducer =
TerminalTraverserState.getFactory(TerminalTraverserState.TerminationReason.NON_MATCHING_INSTRUCTIONS);
final TraverserStateFactory<TerminalTraverserState> 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<InsnNode> finallyInsns = finallyBlockInfo.getInsnsSlice();
final List<InsnNode> candidateInsns = candidateBlockInfo.getInsnsSlice();
final int finallyInsnsSize = finallyInsns.size();
final int candidateInsnsSize = candidateInsns.size();
final int maxIterateCount = Math.min(finallyInsnsSize, candidateInsnsSize);
final List<Pair<InsnNode>> 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<InsnNode> 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();
}
}
@@ -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);
}
}
@@ -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<BlockDepthEntry> 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<BlockDepthEntry> 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<BlockDepthEntry> 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;
}
}
}
@@ -80,10 +80,14 @@ public class ProcessTryCatchRegions extends AbstractRegionVisitor {
Region tryRegion = new Region(replaceRegion);
List<IContainer> 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);
}
@@ -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<EdgeInsnAttr> edgeInsnAttrs = outBlock.getAll(AType.EDGE_INSN);
if (!edgeInsnAttrs.isEmpty()) {
Region elseRegion = new Region(ifRegion);
List<InsnNode> 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<BlockNode> 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<BlockNode> 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
*/
@@ -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<BlockNode> 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<BlockNode> 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<LoopInfo> 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<Edge> exitEdges = loop.getExitEdges();
if (exitEdges.size() < 2) {
return true;
}
// If the header selected does not have an exit edge, raise an exception
Optional<Edge> 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<InsnNode> 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<Edge> exitEdges = loop.getExitEdges();
Queue<Edge> 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<BlockNode> 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<InsnNode> 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<BlockNode> 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<BlockNode> 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<Edge> 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<BlockNode> 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<EdgeInsnAttr> 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<LoopInfo> 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<BlockNode> blocks = BlockUtils.bitSetToBlocks(mth, exit.getDomFrontier());
List<BlockNode> 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<LoopInfo> 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<LoopInfo> exitLoop = mth.getAllLoopsForBlock(exit);
if (!exitLoop.isEmpty()) {
return;
}
List<LoopInfo> inLoops = mth.getAllLoopsForBlock(exitEdge.getSource());
List<LoopInfo> 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<EdgeInsnAttr> 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)) {
@@ -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;
@@ -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<CaseInfo> cases = new ArrayList<>(sw.getCases());
for (CaseInfo caseInfo : cases) {
if (canRemove(caseInfo.getContainer(), outBlock)) {
List<Object> 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<IContainer> 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<BlockNode, List<Object>> blocksMap, Map<BlockNode, BlockNode> 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;
@@ -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<ClassNode, ClassNode> clsUsage = new UseSet<>();
private final UseSet<ClassNode, MethodNode> clsUseInMth = new UseSet<>();
private final UseSet<FieldNode, MethodNode> fieldUsage = new UseSet<>();
// MethodNodeA -> Set of MethodNodes that MethodNodeA is called from
private final UseSet<MethodNode, MethodNode> mthUsage = new UseSet<>();
// MethodNodeA -> Set of MethodNodes that MethodNodeA calls
private final UseSet<MethodNode, MethodNode> mthUses = new UseSet<>();
// MethodNodeA -> Set of IMethodRefs for methods that MethodNodeA calls that cannot be resolved
private final UseSet<MethodNode, IMethodRef> unresolvedMthUsage = new UseSet<>();
private final Map<MethodNode, Boolean> 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<MethodNode, Boolean> 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<MethodNode> resolveMthList(List<MethodNode> mthNodeList) {
return Utils.collectionMap(mthNodeList,
m -> root.resolveDirectMethod(m.getParentClass().getRawName(), m.getMethodInfo().getShortId()));
}
}
@@ -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;
@@ -22,6 +22,10 @@ public class UseSet<K, V> {
return useMap.get(obj);
}
public Set<V> getOrDefault(K obj, Set<V> defaultValue) {
return useMap.getOrDefault(obj, defaultValue);
}
public void visit(BiConsumer<K, Set<V>> consumer) {
for (Map.Entry<K, Set<V>> entry : useMap.entrySet()) {
consumer.accept(entry.getKey(), entry.getValue());
@@ -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<BlockNode> collectAllPredecessors(MethodNode mth, BlockNode startBlock) {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
Function<BlockNode, List<BlockNode>> nextFunc = BlockNode::getPredecessors;
visitDFS(mth, startBlock, nextFunc, list::add);
return list;
}
public static List<BlockNode> collectAllSuccessors(MethodNode mth, BlockNode startBlock, boolean clean) {
List<BlockNode> list = new ArrayList<>(mth.getBasicBlocks().size());
Function<BlockNode, List<BlockNode>> nextFunc = clean ? BlockNode::getCleanSuccessors : BlockNode::getSuccessors;
@@ -513,6 +522,96 @@ public class BlockUtils {
return list;
}
public static List<BlockNode> collectAllSuccessorsUntil(MethodNode mth, BlockNode startBlock, boolean clean,
Predicate<BlockNode> stopCondition) {
List<BlockNode> blocks = new ArrayList<>();
collectAllSuccessorsUntil(mth, blocks, startBlock, clean, stopCondition);
return blocks;
}
private static void collectAllSuccessorsUntil(MethodNode mth, List<BlockNode> blocks, BlockNode currentBlock, boolean clean,
Predicate<BlockNode> stopCondition) {
if (blocks.contains(currentBlock)) {
return;
}
blocks.add(currentBlock);
if (stopCondition.test(currentBlock)) {
return;
}
List<BlockNode> successors = clean ? currentBlock.getCleanSuccessors() : currentBlock.getSuccessors();
for (BlockNode successor : successors) {
collectAllSuccessorsUntil(mth, blocks, successor, clean, stopCondition);
}
}
@Nullable
public static BlockNode getBottomCommonPredecessor(MethodNode mth, List<BlockNode> blocks, Set<BlockNode> containedBlocks) {
return getBottomCommonPredecessor(mth, blocks, containedBlocks, false);
}
@Nullable
public static BlockNode getBottomCommonPredecessor(MethodNode mth, List<BlockNode> blocks, Set<BlockNode> containedBlocks,
boolean addTopBlock) {
if (blocks.isEmpty()) {
return null;
}
Set<BlockNode> 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<BlockNode> predecessors = collectAllPredecessors(mth, nextBlock);
visitedPredecessorsByAll.retainAll(predecessors);
}
return BlockUtils.getBottomBlock(new ArrayList<>(visitedPredecessorsByAll));
}
@Nullable
public static BlockNode getTopCommonSuccessor(MethodNode mth, List<BlockNode> blocks, boolean cleanOnly) {
return getTopCommonSuccessor(mth, blocks, cleanOnly, false);
}
@Nullable
public static BlockNode getTopCommonSuccessor(MethodNode mth, List<BlockNode> blocks, boolean cleanOnly, boolean addTopBlock) {
if (blocks.isEmpty()) {
return null;
}
Set<BlockNode> 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<BlockNode> successors = collectAllSuccessors(mth, nextBlock, cleanOnly);
visitedSuccessorsByAll.retainAll(successors);
}
return BlockUtils.getTopBlock(new ArrayList<>(visitedSuccessorsByAll));
}
public static void visitDFS(MethodNode mth, Consumer<BlockNode> 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<BlockNode> 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<BlockNode> getOnePath(BlockNode start, BlockNode end) {
return collectPathUntil(start, end, false, b -> true);
}
private static void addPredecessors(Set<BlockNode> 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<BlockNode> pred) {
List<BlockNode> 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<BlockNode> collectPathUntil(BlockNode from, BlockNode until, boolean clean, Predicate<BlockNode> pred) {
List<BlockNode> 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<BlockNode> internalCollectPathUntil(BlockNode from, BlockNode until, BitSet visited, boolean clean,
Predicate<BlockNode> pred) {
List<BlockNode> 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<BlockNode> path = new ArrayList<>();
path.add(s);
return path;
}
int id = s.getPos();
if (!visited.get(id)) {
visited.set(id);
List<BlockNode> 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<BlockNode> pred) {
if (start == end) {
return true;
}
return traverseSuccessorsUntil(start, end, new BitSet(), false, pred);
}
public static BlockNode getTopBlock(List<BlockNode> blocks) {
if (blocks.size() == 1) {
return blocks.get(0);
@@ -689,16 +885,46 @@ public class BlockUtils {
*/
@Nullable
public static BlockNode getBottomBlock(List<BlockNode> blocks) {
return getBottomBlock(blocks, false);
}
public static BlockNode getBottomBlock(List<BlockNode> 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<BlockNode> followEmptyUpPathWithinSet(BlockNode start, Collection<BlockNode> traversableBlocks) {
List<BlockNode> results = new LinkedList<>();
followEmptyUpPathWithinSet(results, start, traversableBlocks, new HashSet<>());
return results;
}
public static void followEmptyUpPathWithinSet(List<BlockNode> results, BlockNode start, Collection<BlockNode> traversableBlocks,
Collection<BlockNode> traversedBlocks) {
List<BlockNode> 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<BlockNode> visitor) {
visitBlocksOnEmptyPath(start, visitor, false);
}
public static void visitBlocksOnEmptyPath(BlockNode start, Consumer<BlockNode> 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<BlockNode> successors = block.getCleanSuccessors();
if (successors.size() != 1) {
List<BlockNode> 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<IBlock> 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) {
@@ -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<IRegion> 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<IRegion> 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<BlockNode> blocks = mth.getBasicBlocks();
if (blocks == null) {
InsnNode[] insnArr = mth.getInstructions();
if (insnArr == null) {
return null;
}
BlockNode block = new BlockNode(0, 0, 0);
List<InsnNode> 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<IBlock> 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<IBlock> 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<IBlock> 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<InsnNode> 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<ArgType> 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<String> 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();
}
}
@@ -0,0 +1,42 @@
package jadx.core.utils;
public class Pair<T> {
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 + ')';
}
}
@@ -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<IContainer> 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<LoopInfo> getLoopsStartInRegion(MethodNode mth, IRegion r) {
List<LoopInfo> 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<IBlock>() {
@Override
public void accept(IBlock t) {
t.add(flag);
}
});
}
}
@@ -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) {
@@ -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);
}
@@ -0,0 +1,10 @@
package jadx.core.dex.trycatch;
import org.junit.jupiter.api.Nested;
public class TryCatchBlockAttrTest {
@Nested
public class TryCatchBlockAttrIntegration {
}
}
@@ -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 {
@@ -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) {");
}
}
@@ -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 {");
}
}
@@ -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)");
}
}
@@ -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");
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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;");
}
}
@@ -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()
@@ -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");
}
}
@@ -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)");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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");
}
}
@@ -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")
@@ -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()
@@ -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://\"))");
}
}
@@ -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;");
}
}
@@ -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)");
}
}
@@ -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");
}
}

Some files were not shown because too many files have changed in this diff Show More