/* * 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.comparator; 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 org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; 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 { public static final int NO_DIFFERENCE = 0; public static final int DIFFERENCE = 1; public static final int PROBLEM = 2; private final 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 = false; private boolean enableDebugInfoComparison = false; 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 boolean enableBinaryCodeComparison = false; private static final List skippedMethods = new ArrayList(); @Nonnull private static final String INIT_NAME = ""; @Nonnull private static final String STATIC_INIT_NAME = ""; /** * Launch the comparison between a reference Dex {@code File} and a candidate Dex {@code File}. * * @param compareDebugInfo also compare debug infos * @param strict if false, the candidate Dex must at least contain all the structures of * the reference Dex; if true, the candidate Dex must exactly contain all the * structures of the reference Dex * @param compareDebugInfoBinarily enable binary comparison of debug infos, allowed only if * compareDebugInfo is enabled * @param compareInstructionNumber enable comparison of number of instructions * @param instructionNumberTolerance tolerance factor for comparison of number of instructions */ public DexComparator( boolean compareDebugInfo, boolean strict, boolean compareDebugInfoBinarily, boolean compareInstructionNumber, float instructionNumberTolerance) { logger = Logger.getLogger(this.getClass().getName()); logger.setLevel(WARNING_LEVEL); this.strict = strict; enableBinaryDebugInfoComparison = compareDebugInfoBinarily; enableInstructionNumberComparison = compareInstructionNumber; this.instructionNumberTolerance = instructionNumberTolerance; enableDebugInfoComparison = compareDebugInfo; } /** * Launch the comparison between a reference Dex {@code File} and a candidate Dex {@code File}. * * @param compareDebugInfo also compare debug infos * @param strict if false, the candidate Dex must at least contain all the structures of * the reference Dex; if true, the candidate Dex must exactly contain all the * structures of the reference Dex * @param compareDebugInfoBinarily enable binary comparison of debug infos, allowed only if * compareDebugInfo is enabled * @param compareCodeBinarily enable code binary comparison */ public DexComparator( boolean compareDebugInfo, boolean strict, boolean compareDebugInfoBinarily, boolean compareCodeBinarily) { logger = Logger.getLogger(this.getClass().getName()); logger.setLevel(WARNING_LEVEL); this.strict = strict; enableBinaryDebugInfoComparison = compareDebugInfoBinarily; enableBinaryCodeComparison = compareCodeBinarily; enableDebugInfoComparison = compareDebugInfo; } public void compare(@Nonnull File referenceFile, @Nonnull File candidateFile) throws IOException, DifferenceFoundException { if (enableBinaryDebugInfoComparison && !enableDebugInfoComparison) { throw new IllegalArgumentException( "Debug info binary comparison cannot be enabled if debug info comparison is not enabled"); } referenceDexFile = new DexBuffer(referenceFile); referenceData = referenceDexFile.getBytes(); refThisIndex = referenceDexFile.strings().indexOf("this"); candidateDexFile = new DexBuffer(candidateFile); candidateData = candidateDexFile.getBytes(); candidateThisIndex = candidateDexFile.strings().indexOf("this"); if (!IGNORE_ID_COMPARISON) { checkStringIds(); checkTypeIds(); checkProtoIds(); checkFieldIds(); checkMethodIds(); } /* build a lookup table for candidate classes */ HashMap candidateClassDefItemLookUpTable = new HashMap(); for (ClassDef classDef : candidateDexFile.classDefs()) { String typeName = classDef.getTypeName(); candidateClassDefItemLookUpTable.put(typeName, classDef); } Iterable refClassDefs = referenceDexFile.classDefs(); for (ClassDef classDefItem : refClassDefs) { if (!IGNORE_ANONYMOUS_CLASSES || !isAnomymousTypeName(classDefItem.getTypeName())) { String className = classDefItem.getTypeName(); 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", classDefItem.getTypeName()); if (!TOLERATE_MISSING_SYNTHETICS || !isSynthetic(classDefItem.getAccessFlags())) { throw new DifferenceFoundException("Class " + classDefItem.getTypeName() + " was not found in candidate."); } } } } if (strict) { for (ClassDef classDefItem : candidateClassDefItemLookUpTable.values()) { if (!IGNORE_ANONYMOUS_CLASSES || !isAnomymousTypeName( classDefItem.getTypeName())) { String className = classDefItem.getTypeName(); 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 referenceFieldIds = referenceDexFile.fieldIds(); List candidateFieldIds = candidateDexFile.fieldIds(); List referenceFieldNames = getFieldNameList(referenceFieldIds, referenceDexFile); List candidateFieldNames = getFieldNameList(candidateFieldIds, candidateDexFile); checkStringIterables(referenceFieldNames, candidateFieldNames, "Field"); } private void checkMethodIds() throws DifferenceFoundException { List referenceMethodIds = referenceDexFile.methodIds(); List candidateMethodIds = candidateDexFile.methodIds(); List referenceMethodNames = getMethodNameList(referenceMethodIds, referenceDexFile); List candidateMethodNames = getMethodNameList(candidateMethodIds, candidateDexFile); checkStringIterables(referenceMethodNames, candidateMethodNames, "Method"); } private void checkProtoIds() throws DifferenceFoundException { List referenceProtoIds = referenceDexFile.protoIds(); List candidateProtoIds = candidateDexFile.protoIds(); List referenceProtoStrings = getProtoStringList(referenceProtoIds, referenceDexFile); List candidateProtoStrings = getProtoStringList(candidateProtoIds, candidateDexFile); checkStringIterables(referenceProtoStrings, candidateProtoStrings, "Proto"); } private static List getProtoStringList(List protoIds, DexBuffer dex) { List protoStrings = new ArrayList(); 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 getFieldNameList(List fieldIds, DexBuffer dex) { List fieldNames = new ArrayList(); for (FieldId fieldId : fieldIds) { fieldNames.add(dex.strings().get(fieldId.getNameIndex())); } return fieldNames; } private static List getMethodNameList(List methodIds, DexBuffer dex) { List methodNames = new ArrayList(); 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 referenceStrings, Iterable candidateStrings, String logTypeName) throws DifferenceFoundException { Iterator 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 = classDefItem.getTypeName(); 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 = classDefItem.getTypeName(); 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 = classDefItem.getTypeName(); 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 = classDefItem.getTypeName(); /* 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 = classDefItem.getTypeName(); short[] interfaces = classDefItem.getInterfaces(); short[] candidateInterfaces = candidateClassDefItem.getInterfaces(); List interfacesList = getInterfaceNames(referenceDexFile, interfaces); List 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 = classDefItem.getTypeName(); 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 foundFields = null; if (strict) { foundFields = new ArrayList(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 candidateFieldList = new ArrayList(Arrays.asList(candidateFields)); candidateFieldList.removeAll(foundFields); // remove tolerated fields Iterator 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 foundMethods = null; if (strict) { foundMethods = new ArrayList(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, instructionNumberTolerance); } /* Access flags */ // TODO(?): remove testing of debugInfo and do something else to be able to not check // structure when comparing debug info if ((!enableDebugInfoComparison) && (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 (enableDebugInfoComparison) { checkDebugInfo(encMeth, candidateEncMeth, className); } if (enableBinaryCodeComparison) { checkCodeBinarily(encMeth, candidateEncMeth, className, refMethodName, refMethodProto); } 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 candidateMethodList = new ArrayList(Arrays.asList(candidateMethods)); candidateMethodList.removeAll(foundMethods); // remove tolerated methods Iterator 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 checkCodeBinarily(@Nonnull Method encMeth, @Nonnull Method candidateEncMeth, @Nonnull String className, @Nonnull String methodName, @Nonnull String methodProto) throws DifferenceFoundException { handleInstructionNumberComparison(className, methodName, methodProto, encMeth, candidateEncMeth, 0); if (encMeth.getCodeOffset() == 0) { // we already checked that if the reference has no code, neither does the candidate assert candidateEncMeth.getCodeOffset() == 0; return; } Code refMethCode = referenceDexFile.readCode(encMeth); Code candMethCode = candidateDexFile.readCode(candidateEncMeth); short[] refInstructions = refMethCode.getInstructions(); short[] candInstructions = candMethCode.getInstructions(); // number of instructions should have been checked already assert refInstructions.length == candInstructions.length; int size = refInstructions.length; int index = 0; while (index < size) { if (refInstructions[index] != candInstructions[index]) { String encMethOffset = getInstructionOffsetAsHexString(encMeth, index); String candMethOffset = getInstructionOffsetAsHexString(candidateEncMeth, index); throw new DifferenceFoundException("Binary instructions of '" + className + "." + methodName + methodProto + "' do not match at index " + index + ". Address for reference: " + encMethOffset + ". Address for candidate: " + candMethOffset + "."); } index++; } } private String getInstructionOffsetAsHexString(@Nonnull Method encMeth, int index) { return "0x" + Integer.toHexString( encMeth.getCodeOffset() + com.android.jack.dx.dex.file.Code.HEADER_SIZE + index * 2); } private void handleInstructionNumberComparison(@Nonnull String className, @Nonnull String methodName, @Nonnull String methodProto, @Nonnull Method refMeth, @Nonnull Method candidateMeth, float instructionNumberTolerance) throws DifferenceFoundException { if (refMeth.getCodeOffset() == 0 && candidateMeth.getCodeOffset() == 0) { logger.log(DEBUG_LEVEL, "Method {0}.{1}{2} code existence 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() - codeItem.getDebugInfoOffset()); } 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 getInterfaceNames(DexBuffer dex, short[] interfaces) { List interfaceNames = new ArrayList(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; } } public static void main(@Nonnull String[] args) { DexComparatorOptions options = new DexComparatorOptions(); CmdLineParser parser = new CmdLineParser(options); parser.setUsageWidth(100); try { parser.parseArgument(args); DexComparator dc = null; if (options.compareInstructionNumber) { if (options.enableBinaryCodeComparison) { throw new CmdLineException(parser, "Instruction number comparison is not allowed with binary code comparison"); } dc = new DexComparator(options.enableDebugInfoComparison, options.strict, options.enableBinaryDebugInfoComparison, options.compareInstructionNumber, options.instructionNumberTolerance); } else { dc = new DexComparator(options.enableDebugInfoComparison, options.strict, options.enableBinaryDebugInfoComparison, options.enableBinaryCodeComparison); } dc.compare(options.referenceFile, options.candidateFile); new DexAnnotationsComparator().compare(options.referenceFile, options.candidateFile); System.exit(NO_DIFFERENCE); } catch (CmdLineException e) { System.err.println(e.getMessage()); parser.printUsage(System.err); System.exit(PROBLEM); } catch (IOException e) { System.err.println(e.getMessage()); System.exit(PROBLEM); } catch (DifferenceFoundException e) { System.err.println(e.getMessage()); System.exit(DIFFERENCE); } } }