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