Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package android.signature.cts;
     17 
     18 import java.lang.reflect.Constructor;
     19 import java.lang.reflect.Field;
     20 import java.lang.reflect.Method;
     21 import java.lang.reflect.Modifier;
     22 import java.lang.reflect.Type;
     23 import java.util.HashSet;
     24 import java.util.Objects;
     25 import java.util.Set;
     26 
     27 /**
     28  * Checks that the runtime representation of a class matches the API representation of a class.
     29  */
     30 public class ApiComplianceChecker extends AbstractApiChecker {
     31 
     32     /** Indicates that the class is an annotation. */
     33     private static final int CLASS_MODIFIER_ANNOTATION = 0x00002000;
     34 
     35     /** Indicates that the class is an enum. */
     36     private static final int CLASS_MODIFIER_ENUM       = 0x00004000;
     37 
     38     /** Indicates that the method is a bridge method. */
     39     private static final int METHOD_MODIFIER_BRIDGE    = 0x00000040;
     40 
     41     /** Indicates that the method is takes a variable number of arguments. */
     42     private static final int METHOD_MODIFIER_VAR_ARGS  = 0x00000080;
     43 
     44     /** Indicates that the method is a synthetic method. */
     45     private static final int METHOD_MODIFIER_SYNTHETIC = 0x00001000;
     46 
     47     private final InterfaceChecker interfaceChecker;
     48 
     49     public ApiComplianceChecker(ResultObserver resultObserver, ClassProvider classProvider) {
     50         super(classProvider, resultObserver);
     51         interfaceChecker = new InterfaceChecker(resultObserver, classProvider);
     52     }
     53 
     54     @Override
     55     public void checkDeferred() {
     56         interfaceChecker.checkQueued();
     57     }
     58 
     59     @Override
     60     protected boolean checkClass(JDiffClassDescription classDescription, Class<?> runtimeClass) {
     61         if (JDiffClassDescription.JDiffType.INTERFACE.equals(classDescription.getClassType())) {
     62             // Queue the interface for deferred checking.
     63             interfaceChecker.queueForDeferredCheck(classDescription, runtimeClass);
     64         }
     65 
     66         String reason;
     67         if ((reason = checkClassModifiersCompliance(classDescription, runtimeClass)) != null) {
     68             resultObserver.notifyFailure(FailureType.mismatch(classDescription),
     69                     classDescription.getAbsoluteClassName(),
     70                     String.format("Non-compatible class found when looking for %s - because %s",
     71                             classDescription.toSignatureString(), reason));
     72             return false;
     73         }
     74 
     75         if (!checkClassAnnotationCompliance(classDescription, runtimeClass)) {
     76             resultObserver.notifyFailure(FailureType.mismatch(classDescription),
     77                     classDescription.getAbsoluteClassName(), "Annotation mismatch");
     78             return false;
     79         }
     80 
     81         if (!runtimeClass.isAnnotation()) {
     82             // check father class
     83             if (!checkClassExtendsCompliance(classDescription, runtimeClass)) {
     84                 resultObserver.notifyFailure(FailureType.mismatch(classDescription),
     85                         classDescription.getAbsoluteClassName(), "Extends mismatch");
     86                 return false;
     87             }
     88 
     89             // check implements interface
     90             if (!checkClassImplementsCompliance(classDescription, runtimeClass)) {
     91                 resultObserver.notifyFailure(FailureType.mismatch(classDescription),
     92                         classDescription.getAbsoluteClassName(), "Implements mismatch");
     93                 return false;
     94             }
     95         }
     96         return true;
     97     }
     98 
     99     /**
    100      * Checks if the class under test has compliant modifiers compared to the API.
    101      *
    102      * @param classDescription a description of a class in an API.
    103      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
    104      * @return null if modifiers are compliant otherwise a reason why they are not.
    105      */
    106     private static String checkClassModifiersCompliance(JDiffClassDescription classDescription,
    107             Class<?> runtimeClass) {
    108         int reflectionModifiers = runtimeClass.getModifiers();
    109         int apiModifiers = classDescription.getModifier();
    110 
    111         // If the api class isn't abstract
    112         if (((apiModifiers & Modifier.ABSTRACT) == 0) &&
    113                 // but the reflected class is
    114                 ((reflectionModifiers & Modifier.ABSTRACT) != 0) &&
    115                 // and it isn't an enum
    116                 !classDescription.isEnumType()) {
    117             // that is a problem
    118             return "description is abstract but class is not and is not an enum";
    119         }
    120         // ABSTRACT check passed, so mask off ABSTRACT
    121         reflectionModifiers &= ~Modifier.ABSTRACT;
    122         apiModifiers &= ~Modifier.ABSTRACT;
    123 
    124         if (classDescription.isAnnotation()) {
    125             reflectionModifiers &= ~CLASS_MODIFIER_ANNOTATION;
    126         }
    127         if (runtimeClass.isInterface()) {
    128             reflectionModifiers &= ~(Modifier.INTERFACE);
    129         }
    130         if (classDescription.isEnumType() && runtimeClass.isEnum()) {
    131             reflectionModifiers &= ~CLASS_MODIFIER_ENUM;
    132         }
    133 
    134         if ((reflectionModifiers == apiModifiers)
    135                 && (classDescription.isEnumType() == runtimeClass.isEnum())) {
    136             return null;
    137         } else {
    138             return String.format("modifier mismatch - description (%s), class (%s)",
    139                     Modifier.toString(apiModifiers), Modifier.toString(reflectionModifiers));
    140         }
    141     }
    142 
    143     /**
    144      * Checks if the class under test is compliant with regards to
    145      * annnotations when compared to the API.
    146      *
    147      * @param classDescription a description of a class in an API.
    148      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
    149      * @return true if the class is compliant
    150      */
    151     private static boolean checkClassAnnotationCompliance(JDiffClassDescription classDescription,
    152             Class<?> runtimeClass) {
    153         if (runtimeClass.isAnnotation()) {
    154             // check annotation
    155             for (String inter : classDescription.getImplInterfaces()) {
    156                 if ("java.lang.annotation.Annotation".equals(inter)) {
    157                     return true;
    158                 }
    159             }
    160             return false;
    161         }
    162         return true;
    163     }
    164 
    165     /**
    166      * Checks if the class under test extends the proper classes
    167      * according to the API.
    168      *
    169      * @param classDescription a description of a class in an API.
    170      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
    171      * @return true if the class is compliant.
    172      */
    173     private static boolean checkClassExtendsCompliance(JDiffClassDescription classDescription,
    174             Class<?> runtimeClass) {
    175         // Nothing to check if it doesn't extend anything.
    176         if (classDescription.getExtendedClass() != null) {
    177             Class<?> superClass = runtimeClass.getSuperclass();
    178 
    179             while (superClass != null) {
    180                 if (superClass.getCanonicalName().equals(classDescription.getExtendedClass())) {
    181                     return true;
    182                 }
    183                 superClass = superClass.getSuperclass();
    184             }
    185             // Couldn't find a matching superclass.
    186             return false;
    187         }
    188         return true;
    189     }
    190 
    191     /**
    192      * Checks if the class under test implements the proper interfaces
    193      * according to the API.
    194      *
    195      * @param classDescription a description of a class in an API.
    196      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
    197      * @return true if the class is compliant
    198      */
    199     private static boolean checkClassImplementsCompliance(JDiffClassDescription classDescription,
    200             Class<?> runtimeClass) {
    201         Set<String> interFaceSet = new HashSet<>();
    202 
    203         addInterfacesToSetByName(runtimeClass, interFaceSet);
    204 
    205         for (String inter : classDescription.getImplInterfaces()) {
    206             if (!interFaceSet.contains(inter)) {
    207                 return false;
    208             }
    209         }
    210         return true;
    211     }
    212 
    213     private static void addInterfacesToSetByName(Class<?> runtimeClass, Set<String> interFaceSet) {
    214         Class<?>[] interfaces = runtimeClass.getInterfaces();
    215         for (Class<?> c : interfaces) {
    216             interFaceSet.add(c.getCanonicalName());
    217         }
    218 
    219         // Add the interfaces that the super class implements as well just in case the super class
    220         // is hidden.
    221         Class<?> superClass = runtimeClass.getSuperclass();
    222         if (superClass != null) {
    223             addInterfacesToSetByName(superClass, interFaceSet);
    224         }
    225     }
    226 
    227     @Override
    228     protected void checkField(JDiffClassDescription classDescription, Class<?> runtimeClass,
    229             JDiffClassDescription.JDiffField fieldDescription, Field field) {
    230         if (field.getModifiers() != fieldDescription.mModifier) {
    231             resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
    232                     fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
    233                     "Non-compatible field modifiers found when looking for " +
    234                             fieldDescription.toSignatureString());
    235         } else if (!checkFieldValueCompliance(fieldDescription, field)) {
    236             resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
    237                     fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
    238                     "Incorrect field value found when looking for " +
    239                             fieldDescription.toSignatureString());
    240         } else if (!field.getType().getCanonicalName().equals(fieldDescription.mFieldType)) {
    241             // type name does not match, but this might be a generic
    242             String genericTypeName = null;
    243             Type type = field.getGenericType();
    244             if (type != null) {
    245                 genericTypeName = type instanceof Class ? ((Class) type).getName() :
    246                         type.toString().replace('$', '.');
    247             }
    248             if (genericTypeName == null || !genericTypeName.equals(fieldDescription.mFieldType)) {
    249                 resultObserver.notifyFailure(
    250                         FailureType.MISMATCH_FIELD,
    251                         fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
    252                         "Non-compatible field type found when looking for " +
    253                                 fieldDescription.toSignatureString());
    254             }
    255         }
    256     }
    257 
    258     /**
    259      * Checks whether the field values are compatible.
    260      *
    261      * @param apiField The field as defined by the platform API.
    262      * @param deviceField The field as defined by the device under test.
    263      */
    264     private static boolean checkFieldValueCompliance(JDiffClassDescription.JDiffField apiField, Field deviceField) {
    265         if ((apiField.mModifier & Modifier.FINAL) == 0 ||
    266                 (apiField.mModifier & Modifier.STATIC) == 0) {
    267             // Only final static fields can have fixed values.
    268             return true;
    269         }
    270         if (apiField.getValueString() == null) {
    271             // If we don't define a constant value for it, then it can be anything.
    272             return true;
    273         }
    274         // Some fields may be protected or package-private
    275         deviceField.setAccessible(true);
    276         try {
    277             switch (apiField.mFieldType) {
    278                 case "byte":
    279                     return Objects.equals(apiField.getValueString(),
    280                             Byte.toString(deviceField.getByte(null)));
    281                 case "char":
    282                     return Objects.equals(apiField.getValueString(),
    283                             Integer.toString(deviceField.getChar(null)));
    284                 case "short":
    285                     return Objects.equals(apiField.getValueString(),
    286                             Short.toString(deviceField.getShort(null)));
    287                 case "int":
    288                     return Objects.equals(apiField.getValueString(),
    289                             Integer.toString(deviceField.getInt(null)));
    290                 case "long":
    291                     return Objects.equals(apiField.getValueString(),
    292                             Long.toString(deviceField.getLong(null)) + "L");
    293                 case "float":
    294                     return Objects.equals(apiField.getValueString(),
    295                             canonicalizeFloatingPoint(
    296                                     Float.toString(deviceField.getFloat(null)), "f"));
    297                 case "double":
    298                     return Objects.equals(apiField.getValueString(),
    299                             canonicalizeFloatingPoint(
    300                                     Double.toString(deviceField.getDouble(null)), ""));
    301                 case "boolean":
    302                     return Objects.equals(apiField.getValueString(),
    303                             Boolean.toString(deviceField.getBoolean(null)));
    304                 case "java.lang.String":
    305                     String value = apiField.getValueString();
    306                     // Remove the quotes the value string is wrapped in
    307                     value = unescapeFieldStringValue(value.substring(1, value.length() - 1));
    308                     return Objects.equals(value, deviceField.get(null));
    309                 default:
    310                     return true;
    311             }
    312         } catch (IllegalAccessException e) {
    313             throw new RuntimeException(e);
    314         }
    315     }
    316 
    317     /**
    318      * Canonicalize the string representation of floating point numbers.
    319      *
    320      * This needs to be kept in sync with the doclava canonicalization.
    321      */
    322     private static String canonicalizeFloatingPoint(String val, String suffix) {
    323         switch (val) {
    324             case "Infinity":
    325                 return "(1.0" + suffix + "/0.0" + suffix + ")";
    326             case "-Infinity":
    327                 return "(-1.0" + suffix + "/0.0" + suffix + ")";
    328             case "NaN":
    329                 return "(0.0" + suffix + "/0.0" + suffix + ")";
    330         }
    331 
    332         if (val.indexOf('E') != -1) {
    333             return val + suffix;
    334         }
    335 
    336         // 1.0 is the only case where a trailing "0" is allowed.
    337         // 1.00 is canonicalized as 1.0.
    338         int i = val.length() - 1;
    339         int d = val.indexOf('.');
    340         while (i >= d + 2 && val.charAt(i) == '0') {
    341             val = val.substring(0, i--);
    342         }
    343         return val + suffix;
    344     }
    345 
    346     // This unescapes the string format used by doclava and so needs to be kept in sync with any
    347     // changes made to that format.
    348     private static String unescapeFieldStringValue(String str) {
    349         final int N = str.length();
    350 
    351         // If there's no special encoding strings in the string then just return it.
    352         if (str.indexOf('\\') == -1) {
    353             return str;
    354         }
    355 
    356         final StringBuilder buf = new StringBuilder(str.length());
    357         char escaped = 0;
    358         final int START = 0;
    359         final int CHAR1 = 1;
    360         final int CHAR2 = 2;
    361         final int CHAR3 = 3;
    362         final int CHAR4 = 4;
    363         final int ESCAPE = 5;
    364         int state = START;
    365 
    366         for (int i = 0; i < N; i++) {
    367             final char c = str.charAt(i);
    368             switch (state) {
    369                 case START:
    370                     if (c == '\\') {
    371                         state = ESCAPE;
    372                     } else {
    373                         buf.append(c);
    374                     }
    375                     break;
    376                 case ESCAPE:
    377                     switch (c) {
    378                         case '\\':
    379                             buf.append('\\');
    380                             state = START;
    381                             break;
    382                         case 't':
    383                             buf.append('\t');
    384                             state = START;
    385                             break;
    386                         case 'b':
    387                             buf.append('\b');
    388                             state = START;
    389                             break;
    390                         case 'r':
    391                             buf.append('\r');
    392                             state = START;
    393                             break;
    394                         case 'n':
    395                             buf.append('\n');
    396                             state = START;
    397                             break;
    398                         case 'f':
    399                             buf.append('\f');
    400                             state = START;
    401                             break;
    402                         case '\'':
    403                             buf.append('\'');
    404                             state = START;
    405                             break;
    406                         case '\"':
    407                             buf.append('\"');
    408                             state = START;
    409                             break;
    410                         case 'u':
    411                             state = CHAR1;
    412                             escaped = 0;
    413                             break;
    414                     }
    415                     break;
    416                 case CHAR1:
    417                 case CHAR2:
    418                 case CHAR3:
    419                 case CHAR4:
    420                     escaped <<= 4;
    421                     if (c >= '0' && c <= '9') {
    422                         escaped |= c - '0';
    423                     } else if (c >= 'a' && c <= 'f') {
    424                         escaped |= 10 + (c - 'a');
    425                     } else if (c >= 'A' && c <= 'F') {
    426                         escaped |= 10 + (c - 'A');
    427                     } else {
    428                         throw new RuntimeException(
    429                                 "bad escape sequence: '" + c + "' at pos " + i + " in: \""
    430                                         + str + "\"");
    431                     }
    432                     if (state == CHAR4) {
    433                         buf.append(escaped);
    434                         state = START;
    435                     } else {
    436                         state++;
    437                     }
    438                     break;
    439             }
    440         }
    441         if (state != START) {
    442             throw new RuntimeException("unfinished escape sequence: " + str);
    443         }
    444         return buf.toString();
    445     }
    446 
    447     @Override
    448     protected void checkConstructor(JDiffClassDescription classDescription, Class<?> runtimeClass,
    449             JDiffClassDescription.JDiffConstructor ctorDescription, Constructor<?> ctor) {
    450         if (ctor.isVarArgs()) {// some method's parameter are variable args
    451             ctorDescription.mModifier |= METHOD_MODIFIER_VAR_ARGS;
    452         }
    453         if (ctor.getModifiers() != ctorDescription.mModifier) {
    454             resultObserver.notifyFailure(
    455                     FailureType.MISMATCH_METHOD,
    456                     ctorDescription.toReadableString(classDescription.getAbsoluteClassName()),
    457                     "Non-compatible method found when looking for " +
    458                             ctorDescription.toSignatureString());
    459         }
    460     }
    461 
    462     @Override
    463     protected void checkMethod(JDiffClassDescription classDescription, Class<?> runtimeClass,
    464             JDiffClassDescription.JDiffMethod methodDescription, Method method) {
    465         if (method.isVarArgs()) {
    466             methodDescription.mModifier |= METHOD_MODIFIER_VAR_ARGS;
    467         }
    468         if (method.isBridge()) {
    469             methodDescription.mModifier |= METHOD_MODIFIER_BRIDGE;
    470         }
    471         if (method.isSynthetic()) {
    472             methodDescription.mModifier |= METHOD_MODIFIER_SYNTHETIC;
    473         }
    474 
    475         // FIXME: A workaround to fix the final mismatch on enumeration
    476         if (runtimeClass.isEnum() && methodDescription.mName.equals("values")) {
    477             return;
    478         }
    479 
    480         String reason;
    481         if ((reason = areMethodsModifiedCompatible(
    482                 classDescription, methodDescription, method)) != null) {
    483             resultObserver.notifyFailure(FailureType.MISMATCH_METHOD,
    484                     methodDescription.toReadableString(classDescription.getAbsoluteClassName()),
    485                     String.format("Non-compatible method found when looking for %s - because %s",
    486                             methodDescription.toSignatureString(), reason));
    487         }
    488     }
    489 
    490     /**
    491      * Checks to ensure that the modifiers value for two methods are compatible.
    492      *
    493      * Allowable differences are:
    494      *   - synchronized is allowed to be removed from an apiMethod
    495      *     that has it
    496      *   - the native modified is ignored
    497      *
    498      * @param classDescription a description of a class in an API.
    499      * @param apiMethod the method read from the api file.
    500      * @param reflectedMethod the method found via reflection.
    501      * @return null if the method modifiers are compatible otherwise the reason why not.
    502      */
    503     private static String areMethodsModifiedCompatible(
    504             JDiffClassDescription classDescription,
    505             JDiffClassDescription.JDiffMethod apiMethod,
    506             Method reflectedMethod) {
    507 
    508         // If the apiMethod isn't synchronized
    509         if (((apiMethod.mModifier & Modifier.SYNCHRONIZED) == 0) &&
    510                 // but the reflected method is
    511                 ((reflectedMethod.getModifiers() & Modifier.SYNCHRONIZED) != 0)) {
    512             // that is a problem
    513             return "description is synchronize but class is not";
    514         }
    515 
    516         // Mask off NATIVE since it is a don't care.  Also mask off
    517         // SYNCHRONIZED since we've already handled that check.
    518         int ignoredMods = (Modifier.NATIVE | Modifier.SYNCHRONIZED | Modifier.STRICT);
    519         int reflectionModifiers = reflectedMethod.getModifiers() & ~ignoredMods;
    520         int apiModifiers = apiMethod.mModifier & ~ignoredMods;
    521 
    522         // We can ignore FINAL for classes
    523         if ((classDescription.getModifier() & Modifier.FINAL) != 0) {
    524             reflectionModifiers &= ~Modifier.FINAL;
    525             apiModifiers &= ~Modifier.FINAL;
    526         }
    527 
    528         if (reflectionModifiers == apiModifiers) {
    529             return null;
    530         } else {
    531             return String.format("modifier mismatch - description (%s), method (%s)",
    532                     Modifier.toString(apiModifiers), Modifier.toString(reflectionModifiers));
    533         }
    534     }
    535 
    536     public void addBaseClass(JDiffClassDescription classDescription) {
    537         // Keep track of all the base interfaces that may by extended.
    538         if (classDescription.getClassType() == JDiffClassDescription.JDiffType.INTERFACE) {
    539             try {
    540                 Class<?> runtimeClass =
    541                         ReflectionHelper.findMatchingClass(classDescription, classProvider);
    542                 interfaceChecker.queueForDeferredCheck(classDescription, runtimeClass);
    543             } catch (ClassNotFoundException e) {
    544                 // Do nothing.
    545             }
    546         }
    547     }
    548 }
    549