blob: 871c4f9400752f4c9d48202fa89ebbb980b29ada [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 com.android.tools.apk.analyzer.dex;
import com.android.annotations.NonNull;
import com.android.tools.apk.analyzer.dex.tree.*;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import java.util.*;
import org.jf.dexlib2.dexbacked.*;
import org.jf.dexlib2.dexbacked.reference.DexBackedFieldReference;
import org.jf.dexlib2.dexbacked.reference.DexBackedMethodReference;
import org.jf.dexlib2.dexbacked.reference.DexBackedTypeReference;
import org.jf.dexlib2.iface.Annotation;
import org.jf.dexlib2.iface.AnnotationElement;
import org.jf.dexlib2.iface.instruction.DualReferenceInstruction;
import org.jf.dexlib2.iface.instruction.Instruction;
import org.jf.dexlib2.iface.instruction.ReferenceInstruction;
import org.jf.dexlib2.iface.reference.FieldReference;
import org.jf.dexlib2.iface.reference.MethodReference;
import org.jf.dexlib2.iface.reference.Reference;
import org.jf.dexlib2.iface.reference.TypeReference;
import org.jf.dexlib2.iface.value.*;
import org.jf.dexlib2.immutable.reference.*;
public class DexReferences {
private final Multimap<Reference, ImmutableReference> referenceReferences =
HashMultimap.create();
public DexReferences(DexBackedDexFile[] files) {
gatherBackReferences(files);
}
/**
* Goes through all the classes, methods, and fields, and gathers all possible references from
* one type/method/field to another.
*
* @param files dex file
*/
private void gatherBackReferences(@NonNull DexBackedDexFile[] files) {
Map<Reference, ImmutableReference> immutableReferencesBin = new HashMap<>();
for (DexBackedDexFile file : files) {
//build a map from class names (String) to actual TypeReferences,
//as this information is not readily available to query through
//the dexlib2 API.
Map<String, ImmutableTypeReference> typesByName = new HashMap<>();
for (int i = 0, m = file.getTypeCount(); i < m; i++) {
ImmutableTypeReference immutableTypeRef =
ImmutableTypeReference.of(new DexBackedTypeReference(file, i));
typesByName.put(immutableTypeRef.getType(), immutableTypeRef);
}
//loop through all methods referenced in the dex file, mapping the following:
for (int i = 0, m = file.getMethodCount(); i < m; i++) {
MethodReference methodReference = new DexBackedMethodReference(file, i);
//- return type => method
ImmutableReference typeRef = typesByName.get(methodReference.getReturnType());
addReference(typeRef, methodReference, immutableReferencesBin);
//- all parameter types => method
for (CharSequence parameterType : methodReference.getParameterTypes()) {
typeRef = typesByName.get(parameterType.toString());
addReference(typeRef, methodReference, immutableReferencesBin);
}
}
//loop through all classes defined in the dex file, mapping the following:
for (DexBackedClassDef classDef : file.getClasses()) {
//- superclass => class
ImmutableReference typeRef = typesByName.get(classDef.getSuperclass());
addReference(typeRef, classDef, immutableReferencesBin);
//- all implemented interfaces => class
for (String iface : classDef.getInterfaces()) {
typeRef = typesByName.get(iface);
addReference(typeRef, classDef, immutableReferencesBin);
}
//map annotations => class
for (Annotation annotation : classDef.getAnnotations()) {
addAnnotation(immutableReferencesBin, typesByName, classDef, annotation);
}
//loop through all the methods defined in this class
for (DexBackedMethod method : classDef.getMethods()) {
//if the method has an implementation, loop through the bytecode
//mapping any references that exist in dex instructions to the method.
//Fortunately, dexlib2 marks every bytecode instruction that accepts
//a reference with one or two interfaces: ReferenceInstruction
//and DualReferenceInstruction.
DexBackedMethodImplementation impl = method.getImplementation();
if (impl != null) {
for (Instruction instruction : impl.getInstructions()) {
if (instruction instanceof ReferenceInstruction) {
Reference reference =
((ReferenceInstruction) instruction).getReference();
addReferenceAndEnclosingClass(
immutableReferencesBin, typesByName, method, reference);
}
if (instruction instanceof DualReferenceInstruction) {
Reference reference =
((DualReferenceInstruction) instruction).getReference2();
addReferenceAndEnclosingClass(
immutableReferencesBin, typesByName, method, reference);
}
}
}
//map annotations => method
for (Annotation annotation : method.getAnnotations()) {
addAnnotation(immutableReferencesBin, typesByName, method, annotation);
}
}
for (DexBackedField field : classDef.getFields()) {
//map annotations => field
for (Annotation annotation : field.getAnnotations()) {
addAnnotation(immutableReferencesBin, typesByName, field, annotation);
}
}
}
//loop through all fields referenced in this dex file, creating
//a mapping from the field type => field
for (int i = 0, m = file.getFieldCount(); i < m; i++) {
FieldReference fieldRef = new DexBackedFieldReference(file, i);
ImmutableReference typeRef = typesByName.get(fieldRef.getType());
addReference(typeRef, fieldRef, immutableReferencesBin);
}
}
}
private void addAnnotation(
Map<Reference, ImmutableReference> immutableReferencesBin,
Map<String, ImmutableTypeReference> typesByName,
Reference ref,
Annotation annotation) {
ImmutableReference typeRef = typesByName.get(annotation.getType());
addReference(typeRef, ref, immutableReferencesBin);
Set<? extends AnnotationElement> elements = annotation.getElements();
for (AnnotationElement element : elements) {
EncodedValue value = element.getValue();
addEncodedValue(immutableReferencesBin, typesByName, ref, value);
}
}
private void addEncodedValue(
Map<Reference, ImmutableReference> immutableReferencesBin,
Map<String, ImmutableTypeReference> typesByName,
Reference ref,
EncodedValue value) {
if (value instanceof AnnotationEncodedValue) {
ImmutableTypeReference typeRef =
typesByName.get(((AnnotationEncodedValue) value).getType());
addReference(typeRef, ref, immutableReferencesBin);
for (AnnotationElement element : ((AnnotationEncodedValue) value).getElements()) {
addEncodedValue(immutableReferencesBin, typesByName, ref, element.getValue());
}
} else if (value instanceof ArrayEncodedValue) {
for (EncodedValue encodedValue : ((ArrayEncodedValue) value).getValue()) {
addEncodedValue(immutableReferencesBin, typesByName, ref, encodedValue);
}
} else if (value instanceof EnumEncodedValue) {
addReferenceAndEnclosingClass(
immutableReferencesBin,
typesByName,
ref,
((EnumEncodedValue) value).getValue());
} else if (value instanceof TypeEncodedValue) {
ImmutableTypeReference typeRef = typesByName.get(((TypeEncodedValue) value).getValue());
addReference(typeRef, ref, immutableReferencesBin);
}
}
private void addReferenceAndEnclosingClass(
Map<Reference, ImmutableReference> immutableReferencesBin,
Map<String, ImmutableTypeReference> typesByName,
Reference ref,
Reference memberReference) {
addReference(memberReference, ref, immutableReferencesBin);
//also map enclosing class of referenced method/field => this reference
if (memberReference instanceof MethodReference) {
addReference(
typesByName.get(((MethodReference) memberReference).getDefiningClass()),
ref,
immutableReferencesBin);
} else if (memberReference instanceof FieldReference) {
addReference(
typesByName.get(((FieldReference) memberReference).getDefiningClass()),
ref,
immutableReferencesBin);
}
}
//we want to reuse immutable references, not keep creating them
//use the map as a bag of ImmutableReferences for reuse
private void addReference(
Reference ref1,
Reference ref2,
Map<Reference, ImmutableReference> immutableReferencesBin) {
ImmutableReference immutableRef1 = immutableReferencesBin.get(ref1);
if (immutableRef1 == null) {
immutableRef1 = ImmutableReferenceFactory.of(ref1);
immutableReferencesBin.put(immutableRef1, immutableRef1);
}
ImmutableReference immutableRef2 = immutableReferencesBin.get(ref2);
if (immutableRef2 == null) {
immutableRef2 = ImmutableReferenceFactory.of(ref2);
immutableReferencesBin.put(immutableRef2, immutableRef2);
}
if (immutableRef1 instanceof TypeReference) {
String definingType2 = null;
if (immutableRef2 instanceof MethodReference) {
definingType2 = ((MethodReference) immutableRef2).getDefiningClass();
} else if (immutableRef2 instanceof FieldReference) {
definingType2 = ((FieldReference) immutableRef2).getDefiningClass();
}
//we don't want to map a class => member of that class
//as it only creates noise
if (((TypeReference) immutableRef1).getType().equals(definingType2)) {
return;
}
}
referenceReferences.put(immutableRef1, immutableRef2);
}
/**
* Build a full reference tree for the given DEX reference.
*
* @param referenced the dex element you wish to find references for
* @return the root of the reference tree
*/
public DexElementNode getReferenceTreeFor(@NonNull Reference referenced) {
return getReferenceTreeFor(referenced, false);
}
/**
* Build a reference tree for the given DEX reference. Depending on the {@code shallow}
* parameter this returns the full tree root or just the root with first level references
* evaluated.
*
* <p>You can then lazy load deeper references using {@link #addReferencesForNode(DexElementNode
* node, Reference referenced, boolean shallow)}.
*
* <p>To check if references were already loaded for a node use {@link
* #isAlreadyLoaded(DexElementNode)}
*
* @param referenced the dex element you wish to find references for
* @param shallow false to to build the full tree, true to evaluate just the first level
* @return the root of the reference tree
*/
public DexElementNode getReferenceTreeFor(@NonNull Reference referenced, boolean shallow) {
DexElementNode rootNode =
DexElementNodeFactory.from(ImmutableReferenceFactory.of(referenced));
addReferencesForNode(rootNode, shallow);
rootNode.sort(NODE_COMPARATOR);
return rootNode;
}
/**
* Finds references to {@code referenced} and attaches them as tree nodes under {@code node}
*
* <p>Depending on the {@code shallow} parameter this attaches the full reference tree or stops
* evaluation at the first level
*
* @param node the root node under which you wish to attach references
* @param shallow false to to build the full tree, true to evaluate just the first level
*/
public void addReferencesForNode(@NonNull DexElementNode node, boolean shallow) {
Reference referenced = node.getReference();
node.removeAllChildren();
Collection<? extends ImmutableReference> references = referenceReferences.get(referenced);
for (ImmutableReference ref : references) {
if (ref instanceof MethodReference
|| ref instanceof TypeReference
|| ref instanceof FieldReference) {
DexElementNode parentNode = node;
boolean hasCycle = false;
while (parentNode != null) {
if (ref.equals(parentNode.getReference())) {
hasCycle = true;
}
parentNode = parentNode.getParent();
}
if (hasCycle) {
continue;
}
DexElementNode newNode = DexElementNodeFactory.from(ref);
node.setAllowsChildren(true);
node.add(newNode);
if (!shallow) {
addReferencesForNode(newNode, false);
} else {
newNode.setAllowsChildren(true);
//DexPackageNodes are never normally used in a reference tree
//so we'll use them as a sentinel to mark nodes
//that haven't been resolved yet (for lazy loading)
newNode.add(new DexPackageNode("", null));
}
}
}
}
/**
* Checks if the specified dex reference tree node has had its references evaluated and attached
* (if any) or if it still needs to be passed to {@link #addReferencesForNode(DexElementNode,
* Reference, boolean)} for lazy evaluation
*
* @param node the reference tree node to check
* @return true if this node is already evaluated
*/
public static boolean isAlreadyLoaded(DexElementNode node) {
//as per the comment in addReferencesForNode:
//we're checking if the node contains a sentinel (single DexPackageNode child)
return !(node.getChildCount() == 1 && node.getFirstChild() instanceof DexPackageNode);
}
public static final Comparator<DexElementNode> NODE_COMPARATOR =
Comparator.comparing(
o -> {
if (o instanceof DexClassNode) {
return o.getName();
} else if (o instanceof DexMethodNode) {
assert o.getReference() != null;
return ((MethodReference) o.getReference()).getDefiningClass()
+ " "
+ o.getName();
} else if (o instanceof DexFieldNode) {
assert o.getReference() != null;
return ((FieldReference) o.getReference()).getDefiningClass()
+ " "
+ o.getName();
}
return "";
});
}