summaryrefslogtreecommitdiffstats
path: root/dexcomparator/src/com/android/jack/DexComparator.java
diff options
context:
space:
mode:
Diffstat (limited to 'dexcomparator/src/com/android/jack/DexComparator.java')
-rw-r--r--dexcomparator/src/com/android/jack/DexComparator.java930
1 files changed, 930 insertions, 0 deletions
diff --git a/dexcomparator/src/com/android/jack/DexComparator.java b/dexcomparator/src/com/android/jack/DexComparator.java
new file mode 100644
index 00000000..89ab8cd9
--- /dev/null
+++ b/dexcomparator/src/com/android/jack/DexComparator.java
@@ -0,0 +1,930 @@
+/*
+ * 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;
+ }
+ }
+
+}