1 /* 2 * Copyright (C) 2019 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.JDiffConstructor; 19 import android.signature.cts.JDiffClassDescription.JDiffField; 20 import android.signature.cts.JDiffClassDescription.JDiffMethod; 21 import android.util.Log; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.lang.reflect.Modifier; 25 import java.util.Collections; 26 import java.util.HashSet; 27 import java.util.Set; 28 import java.util.Spliterator; 29 import java.util.function.Consumer; 30 import java.util.stream.Stream; 31 import java.util.stream.StreamSupport; 32 import java.util.zip.GZIPInputStream; 33 34 import org.xmlpull.v1.XmlPullParser; 35 import org.xmlpull.v1.XmlPullParserException; 36 import org.xmlpull.v1.XmlPullParserFactory; 37 38 /** 39 * Parser for the XML representation of an API specification. 40 */ 41 class XmlApiParser extends ApiParser { 42 43 private static final String TAG_ROOT = "api"; 44 45 private static final String TAG_PACKAGE = "package"; 46 47 private static final String TAG_CLASS = "class"; 48 49 private static final String TAG_INTERFACE = "interface"; 50 51 private static final String TAG_IMPLEMENTS = "implements"; 52 53 private static final String TAG_CONSTRUCTOR = "constructor"; 54 55 private static final String TAG_METHOD = "method"; 56 57 private static final String TAG_PARAM = "parameter"; 58 59 private static final String TAG_EXCEPTION = "exception"; 60 61 private static final String TAG_FIELD = "field"; 62 63 private static final String ATTRIBUTE_NAME = "name"; 64 65 private static final String ATTRIBUTE_TYPE = "type"; 66 67 private static final String ATTRIBUTE_VALUE = "value"; 68 69 private static final String ATTRIBUTE_EXTENDS = "extends"; 70 71 private static final String ATTRIBUTE_RETURN = "return"; 72 73 private static final String MODIFIER_ABSTRACT = "abstract"; 74 75 private static final String MODIFIER_FINAL = "final"; 76 77 private static final String MODIFIER_NATIVE = "native"; 78 79 private static final String MODIFIER_PRIVATE = "private"; 80 81 private static final String MODIFIER_PROTECTED = "protected"; 82 83 private static final String MODIFIER_PUBLIC = "public"; 84 85 private static final String MODIFIER_STATIC = "static"; 86 87 private static final String MODIFIER_SYNCHRONIZED = "synchronized"; 88 89 private static final String MODIFIER_TRANSIENT = "transient"; 90 91 private static final String MODIFIER_VOLATILE = "volatile"; 92 93 private static final String MODIFIER_VISIBILITY = "visibility"; 94 95 private static final String MODIFIER_ENUM_CONSTANT = "metalava:enumConstant"; 96 97 private static final Set<String> KEY_TAG_SET; 98 99 static { 100 KEY_TAG_SET = new HashSet<>(); Collections.addAll(KEY_TAG_SET, TAG_PACKAGE, TAG_CLASS, TAG_INTERFACE, TAG_IMPLEMENTS, TAG_CONSTRUCTOR, TAG_METHOD, TAG_PARAM, TAG_EXCEPTION, TAG_FIELD)101 Collections.addAll(KEY_TAG_SET, 102 TAG_PACKAGE, 103 TAG_CLASS, 104 TAG_INTERFACE, 105 TAG_IMPLEMENTS, 106 TAG_CONSTRUCTOR, 107 TAG_METHOD, 108 TAG_PARAM, 109 TAG_EXCEPTION, 110 TAG_FIELD); 111 } 112 113 private final String tag; 114 private final boolean gzipped; 115 116 private final XmlPullParserFactory factory; 117 XmlApiParser(String tag, boolean gzipped)118 XmlApiParser(String tag, boolean gzipped) { 119 this.tag = tag; 120 this.gzipped = gzipped; 121 try { 122 factory = XmlPullParserFactory.newInstance(); 123 } catch (XmlPullParserException e) { 124 throw new RuntimeException(e); 125 } 126 } 127 128 /** 129 * Load field information from xml to memory. 130 * 131 * @param currentClass 132 * of the class being examined which will be shown in error messages 133 * @param parser 134 * The XmlPullParser which carries the xml information. 135 * @return the new field 136 */ loadFieldInfo( JDiffClassDescription currentClass, XmlPullParser parser)137 private static JDiffField loadFieldInfo( 138 JDiffClassDescription currentClass, XmlPullParser parser) { 139 String fieldName = parser.getAttributeValue(null, ATTRIBUTE_NAME); 140 String fieldType = canonicalizeType(parser.getAttributeValue(null, ATTRIBUTE_TYPE)); 141 int modifier = jdiffModifierToReflectionFormat(currentClass.getClassName(), parser); 142 String value = parser.getAttributeValue(null, ATTRIBUTE_VALUE); 143 144 // Canonicalize the expected value to ensure that it is consistent with the values obtained 145 // using reflection by ApiComplianceChecker.getFieldValueAsString(...). 146 if (value != null) { 147 148 // An unquoted null String value actually means null. It cannot be confused with a 149 // String containing the word null as that would be surrounded with double quotes. 150 if (value.equals("null")) { 151 value = null; 152 } else { 153 switch (fieldType) { 154 case "java.lang.String": 155 value = unescapeFieldStringValue(value); 156 break; 157 158 case "char": 159 // A character may be encoded in XML as its numeric value. Convert it to a 160 // string containing the single character. 161 try { 162 char c = (char) Integer.parseInt(value); 163 value = String.valueOf(c); 164 } catch (NumberFormatException e) { 165 // If not, it must be a string "'?'". Extract the second character, 166 // but we need to unescape it. 167 int len = value.length(); 168 if (value.charAt(0) == '\'' && value.charAt(len - 1) == '\'') { 169 String sub = value.substring(1, len - 1); 170 value = unescapeFieldStringValue(sub); 171 } else { 172 throw new NumberFormatException(String.format( 173 "Cannot parse the value of field '%s': invalid number '%s'", 174 fieldName, value)); 175 } 176 } 177 break; 178 179 case "double": 180 switch (value) { 181 case "(-1.0/0.0)": 182 value = "-Infinity"; 183 break; 184 case "(0.0/0.0)": 185 value = "NaN"; 186 break; 187 case "(1.0/0.0)": 188 value = "Infinity"; 189 break; 190 } 191 break; 192 193 case "float": 194 switch (value) { 195 case "(-1.0f/0.0f)": 196 value = "-Infinity"; 197 break; 198 case "(0.0f/0.0f)": 199 value = "NaN"; 200 break; 201 case "(1.0f/0.0f)": 202 value = "Infinity"; 203 break; 204 default: 205 // Remove the trailing f. 206 if (value.endsWith("f")) { 207 value = value.substring(0, value.length() - 1); 208 } 209 } 210 break; 211 212 case "long": 213 // Remove the trailing L. 214 if (value.endsWith("L")) { 215 value = value.substring(0, value.length() - 1); 216 } 217 break; 218 } 219 } 220 } 221 222 return new JDiffField(fieldName, fieldType, modifier, value); 223 } 224 225 /** 226 * Load method information from xml to memory. 227 * 228 * @param className 229 * of the class being examined which will be shown in error messages 230 * @param parser 231 * The XmlPullParser which carries the xml information. 232 * @return the newly loaded method. 233 */ loadMethodInfo(String className, XmlPullParser parser)234 private static JDiffMethod loadMethodInfo(String className, XmlPullParser parser) { 235 String methodName = parser.getAttributeValue(null, ATTRIBUTE_NAME); 236 String returnType = parser.getAttributeValue(null, ATTRIBUTE_RETURN); 237 int modifier = jdiffModifierToReflectionFormat(className, parser); 238 return new JDiffMethod(methodName, modifier, canonicalizeType(returnType)); 239 } 240 241 /** 242 * Load constructor information from xml to memory. 243 * 244 * @param parser 245 * The XmlPullParser which carries the xml information. 246 * @param currentClass 247 * the current class being loaded. 248 * @return the new constructor 249 */ loadConstructorInfo( XmlPullParser parser, JDiffClassDescription currentClass)250 private static JDiffConstructor loadConstructorInfo( 251 XmlPullParser parser, JDiffClassDescription currentClass) { 252 String name = currentClass.getClassName(); 253 int modifier = jdiffModifierToReflectionFormat(name, parser); 254 return new JDiffConstructor(name, modifier); 255 } 256 257 /** 258 * Load class or interface information to memory. 259 * 260 * @param parser 261 * The XmlPullParser which carries the xml information. 262 * @param isInterface 263 * true if the current class is an interface, otherwise is false. 264 * @param pkg 265 * the name of the java package this class can be found in. 266 * @return the new class description. 267 */ loadClassInfo( XmlPullParser parser, boolean isInterface, String pkg)268 private static JDiffClassDescription loadClassInfo( 269 XmlPullParser parser, boolean isInterface, String pkg) { 270 String className = parser.getAttributeValue(null, ATTRIBUTE_NAME); 271 JDiffClassDescription currentClass = new JDiffClassDescription(pkg, className); 272 273 currentClass.setType(isInterface ? JDiffClassDescription.JDiffType.INTERFACE : 274 JDiffClassDescription.JDiffType.CLASS); 275 276 String superClass = stripGenericsArgs(parser.getAttributeValue(null, ATTRIBUTE_EXTENDS)); 277 int modifiers = jdiffModifierToReflectionFormat(className, parser); 278 if (isInterface) { 279 if (superClass != null) { 280 currentClass.addImplInterface(superClass); 281 } 282 } else { 283 if ("java.lang.annotation.Annotation".equals(superClass)) { 284 // ApiComplianceChecker expects "java.lang.annotation.Annotation" to be in 285 // the "impl interfaces". 286 currentClass.addImplInterface(superClass); 287 } else { 288 currentClass.setExtendsClass(superClass); 289 } 290 } 291 currentClass.setModifier(modifiers); 292 return currentClass; 293 } 294 295 /** 296 * Transfer string modifier to int one. 297 * 298 * @param name 299 * of the class/method/field being examined which will be shown in error messages 300 * @param parser 301 * XML resource parser 302 * @return converted modifier 303 */ jdiffModifierToReflectionFormat(String name, XmlPullParser parser)304 private static int jdiffModifierToReflectionFormat(String name, XmlPullParser parser) { 305 int modifier = 0; 306 for (int i = 0; i < parser.getAttributeCount(); i++) { 307 modifier |= modifierDescriptionToReflectedType(name, parser.getAttributeName(i), 308 parser.getAttributeValue(i)); 309 } 310 return modifier; 311 } 312 313 /** 314 * Convert string modifier to int modifier. 315 * 316 * @param name 317 * of the class/method/field being examined which will be shown in error messages 318 * @param key 319 * modifier name 320 * @param value 321 * modifier value 322 * @return converted modifier value 323 */ modifierDescriptionToReflectedType(String name, String key, String value)324 private static int modifierDescriptionToReflectedType(String name, String key, String value) { 325 switch (key) { 326 case MODIFIER_ABSTRACT: 327 return value.equals("true") ? Modifier.ABSTRACT : 0; 328 case MODIFIER_FINAL: 329 return value.equals("true") ? Modifier.FINAL : 0; 330 case MODIFIER_NATIVE: 331 return value.equals("true") ? Modifier.NATIVE : 0; 332 case MODIFIER_STATIC: 333 return value.equals("true") ? Modifier.STATIC : 0; 334 case MODIFIER_SYNCHRONIZED: 335 return value.equals("true") ? Modifier.SYNCHRONIZED : 0; 336 case MODIFIER_TRANSIENT: 337 return value.equals("true") ? Modifier.TRANSIENT : 0; 338 case MODIFIER_VOLATILE: 339 return value.equals("true") ? Modifier.VOLATILE : 0; 340 case MODIFIER_VISIBILITY: 341 switch (value) { 342 case MODIFIER_PRIVATE: 343 throw new RuntimeException("Private visibility found in API spec: " + name); 344 case MODIFIER_PROTECTED: 345 return Modifier.PROTECTED; 346 case MODIFIER_PUBLIC: 347 return Modifier.PUBLIC; 348 case "": 349 // If the visibility is "", it means it has no modifier. 350 // which is package private. We should return 0 for this modifier. 351 return 0; 352 default: 353 throw new RuntimeException("Unknown modifier found in API spec: " + value); 354 } 355 case MODIFIER_ENUM_CONSTANT: 356 return value.equals("true") ? ApiComplianceChecker.FIELD_MODIFIER_ENUM_VALUE : 0; 357 } 358 return 0; 359 } 360 361 @Override parseAsStream(VirtualPath path)362 public Stream<JDiffClassDescription> parseAsStream(VirtualPath path) { 363 XmlPullParser parser; 364 try { 365 parser = factory.newPullParser(); 366 InputStream input = path.newInputStream(); 367 if (gzipped) { 368 input = new GZIPInputStream(input); 369 } 370 parser.setInput(input, null); 371 return StreamSupport 372 .stream(new ClassDescriptionSpliterator(parser), false); 373 } catch (XmlPullParserException | IOException e) { 374 throw new RuntimeException("Could not parse " + path, e); 375 } 376 } 377 stripGenericsArgs(String typeName)378 private static String stripGenericsArgs(String typeName) { 379 return typeName == null ? null : typeName.replaceFirst("<.*", ""); 380 } 381 382 private class ClassDescriptionSpliterator implements Spliterator<JDiffClassDescription> { 383 384 private final XmlPullParser parser; 385 386 JDiffClassDescription currentClass = null; 387 388 String currentPackage = ""; 389 390 JDiffMethod currentMethod = null; 391 ClassDescriptionSpliterator(XmlPullParser parser)392 ClassDescriptionSpliterator(XmlPullParser parser) 393 throws IOException, XmlPullParserException { 394 this.parser = parser; 395 logd(String.format("Name: %s", parser.getName())); 396 logd(String.format("Text: %s", parser.getText())); 397 logd(String.format("Namespace: %s", parser.getNamespace())); 398 logd(String.format("Line Number: %s", parser.getLineNumber())); 399 logd(String.format("Column Number: %s", parser.getColumnNumber())); 400 logd(String.format("Position Description: %s", parser.getPositionDescription())); 401 beginDocument(parser); 402 } 403 404 @Override tryAdvance(Consumer<? super JDiffClassDescription> action)405 public boolean tryAdvance(Consumer<? super JDiffClassDescription> action) { 406 JDiffClassDescription classDescription; 407 try { 408 classDescription = next(); 409 } catch (IOException | XmlPullParserException e) { 410 throw new RuntimeException(e); 411 } 412 413 if (classDescription == null) { 414 return false; 415 } 416 action.accept(classDescription); 417 return true; 418 } 419 420 @Override trySplit()421 public Spliterator<JDiffClassDescription> trySplit() { 422 return null; 423 } 424 425 @Override estimateSize()426 public long estimateSize() { 427 return Long.MAX_VALUE; 428 } 429 430 @Override characteristics()431 public int characteristics() { 432 return ORDERED | DISTINCT | NONNULL | IMMUTABLE; 433 } 434 beginDocument(XmlPullParser parser)435 private void beginDocument(XmlPullParser parser) 436 throws XmlPullParserException, IOException { 437 int type; 438 do { 439 type = parser.next(); 440 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT); 441 442 if (type != XmlPullParser.START_TAG) { 443 throw new XmlPullParserException("No start tag found"); 444 } 445 446 if (!parser.getName().equals(TAG_ROOT)) { 447 throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + 448 ", expected " + TAG_ROOT); 449 } 450 } 451 next()452 private JDiffClassDescription next() throws IOException, XmlPullParserException { 453 int type; 454 while (true) { 455 do { 456 type = parser.next(); 457 } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT 458 && type != XmlPullParser.END_TAG); 459 460 if (type == XmlPullParser.END_DOCUMENT) { 461 logd("Reached end of document"); 462 break; 463 } 464 465 String tagname = parser.getName(); 466 if (type == XmlPullParser.END_TAG) { 467 if (TAG_CLASS.equals(tagname) || TAG_INTERFACE.equals(tagname)) { 468 logd("Reached end of class: " + currentClass); 469 return currentClass; 470 } else if (TAG_PACKAGE.equals(tagname)) { 471 currentPackage = ""; 472 } 473 continue; 474 } 475 476 if (!KEY_TAG_SET.contains(tagname)) { 477 continue; 478 } 479 480 switch (tagname) { 481 case TAG_PACKAGE: 482 currentPackage = parser.getAttributeValue(null, ATTRIBUTE_NAME); 483 break; 484 485 case TAG_CLASS: 486 currentClass = loadClassInfo(parser, false, currentPackage); 487 break; 488 489 case TAG_INTERFACE: 490 currentClass = loadClassInfo(parser, true, currentPackage); 491 break; 492 493 case TAG_IMPLEMENTS: 494 currentClass.addImplInterface(stripGenericsArgs( 495 parser.getAttributeValue(null, ATTRIBUTE_NAME))); 496 break; 497 498 case TAG_CONSTRUCTOR: 499 JDiffConstructor constructor = 500 loadConstructorInfo(parser, currentClass); 501 currentClass.addConstructor(constructor); 502 currentMethod = constructor; 503 break; 504 505 case TAG_METHOD: 506 currentMethod = loadMethodInfo(currentClass.getClassName(), parser); 507 currentClass.addMethod(currentMethod); 508 break; 509 510 case TAG_PARAM: 511 String paramType = parser.getAttributeValue(null, ATTRIBUTE_TYPE); 512 currentMethod.addParam(canonicalizeType(paramType)); 513 break; 514 515 case TAG_EXCEPTION: 516 currentMethod.addException(parser.getAttributeValue(null, ATTRIBUTE_TYPE)); 517 break; 518 519 case TAG_FIELD: 520 JDiffField field = loadFieldInfo(currentClass, parser); 521 currentClass.addField(field); 522 break; 523 524 default: 525 throw new RuntimeException("unknown tag exception:" + tagname); 526 } 527 528 if (currentPackage != null) { 529 logd(String.format("currentPackage: %s", currentPackage)); 530 } 531 if (currentClass != null) { 532 logd(String.format("currentClass: %s", currentClass.toSignatureString())); 533 } 534 if (currentMethod != null) { 535 logd(String.format("currentMethod: %s", currentMethod.toSignatureString())); 536 } 537 } 538 539 return null; 540 } 541 } 542 logd(String msg)543 private void logd(String msg) { 544 Log.d(tag, msg); 545 } 546 547 // This unescapes the string format used by doclava and so needs to be kept in sync with any 548 // changes made to that format. unescapeFieldStringValue(String str)549 private static String unescapeFieldStringValue(String str) { 550 // Skip over leading and trailing ". 551 int start = 0; 552 if (str.charAt(start) == '"') { 553 ++start; 554 } 555 int end = str.length(); 556 if (str.charAt(end - 1) == '"') { 557 --end; 558 } 559 560 // If there's no special encoding strings in the string then just return it without the 561 // leading and trailing "s. 562 if (str.indexOf('\\') == -1) { 563 return str.substring(start, end); 564 } 565 566 final StringBuilder buf = new StringBuilder(str.length()); 567 char escaped = 0; 568 final int START = 0; 569 final int CHAR1 = 1; 570 final int CHAR2 = 2; 571 final int CHAR3 = 3; 572 final int CHAR4 = 4; 573 final int ESCAPE = 5; 574 int state = START; 575 576 for (int i = start; i < end; i++) { 577 final char c = str.charAt(i); 578 switch (state) { 579 case START: 580 if (c == '\\') { 581 state = ESCAPE; 582 } else { 583 buf.append(c); 584 } 585 break; 586 case ESCAPE: 587 switch (c) { 588 case '\\': 589 buf.append('\\'); 590 state = START; 591 break; 592 case 't': 593 buf.append('\t'); 594 state = START; 595 break; 596 case 'b': 597 buf.append('\b'); 598 state = START; 599 break; 600 case 'r': 601 buf.append('\r'); 602 state = START; 603 break; 604 case 'n': 605 buf.append('\n'); 606 state = START; 607 break; 608 case 'f': 609 buf.append('\f'); 610 state = START; 611 break; 612 case '\'': 613 buf.append('\''); 614 state = START; 615 break; 616 case '\"': 617 buf.append('\"'); 618 state = START; 619 break; 620 case 'u': 621 state = CHAR1; 622 escaped = 0; 623 break; 624 } 625 break; 626 case CHAR1: 627 case CHAR2: 628 case CHAR3: 629 case CHAR4: 630 escaped <<= 4; 631 if (c >= '0' && c <= '9') { 632 escaped |= c - '0'; 633 } else if (c >= 'a' && c <= 'f') { 634 escaped |= 10 + (c - 'a'); 635 } else if (c >= 'A' && c <= 'F') { 636 escaped |= 10 + (c - 'A'); 637 } else { 638 throw new RuntimeException( 639 "bad escape sequence: '" + c + "' at pos " + i + " in: \"" 640 + str + "\""); 641 } 642 if (state == CHAR4) { 643 buf.append(escaped); 644 state = START; 645 } else { 646 state++; 647 } 648 break; 649 } 650 } 651 if (state != START) { 652 throw new RuntimeException("unfinished escape sequence: " + str); 653 } 654 return buf.toString(); 655 } 656 657 /** 658 * Canonicalize a possibly generic type. 659 */ canonicalizeType(String type)660 private static String canonicalizeType(String type) { 661 // Remove trailing spaces after commas. 662 return type.replace(", ", ","); 663 } 664 } 665