diff --git a/base/src/main/java/proguard/analysis/datastructure/callgraph/CallGraph.java b/base/src/main/java/proguard/analysis/datastructure/callgraph/CallGraph.java index 308b5cc3c..616266881 100644 --- a/base/src/main/java/proguard/analysis/datastructure/callgraph/CallGraph.java +++ b/base/src/main/java/proguard/analysis/datastructure/callgraph/CallGraph.java @@ -18,12 +18,16 @@ package proguard.analysis.datastructure.callgraph; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import proguard.classfile.ClassPool; import proguard.classfile.Clazz; import proguard.classfile.MethodSignature; @@ -32,8 +36,6 @@ /** * Collection of all {@link Call}s in a program, optimized for retrieval of incoming and outgoing * edges for any method in constant time. - * - * @author Samuel Hopstock */ public class CallGraph { @@ -41,9 +43,6 @@ public class CallGraph { public final Map> incoming; public final Map> outgoing; - /** If true, incoming edges are not explored further for known entry points. */ - private static final boolean STOP_AT_ENTRYPOINT = true; - private final boolean concurrent; /** Create an empty call graph. */ @@ -99,21 +98,25 @@ public void clear() { } /** - * See {@link #reconstructCallGraph(ClassPool, MethodSignature, int, int)} + * See {@link #reconstructCallGraph(ClassPool, MethodSignature, int, int, Set)} * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping. class names to the actual {@link Clazz}. * @param start The {@link MethodSignature} of the method whose incoming call graph should be * calculated. + * @param stopMethods Set of {@link MethodSignature} to stop exploration at, if desired. * @return A {@link Node} that represents the single call graph root, i.e. the start method. */ - public Node reconstructCallGraph(ClassPool programClassPool, MethodSignature start) { + public Node reconstructCallGraph( + ClassPool programClassPool, MethodSignature start, Set stopMethods) { return CallGraphWalker.predecessorPathsAccept( - this, start, n -> handleUntilEntryPoint(programClassPool, n, null)); + this, start, n -> handleUntil(programClassPool, n, stopMethods, null)); } /** - * Calculate the incoming call graph for a method of interest, showing how it can be reached. + * Calculate the incoming call graph for a method of interest, showing how it can be reached from + * a given Set of stop methods, which typically are Android lifecycle methods such as an + * Activity's onCreate() method: * *

We have an inverted tree structure like the following example: * @@ -128,11 +131,6 @@ public Node reconstructCallGraph(ClassPool programClassPool, MethodSignature sta * shows that it can be reached from {@code onCreate()} via {@code proxy()}, and also directly * from {@code onResume()} or {@code unusedMethod()}. * - *

Generally, we still can't be sure whether the top most methods (leaves in the tree) can be - * reached themselves, if we don't find any incoming edges. But if these methods are {@link - * EntryPoint}s of an Android app, they will most likely be called at some point in the app - * lifecycle. - * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping. class names to the actual {@link Clazz}. * @param start The {@link MethodSignature} of the method whose incoming call graph should be @@ -141,100 +139,75 @@ public Node reconstructCallGraph(ClassPool programClassPool, MethodSignature sta * CallGraphWalker#MAX_DEPTH_DEFAULT}. * @param maxWidth maximal width of reconstructed {@link CallGraph} similar to {@link * CallGraphWalker#MAX_WIDTH_DEFAULT}. + * @param stopMethods Set of method signatures to stop exploration, for example for entry points * @return A {@link Node} that represents the single call graph root, i.e. the start method. */ public Node reconstructCallGraph( - ClassPool programClassPool, MethodSignature start, int maxDepth, int maxWidth) { + ClassPool programClassPool, + MethodSignature start, + int maxDepth, + int maxWidth, + Set stopMethods) { return CallGraphWalker.predecessorPathsAccept( - this, start, n -> handleUntilEntryPoint(programClassPool, n, null), maxDepth, maxWidth); + this, start, n -> handleUntil(programClassPool, n, stopMethods, null), maxDepth, maxWidth); } /** - * Extension of {@link #reconstructCallGraph(ClassPool, MethodSignature)} that also collects all - * {@link EntryPoint}s found along the way. + * Extension of {@link #reconstructCallGraph(ClassPool, MethodSignature, Set)} that also collects + * all reached stop methods. * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping. * @param start The {@link MethodSignature} of the method whose incoming call graph should be * calculated. - * @param entryPoints A set that will be filled with all {@link EntryPoint}s that are part of the - * incoming call graph. + * @param stopMethods A set of {@link MethodSignature} to stop exploration, e.g. app entry points + * @param reachedMethods A set that will be filled with all reached stop methods * @return A {@link Node} that represents the single call graph root, i.e. the start method. */ public Node reconstructCallGraph( - ClassPool programClassPool, MethodSignature start, Set entryPoints) { + ClassPool programClassPool, + MethodSignature start, + Set stopMethods, + Set reachedMethods) { return CallGraphWalker.predecessorPathsAccept( - this, start, n -> handleUntilEntryPoint(programClassPool, n, entryPoints)); + this, start, n -> handleUntil(programClassPool, n, stopMethods, reachedMethods)); } /** * Handler implementation for {@link CallGraphWalker#predecessorPathsAccept(CallGraph, - * MethodSignature, Predicate)} that checks discovered paths if they have arrived at a known entry - * point. + * MethodSignature, Predicate)} that checks if one of a given set of stop methods has been reached + * along the call graph paths. * * @param programClassPool The current {@link ClassPool} of the program that can be used for * mapping class names to the actual {@link Clazz}. - * @param curr The {@link Node} that represents the currently discovered call graph node and its - * successors. - * @param entryPoints a set containing the entrypoints seen on this path, will be filled during - * the reconstruction of the callgraph. - * @return true if we have arrived at an entry point, so that the {@link CallGraphWalker} stops - * exploring this particular path. + * @param current The {@link Node} that represents the currently discovered call graph node and + * its successors. + * @param reachedMethods A set for collecting reached stop methods, can be null. + * @return true if call graph exploration should continue, false otherwise. */ - private boolean handleUntilEntryPoint( - ClassPool programClassPool, Node curr, Set entryPoints) { - // Get all classes that contain known entryPoints and are superclasses of the current one - Clazz currClass = programClassPool.getClass(curr.signature.getClassName()); - if (currClass == null) { - log.warn("Could not find class {} in class pool", curr.signature.getClassName()); - curr.isTruncated = true; + private boolean handleUntil( + ClassPool programClassPool, + Node current, + Set stopMethods, + @Nullable Set reachedMethods) { + + MethodSignature currentSignature = current.signature; + String currentClassName = currentSignature.getClassName(); + Clazz currentClass = programClassPool.getClass(currentClassName); + + if (currentClass == null) { + log.warn("Could not find class {} in class pool", currentClassName); + current.isTruncated = true; return false; } - Set entrypointSuperclassNames = - EntryPoint.WELL_KNOWN_ENTRYPOINT_CLASSES.stream() - .filter(e -> classExtendsOrEquals(currClass, e.replace('.', '/'))) - .collect(Collectors.toSet()); - // If we are in a method overriding any known entrypoint, that's a call graph leaf - Optional matchingEntrypoint = - EntryPoint.WELL_KNOWN_ENTRYPOINTS.stream() - .filter( - e -> - entrypointSuperclassNames.contains(e.className) - && e.methodName.equals(curr.signature.method)) - .findFirst(); - if (matchingEntrypoint.isPresent()) { - curr.matchingEntrypoint = matchingEntrypoint.get(); - if (entryPoints != null) { - entryPoints.add( - new EntryPoint( - matchingEntrypoint.get().type, - curr.signature.getClassName(), - curr.signature.method)); - } - return !STOP_AT_ENTRYPOINT; - } - return true; - } - /** - * Check if a {@link Clazz} either matches the provided class name or extends this provided class. - * Both direct and transitive inheritance is allowed. - * - * @param currClass The {@link Clazz} that might be equal to or a subclass of the provided class - * name. - * @param className The potential super class name. - * @return True if currClass is equal to className or is one of its subclasses. - */ - private boolean classExtendsOrEquals(Clazz currClass, String className) { - if (Objects.equals(currClass.getSuperName(), className)) { - return true; - } - if (currClass.getSuperClass() != null) { - if (Objects.equals(currClass.getSuperClass().getName(), className)) { - return true; + if (stopMethods.contains(currentSignature)) { + if (reachedMethods != null) { + reachedMethods.add(currentSignature); } - return classExtendsOrEquals(currClass.getSuperClass(), className); + return false; } - return false; + + return true; } } diff --git a/base/src/main/java/proguard/analysis/datastructure/callgraph/EntryPoint.java b/base/src/main/java/proguard/analysis/datastructure/callgraph/EntryPoint.java deleted file mode 100644 index d9c22872a..000000000 --- a/base/src/main/java/proguard/analysis/datastructure/callgraph/EntryPoint.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * ProGuardCORE -- library to process Java bytecode. - * - * Copyright (c) 2002-2021 Guardsquare NV - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package proguard.analysis.datastructure.callgraph; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import proguard.classfile.MethodDescriptor; -import proguard.classfile.MethodSignature; - -/** - * This class serves as a static collection of entry points for an app. An entry point is a method - * that is provided by the Android framework and can be hooked by the app to be called at a specific - * point in the app lifecycle. - * - *

The list of well known entry points has been curated using the official - * documentation and a third-party - * collection. - * - * @author Samuel Hopstock - */ -public class EntryPoint { - - public enum Type { - ACTIVITY, - SERVICE, - BROADCAST_RECEIVER, - CONTENT_PROVIDER - } - - public static final List ENTRY_POINTS_ACTIVITY = - Arrays.asList( - new EntryPoint(Type.ACTIVITY, "android.app.Activity", ""), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", ""), - // General lifecycle - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onCreate"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onStart"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onRestart"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onResume"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onPause"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onStop"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onDestroy"), - // Additional known entrypoints - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onAttachFragment"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onContentChanged"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onActivityResult"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onRestoreInstanceState"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onPostCreate"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onPostResume"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onAttachedToWindow"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onCreateOptionsMenu"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onPrepareOptionsMenu"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onUserInteraction"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onUserLeaveHint"), - new EntryPoint(Type.ACTIVITY, "android.app.Activity", "onSaveInstanceState")); - public static final List ENTRY_POINTS_FRAGMENT = - Arrays.asList( - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", ""), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", ""), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onInflate"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onAttach"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onCreate"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onCreateView"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onViewCreated"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onActivityCreated"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onViewStateRestored"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onStart"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onResume"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onCreateOptionsMenu"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onPrepareOptionsMenu"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onPause"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onSaveInstanceState"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onStop"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onDestroyView"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onDestroy"), - new EntryPoint(Type.ACTIVITY, "android.app.Fragment", "onDetach")); - public static final List ENTRY_POINTS_SERVICE = - Arrays.asList( - new EntryPoint(Type.SERVICE, "android.app.Service", ""), - new EntryPoint(Type.SERVICE, "android.app.Service", ""), - new EntryPoint(Type.SERVICE, "android.app.Service", "onCreate"), - new EntryPoint(Type.SERVICE, "android.app.Service", "onStartCommand"), - new EntryPoint(Type.SERVICE, "android.app.Service", "onBind"), - new EntryPoint(Type.SERVICE, "android.app.Service", "onUnbind"), - new EntryPoint(Type.SERVICE, "android.app.Service", "onRebind"), - new EntryPoint(Type.SERVICE, "android.app.Service", "onDestroy")); - public static final List ENTRY_POINTS_BROADCAST_RECEIVER = - Arrays.asList( - new EntryPoint(Type.BROADCAST_RECEIVER, "android.content.BroadcastReceiver", ""), - new EntryPoint(Type.BROADCAST_RECEIVER, "android.content.BroadcastReceiver", ""), - new EntryPoint( - Type.BROADCAST_RECEIVER, "android.content.BroadcastReceiver", "onReceive")); - public static final List ENTRY_POINTS_CONTENT_PROVIDER = - Arrays.asList( - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", ""), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", ""), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "onCreate"), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "query"), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "insert"), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "update"), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "delete"), - new EntryPoint(Type.CONTENT_PROVIDER, "android.content.ContentProvider", "getType")); - public static final List WELL_KNOWN_ENTRYPOINTS = - Stream.of( - ENTRY_POINTS_ACTIVITY, - ENTRY_POINTS_FRAGMENT, - ENTRY_POINTS_SERVICE, - ENTRY_POINTS_BROADCAST_RECEIVER, - ENTRY_POINTS_CONTENT_PROVIDER) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - public static final Set WELL_KNOWN_ENTRYPOINT_CLASSES = - WELL_KNOWN_ENTRYPOINTS.stream().map(e -> e.className).collect(Collectors.toSet()); - - public final String className; - public final String methodName; - public final Type type; - - public EntryPoint(Type type, String className, String methodName) { - this.type = type; - this.className = className; - this.methodName = methodName; - } - - public static List getEntryPointsForType(Type type) { - switch (type) { - case ACTIVITY: - return ENTRY_POINTS_ACTIVITY; - case SERVICE: - return ENTRY_POINTS_SERVICE; - case BROADCAST_RECEIVER: - return ENTRY_POINTS_BROADCAST_RECEIVER; - case CONTENT_PROVIDER: - return ENTRY_POINTS_CONTENT_PROVIDER; - default: - throw new IllegalStateException("Unsupported entry point type"); - } - } - - public MethodSignature toSignature() { - return new MethodSignature(className.replace('.', '/'), methodName, (MethodDescriptor) null); - } - - @Override - public String toString() { - return toSignature().toString() + " (" + type + ")"; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - EntryPoint that = (EntryPoint) o; - return Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName); - } - - @Override - public int hashCode() { - return Objects.hash(className, methodName); - } -} diff --git a/base/src/main/java/proguard/analysis/datastructure/callgraph/Node.java b/base/src/main/java/proguard/analysis/datastructure/callgraph/Node.java index 473c5a75d..e92131f84 100644 --- a/base/src/main/java/proguard/analysis/datastructure/callgraph/Node.java +++ b/base/src/main/java/proguard/analysis/datastructure/callgraph/Node.java @@ -48,7 +48,6 @@ public class Node { public final Set outgoingCallLocations = new HashSet<>(); public final Set successors = new HashSet<>(); - public EntryPoint matchingEntrypoint = null; public boolean isTruncated = false; public Node(MethodSignature signature) { diff --git a/docs/md/releasenotes.md b/docs/md/releasenotes.md index 565ad17cc..b63b38a5c 100644 --- a/docs/md/releasenotes.md +++ b/docs/md/releasenotes.md @@ -1,3 +1,10 @@ +## Version 9.1.6 + +### API changes + +- Make `CallGraph` reconstruction/traversal methods generic in that they now take a `Set` to determine when to stop exploration +- Delete `EntryPoint` class + ## Version 9.1.5 ### Improved