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 
17 package com.android.class2greylist;
18 
19 import java.util.ArrayList;
20 import java.util.List;
21 import java.util.Objects;
22 
23 /**
24  * Class which can parse either dex style signatures (e.g. Lfoo/bar/baz$bat;->foo()V) or javadoc
25  * links to class members (e.g. {@link #toString()} or {@link java.util.List#clear()}).
26  */
27 public class ApiComponents {
28     private static final String PRIMITIVE_TYPES = "ZBCSIJFD";
29     private final PackageAndClassName mPackageAndClassName;
30     // The reference can be just to a class, in which case mMemberName should be empty.
31     private final String mMemberName;
32     // If the member being referenced is a field, this will always be empty.
33     private final String mMethodParameterTypes;
34 
ApiComponents(PackageAndClassName packageAndClassName, String memberName, String methodParameterTypes)35     private ApiComponents(PackageAndClassName packageAndClassName, String memberName,
36             String methodParameterTypes) {
37         mPackageAndClassName = packageAndClassName;
38         mMemberName = memberName;
39         mMethodParameterTypes = methodParameterTypes;
40     }
41 
42     @Override
toString()43     public String toString() {
44         StringBuilder sb = new StringBuilder()
45                 .append(mPackageAndClassName.packageName)
46                 .append(".")
47                 .append(mPackageAndClassName.className);
48         if (!mMemberName.isEmpty()) {
49             sb.append("#").append(mMemberName).append("(").append(mMethodParameterTypes).append(
50                     ")");
51         }
52         return sb.toString();
53     }
54 
getPackageAndClassName()55     public PackageAndClassName getPackageAndClassName() {
56         return mPackageAndClassName;
57     }
58 
getMemberName()59     public String getMemberName() {
60         return mMemberName;
61     }
62 
getMethodParameterTypes()63     public String getMethodParameterTypes() {
64         return mMethodParameterTypes;
65     }
66 
67     /**
68      * Parse a JNI class descriptor. e.g. Lfoo/bar/Baz;
69      *
70      * @param sc Cursor over string assumed to contain a JNI class descriptor.
71      * @return The fully qualified class, in 'dot notation' (e.g. foo.bar.Baz for a class named Baz
72      * in the foo.bar package). The cursor will be placed after the semicolon.
73      */
parseJNIClassDescriptor(StringCursor sc)74     private static String parseJNIClassDescriptor(StringCursor sc)
75             throws SignatureSyntaxError, StringCursorOutOfBoundsException {
76         if (sc.peek() != 'L') {
77             throw new SignatureSyntaxError(
78                     "Expected JNI class descriptor to start with L, but instead got " + sc.peek(),
79                     sc);
80         }
81         // Consume the L.
82         sc.next();
83         int semiColonPos = sc.find(';');
84         if (semiColonPos == -1) {
85             throw new SignatureSyntaxError("Expected semicolon at the end of JNI class descriptor",
86                     sc);
87         }
88         String jniClassDescriptor = sc.next(semiColonPos);
89         // Consume the semicolon.
90         sc.next();
91         return jniClassDescriptor.replace("/", ".");
92     }
93 
94     /**
95      * Parse a primitive JNI type
96      *
97      * @param sc Cursor over a string assumed to contain a primitive JNI type.
98      * @return String containing parsed primitive JNI type.
99      */
parseJNIPrimitiveType(StringCursor sc)100     private static String parseJNIPrimitiveType(StringCursor sc)
101             throws SignatureSyntaxError, StringCursorOutOfBoundsException {
102         char c = sc.next();
103         switch (c) {
104             case 'Z':
105                 return "boolean";
106             case 'B':
107                 return "byte";
108             case 'C':
109                 return "char";
110             case 'S':
111                 return "short";
112             case 'I':
113                 return "int";
114             case 'J':
115                 return "long";
116             case 'F':
117                 return "float";
118             case 'D':
119                 return "double";
120             default:
121                 throw new SignatureSyntaxError(c + " is not a primitive type!", sc);
122         }
123     }
124 
125     /**
126      * Parse a JNI type; can be either a primitive or object type. Arrays are handled separately.
127      *
128      * @param sc Cursor over the string assumed to contain a JNI type.
129      * @return String containing parsed JNI type.
130      */
parseJniTypeWithoutArrayDimensions(StringCursor sc)131     private static String parseJniTypeWithoutArrayDimensions(StringCursor sc)
132             throws SignatureSyntaxError, StringCursorOutOfBoundsException {
133         char c = sc.peek();
134         if (PRIMITIVE_TYPES.indexOf(c) != -1) {
135             return parseJNIPrimitiveType(sc);
136         } else if (c == 'L') {
137             return parseJNIClassDescriptor(sc);
138         }
139         throw new SignatureSyntaxError("Illegal token " + c + " within signature", sc);
140     }
141 
142     /**
143      * Parse a JNI type.
144      *
145      * This parameter can be an array, in which case it will be preceded by a number of open square
146      * brackets (corresponding to its dimensionality)
147      *
148      * @param sc Cursor over the string assumed to contain a JNI type.
149      * @return Same as {@link #parseJniTypeWithoutArrayDimensions}, but also handle arrays.
150      */
parseJniType(StringCursor sc)151     private static String parseJniType(StringCursor sc)
152             throws SignatureSyntaxError, StringCursorOutOfBoundsException {
153         int arrayDimension = 0;
154         while (sc.peek() == '[') {
155             ++arrayDimension;
156             sc.next();
157         }
158         StringBuilder sb = new StringBuilder();
159         sb.append(parseJniTypeWithoutArrayDimensions(sc));
160         for (int i = 0; i < arrayDimension; ++i) {
161             sb.append("[]");
162         }
163         return sb.toString();
164     }
165 
166     /**
167      * Converts the parameters of method from JNI notation to Javadoc link notation. e.g.
168      * "(IILfoo/bar/Baz;)V" turns into "int, int, foo.bar.Baz". The parentheses and return type are
169      * discarded.
170      *
171      * @param sc Cursor over the string assumed to contain a JNI method parameters.
172      * @return Comma separated list of parameter types.
173      */
convertJNIMethodParametersToJavadoc(StringCursor sc)174     private static String convertJNIMethodParametersToJavadoc(StringCursor sc)
175             throws SignatureSyntaxError, StringCursorOutOfBoundsException {
176         List<String> methodParameterTypes = new ArrayList<>();
177         if (sc.next() != '(') {
178             throw new IllegalArgumentException("Trying to parse method params of an invalid dex " +
179                     "signature: " + sc.getOriginalString());
180         }
181         while (sc.peek() != ')') {
182             methodParameterTypes.add(parseJniType(sc));
183         }
184         return String.join(", ", methodParameterTypes);
185     }
186 
187     /**
188      * Generate ApiComponents from a dex signature.
189      *
190      * This is used to extract the necessary context for an alternative API to try to infer missing
191      * information.
192      *
193      * @param signature Dex signature.
194      * @return ApiComponents instance with populated package, class name, and parameter types if
195      * applicable.
196      */
fromDexSignature(String signature)197     public static ApiComponents fromDexSignature(String signature) throws SignatureSyntaxError {
198         StringCursor sc = new StringCursor(signature);
199         try {
200             String fullyQualifiedClass = parseJNIClassDescriptor(sc);
201 
202             PackageAndClassName packageAndClassName =
203                     PackageAndClassName.splitClassName(fullyQualifiedClass);
204             if (!sc.peek(2).equals("->")) {
205                 throw new SignatureSyntaxError("Expected '->'", sc);
206             }
207             // Consume "->"
208             sc.next(2);
209             String memberName = "";
210             String methodParameterTypes = "";
211             int leftParenPos = sc.find('(');
212             if (leftParenPos != -1) {
213                 memberName = sc.next(leftParenPos);
214                 methodParameterTypes = convertJNIMethodParametersToJavadoc(sc);
215             } else {
216                 int colonPos = sc.find(':');
217                 if (colonPos == -1) {
218                     throw new IllegalArgumentException("Expected : or -> beyond position "
219                             + sc.position() + " in " + signature);
220                 } else {
221                     memberName = sc.next(colonPos);
222                     // Consume the ':'.
223                     sc.next();
224                     // Consume the type.
225                     parseJniType(sc);
226                 }
227             }
228             return new ApiComponents(packageAndClassName, memberName, methodParameterTypes);
229         } catch (StringCursorOutOfBoundsException e) {
230             throw new SignatureSyntaxError(
231                     "Unexpectedly reached end of string while trying to parse signature ", sc);
232         }
233     }
234 
235     /**
236      * Generate ApiComponents from a link tag.
237      *
238      * @param linkTag          The contents of a link tag.
239      * @param contextSignature The signature of the private API that this is an alternative for.
240      *                         Used to infer unspecified components.
241      */
fromLinkTag(String linkTag, String contextSignature)242     public static ApiComponents fromLinkTag(String linkTag, String contextSignature)
243             throws JavadocLinkSyntaxError {
244         ApiComponents contextAlternative;
245         try {
246             contextAlternative = fromDexSignature(contextSignature);
247         } catch (SignatureSyntaxError e) {
248             throw new RuntimeException(
249                     "Failed to parse the context signature for public alternative!");
250         }
251         StringCursor sc = new StringCursor(linkTag);
252         try {
253 
254             String memberName = "";
255             String methodParameterTypes = "";
256 
257             int tagPos = sc.find('#');
258             String fullyQualifiedClassName = sc.next(tagPos);
259 
260             PackageAndClassName packageAndClassName =
261                     PackageAndClassName.splitClassName(fullyQualifiedClassName);
262 
263             if (packageAndClassName.packageName.isEmpty()) {
264                 packageAndClassName.packageName = contextAlternative.getPackageAndClassName()
265                         .packageName;
266             }
267 
268             if (packageAndClassName.className.isEmpty()) {
269                 packageAndClassName.className = contextAlternative.getPackageAndClassName()
270                         .className;
271             }
272 
273             if (tagPos == -1) {
274                 // This suggested alternative is just a class. We can allow that.
275                 return new ApiComponents(packageAndClassName, "", "");
276             } else {
277                 // Consume the #.
278                 sc.next();
279             }
280 
281             int leftParenPos = sc.find('(');
282             memberName = sc.next(leftParenPos);
283             if (leftParenPos != -1) {
284                 // Consume the '('.
285                 sc.next();
286                 int rightParenPos = sc.find(')');
287                 if (rightParenPos == -1) {
288                     throw new JavadocLinkSyntaxError(
289                             "Linked method is missing a closing parenthesis", sc);
290                 } else {
291                     methodParameterTypes = sc.next(rightParenPos);
292                 }
293             }
294 
295             return new ApiComponents(packageAndClassName, memberName, methodParameterTypes);
296         } catch (StringCursorOutOfBoundsException e) {
297             throw new JavadocLinkSyntaxError(
298                     "Unexpectedly reached end of string while trying to parse javadoc link", sc);
299         }
300     }
301 
302     @Override
equals(Object obj)303     public boolean equals(Object obj) {
304         if (!(obj instanceof ApiComponents)) {
305             return false;
306         }
307         ApiComponents other = (ApiComponents) obj;
308         return mPackageAndClassName.equals(other.mPackageAndClassName) && mMemberName.equals(
309                 other.mMemberName) && mMethodParameterTypes.equals(other.mMethodParameterTypes);
310     }
311 
312     @Override
hashCode()313     public int hashCode() {
314         return Objects.hash(mPackageAndClassName, mMemberName, mMethodParameterTypes);
315     }
316 
317     /**
318      * Less restrictive comparator to use in case a link tag is missing a method's parameters.
319      * e.g. foo.bar.Baz#foo will be considered the same as foo.bar.Baz#foo(int, int) and
320      * foo.bar.Baz#foo(long, long). If the class only has one method with that name, then specifying
321      * its parameter types is optional within the link tag.
322      */
equalsIgnoringParam(ApiComponents other)323     public boolean equalsIgnoringParam(ApiComponents other) {
324         return mPackageAndClassName.equals(other.mPackageAndClassName) &&
325                 mMemberName.equals(other.mMemberName);
326     }
327 }
328