blob: 89ab8cd949fbe3b235b25af5fc21b2d8dccf0c0e [file] [log] [blame]
/*
* Copyright (C) 2012 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.jack;
import com.android.jack.comparator.DebugInfo;
import com.android.jack.comparator.DebugInfo.LocalVar;
import com.android.jack.dx.dex.file.DebugInfoDecoder;
import com.android.jack.dx.io.ClassData;
import com.android.jack.dx.io.ClassData.Method;
import com.android.jack.dx.io.ClassDef;
import com.android.jack.dx.io.Code;
import com.android.jack.dx.io.DexBuffer;
import com.android.jack.dx.io.FieldId;
import com.android.jack.dx.io.MethodId;
import com.android.jack.dx.io.ProtoId;
import com.android.jack.dx.rop.code.AccessFlags;
import com.android.jack.dx.rop.type.Prototype;
import com.android.jack.dx.util.ByteInput;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
/**
* This tool compares the structure of two dex files.
*/
public class DexComparator {
private Logger logger;
private DexBuffer referenceDexFile;
private DexBuffer candidateDexFile;
private static final Level ERROR_LEVEL = Level.SEVERE;
private static final Level WARNING_LEVEL = Level.WARNING;
private static final Level DEBUG_LEVEL = Level.FINE;
private boolean strict;
private boolean debugInfo;
private byte[] referenceData;
private byte[] candidateData;
private int refThisIndex;
private int candidateThisIndex;
private static final boolean IGNORE_ID_COMPARISON = true;
private static final boolean IGNORE_ANONYMOUS_CLASSES = true;
private static final boolean TOLERATE_MISSING_SYNTHETICS = true;
private static final boolean TOLERATE_MISSING_INITS = true;
private static final boolean TOLERATE_MISSING_CLINITS = true;
private boolean enableBinaryDebugInfoComparison = false;
private boolean enableInstructionNumberComparison = false;
private float instructionNumberTolerance = 0f;
private static final List<String> skippedMethods = new ArrayList<String>();
static {
skippedMethods.add("Ljava/lang/Throwable;."
+ "countDuplicates([Ljava/lang/StackTraceElement;[Ljava/lang/StackTraceElement;)I");
}
@Nonnull
private static final String INIT_NAME = "<init>";
@Nonnull
private static final String STATIC_INIT_NAME = "<clinit>";
/**
* Launch the comparison between a reference Dex {@code File} and a candidate Dex {@code File}.
*
* @param referenceFile the reference Dex {@code File}
* @param candidateFile the candidate Dex {@code File}
* @param withDebugInfo also compare debug infos
* @param strict if false, the candidate Dex must <i>at least<i/> contain all the structures of
* the reference Dex; if true, the candidate Dex must <i>exactly<i/> contain all the
* structures of the reference Dex
* @param compareDebugInfoBinary enable binary comparison of debug infos
* @param compareInstructionNumber enable comparison of number of instructions
* @param instructionNumberTolerance tolerance factor for comparison of number of instructions
* @throws DifferenceFoundException if a difference between the two Dex files is found
* @throws IOException if an error occurs while loading the dex files
*/
public void compare(File referenceFile,
File candidateFile,
boolean withDebugInfo,
boolean strict,
boolean compareDebugInfoBinary,
boolean compareInstructionNumber,
float instructionNumberTolerance) throws DifferenceFoundException, IOException {
logger = Logger.getLogger(this.getClass().getName());
logger.setLevel(WARNING_LEVEL);
referenceDexFile = new DexBuffer(referenceFile);
referenceData = referenceDexFile.getBytes();
refThisIndex = referenceDexFile.strings().indexOf("this");
candidateDexFile = new DexBuffer(candidateFile);
candidateData = candidateDexFile.getBytes();
candidateThisIndex = candidateDexFile.strings().indexOf("this");
this.strict = strict;
enableBinaryDebugInfoComparison = compareDebugInfoBinary;
enableInstructionNumberComparison = compareInstructionNumber;
this.instructionNumberTolerance = instructionNumberTolerance;
debugInfo = withDebugInfo;
if (!IGNORE_ID_COMPARISON) {
checkStringIds();
checkTypeIds();
checkProtoIds();
checkFieldIds();
checkMethodIds();
}
/* build a lookup table for candidate classes */
HashMap<String, ClassDef> candidateClassDefItemLookUpTable = new HashMap<String, ClassDef>();
for (ClassDef classDef : candidateDexFile.classDefs()) {
String typeName = candidateDexFile.typeNames().get(classDef.getTypeIndex());
candidateClassDefItemLookUpTable.put(typeName, classDef);
}
Iterable<ClassDef> refClassDefs = referenceDexFile.classDefs();
for (ClassDef classDefItem : refClassDefs) {
if (!IGNORE_ANONYMOUS_CLASSES
|| !isAnomymousTypeName(referenceDexFile.typeNames().get(classDefItem.getTypeIndex()))) {
String className = getClassName(referenceDexFile, classDefItem);
ClassDef candidateClassDefItem =
candidateClassDefItemLookUpTable.get(className);
/* class */
if (candidateClassDefItem != null) {
logger.log(DEBUG_LEVEL, "Class {0} OK", className);
checkAccessFlags(classDefItem, candidateClassDefItem);
checkSuperclass(classDefItem, candidateClassDefItem);
checkInterfaces(classDefItem, candidateClassDefItem);
checkClassData(classDefItem, candidateClassDefItem);
candidateClassDefItemLookUpTable.remove(className);
} else {
logger.log(
ERROR_LEVEL, "Class {0} NOK: missing", getClassName(referenceDexFile, classDefItem));
if (!TOLERATE_MISSING_SYNTHETICS || !isSynthetic(classDefItem.getAccessFlags())) {
throw new DifferenceFoundException("Class "
+ getClassName(referenceDexFile, classDefItem) + " was not found in candidate.");
}
}
}
}
if (strict) {
for (ClassDef classDefItem : candidateClassDefItemLookUpTable.values()) {
if (!IGNORE_ANONYMOUS_CLASSES || !isAnomymousTypeName(
candidateDexFile.typeNames().get(classDefItem.getTypeIndex()))) {
String className = getClassName(candidateDexFile, classDefItem);
logger.log(
ERROR_LEVEL, "Class {0} NOK: missing", className);
if (!TOLERATE_MISSING_SYNTHETICS || !isSynthetic(classDefItem.getAccessFlags())) {
throw new DifferenceFoundException("Class " + className
+ " was not found in reference.");
}
}
}
}
}
private void checkStringIds() throws DifferenceFoundException {
checkStringIterables(referenceDexFile.strings(), candidateDexFile.strings(), "String");
}
private void checkTypeIds() throws DifferenceFoundException {
checkStringIterables(referenceDexFile.typeNames(), candidateDexFile.typeNames(), "Type");
}
private void checkFieldIds() throws DifferenceFoundException {
List<FieldId> referenceFieldIds = referenceDexFile.fieldIds();
List<FieldId> candidateFieldIds = candidateDexFile.fieldIds();
List<String> referenceFieldNames = getFieldNameList(referenceFieldIds, referenceDexFile);
List<String> candidateFieldNames = getFieldNameList(candidateFieldIds, candidateDexFile);
checkStringIterables(referenceFieldNames, candidateFieldNames, "Field");
}
private void checkMethodIds() throws DifferenceFoundException {
List<MethodId> referenceMethodIds = referenceDexFile.methodIds();
List<MethodId> candidateMethodIds = candidateDexFile.methodIds();
List<String> referenceMethodNames = getMethodNameList(referenceMethodIds, referenceDexFile);
List<String> candidateMethodNames = getMethodNameList(candidateMethodIds, candidateDexFile);
checkStringIterables(referenceMethodNames, candidateMethodNames, "Method");
}
private void checkProtoIds() throws DifferenceFoundException {
List<ProtoId> referenceProtoIds = referenceDexFile.protoIds();
List<ProtoId> candidateProtoIds = candidateDexFile.protoIds();
List<String> referenceProtoStrings = getProtoStringList(referenceProtoIds, referenceDexFile);
List<String> candidateProtoStrings = getProtoStringList(candidateProtoIds, candidateDexFile);
checkStringIterables(referenceProtoStrings, candidateProtoStrings, "Proto");
}
private static List<String> getProtoStringList(List<ProtoId> protoIds, DexBuffer dex) {
List<String> protoStrings = new ArrayList<String>();
for (ProtoId protoId : protoIds) {
protoStrings.add(getProtoString(protoId, dex));
}
return protoStrings;
}
private static String getProtoString(ProtoId protoId, DexBuffer dex) {
return dex.readTypeList(protoId.getParametersOffset()) + dex.typeNames().get(
protoId.getReturnTypeIndex());
}
private static List<String> getFieldNameList(List<FieldId> fieldIds, DexBuffer dex) {
List<String> fieldNames = new ArrayList<String>();
for (FieldId fieldId : fieldIds) {
fieldNames.add(dex.strings().get(fieldId.getNameIndex()));
}
return fieldNames;
}
private static List<String> getMethodNameList(List<MethodId> methodIds, DexBuffer dex) {
List<String> methodNames = new ArrayList<String>();
for (MethodId methodId : methodIds) {
ProtoId protoId = dex.protoIds().get(methodId.getProtoIndex());
String sortableMethodName = dex.typeNames().get(methodId.getDeclaringClassIndex()) + "."
+ dex.strings().get(methodId.getNameIndex()) + getProtoString(protoId, dex);
methodNames.add(sortableMethodName);
}
return methodNames;
}
private void checkStringIterables(
Iterable<String> referenceStrings, Iterable<String> candidateStrings, String logTypeName)
throws DifferenceFoundException {
Iterator<String> candidateStringIter = candidateStrings.iterator();
for (String refString : referenceStrings) {
boolean found = false;
while (!found) {
if (!candidateStringIter.hasNext()) {
throw new DifferenceFoundException(logTypeName + " '" + refString
+ "' was not found in candidate as expected");
}
String candidateString = candidateStringIter.next();
int stringComparison = candidateString.compareTo(refString);
if (stringComparison == 0) {
found = true;
logger.log(DEBUG_LEVEL, "{0} {1} OK", new Object[] {logTypeName, refString});
} else if (stringComparison > 0 || strict) { // candidateString is after refString
logger.log(ERROR_LEVEL, "{0} {1} NOK: missing", new Object[] {logTypeName, refString});
throw new DifferenceFoundException(logTypeName + " '" + refString
+ "' was not found in candidate as expected");
}
}
}
if (strict && candidateStringIter.hasNext()) {
String leftOverString = candidateStringIter.next();
throw new DifferenceFoundException(logTypeName + " '" + leftOverString
+ "' is in candidate but not in reference");
}
}
private void checkAccessFlags(ClassDef classDefItem, ClassDef candidateClassDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
int candidateAccessFlags = candidateClassDefItem.getAccessFlags();
int refAccessFlags = classDefItem.getAccessFlags();
if (refAccessFlags == candidateAccessFlags) {
logger.log(DEBUG_LEVEL, "Class Access Flags of {0} OK", className);
} else {
logger.log(ERROR_LEVEL,
"Class Access Flags of {0} NOK: reference = {1}, candidate = {2}", new Object[] {
className, Integer.valueOf(refAccessFlags),
Integer.valueOf(candidateAccessFlags)});
throw new DifferenceFoundException("Access flags do not match for Class '" + className
+ "'. Candidate flags: " + candidateAccessFlags + ". Reference flags: "
+ refAccessFlags + ".");
}
}
private void checkClassData(ClassDef classDefItem, ClassDef candidateClassDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
boolean referenceDexFileHasClassData = classDefItem.getClassDataOffset() != 0;
boolean candidateDexFileHasClassData = candidateClassDefItem.getClassDataOffset() != 0;
if (!referenceDexFileHasClassData && !candidateDexFileHasClassData) {
logger.log(DEBUG_LEVEL, "ClassData of {0} OK: both are null", className);
} else if (!referenceDexFileHasClassData || !candidateDexFileHasClassData) {
// If one DexFile has no ClassData, we have to check if all the
// methods in the other one are tolerated
ClassData.Field[] emptyFieldList = new ClassData.Field[0];
ClassData.Method[] emptyMethodList = new ClassData.Method[0];
ClassData classDataItem;
if (referenceDexFileHasClassData) {
classDataItem = referenceDexFile.readClassData(classDefItem);
handleFields(classDataItem.getInstanceFields(), emptyFieldList, className);
handleFields(classDataItem.getStaticFields(), emptyFieldList, className);
handleMethods(classDataItem.allMethods(), emptyMethodList, className);
} else {
assert candidateDexFileHasClassData;
classDataItem = candidateDexFile.readClassData(candidateClassDefItem);
handleFields(emptyFieldList, classDataItem.getInstanceFields(), className);
handleFields(emptyFieldList, classDataItem.getStaticFields(), className);
handleMethods(emptyMethodList, classDataItem.allMethods(), className);
}
} else {
// TODO(benoitlamarche): check annotations
ClassData classDataItem = referenceDexFile.readClassData(classDefItem);
ClassData candidateClassDataItem = candidateDexFile.readClassData(candidateClassDefItem);
checkFields(classDataItem, candidateClassDataItem, classDefItem);
checkMethods(classDataItem, candidateClassDataItem, classDefItem);
}
}
private void checkMethods(
ClassData classDataItem, ClassData candidateClassDataItem, ClassDef classDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
ClassData.Method[] methods = classDataItem.allMethods();
ClassData.Method[] candidateMethods = candidateClassDataItem.allMethods();
handleMethods(methods, candidateMethods, className);
}
private void checkFields(
ClassData classDataItem, ClassData candidateClassDataItem, ClassDef classDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
/* Instance fields */
{
ClassData.Field[] instanceFields = classDataItem.getInstanceFields();
ClassData.Field[] candidateInstanceFields = candidateClassDataItem.getInstanceFields();
handleFields(instanceFields, candidateInstanceFields, className);
}
/* Static fields */
// TODO(benoitlamarche): should static initializers be checked?
{
ClassData.Field[] instanceFields = classDataItem.getStaticFields();
ClassData.Field[] candidateInstanceFields = candidateClassDataItem.getStaticFields();
handleFields(instanceFields, candidateInstanceFields, className);
}
}
private void checkInterfaces(ClassDef classDefItem, ClassDef candidateClassDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
short[] interfaces = classDefItem.getInterfaces();
short[] candidateInterfaces = candidateClassDefItem.getInterfaces();
List<String> interfacesList = getInterfaceNames(referenceDexFile, interfaces);
List<String> candidateInterfacesList = getInterfaceNames(candidateDexFile, candidateInterfaces);
for (String interfaceName : interfacesList) {
boolean contained = candidateInterfacesList.remove(interfaceName);
if (contained) {
logger.log(DEBUG_LEVEL, "Implemented interface of {0} OK: {1}",
new Object[] {className, interfaceName});
} else {
logger.log(ERROR_LEVEL, "Implemented interface of {0} NOK: {1} missing in candidate",
new Object[] {className, interfaceName});
throw new DifferenceFoundException("Interface " + interfaceName + " is not implemented by "
+ className + " in candidate");
}
}
if (!candidateInterfacesList.isEmpty()) {
String leftOverInterface = candidateInterfacesList.get(0);
logger.log(ERROR_LEVEL, "Implemented interface of {0} NOK: {1} missing in reference",
new Object[] {className, leftOverInterface});
throw new DifferenceFoundException("Interface " + leftOverInterface
+ " is not implemented by " + className + " in reference");
}
}
private void checkSuperclass(ClassDef classDefItem, ClassDef candidateClassDefItem)
throws DifferenceFoundException {
String className = getClassName(referenceDexFile, classDefItem);
String superClass = (classDefItem.getSupertypeIndex() == ClassDef.NO_INDEX) ? ("empty")
: (getSuperclassName(referenceDexFile, classDefItem));
String candidateSuperClass =
(candidateClassDefItem.getSupertypeIndex() == ClassDef.NO_INDEX) ? ("empty")
: (getSuperclassName(candidateDexFile, candidateClassDefItem));
if (superClass.equals(candidateSuperClass)) {
logger.log(DEBUG_LEVEL, "Superclass of {0} OK: {1}", new Object[] {className, superClass});
} else {
logger.log(ERROR_LEVEL, "Superclass of {0} NOK: reference = {1}, candidate = {2}",
new Object[] {className, superClass, candidateSuperClass});
throw new DifferenceFoundException("Superclasses of '" + className
+ "' do not match. Candidate superclass: " + candidateSuperClass
+ ". Reference superclass: " + superClass + ".");
}
}
/**
* Checks that all the elements of {@code referenceFields} can be found in
* {@code candidateFields} based on their name and type, and check accessFlags are the same. If in
* strict mode, all the elements of {@code candidateFields} must also be in
* {@code referenceFields}.
*
* @param referenceFields Contains fields of current class in reference dex file.
* @param candidateFields Contains fields of current class in candidate dex file
* @param className Name of the current class
* @throws DifferenceFoundException If a difference is found while comparing fields
*/
private void handleFields(ClassData.Field[] referenceFields,
ClassData.Field[] candidateFields, String className) throws DifferenceFoundException {
boolean isFound;
List<ClassData.Field> foundFields = null;
if (strict) {
foundFields = new ArrayList<ClassData.Field>(candidateFields.length);
}
for (ClassData.Field encField : referenceFields) {
isFound = false;
String refFieldName = getFieldName(referenceDexFile, encField.getFieldIndex());
String refFieldType = getFieldTypeName(referenceDexFile, encField.getFieldIndex());
for (ClassData.Field candidateEncField : candidateFields) {
String candFieldName = getFieldName(candidateDexFile, candidateEncField.getFieldIndex());
String candFieldType = getFieldTypeName(
candidateDexFile, candidateEncField.getFieldIndex());
if (refFieldName.equals(candFieldName) && refFieldType.equals(candFieldType)) {
logger.log(DEBUG_LEVEL,
"Field {0}.{1} OK", new Object[] {className, refFieldName});
/* Access flags */
if (encField.getAccessFlags() != candidateEncField.getAccessFlags()) {
logger.log(ERROR_LEVEL,
"Access Flags for Field {0}.{1} NOK: reference = {2}, candidate = {3}",
new Object[] {className, refFieldName, Integer.valueOf(encField.getAccessFlags()),
Integer.valueOf(candidateEncField.getAccessFlags())});
throw new DifferenceFoundException("Access flags do not match for Field '" + className
+ "." + refFieldName + "'. Candidate flags: " + candidateEncField.getAccessFlags()
+ ". Reference flags: " + encField.getAccessFlags() + ".");
} else {
logger.log(DEBUG_LEVEL, "Field Access Flags of {0}.{1} OK",
new Object[] {className, refFieldName});
isFound = true;
if (strict) {
assert foundFields != null;
foundFields.add(candidateEncField);
}
break;
}
}
}
if (!isFound && !isTolerated(encField)) {
logger.log(
ERROR_LEVEL, "Field {0}.{1} NOK: missing", new Object[] {className, refFieldName});
throw new DifferenceFoundException("Field " + className + "." + refFieldName + " of type '"
+ refFieldType + "' not found in candidate file.");
}
}
if (strict) {
List<ClassData.Field> candidateFieldList =
new ArrayList<ClassData.Field>(Arrays.asList(candidateFields));
candidateFieldList.removeAll(foundFields);
// remove tolerated fields
Iterator<ClassData.Field> candidateFieldIter = candidateFieldList.iterator();
while (candidateFieldIter.hasNext()) {
ClassData.Field field = candidateFieldIter.next();
if (isTolerated(field)) {
candidateFieldIter.remove();
}
}
if (!candidateFieldList.isEmpty()) {
StringBuffer sb = new StringBuffer(
"Too many fields in candidate for class '" + className + "'. Unwanted fields are: ");
for (ClassData.Field unwantedField: candidateFieldList) {
sb.append(getFieldTypeName(candidateDexFile, unwantedField.getFieldIndex()));
sb.append(" ");
sb.append(getFieldName(candidateDexFile, unwantedField.getFieldIndex()));
sb.append(" - ");
}
throw new DifferenceFoundException(sb.toString());
}
}
}
/**
* Checks that all the elements of {@code referenceMethods} can be found in
* {@code candidateMethods} based on their name and prototype, and check accessFlags are the same.
* If in strict mode, all the elements of {@code candidateMethods} must also be in
* {@code referenceMethods}.
*
* @param referenceMethods Contains methods of current class in reference dex file.
* @param candidateMethods Contains methods of current class in candidate dex file
* @param className Name of the current class
* @throws DifferenceFoundException If a difference is found while comparing methods
*/
private void handleMethods(
ClassData.Method[] referenceMethods, ClassData.Method[] candidateMethods, String className)
throws DifferenceFoundException {
boolean isFound;
List<ClassData.Method> foundMethods = null;
if (strict) {
foundMethods = new ArrayList<ClassData.Method>(candidateMethods.length);
}
for (ClassData.Method encMeth : referenceMethods) {
isFound = false;
String refMethodName = getMethodName(referenceDexFile, encMeth.getMethodIndex());
String refMethodProto = getMethodProto(referenceDexFile, encMeth.getMethodIndex());
if (isSkipped(className, refMethodName, refMethodProto)) {
continue;
}
for (ClassData.Method candidateEncMeth : candidateMethods) {
String candMethodName = getMethodName(candidateDexFile, candidateEncMeth.getMethodIndex());
String candMethodProto = getMethodProto(
candidateDexFile, candidateEncMeth.getMethodIndex());
if (refMethodName.equals(candMethodName) && refMethodProto.equals(candMethodProto)) {
logger.log(DEBUG_LEVEL,
"Method {0}.{1}{2} OK", new Object[] {className, refMethodName, refMethodProto});
if (enableInstructionNumberComparison) {
handleInstructionNumberComparison(
className, refMethodName, refMethodProto, encMeth, candidateEncMeth);
}
/* Access flags */
// TODO(?): remove testing of debugInfo and do something else to be able to not check
// structure when comparing debug info
if ((!debugInfo) && (encMeth.getAccessFlags() != candidateEncMeth.getAccessFlags())) {
logger.log(ERROR_LEVEL,
"Method Access Flags of {0}.{1}{2} NOK: reference = {3}, candidate = {4}",
new Object[] {className, refMethodName, refMethodProto, Integer.valueOf(
encMeth.getAccessFlags()), Integer.valueOf(candidateEncMeth.getAccessFlags())});
throw new DifferenceFoundException("Access flags do not match for Method '" + className
+ "." + refMethodName + "'. Candidate flags: " + candidateEncMeth.getAccessFlags()
+ ". Reference flags: " + encMeth.getAccessFlags() + ".");
} else {
logger.log(DEBUG_LEVEL, "Access Flags for Method {0}.{1}{2} OK",
new Object[] {className, refMethodName, refMethodProto});
isFound = true;
if (strict) {
assert foundMethods != null;
foundMethods.add(candidateEncMeth);
}
if (debugInfo) {
checkDebugInfo(encMeth, candidateEncMeth, className);
}
break;
}
}
}
if (!isFound && !isTolerated(encMeth, refMethodName)) {
logger.log(ERROR_LEVEL, "Method {0}.{1}{2} NOK: missing",
new Object[] {className, refMethodName, refMethodProto});
throw new DifferenceFoundException("Method " + className + "." + refMethodName
+ refMethodProto + " not found in candidate file.");
}
}
if (strict) {
List<ClassData.Method> candidateMethodList =
new ArrayList<ClassData.Method>(Arrays.asList(candidateMethods));
candidateMethodList.removeAll(foundMethods);
// remove tolerated methods
Iterator<ClassData.Method> candidateMethodIter = candidateMethodList.iterator();
while (candidateMethodIter.hasNext()) {
ClassData.Method method = candidateMethodIter.next();
String methodName = getMethodName(candidateDexFile, method.getMethodIndex());
if (isTolerated(method, methodName)) {
candidateMethodIter.remove();
}
}
if (!candidateMethodList.isEmpty()) {
StringBuffer sb = new StringBuffer(
"Too many methods in candidate for class '" + className + "'. Unwanted methods are: ");
for (ClassData.Method unwantedMethod: candidateMethodList) {
sb.append(getMethodName(candidateDexFile, unwantedMethod.getMethodIndex()));
sb.append(getMethodProto(candidateDexFile, unwantedMethod.getMethodIndex()));
sb.append(" - ");
}
throw new DifferenceFoundException(sb.toString());
}
}
}
private void handleInstructionNumberComparison(
String className, String methodName, String methodProto, Method refMeth, Method candidateMeth)
throws DifferenceFoundException {
if (refMeth.getCodeOffset() == 0 && candidateMeth.getCodeOffset() == 0) {
logger.log(DEBUG_LEVEL, "Method {0}.{1}{2} code comparison OK",
new Object[] {className, methodName, methodProto});
return;
}
if (refMeth.getCodeOffset() != 0 && candidateMeth.getCodeOffset() == 0) {
logger.log(
ERROR_LEVEL,
"Method {0}.{1}{2} NOK: candidate has no code whereas reference has",
new Object[] {className, methodName, methodProto});
throw getDifferenceFoundException(className, refMeth, referenceDexFile,
"Candidate method has no code whereas reference has");
}
if (refMeth.getCodeOffset() == 0 && candidateMeth.getCodeOffset() != 0) {
logger.log(
ERROR_LEVEL,
"Method {0}.{1}{2} NOK: candidate has code whereas reference has not",
new Object[] {className, methodName, methodProto});
throw getDifferenceFoundException(className, refMeth, referenceDexFile,
"Candidate method has code whereas reference has not");
}
int refInsSize = referenceDexFile.readCode(refMeth).getInstructions().length;
int candidateInsSize = candidateDexFile.readCode(candidateMeth).getInstructions().length;
float ratio;
if (refInsSize != 0) {
ratio = ((float) (candidateInsSize - refInsSize)) / refInsSize;
} else {
if (candidateInsSize == 0) {
ratio = 0f;
} else {
ratio = 1f;
}
}
boolean tolerated = ratio <= instructionNumberTolerance;
if (!tolerated) {
logger.log(WARNING_LEVEL,
"Method {0}.{1}{2} NOK: number of instructions differs more than allowed: "
+ "percentage = {3}%, reference = {4}, candidate = {5}, delta allowed = {6}%",
new Object[] {className,
methodName,
methodProto,
Float.valueOf(ratio * 100),
Integer.valueOf(refInsSize),
Integer.valueOf(candidateInsSize),
Float.valueOf(instructionNumberTolerance * 100)});
}
}
private void handleBinaryDebugInfoComparison(String className,
String methodName,
String methodProto,
Method refMeth,
DebugInfo refDbgInfo,
DebugInfo candidateDbgInfo)
throws DifferenceFoundException {
byte[] refBytes = referenceDexFile.getBytes();
byte[] candidateBytes = candidateDexFile.getBytes();
int refDbgInfOffset = refDbgInfo.getDebugInfoOffset();
int candidateDbgInfOffset = candidateDbgInfo.getDebugInfoOffset();
int refDbgInfoLength = refDbgInfo.getSizeInBytes();
int candidateDbgInfoLength = candidateDbgInfo.getSizeInBytes();
int i = 0;
for (; (i < refDbgInfoLength) && ((refDbgInfOffset + i) < refBytes.length); ++i) {
if ((candidateDbgInfOffset + i) >= candidateBytes.length
|| i >= candidateDbgInfoLength) {
logger.log(
ERROR_LEVEL, "Method {0}.{1}{2} NOK: debug infos size is smaller than reference",
new Object[] {className, methodName, methodProto});
throw getDifferenceFoundException(className, refMeth, referenceDexFile,
"There's less debug infos in candidate than in reference");
} else if (refBytes[refDbgInfOffset + i] != candidateBytes[candidateDbgInfOffset + i]) {
logger.log(ERROR_LEVEL, "Method {0}.{1}{2} NOK: debug infos differ",
new Object[] {className, methodName, methodProto});
throw getDifferenceFoundException(className, refMeth, referenceDexFile,
"Debug infos differ");
}
}
assert (refDbgInfOffset + i) < refBytes.length;
if (i == candidateDbgInfoLength) {
logger.log(DEBUG_LEVEL, "Method {0}.{1}{2} debug infos comparison OK",
new Object[] {className, methodName, methodProto});
} else {
logger.log(ERROR_LEVEL, "Method {0}.{1}{2} NOK: debug infos size is larger than reference",
new Object[] {className, methodName, methodProto});
throw getDifferenceFoundException(className, refMeth, referenceDexFile,
"There's more debug infos in candidate than in reference");
}
}
private boolean isSkipped(String className, String methodName, String methodProto) {
boolean isSkipped = skippedMethods.contains(className + "." + methodName + methodProto);
return isSkipped;
}
private boolean isTolerated(ClassData.Field field) {
return TOLERATE_MISSING_SYNTHETICS && isSynthetic(field.getAccessFlags());
}
private boolean isTolerated(ClassData.Method method, String methodName) {
boolean tolerated = (TOLERATE_MISSING_SYNTHETICS && isSynthetic(method.getAccessFlags())) ||
(TOLERATE_MISSING_INITS && methodName.equals(INIT_NAME)) ||
(TOLERATE_MISSING_CLINITS && methodName.equals(STATIC_INIT_NAME));
return tolerated;
}
private boolean isTolerated(LocalVar localVar) {
boolean tolerated = TOLERATE_MISSING_SYNTHETICS && localVar.isSynthetic();
return tolerated;
}
private void checkDebugInfo(Method reference, Method candidate, String className)
throws DifferenceFoundException {
if (isSynthetic(reference.getAccessFlags())) {
assert isSynthetic(candidate.getAccessFlags());
// ignore synthetic methods
return;
}
if (AccessFlags.isConstructor(reference.getAccessFlags())) {
assert AccessFlags.isConstructor(candidate.getAccessFlags());
// Ignore all constructors because debug infos for default constructors may not use the same
// line numbers. It would be better to ignore only default constructors but they are not
// flagged as synthetic, and in the case of inner classes may have parameters.
return;
}
if (reference.getCodeOffset() == 0) {
if (candidate.getCodeOffset() != 0) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Candidate has code while reference has not");
} else {
return;
}
} else if (candidate.getCodeOffset() == 0) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Candidate is missing code");
}
DebugInfo refInfo = decodeDebugInfo(reference, referenceDexFile, referenceData,
refThisIndex);
DebugInfo candidateInfo = decodeDebugInfo(candidate, candidateDexFile, candidateData,
candidateThisIndex);
if (refInfo == null) {
if (candidateInfo != null) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Candidate has debug info while reference has not");
} else {
return;
}
} else if (candidateInfo == null) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Candidate is missing debug info");
}
if (enableBinaryDebugInfoComparison) {
String refMethodName = getMethodName(referenceDexFile, reference.getMethodIndex());
String refMethodProto = getMethodProto(referenceDexFile, candidate.getMethodIndex());
handleBinaryDebugInfoComparison(className,
refMethodName,
refMethodProto,
candidate,
refInfo,
candidateInfo);
}
for (LocalVar refLocal : refInfo.getLocals()) {
LocalVar candidateLocal = candidateInfo.getLocal(refLocal);
if (candidateLocal == null) {
if (!isTolerated(refLocal)) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Missing local variable in candidate: " + refLocal.getTypeSignature() + " "
+ refLocal.getName());
}
} else {
if (!refLocal.getScope().equals(candidateLocal.getScope())) {
throw getDifferenceFoundException(className, reference, referenceDexFile,
"Scope differs for local: " + refLocal.getTypeSignature() + " " +
refLocal.getName() + ", reference: " + refLocal.getScope() +
", candidate:" + candidateLocal.getScope());
}
}
}
}
private DifferenceFoundException getDifferenceFoundException(String inClass, Method inMethod,
DexBuffer dexOfMethod, String message) {
return new DifferenceFoundException("In method " + inClass + "." +
getMethodName(dexOfMethod, inMethod.getMethodIndex()) +
getMethodProto(dexOfMethod, inMethod.getMethodIndex()) + ":" + message);
}
private static DebugInfo decodeDebugInfo(Method method, DexBuffer dex, byte[] dexData,
int thisIdx) {
boolean isStatic = (method.getAccessFlags() & AccessFlags.ACC_STATIC) != 0;
Prototype prototype = Prototype.intern(getMethodProto(
dex, method.getMethodIndex()));
Code codeItem = dex.readCode(method);
if (codeItem.getDebugInfoOffset() == 0) {
return null;
}
ByteArrayInput bai = new ByteArrayInput(codeItem.getDebugInfoOffset(), dexData);
DebugInfoDecoder decoder = new DebugInfoDecoder(
bai,
codeItem.getRegistersSize(),
isStatic,
prototype,
thisIdx);
decoder.decode();
return new DebugInfo(decoder, dex, codeItem, bai.getPosition());
}
private static String getClassName(DexBuffer dex, ClassDef classDef) {
return dex.typeNames().get(classDef.getTypeIndex());
}
private static String getMethodName(DexBuffer dex, int methodIndex) {
MethodId methodId = dex.methodIds().get(methodIndex);
return dex.strings().get(methodId.getNameIndex());
}
private static String getMethodProto(DexBuffer dex, int methodIndex) {
MethodId methodId = dex.methodIds().get(methodIndex);
ProtoId protoId = dex.protoIds().get(methodId.getProtoIndex());
return getProtoString(protoId, dex);
}
private static String getFieldName(DexBuffer dex, int fieldIndex) {
FieldId fieldId = dex.fieldIds().get(fieldIndex);
return dex.strings().get(fieldId.getNameIndex());
}
private static String getFieldTypeName(DexBuffer dex, int fieldIndex) {
FieldId fieldId = dex.fieldIds().get(fieldIndex);
Integer stringIndex = dex.typeIds().get(fieldId.getTypeIndex());
return dex.strings().get(stringIndex.intValue());
}
private static String getSuperclassName(DexBuffer dex, ClassDef classDef) {
return dex.typeNames().get(classDef.getSupertypeIndex());
}
private static List<String> getInterfaceNames(DexBuffer dex, short[] interfaces) {
List<String> interfaceNames = new ArrayList<String>(interfaces.length);
for (short interfIndex : interfaces) {
String typeName = dex.typeNames().get(interfIndex);
interfaceNames.add(typeName);
}
return interfaceNames;
}
private static boolean isAnomymousTypeName(String typeName) {
//TODO(benoitlamarche): use Annotations to determine if the class is anonymous
int location = typeName.lastIndexOf('$');
if (location != -1) {
String num = typeName.substring(location + 1, typeName.length() - 1);
try {
Integer.parseInt(num);
return true;
} catch (NumberFormatException e) {
return false;
}
} else {
return false;
}
}
private static boolean isSynthetic(int modifier) {
return ((modifier & AccessFlags.ACC_SYNTHETIC) == AccessFlags.ACC_SYNTHETIC);
}
private static class ByteArrayInput implements ByteInput {
private final byte[] bytes;
private int position;
public ByteArrayInput(int start, byte... bytes) {
this.position = start;
this.bytes = bytes;
}
@Override
public byte readByte() {
return bytes[position++];
}
public int getPosition() {
return position;
}
}
}