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