diff options
Diffstat (limited to 'dexcomparator/src/com/android/jack/DexComparator.java')
| -rw-r--r-- | dexcomparator/src/com/android/jack/DexComparator.java | 930 |
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; + } + } + +} |
