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.class2nonsdklist; 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