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 android.signature.cts.JDiffClassDescription.JDiffField;
19 import android.signature.cts.ReflectionHelper.DefaultTypeComparator;
20 import java.lang.reflect.Constructor;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.Modifier;
24 import java.util.Formatter;
25 import java.util.HashSet;
26 import java.util.Objects;
27 import java.util.Set;
28 
29 /**
30  * Checks that the runtime representation of a class matches the API representation of a class.
31  */
32 public class ApiComplianceChecker extends AbstractApiChecker {
33 
34     /**
35      * A set of method signatures whose abstract modifier should be ignored.
36      *
37      * <p>If a class is not intended to be created or extended by application developers and all
38      * instances are created and supplied by Android itself then the abstract modifier has no
39      * impact on runtime compatibility.
40      */
41     private static final Set<String> IGNORE_METHOD_ABSTRACT_MODIFIER_WHITE_LIST = new HashSet<>();
42     static {
43         // This method was previously abstract and is now not abstract. As the
44         // CtsSystemApiSignatureTestCases package tests both the old and new specifications, with
45         // and without the abstract modifier this needs to ignore the abstract modifier.
46         IGNORE_METHOD_ABSTRACT_MODIFIER_WHITE_LIST.add(
47                 "public int android.service.euicc.EuiccService.onDownloadSubscription("
48                         + "int,android.telephony.euicc.DownloadableSubscription,boolean,boolean)");
49     }
50 
51     /** Indicates that the class is an annotation. */
52     private static final int CLASS_MODIFIER_ANNOTATION = 0x00002000;
53 
54     /** Indicates that the class is an enum. */
55     private static final int CLASS_MODIFIER_ENUM       = 0x00004000;
56 
57     /** Indicates that the method is a bridge method. */
58     private static final int METHOD_MODIFIER_BRIDGE    = 0x00000040;
59 
60     /** Indicates that the method is takes a variable number of arguments. */
61     private static final int METHOD_MODIFIER_VAR_ARGS  = 0x00000080;
62 
63     /** Indicates that the method is a synthetic method. */
64     private static final int METHOD_MODIFIER_SYNTHETIC = 0x00001000;
65 
66     /** Indicates that a field is an enum value. */
67     public static final int FIELD_MODIFIER_ENUM_VALUE = 0x00004000;
68 
69     private final InterfaceChecker interfaceChecker;
70 
ApiComplianceChecker(ResultObserver resultObserver, ClassProvider classProvider)71     public ApiComplianceChecker(ResultObserver resultObserver, ClassProvider classProvider) {
72         super(classProvider, resultObserver);
73         interfaceChecker = new InterfaceChecker(resultObserver, classProvider);
74     }
75 
76     @Override
checkDeferred()77     public void checkDeferred() {
78         interfaceChecker.checkQueued();
79     }
80 
81     @Override
checkClass(JDiffClassDescription classDescription, Class<?> runtimeClass)82     protected boolean checkClass(JDiffClassDescription classDescription, Class<?> runtimeClass) {
83         if (JDiffClassDescription.JDiffType.INTERFACE.equals(classDescription.getClassType())) {
84             // Queue the interface for deferred checking.
85             interfaceChecker.queueForDeferredCheck(classDescription, runtimeClass);
86         }
87 
88         String reason;
89         if ((reason = checkClassModifiersCompliance(classDescription, runtimeClass)) != null) {
90             resultObserver.notifyFailure(FailureType.mismatch(classDescription),
91                     classDescription.getAbsoluteClassName(),
92                     String.format("Non-compatible class found when looking for %s - because %s",
93                             classDescription.toSignatureString(), reason));
94             return false;
95         }
96 
97         if (!checkClassAnnotationCompliance(classDescription, runtimeClass)) {
98             resultObserver.notifyFailure(FailureType.mismatch(classDescription),
99                     classDescription.getAbsoluteClassName(), "Annotation mismatch");
100             return false;
101         }
102 
103         if (!runtimeClass.isAnnotation()) {
104             // check father class
105             if (!checkClassExtendsCompliance(classDescription, runtimeClass)) {
106                 resultObserver.notifyFailure(FailureType.mismatch(classDescription),
107                         classDescription.getAbsoluteClassName(),
108                         "Extends mismatch, expected " + classDescription.getExtendedClass());
109                 return false;
110             }
111 
112             // check implements interface
113             if (!checkClassImplementsCompliance(classDescription, runtimeClass)) {
114                 resultObserver.notifyFailure(FailureType.mismatch(classDescription),
115                         classDescription.getAbsoluteClassName(),
116                         "Implements mismatch, expected " + classDescription.getImplInterfaces());
117                 return false;
118             }
119         }
120         return true;
121     }
122 
123     /**
124      * Checks if the class under test has compliant modifiers compared to the API.
125      *
126      * @param classDescription a description of a class in an API.
127      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
128      * @return null if modifiers are compliant otherwise a reason why they are not.
129      */
checkClassModifiersCompliance(JDiffClassDescription classDescription, Class<?> runtimeClass)130     private static String checkClassModifiersCompliance(JDiffClassDescription classDescription,
131             Class<?> runtimeClass) {
132         int reflectionModifiers = runtimeClass.getModifiers();
133         int apiModifiers = classDescription.getModifier();
134 
135         // If the api class isn't abstract
136         if (((apiModifiers & Modifier.ABSTRACT) == 0) &&
137                 // but the reflected class is
138                 ((reflectionModifiers & Modifier.ABSTRACT) != 0) &&
139                 // interfaces are implicitly abstract (JLS 9.1.1.1)
140                 classDescription.getClassType() != JDiffClassDescription.JDiffType.INTERFACE &&
141                 // and it isn't an enum
142                 !classDescription.isEnumType()) {
143             // that is a problem
144             return "description is abstract but class is not and is not an enum";
145         }
146         // ABSTRACT check passed, so mask off ABSTRACT
147         reflectionModifiers &= ~Modifier.ABSTRACT;
148         apiModifiers &= ~Modifier.ABSTRACT;
149 
150         if (classDescription.isAnnotation()) {
151             reflectionModifiers &= ~CLASS_MODIFIER_ANNOTATION;
152         }
153         if (runtimeClass.isInterface()) {
154             reflectionModifiers &= ~(Modifier.INTERFACE);
155         }
156         if (classDescription.isEnumType() && runtimeClass.isEnum()) {
157             reflectionModifiers &= ~CLASS_MODIFIER_ENUM;
158 
159             // Most enums are marked as final, however enums that have one or more constants that
160             // override a method from the class cannot be marked as final because those constants
161             // are represented as a subclass. As enum classes cannot be extended (except for its own
162             // constants) there is no benefit in checking final modifier so just ignore them.
163             reflectionModifiers &= ~Modifier.FINAL;
164             apiModifiers &= ~Modifier.FINAL;
165         }
166 
167         if ((reflectionModifiers == apiModifiers)
168                 && (classDescription.isEnumType() == runtimeClass.isEnum())) {
169             return null;
170         } else {
171             return String.format("modifier mismatch - description (%s), class (%s)",
172                     getModifierString(apiModifiers), getModifierString(reflectionModifiers));
173         }
174     }
175 
176     /**
177      * Checks if the class under test is compliant with regards to
178      * annnotations when compared to the API.
179      *
180      * @param classDescription a description of a class in an API.
181      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
182      * @return true if the class is compliant
183      */
checkClassAnnotationCompliance(JDiffClassDescription classDescription, Class<?> runtimeClass)184     private static boolean checkClassAnnotationCompliance(JDiffClassDescription classDescription,
185             Class<?> runtimeClass) {
186         if (runtimeClass.isAnnotation()) {
187             // check annotation
188             for (String inter : classDescription.getImplInterfaces()) {
189                 if ("java.lang.annotation.Annotation".equals(inter)) {
190                     return true;
191                 }
192             }
193             return false;
194         }
195         return true;
196     }
197 
198     /**
199      * Checks if the class under test extends the proper classes
200      * according to the API.
201      *
202      * @param classDescription a description of a class in an API.
203      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
204      * @return true if the class is compliant.
205      */
checkClassExtendsCompliance(JDiffClassDescription classDescription, Class<?> runtimeClass)206     private static boolean checkClassExtendsCompliance(JDiffClassDescription classDescription,
207             Class<?> runtimeClass) {
208         // Nothing to check if it doesn't extend anything.
209         if (classDescription.getExtendedClass() != null) {
210             Class<?> superClass = runtimeClass.getSuperclass();
211 
212             while (superClass != null) {
213                 if (superClass.getCanonicalName().equals(classDescription.getExtendedClass())) {
214                     return true;
215                 }
216                 superClass = superClass.getSuperclass();
217             }
218             // Couldn't find a matching superclass.
219             return false;
220         }
221         return true;
222     }
223 
224     /**
225      * Checks if the class under test implements the proper interfaces
226      * according to the API.
227      *
228      * @param classDescription a description of a class in an API.
229      * @param runtimeClass the runtime class corresponding to {@code classDescription}.
230      * @return true if the class is compliant
231      */
checkClassImplementsCompliance(JDiffClassDescription classDescription, Class<?> runtimeClass)232     private static boolean checkClassImplementsCompliance(JDiffClassDescription classDescription,
233             Class<?> runtimeClass) {
234         Set<String> interFaceSet = new HashSet<>();
235 
236         addInterfacesToSetByName(runtimeClass, interFaceSet);
237 
238         for (String inter : classDescription.getImplInterfaces()) {
239             if (!interFaceSet.contains(inter)) {
240                 return false;
241             }
242         }
243         return true;
244     }
245 
addInterfacesToSetByName(Class<?> runtimeClass, Set<String> interFaceSet)246     private static void addInterfacesToSetByName(Class<?> runtimeClass, Set<String> interFaceSet) {
247         Class<?>[] interfaces = runtimeClass.getInterfaces();
248         for (Class<?> c : interfaces) {
249             interFaceSet.add(c.getCanonicalName());
250             // Add grandparent interfaces in case the parent interface is hidden.
251             addInterfacesToSetByName(c, interFaceSet);
252         }
253 
254         // Add the interfaces that the super class implements as well just in case the super class
255         // is hidden.
256         Class<?> superClass = runtimeClass.getSuperclass();
257         if (superClass != null) {
258             addInterfacesToSetByName(superClass, interFaceSet);
259         }
260     }
261 
262     @Override
checkField(JDiffClassDescription classDescription, Class<?> runtimeClass, JDiffField fieldDescription, Field field)263     protected void checkField(JDiffClassDescription classDescription, Class<?> runtimeClass,
264             JDiffField fieldDescription, Field field) {
265         int expectedModifiers = fieldDescription.mModifier;
266         int actualModifiers = field.getModifiers();
267         if (actualModifiers != expectedModifiers) {
268             resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
269                     fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
270                     String.format(
271                             "Incompatible field modifiers, expected %s, found %s",
272                             getModifierString(expectedModifiers),
273                             getModifierString(actualModifiers)));
274         }
275 
276         String expectedFieldType = fieldDescription.mFieldType;
277         String actualFieldType = ReflectionHelper.typeToString(field.getGenericType());
278         if (!DefaultTypeComparator.INSTANCE.compare(expectedFieldType, actualFieldType)) {
279             resultObserver.notifyFailure(
280                     FailureType.MISMATCH_FIELD,
281                     fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
282                     String.format("Incompatible field type found, expected %s, found %s",
283                             expectedFieldType, actualFieldType));
284         }
285 
286         String message = checkFieldValueCompliance(fieldDescription, field);
287         if (message != null) {
288             resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
289                     fieldDescription.toReadableString(classDescription.getAbsoluteClassName()),
290                     message);
291         }
292     }
293 
294     private static final int BRIDGE    = 0x00000040;
295     private static final int VARARGS   = 0x00000080;
296     private static final int SYNTHETIC = 0x00001000;
297     private static final int ANNOTATION  = 0x00002000;
298     private static final int ENUM      = 0x00004000;
299     private static final int MANDATED  = 0x00008000;
300 
getModifierString(int modifiers)301     private static String getModifierString(int modifiers) {
302         Formatter formatter = new Formatter();
303         String m = Modifier.toString(modifiers);
304         formatter.format("<%s", m);
305         String sep = m.isEmpty() ? "" : " ";
306         if ((modifiers & BRIDGE) != 0) {
307             formatter.format("%senum", sep);
308             sep = " ";
309         }
310         if ((modifiers & VARARGS) != 0) {
311             formatter.format("%svarargs", sep);
312             sep = " ";
313         }
314         if ((modifiers & SYNTHETIC) != 0) {
315             formatter.format("%ssynthetic", sep);
316             sep = " ";
317         }
318         if ((modifiers & ANNOTATION) != 0) {
319             formatter.format("%sannotation", sep);
320             sep = " ";
321         }
322         if ((modifiers & ENUM) != 0) {
323             formatter.format("%senum", sep);
324             sep = " ";
325         }
326         if ((modifiers & MANDATED) != 0) {
327             formatter.format("%smandated", sep);
328         }
329         return formatter.format("> (0x%x)", modifiers).toString();
330     }
331 
332     /**
333      * Checks whether the field values are compatible.
334      *
335      * @param apiField The field as defined by the platform API.
336      * @param deviceField The field as defined by the device under test.
337      */
checkFieldValueCompliance(JDiffField apiField, Field deviceField)338     private static String checkFieldValueCompliance(JDiffField apiField, Field deviceField) {
339         if ((apiField.mModifier & Modifier.FINAL) == 0 ||
340                 (apiField.mModifier & Modifier.STATIC) == 0) {
341             // Only final static fields can have fixed values.
342             return null;
343         }
344         String apiFieldValue = apiField.getValueString();
345         if (apiFieldValue == null) {
346             // If we don't define a constant value for it, then it can be anything.
347             return null;
348         }
349 
350         // Convert char into a number to match the value returned from device field. The device
351         // field does not
352         if (deviceField.getType() == char.class) {
353             apiFieldValue = convertCharToCanonicalValue(apiFieldValue.charAt(0));
354         }
355 
356         String deviceFieldValue = getFieldValueAsString(deviceField);
357         if (!Objects.equals(apiFieldValue, deviceFieldValue)) {
358             return String.format("Incorrect field value, expected <%s>, found <%s>",
359                     apiFieldValue, deviceFieldValue);
360 
361         }
362 
363         return null;
364     }
365 
getFieldValueAsString(Field deviceField)366     private static String getFieldValueAsString(Field deviceField) {
367         // Some fields may be protected or package-private
368         deviceField.setAccessible(true);
369         try {
370             Class<?> fieldType = deviceField.getType();
371             if (fieldType == byte.class) {
372                 return Byte.toString(deviceField.getByte(null));
373             } else if (fieldType == char.class) {
374                 return convertCharToCanonicalValue(deviceField.getChar(null));
375             } else if (fieldType == short.class) {
376                 return  Short.toString(deviceField.getShort(null));
377             } else if (fieldType == int.class) {
378                 return  Integer.toString(deviceField.getInt(null));
379             } else if (fieldType == long.class) {
380                 return Long.toString(deviceField.getLong(null));
381             } else if (fieldType == float.class) {
382                 return  canonicalizeFloatingPoint(
383                                 Float.toString(deviceField.getFloat(null)));
384             } else if (fieldType == double.class) {
385                 return  canonicalizeFloatingPoint(
386                                 Double.toString(deviceField.getDouble(null)));
387             } else if (fieldType == boolean.class) {
388                 return  Boolean.toString(deviceField.getBoolean(null));
389             } else if (fieldType == java.lang.String.class) {
390                 return (String) deviceField.get(null);
391             } else {
392                 return null;
393             }
394         } catch (IllegalAccessException e) {
395             throw new RuntimeException(e);
396         }
397     }
398 
convertCharToCanonicalValue(char c)399     private static String convertCharToCanonicalValue(char c) {
400         return String.format("'%c' (0x%x)", c, (int) c);
401     }
402 
403     /**
404      * Canonicalize the string representation of floating point numbers.
405      *
406      * This needs to be kept in sync with the doclava canonicalization.
407      */
canonicalizeFloatingPoint(String val)408     private static String canonicalizeFloatingPoint(String val) {
409         switch (val) {
410             case "Infinity":
411             case "-Infinity":
412             case "NaN":
413                 return val;
414         }
415 
416         if (val.indexOf('E') != -1) {
417             return val;
418         }
419 
420         // 1.0 is the only case where a trailing "0" is allowed.
421         // 1.00 is canonicalized as 1.0.
422         int i = val.length() - 1;
423         int d = val.indexOf('.');
424         while (i >= d + 2 && val.charAt(i) == '0') {
425             val = val.substring(0, i--);
426         }
427         return val;
428     }
429 
430     @Override
checkConstructor(JDiffClassDescription classDescription, Class<?> runtimeClass, JDiffClassDescription.JDiffConstructor ctorDescription, Constructor<?> ctor)431     protected void checkConstructor(JDiffClassDescription classDescription, Class<?> runtimeClass,
432             JDiffClassDescription.JDiffConstructor ctorDescription, Constructor<?> ctor) {
433         if (ctor.isVarArgs()) {// some method's parameter are variable args
434             ctorDescription.mModifier |= METHOD_MODIFIER_VAR_ARGS;
435         }
436         if (ctor.getModifiers() != ctorDescription.mModifier) {
437             resultObserver.notifyFailure(
438                     FailureType.MISMATCH_METHOD,
439                     ctorDescription.toReadableString(classDescription.getAbsoluteClassName()),
440                     "Non-compatible method found when looking for " +
441                             ctorDescription.toSignatureString());
442         }
443     }
444 
445     @Override
checkMethod(JDiffClassDescription classDescription, Class<?> runtimeClass, JDiffClassDescription.JDiffMethod methodDescription, Method method)446     protected void checkMethod(JDiffClassDescription classDescription, Class<?> runtimeClass,
447             JDiffClassDescription.JDiffMethod methodDescription, Method method) {
448         if (method.isVarArgs()) {
449             methodDescription.mModifier |= METHOD_MODIFIER_VAR_ARGS;
450         }
451         if (method.isBridge()) {
452             methodDescription.mModifier |= METHOD_MODIFIER_BRIDGE;
453         }
454         if (method.isSynthetic()) {
455             methodDescription.mModifier |= METHOD_MODIFIER_SYNTHETIC;
456         }
457 
458         // FIXME: A workaround to fix the final mismatch on enumeration
459         if (runtimeClass.isEnum() && methodDescription.mName.equals("values")) {
460             return;
461         }
462 
463         String reason;
464         if ((reason = areMethodsModifierCompatible(
465                 classDescription, methodDescription, method)) != null) {
466             resultObserver.notifyFailure(FailureType.MISMATCH_METHOD,
467                     methodDescription.toReadableString(classDescription.getAbsoluteClassName()),
468                     String.format("Non-compatible method found when looking for %s - because %s",
469                             methodDescription.toSignatureString(), reason));
470         }
471     }
472 
473     /**
474      * Checks to ensure that the modifiers value for two methods are compatible.
475      *
476      * Allowable differences are:
477      *   - the native modifier is ignored
478      *
479      * @param classDescription a description of a class in an API.
480      * @param apiMethod the method read from the api file.
481      * @param reflectedMethod the method found via reflection.
482      * @return null if the method modifiers are compatible otherwise the reason why not.
483      */
areMethodsModifierCompatible( JDiffClassDescription classDescription, JDiffClassDescription.JDiffMethod apiMethod, Method reflectedMethod)484     private static String areMethodsModifierCompatible(
485             JDiffClassDescription classDescription,
486             JDiffClassDescription.JDiffMethod apiMethod,
487             Method reflectedMethod) {
488 
489         // Mask off NATIVE since it is a don't care.  Also mask off
490         // SYNCHRONIZED since it is not considered API significant (b/112626813)
491         int ignoredMods = (Modifier.NATIVE | Modifier.SYNCHRONIZED | Modifier.STRICT);
492         int reflectionModifiers = reflectedMethod.getModifiers() & ~ignoredMods;
493         int apiModifiers = apiMethod.mModifier & ~ignoredMods;
494 
495         // A method can become non-abstract
496         if ((reflectionModifiers & Modifier.ABSTRACT) == 0) {
497             apiModifiers &= ~Modifier.ABSTRACT;
498         }
499 
500         // We can ignore FINAL for classes
501         if ((classDescription.getModifier() & Modifier.FINAL) != 0) {
502             reflectionModifiers &= ~Modifier.FINAL;
503             apiModifiers &= ~Modifier.FINAL;
504         }
505 
506         String genericString = reflectedMethod.toGenericString();
507         if (IGNORE_METHOD_ABSTRACT_MODIFIER_WHITE_LIST.contains(genericString)) {
508             reflectionModifiers &= ~Modifier.ABSTRACT;
509             apiModifiers &= ~Modifier.ABSTRACT;
510         }
511 
512         if (reflectionModifiers == apiModifiers) {
513             return null;
514         } else {
515             return String.format("modifier mismatch - description (%s), method (%s), for %s",
516                     getModifierString(apiModifiers), getModifierString(reflectionModifiers), genericString);
517         }
518     }
519 
addBaseClass(JDiffClassDescription classDescription)520     public void addBaseClass(JDiffClassDescription classDescription) {
521         // Keep track of all the base interfaces that may by extended.
522         if (classDescription.getClassType() == JDiffClassDescription.JDiffType.INTERFACE) {
523             try {
524                 Class<?> runtimeClass =
525                         ReflectionHelper.findMatchingClass(classDescription, classProvider);
526                 interfaceChecker.queueForDeferredCheck(classDescription, runtimeClass);
527             } catch (ClassNotFoundException e) {
528                 // Do nothing.
529             }
530         }
531     }
532 }
533