| /* |
| * 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; |
| } |
| } |
| |
| } |