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         if (Log.isLoggable(tag, Log.DEBUG)) {
545             Log.d(tag, msg);
546         }
547     }
548 
549     // This unescapes the string format used by doclava and so needs to be kept in sync with any
550     // changes made to that format.
unescapeFieldStringValue(String str)551     private static String unescapeFieldStringValue(String str) {
552         // Skip over leading and trailing ".
553         int start = 0;
554         if (str.charAt(start) == '"') {
555             ++start;
556         }
557         int end = str.length();
558         if (str.charAt(end - 1) == '"') {
559             --end;
560         }
561 
562         // If there's no special encoding strings in the string then just return it without the
563         // leading and trailing "s.
564         if (str.indexOf('\\') == -1) {
565             return str.substring(start, end);
566         }
567 
568         final StringBuilder buf = new StringBuilder(str.length());
569         char escaped = 0;
570         final int START = 0;
571         final int CHAR1 = 1;
572         final int CHAR2 = 2;
573         final int CHAR3 = 3;
574         final int CHAR4 = 4;
575         final int ESCAPE = 5;
576         int state = START;
577 
578         for (int i = start; i < end; i++) {
579             final char c = str.charAt(i);
580             switch (state) {
581                 case START:
582                     if (c == '\\') {
583                         state = ESCAPE;
584                     } else {
585                         buf.append(c);
586                     }
587                     break;
588                 case ESCAPE:
589                     switch (c) {
590                         case '\\':
591                             buf.append('\\');
592                             state = START;
593                             break;
594                         case 't':
595                             buf.append('\t');
596                             state = START;
597                             break;
598                         case 'b':
599                             buf.append('\b');
600                             state = START;
601                             break;
602                         case 'r':
603                             buf.append('\r');
604                             state = START;
605                             break;
606                         case 'n':
607                             buf.append('\n');
608                             state = START;
609                             break;
610                         case 'f':
611                             buf.append('\f');
612                             state = START;
613                             break;
614                         case '\'':
615                             buf.append('\'');
616                             state = START;
617                             break;
618                         case '\"':
619                             buf.append('\"');
620                             state = START;
621                             break;
622                         case 'u':
623                             state = CHAR1;
624                             escaped = 0;
625                             break;
626                     }
627                     break;
628                 case CHAR1:
629                 case CHAR2:
630                 case CHAR3:
631                 case CHAR4:
632                     escaped <<= 4;
633                     if (c >= '0' && c <= '9') {
634                         escaped |= c - '0';
635                     } else if (c >= 'a' && c <= 'f') {
636                         escaped |= 10 + (c - 'a');
637                     } else if (c >= 'A' && c <= 'F') {
638                         escaped |= 10 + (c - 'A');
639                     } else {
640                         throw new RuntimeException(
641                                 "bad escape sequence: '" + c + "' at pos " + i + " in: \""
642                                         + str + "\"");
643                     }
644                     if (state == CHAR4) {
645                         buf.append(escaped);
646                         state = START;
647                     } else {
648                         state++;
649                     }
650                     break;
651             }
652         }
653         if (state != START) {
654             throw new RuntimeException("unfinished escape sequence: " + str);
655         }
656         return buf.toString();
657     }
658 
659     /**
660      * Canonicalize a possibly generic type.
661      */
canonicalizeType(String type)662     private static String canonicalizeType(String type) {
663         // Remove trailing spaces after commas.
664         return type.replace(", ", ",");
665     }
666 }
667