blob: 8dbf5e7801a3a8aa1377ec0665eda1f67c4e259f [file] [log] [blame]
/*
* Copyright (C) 2017 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 android.signature.cts;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Checks that the runtime representation of a class matches the API representation of a class.
*/
public class ApiComplianceChecker {
/** Indicates that the class is an annotation. */
private static final int CLASS_MODIFIER_ANNOTATION = 0x00002000;
/** Indicates that the class is an enum. */
private static final int CLASS_MODIFIER_ENUM = 0x00004000;
/** Indicates that the method is a bridge method. */
private static final int METHOD_MODIFIER_BRIDGE = 0x00000040;
/** Indicates that the method is takes a variable number of arguments. */
private static final int METHOD_MODIFIER_VAR_ARGS = 0x00000080;
/** Indicates that the method is a synthetic method. */
private static final int METHOD_MODIFIER_SYNTHETIC = 0x00001000;
private static final Set<String> HIDDEN_INTERFACE_WHITELIST = new HashSet<>();
static {
// Interfaces that define @hide or @SystemApi or @TestApi methods will by definition contain
// methods that do not appear in current.txt. Interfaces added to this
// list are probably not meant to be implemented in an application.
HIDDEN_INTERFACE_WHITELIST.add("public abstract boolean android.companion.DeviceFilter.matches(D)");
HIDDEN_INTERFACE_WHITELIST.add("public static <D> boolean android.companion.DeviceFilter.matches(android.companion.DeviceFilter<D>,D)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract java.lang.String android.companion.DeviceFilter.getDeviceDisplayName(D)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract int android.companion.DeviceFilter.getMediumType()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.nfc.tech.TagTechnology.reconnect() throws java.io.IOException");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.os.IBinder.shellCommand(java.io.FileDescriptor,java.io.FileDescriptor,java.io.FileDescriptor,java.lang.String[],android.os.ShellCallback,android.os.ResultReceiver) throws android.os.RemoteException");
HIDDEN_INTERFACE_WHITELIST.add("public abstract int android.text.ParcelableSpan.getSpanTypeIdInternal()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.text.ParcelableSpan.writeToParcelInternal(android.os.Parcel,int)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.view.WindowManager.requestAppKeyboardShortcuts(android.view.WindowManager$KeyboardShortcutsReceiver,int)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract boolean javax.microedition.khronos.egl.EGL10.eglReleaseThread()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void org.w3c.dom.ls.LSSerializer.setFilter(org.w3c.dom.ls.LSSerializerFilter)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract org.w3c.dom.ls.LSSerializerFilter org.w3c.dom.ls.LSSerializer.getFilter()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract android.graphics.Region android.view.WindowManager.getCurrentImeTouchRegion()");
}
private final ResultObserver resultObserver;
public ApiComplianceChecker(ResultObserver resultObserver) {
this.resultObserver = resultObserver;
}
private static void loge(String message, Exception exception) {
System.err.println(String.format("%s: %s", message, exception));
}
private void logMismatchInterfaceSignature(JDiffClassDescription.JDiffType mClassType,
String classFullName, String errorMessage) {
if (JDiffClassDescription.JDiffType.INTERFACE.equals(mClassType)) {
resultObserver.notifyFailure(FailureType.MISMATCH_INTERFACE,
classFullName,
errorMessage);
} else {
resultObserver.notifyFailure(FailureType.MISMATCH_CLASS,
classFullName,
errorMessage);
}
}
/**
* Checks test class's name, modifier, fields, constructors, and
* methods.
*/
public void checkSignatureCompliance(JDiffClassDescription classDescription) {
Class<?> runtimeClass = checkClassCompliance(classDescription);
if (runtimeClass != null) {
checkFieldsCompliance(classDescription, runtimeClass);
checkConstructorCompliance(classDescription, runtimeClass);
checkMethodCompliance(classDescription, runtimeClass);
}
}
/**
* Checks that the class found through reflection matches the
* specification from the API xml file.
*
* @param classDescription a description of a class in an API.
*/
@SuppressWarnings("unchecked")
private Class<?> checkClassCompliance(JDiffClassDescription classDescription) {
try {
Class<?> runtimeClass = findRequiredClass(classDescription);
if (runtimeClass == null) {
// No class found, notify the observer according to the class type
if (JDiffClassDescription.JDiffType.INTERFACE.equals(
classDescription.getClassType())) {
resultObserver.notifyFailure(FailureType.MISSING_INTERFACE,
classDescription.getAbsoluteClassName(),
"Classloader is unable to find " + classDescription
.getAbsoluteClassName());
} else {
resultObserver.notifyFailure(FailureType.MISSING_CLASS,
classDescription.getAbsoluteClassName(),
"Classloader is unable to find " + classDescription
.getAbsoluteClassName());
}
return null;
}
List<String> methods = checkInterfaceMethodCompliance(classDescription, runtimeClass);
if (JDiffClassDescription.JDiffType.INTERFACE.equals(classDescription.getClassType()) && methods.size() > 0) {
resultObserver.notifyFailure(FailureType.MISMATCH_INTERFACE_METHOD,
classDescription.getAbsoluteClassName(), "Interfaces cannot be modified: "
+ classDescription.getAbsoluteClassName() + ": " + methods);
return null;
}
if (!checkClassModifiersCompliance(classDescription, runtimeClass)) {
logMismatchInterfaceSignature(classDescription.getClassType(),
classDescription.getAbsoluteClassName(),
"Non-compatible class found when looking for " +
classDescription.toSignatureString());
return null;
}
if (!checkClassAnnotationCompliance(classDescription, runtimeClass)) {
logMismatchInterfaceSignature(classDescription.getClassType(),
classDescription.getAbsoluteClassName(),
"Annotation mismatch");
return null;
}
if (!runtimeClass.isAnnotation()) {
// check father class
if (!checkClassExtendsCompliance(classDescription, runtimeClass)) {
logMismatchInterfaceSignature(classDescription.getClassType(),
classDescription.getAbsoluteClassName(),
"Extends mismatch");
return null;
}
// check implements interface
if (!checkClassImplementsCompliance(classDescription, runtimeClass)) {
logMismatchInterfaceSignature(classDescription.getClassType(),
classDescription.getAbsoluteClassName(),
"Implements mismatch");
return null;
}
}
return runtimeClass;
} catch (Exception e) {
loge("Got exception when checking field compliance", e);
resultObserver.notifyFailure(
FailureType.CAUGHT_EXCEPTION,
classDescription.getAbsoluteClassName(),
"Exception!");
return null;
}
}
private Class<?> findRequiredClass(JDiffClassDescription classDescription) {
try {
return ReflectionHelper.findMatchingClass(classDescription);
} catch (ClassNotFoundException e) {
loge("ClassNotFoundException for " + classDescription.getAbsoluteClassName(), e);
return null;
}
}
/**
* Validate that an interfaces method count is as expected.
*
* @param classDescription the class's API description.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
*/
private static List<String> checkInterfaceMethodCompliance(
JDiffClassDescription classDescription, Class<?> runtimeClass) {
List<String> unexpectedMethods = new ArrayList<>();
for (Method method : runtimeClass.getDeclaredMethods()) {
if (method.isDefault()) {
continue;
}
if (method.isSynthetic()) {
continue;
}
if (method.isBridge()) {
continue;
}
if (HIDDEN_INTERFACE_WHITELIST.contains(method.toGenericString())) {
continue;
}
boolean foundMatch = false;
for (JDiffClassDescription.JDiffMethod jdiffMethod : classDescription.getMethods()) {
if (ReflectionHelper.matches(jdiffMethod, method)) {
foundMatch = true;
}
}
if (!foundMatch) {
unexpectedMethods.add(method.toGenericString());
}
}
return unexpectedMethods;
}
/**
* Checks if the class under test has compliant modifiers compared to the API.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
* @return true if modifiers are compliant.
*/
private static boolean checkClassModifiersCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
int reflectionModifier = runtimeClass.getModifiers();
int apiModifier = classDescription.getModifier();
// If the api class isn't abstract
if (((apiModifier & Modifier.ABSTRACT) == 0) &&
// but the reflected class is
((reflectionModifier & Modifier.ABSTRACT) != 0) &&
// and it isn't an enum
!classDescription.isEnumType()) {
// that is a problem
return false;
}
// ABSTRACT check passed, so mask off ABSTRACT
reflectionModifier &= ~Modifier.ABSTRACT;
apiModifier &= ~Modifier.ABSTRACT;
if (classDescription.isAnnotation()) {
reflectionModifier &= ~CLASS_MODIFIER_ANNOTATION;
}
if (runtimeClass.isInterface()) {
reflectionModifier &= ~(Modifier.INTERFACE);
}
if (classDescription.isEnumType() && runtimeClass.isEnum()) {
reflectionModifier &= ~CLASS_MODIFIER_ENUM;
}
return ((reflectionModifier == apiModifier) &&
(classDescription.isEnumType() == runtimeClass.isEnum()));
}
/**
* Checks if the class under test is compliant with regards to
* annnotations when compared to the API.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
* @return true if the class is compliant
*/
private static boolean checkClassAnnotationCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
if (runtimeClass.isAnnotation()) {
// check annotation
for (String inter : classDescription.getImplInterfaces()) {
if ("java.lang.annotation.Annotation".equals(inter)) {
return true;
}
}
return false;
}
return true;
}
/**
* Checks if the class under test extends the proper classes
* according to the API.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
* @return true if the class is compliant.
*/
private static boolean checkClassExtendsCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
// Nothing to check if it doesn't extend anything.
if (classDescription.getExtendedClass() != null) {
Class<?> superClass = runtimeClass.getSuperclass();
while (superClass != null) {
if (superClass.getCanonicalName().equals(classDescription.getExtendedClass())) {
return true;
}
superClass = superClass.getSuperclass();
}
// Couldn't find a matching superclass.
return false;
}
return true;
}
/**
* Checks if the class under test implements the proper interfaces
* according to the API.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
* @return true if the class is compliant
*/
private static boolean checkClassImplementsCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
Class<?>[] interfaces = runtimeClass.getInterfaces();
Set<String> interFaceSet = new HashSet<>();
for (Class<?> c : interfaces) {
interFaceSet.add(c.getCanonicalName());
}
for (String inter : classDescription.getImplInterfaces()) {
if (!interFaceSet.contains(inter)) {
return false;
}
}
return true;
}
/**
* Checks all fields in test class for compliance with the API xml.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
*/
@SuppressWarnings("unchecked")
private void checkFieldsCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
// A map of field name to field of the fields contained in runtimeClass.
Map<String, Field> classFieldMap = buildFieldMap(runtimeClass);
for (JDiffClassDescription.JDiffField field : classDescription.getFields()) {
try {
Field f = classFieldMap.get(field.mName);
if (f == null) {
resultObserver.notifyFailure(FailureType.MISSING_FIELD,
field.toReadableString(classDescription.getAbsoluteClassName()),
"No field with correct signature found:" +
field.toSignatureString());
} else if (f.getModifiers() != field.mModifier) {
resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
field.toReadableString(classDescription.getAbsoluteClassName()),
"Non-compatible field modifiers found when looking for " +
field.toSignatureString());
} else if (!checkFieldValueCompliance(field, f)) {
resultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
field.toReadableString(classDescription.getAbsoluteClassName()),
"Incorrect field value found when looking for " +
field.toSignatureString());
} else if (!f.getType().getCanonicalName().equals(field.mFieldType)) {
// type name does not match, but this might be a generic
String genericTypeName = null;
Type type = f.getGenericType();
if (type != null) {
genericTypeName = type instanceof Class ? ((Class) type).getName() :
type.toString().replace('$', '.');
}
if (genericTypeName == null || !genericTypeName.equals(field.mFieldType)) {
resultObserver.notifyFailure(
FailureType.MISMATCH_FIELD,
field.toReadableString(classDescription.getAbsoluteClassName()),
"Non-compatible field type found when looking for " +
field.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking field compliance", e);
resultObserver.notifyFailure(
FailureType.CAUGHT_EXCEPTION,
field.toReadableString(classDescription.getAbsoluteClassName()),
"Exception!");
}
}
}
/**
* Checks whether the field values are compatible.
*
* @param apiField The field as defined by the platform API.
* @param deviceField The field as defined by the device under test.
*/
private static boolean checkFieldValueCompliance(JDiffClassDescription.JDiffField apiField, Field deviceField)
throws IllegalAccessException {
if ((apiField.mModifier & Modifier.FINAL) == 0 ||
(apiField.mModifier & Modifier.STATIC) == 0) {
// Only final static fields can have fixed values.
return true;
}
if (apiField.getValueString() == null) {
// If we don't define a constant value for it, then it can be anything.
return true;
}
// Some fields may be protected or package-private
deviceField.setAccessible(true);
switch (apiField.mFieldType) {
case "byte":
return Objects.equals(apiField.getValueString(),
Byte.toString(deviceField.getByte(null)));
case "char":
return Objects.equals(apiField.getValueString(),
Integer.toString(deviceField.getChar(null)));
case "short":
return Objects.equals(apiField.getValueString(),
Short.toString(deviceField.getShort(null)));
case "int":
return Objects.equals(apiField.getValueString(),
Integer.toString(deviceField.getInt(null)));
case "long":
return Objects.equals(apiField.getValueString(),
Long.toString(deviceField.getLong(null)) + "L");
case "float":
return Objects.equals(apiField.getValueString(),
canonicalizeFloatingPoint(
Float.toString(deviceField.getFloat(null)), "f"));
case "double":
return Objects.equals(apiField.getValueString(),
canonicalizeFloatingPoint(
Double.toString(deviceField.getDouble(null)), ""));
case "boolean":
return Objects.equals(apiField.getValueString(),
Boolean.toString(deviceField.getBoolean(null)));
case "java.lang.String":
String value = apiField.getValueString();
// Remove the quotes the value string is wrapped in
value = unescapeFieldStringValue(value.substring(1, value.length() - 1));
return Objects.equals(value, deviceField.get(null));
default:
return true;
}
}
/**
* Canonicalize the string representation of floating point numbers.
*
* This needs to be kept in sync with the doclava canonicalization.
*/
private static String canonicalizeFloatingPoint(String val, String suffix) {
switch (val) {
case "Infinity":
return "(1.0" + suffix + "/0.0" + suffix + ")";
case "-Infinity":
return "(-1.0" + suffix + "/0.0" + suffix + ")";
case "NaN":
return "(0.0" + suffix + "/0.0" + suffix + ")";
}
if (val.indexOf('E') != -1) {
return val + suffix;
}
// 1.0 is the only case where a trailing "0" is allowed.
// 1.00 is canonicalized as 1.0.
int i = val.length() - 1;
int d = val.indexOf('.');
while (i >= d + 2 && val.charAt(i) == '0') {
val = val.substring(0, i--);
}
return val + suffix;
}
// This unescapes the string format used by doclava and so needs to be kept in sync with any
// changes made to that format.
private static String unescapeFieldStringValue(String str) {
final int N = str.length();
// If there's no special encoding strings in the string then just return it.
if (str.indexOf('\\') == -1) {
return str;
}
final StringBuilder buf = new StringBuilder(str.length());
char escaped = 0;
final int START = 0;
final int CHAR1 = 1;
final int CHAR2 = 2;
final int CHAR3 = 3;
final int CHAR4 = 4;
final int ESCAPE = 5;
int state = START;
for (int i = 0; i < N; i++) {
final char c = str.charAt(i);
switch (state) {
case START:
if (c == '\\') {
state = ESCAPE;
} else {
buf.append(c);
}
break;
case ESCAPE:
switch (c) {
case '\\':
buf.append('\\');
state = START;
break;
case 't':
buf.append('\t');
state = START;
break;
case 'b':
buf.append('\b');
state = START;
break;
case 'r':
buf.append('\r');
state = START;
break;
case 'n':
buf.append('\n');
state = START;
break;
case 'f':
buf.append('\f');
state = START;
break;
case '\'':
buf.append('\'');
state = START;
break;
case '\"':
buf.append('\"');
state = START;
break;
case 'u':
state = CHAR1;
escaped = 0;
break;
}
break;
case CHAR1:
case CHAR2:
case CHAR3:
case CHAR4:
escaped <<= 4;
if (c >= '0' && c <= '9') {
escaped |= c - '0';
} else if (c >= 'a' && c <= 'f') {
escaped |= 10 + (c - 'a');
} else if (c >= 'A' && c <= 'F') {
escaped |= 10 + (c - 'A');
} else {
throw new RuntimeException(
"bad escape sequence: '" + c + "' at pos " + i + " in: \""
+ str + "\"");
}
if (state == CHAR4) {
buf.append(escaped);
state = START;
} else {
state++;
}
break;
}
}
if (state != START) {
throw new RuntimeException("unfinished escape sequence: " + str);
}
return buf.toString();
}
/**
* Scan a class (an its entire inheritance chain) for fields.
*
* @return a {@link Map} of fieldName to {@link Field}
*/
private static Map<String, Field> buildFieldMap(Class testClass) {
Map<String, Field> fieldMap = new HashMap<>();
// Scan the superclass
if (testClass.getSuperclass() != null) {
fieldMap.putAll(buildFieldMap(testClass.getSuperclass()));
}
// Scan the interfaces
for (Class interfaceClass : testClass.getInterfaces()) {
fieldMap.putAll(buildFieldMap(interfaceClass));
}
// Check the fields in the test class
for (Field field : testClass.getDeclaredFields()) {
fieldMap.put(field.getName(), field);
}
return fieldMap;
}
/**
* Checks whether the constructor parsed from API xml file and
* Java reflection are compliant.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
*/
@SuppressWarnings("unchecked")
private void checkConstructorCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
for (JDiffClassDescription.JDiffConstructor con : classDescription.getConstructors()) {
try {
Constructor<?> c = ReflectionHelper.findMatchingConstructor(runtimeClass, con);
if (c == null) {
resultObserver.notifyFailure(FailureType.MISSING_METHOD,
con.toReadableString(classDescription.getAbsoluteClassName()),
"No method with correct signature found:" +
con.toSignatureString());
} else {
if (c.isVarArgs()) {// some method's parameter are variable args
con.mModifier |= METHOD_MODIFIER_VAR_ARGS;
}
if (c.getModifiers() != con.mModifier) {
resultObserver.notifyFailure(
FailureType.MISMATCH_METHOD,
con.toReadableString(classDescription.getAbsoluteClassName()),
"Non-compatible method found when looking for " +
con.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking constructor compliance", e);
resultObserver.notifyFailure(FailureType.CAUGHT_EXCEPTION,
con.toReadableString(classDescription.getAbsoluteClassName()),
"Exception!");
}
}
}
/**
* Checks that the method found through reflection matches the
* specification from the API xml file.
*
* @param classDescription a description of a class in an API.
* @param runtimeClass the runtime class corresponding to {@code classDescription}.
*/
private void checkMethodCompliance(JDiffClassDescription classDescription,
Class<?> runtimeClass) {
for (JDiffClassDescription.JDiffMethod method : classDescription.getMethods()) {
try {
Method m = ReflectionHelper.findMatchingMethod(runtimeClass, method);
if (m == null) {
resultObserver.notifyFailure(FailureType.MISSING_METHOD,
method.toReadableString(classDescription.getAbsoluteClassName()),
"No method with correct signature found:" +
method.toSignatureString());
} else {
if (m.isVarArgs()) {
method.mModifier |= METHOD_MODIFIER_VAR_ARGS;
}
if (m.isBridge()) {
method.mModifier |= METHOD_MODIFIER_BRIDGE;
}
if (m.isSynthetic()) {
method.mModifier |= METHOD_MODIFIER_SYNTHETIC;
}
// FIXME: A workaround to fix the final mismatch on enumeration
if (runtimeClass.isEnum() && method.mName.equals("values")) {
return;
}
if (!areMethodsModifiedCompatible(classDescription, method, m)) {
resultObserver.notifyFailure(FailureType.MISMATCH_METHOD,
method.toReadableString(classDescription.getAbsoluteClassName()),
"Non-compatible method found when looking for " +
method.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking method compliance", e);
resultObserver.notifyFailure(FailureType.CAUGHT_EXCEPTION,
method.toReadableString(classDescription.getAbsoluteClassName()),
"Exception!");
}
}
}
/**
* Checks to ensure that the modifiers value for two methods are compatible.
*
* Allowable differences are:
* - synchronized is allowed to be removed from an apiMethod
* that has it
* - the native modified is ignored
*
* @param classDescription a description of a class in an API.
* @param apiMethod the method read from the api file.
* @param reflectedMethod the method found via reflection.
*/
private static boolean areMethodsModifiedCompatible(
JDiffClassDescription classDescription,
JDiffClassDescription.JDiffMethod apiMethod,
Method reflectedMethod) {
// If the apiMethod isn't synchronized
if (((apiMethod.mModifier & Modifier.SYNCHRONIZED) == 0) &&
// but the reflected method is
((reflectedMethod.getModifiers() & Modifier.SYNCHRONIZED) != 0)) {
// that is a problem
return false;
}
// Mask off NATIVE since it is a don't care. Also mask off
// SYNCHRONIZED since we've already handled that check.
int ignoredMods = (Modifier.NATIVE | Modifier.SYNCHRONIZED | Modifier.STRICT);
int mod1 = reflectedMethod.getModifiers() & ~ignoredMods;
int mod2 = apiMethod.mModifier & ~ignoredMods;
// We can ignore FINAL for classes
if ((classDescription.getModifier() & Modifier.FINAL) != 0) {
mod1 &= ~Modifier.FINAL;
mod2 &= ~Modifier.FINAL;
}
return mod1 == mod2;
}
}