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:
@@ -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);
|
||||
}
|
||||
+81
@@ -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;
|
||||
}
|
||||
}
|
||||
+26
@@ -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;
|
||||
}
|
||||
}
|
||||
+211
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package jadx.core.dex.visitors.finaly.traverser;
|
||||
|
||||
public class TraverserException extends Exception {
|
||||
|
||||
public TraverserException(final String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+14
@@ -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;
|
||||
}
|
||||
}
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package jadx.core.dex.visitors.finaly.traverser.handlers;
|
||||
|
||||
/**
|
||||
* Sealed class
|
||||
*/
|
||||
public abstract class AbstractBlockTraverserHandler {
|
||||
}
|
||||
+46
@@ -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);
|
||||
}
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+237
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+154
@@ -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);
|
||||
}
|
||||
}
|
||||
+56
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
@@ -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();
|
||||
}
|
||||
+89
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
@@ -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;
|
||||
}
|
||||
}
|
||||
+78
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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();
|
||||
}
|
||||
}
|
||||
+97
@@ -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;
|
||||
}
|
||||
}
|
||||
+412
@@ -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;
|
||||
}
|
||||
}
|
||||
+92
@@ -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);
|
||||
}
|
||||
}
|
||||
+43
@@ -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;
|
||||
}
|
||||
}
|
||||
+114
@@ -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);
|
||||
}
|
||||
}
|
||||
+64
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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();
|
||||
}
|
||||
}
|
||||
+56
@@ -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();
|
||||
}
|
||||
}
|
||||
+102
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+8
@@ -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);
|
||||
}
|
||||
+204
@@ -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
|
||||
*/
|
||||
|
||||
+369
-37
@@ -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;
|
||||
|
||||
+46
-17
@@ -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 {");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -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
Reference in New Issue
Block a user