1 /* 2 * Copyright (C) 2010 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.apkcheck; 18 19 import org.xml.sax.*; 20 import org.xml.sax.helpers.*; 21 import java.io.FileReader; 22 import java.io.IOException; 23 import java.io.Reader; 24 import java.util.ArrayList; 25 import java.util.HashSet; 26 import java.util.Iterator; 27 28 29 /** 30 * Checks an APK's dependencies against the published API specification. 31 * 32 * We need to read two XML files (spec and APK) and perform some operations 33 * on the elements. The file formats are similar but not identical, so 34 * we distill it down to common elements. 35 * 36 * We may also want to read some additional API lists representing 37 * libraries that would be included with a "uses-library" directive. 38 * 39 * For performance we want to allow processing of multiple APKs so 40 * we don't have to re-parse the spec file each time. 41 */ 42 public class ApkCheck { 43 /* keep track of current APK file name, for error messages */ 44 private static ApiList sCurrentApk; 45 46 /* show warnings? */ 47 private static boolean sShowWarnings = false; 48 /* show errors? */ 49 private static boolean sShowErrors = true; 50 51 /* names of packages we're allowed to ignore */ 52 private static HashSet<String> sIgnorablePackages = new HashSet<String>(); 53 54 55 /** 56 * Program entry point. 57 */ main(String[] args)58 public static void main(String[] args) { 59 ApiList apiDescr = new ApiList("public-api"); 60 61 if (args.length < 2) { 62 usage(); 63 return; 64 } 65 66 /* process args */ 67 int idx; 68 for (idx = 0; idx < args.length; idx++) { 69 if (args[idx].equals("--help")) { 70 usage(); 71 return; 72 } else if (args[idx].startsWith("--uses-library=")) { 73 String libName = args[idx].substring(args[idx].indexOf('=')+1); 74 if ("BUILTIN".equals(libName)) { 75 Reader reader = Builtin.getReader(); 76 if (!parseXml(apiDescr, reader, "BUILTIN")) 77 return; 78 } else { 79 if (!parseApiDescr(apiDescr, libName)) 80 return; 81 } 82 } else if (args[idx].startsWith("--ignore-package=")) { 83 String pkgName = args[idx].substring(args[idx].indexOf('=')+1); 84 sIgnorablePackages.add(pkgName); 85 } else if (args[idx].equals("--warn")) { 86 sShowWarnings = true; 87 } else if (args[idx].equals("--no-warn")) { 88 sShowWarnings = false; 89 } else if (args[idx].equals("--error")) { 90 sShowErrors = true; 91 } else if (args[idx].equals("--no-error")) { 92 sShowErrors = false; 93 94 } else if (args[idx].startsWith("--")) { 95 if (args[idx].equals("--")) { 96 // remainder are filenames, even if they start with "--" 97 idx++; 98 break; 99 } else { 100 // unknown option specified 101 System.err.println("ERROR: unknown option " + 102 args[idx] + " (use \"--help\" for usage info)"); 103 return; 104 } 105 } else { 106 break; 107 } 108 } 109 if (idx > args.length - 2) { 110 usage(); 111 return; 112 } 113 114 /* parse base API description */ 115 if (!parseApiDescr(apiDescr, args[idx++])) 116 return; 117 118 /* "flatten" superclasses and interfaces */ 119 sCurrentApk = apiDescr; 120 flattenInherited(apiDescr); 121 122 /* walk through list of libs we want to scan */ 123 for ( ; idx < args.length; idx++) { 124 ApiList apkDescr = new ApiList(args[idx]); 125 sCurrentApk = apkDescr; 126 boolean success = parseApiDescr(apkDescr, args[idx]); 127 if (!success) { 128 if (idx < args.length-1) 129 System.err.println("Skipping..."); 130 continue; 131 } 132 133 check(apiDescr, apkDescr); 134 System.out.println(args[idx] + ": summary: " + 135 apkDescr.getErrorCount() + " errors, " + 136 apkDescr.getWarningCount() + " warnings\n"); 137 } 138 } 139 140 /** 141 * Prints usage statement. 142 */ usage()143 static void usage() { 144 System.err.println("Android APK checker v1.0"); 145 System.err.println("Copyright (C) 2010 The Android Open Source Project\n"); 146 System.err.println("Usage: apkcheck [options] public-api.xml apk1.xml ...\n"); 147 System.err.println("Options:"); 148 System.err.println(" --help show this message"); 149 System.err.println(" --uses-library=lib.xml load additional public API list"); 150 System.err.println(" --ignore-package=pkg don't show errors for references to this package"); 151 System.err.println(" --[no-]warn enable or disable display of warnings"); 152 System.err.println(" --[no-]error enable or disable display of errors"); 153 } 154 155 /** 156 * Opens the file and passes it to parseXml. 157 * 158 * TODO: allow '-' as an alias for stdin? 159 */ parseApiDescr(ApiList apiList, String fileName)160 static boolean parseApiDescr(ApiList apiList, String fileName) { 161 boolean result = false; 162 163 try { 164 FileReader fileReader = new FileReader(fileName); 165 result = parseXml(apiList, fileReader, fileName); 166 fileReader.close(); 167 } catch (IOException ioe) { 168 System.err.println("Error opening " + fileName); 169 } 170 return result; 171 } 172 173 /** 174 * Parses an XML file holding an API description. 175 * 176 * @param fileReader Data source. 177 * @param apiList Container to add stuff to. 178 * @param fileName Input file name, only used for debug messages. 179 */ parseXml(ApiList apiList, Reader reader, String fileName)180 static boolean parseXml(ApiList apiList, Reader reader, 181 String fileName) { 182 //System.out.println("--- parsing " + fileName); 183 try { 184 XMLReader xmlReader = XMLReaderFactory.createXMLReader(); 185 ApiDescrHandler handler = new ApiDescrHandler(apiList); 186 xmlReader.setContentHandler(handler); 187 xmlReader.setErrorHandler(handler); 188 xmlReader.parse(new InputSource(reader)); 189 190 //System.out.println("--- parsing complete"); 191 //dumpApi(apiList); 192 return true; 193 } catch (SAXParseException ex) { 194 System.err.println("Error parsing " + fileName + " line " + 195 ex.getLineNumber() + ": " + ex.getMessage()); 196 } catch (Exception ex) { 197 System.err.println("Error while reading " + fileName + ": " + 198 ex.getMessage()); 199 ex.printStackTrace(); 200 } 201 202 // failed 203 return false; 204 } 205 206 /** 207 * Expands lists of fields and methods to recursively include superclass 208 * and interface entries. 209 * 210 * The API description files have entries for every method a class 211 * declares, even if it's present in the superclass (e.g. toString()). 212 * Removal of one of these methods doesn't constitute an API change, 213 * though, so if we don't find a method in a class we need to hunt 214 * through its superclasses. 215 * 216 * We can walk up the hierarchy while analyzing the target APK, 217 * or we can "flatten" the methods declared by the superclasses and 218 * interfaces before we begin the analysis. Expanding up front can be 219 * beneficial if we're analyzing lots of APKs in one go, but detrimental 220 * to startup time if we just want to look at one small APK. 221 * 222 * It also means filling the field/method hash tables with lots of 223 * entries that never get used, possibly worsening the hash table 224 * hit rate. 225 * 226 * We only need to do this for the public API list. The dexdeps output 227 * doesn't have this sort of information anyway. 228 */ flattenInherited(ApiList pubList)229 static void flattenInherited(ApiList pubList) { 230 Iterator<PackageInfo> pkgIter = pubList.getPackageIterator(); 231 while (pkgIter.hasNext()) { 232 PackageInfo pubPkgInfo = pkgIter.next(); 233 234 Iterator<ClassInfo> classIter = pubPkgInfo.getClassIterator(); 235 while (classIter.hasNext()) { 236 ClassInfo pubClassInfo = classIter.next(); 237 238 pubClassInfo.flattenClass(pubList); 239 } 240 } 241 } 242 243 /** 244 * Checks the APK against the public API. 245 * 246 * Run through and find the mismatches. 247 * 248 * @return true if all is well 249 */ check(ApiList pubList, ApiList apkDescr)250 static boolean check(ApiList pubList, ApiList apkDescr) { 251 252 Iterator<PackageInfo> pkgIter = apkDescr.getPackageIterator(); 253 while (pkgIter.hasNext()) { 254 PackageInfo apkPkgInfo = pkgIter.next(); 255 PackageInfo pubPkgInfo = pubList.getPackage(apkPkgInfo.getName()); 256 boolean badPackage = false; 257 258 if (pubPkgInfo == null) { 259 // "illegal package" not a tremendously useful message 260 //apkError("Illegal package ref: " + apkPkgInfo.getName()); 261 badPackage = true; 262 } 263 264 Iterator<ClassInfo> classIter = apkPkgInfo.getClassIterator(); 265 while (classIter.hasNext()) { 266 ClassInfo apkClassInfo = classIter.next(); 267 268 if (badPackage) { 269 /* 270 * The package is not present in the public API file, 271 * but simply saying "bad package" isn't all that 272 * useful, so we emit the names of each of the classes. 273 */ 274 if (isIgnorable(apkPkgInfo)) { 275 apkWarning("Ignoring class ref: " + 276 apkPkgInfo.getName() + "." + apkClassInfo.getName()); 277 } else { 278 apkError("Illegal class ref: " + 279 apkPkgInfo.getName() + "." + apkClassInfo.getName()); 280 } 281 } else { 282 checkClass(pubPkgInfo, apkClassInfo); 283 } 284 } 285 } 286 287 return true; 288 } 289 290 /** 291 * Checks the class against the public API. We check the class 292 * itself and then any fields and methods. 293 */ checkClass(PackageInfo pubPkgInfo, ClassInfo classInfo)294 static boolean checkClass(PackageInfo pubPkgInfo, ClassInfo classInfo) { 295 296 ClassInfo pubClassInfo = pubPkgInfo.getClass(classInfo.getName()); 297 298 if (pubClassInfo == null) { 299 if (isIgnorable(pubPkgInfo)) { 300 apkWarning("Ignoring class ref: " + 301 pubPkgInfo.getName() + "." + classInfo.getName()); 302 } else if (classInfo.hasNoFieldMethod()) { 303 apkWarning("Hidden class referenced: " + 304 pubPkgInfo.getName() + "." + classInfo.getName()); 305 } else { 306 apkError("Illegal class ref: " + 307 pubPkgInfo.getName() + "." + classInfo.getName()); 308 // could list specific fields/methods used 309 } 310 return false; 311 } 312 313 /* 314 * Check the contents of classInfo against pubClassInfo. 315 */ 316 Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator(); 317 while (fieldIter.hasNext()) { 318 FieldInfo apkFieldInfo = fieldIter.next(); 319 String nameAndType = apkFieldInfo.getNameAndType(); 320 FieldInfo pubFieldInfo = pubClassInfo.getField(nameAndType); 321 if (pubFieldInfo == null) { 322 if (pubClassInfo.isEnum()) { 323 apkWarning("Enum field ref: " + pubPkgInfo.getName() + 324 "." + classInfo.getName() + "." + nameAndType); 325 } else { 326 apkError("Illegal field ref: " + pubPkgInfo.getName() + 327 "." + classInfo.getName() + "." + nameAndType); 328 } 329 } 330 } 331 332 Iterator<MethodInfo> methodIter = classInfo.getMethodIterator(); 333 while (methodIter.hasNext()) { 334 MethodInfo apkMethodInfo = methodIter.next(); 335 String nameAndDescr = apkMethodInfo.getNameAndDescriptor(); 336 MethodInfo pubMethodInfo = pubClassInfo.getMethod(nameAndDescr); 337 if (pubMethodInfo == null) { 338 pubMethodInfo = pubClassInfo.getMethodIgnoringReturn(nameAndDescr); 339 if (pubMethodInfo == null) { 340 if (pubClassInfo.isAnnotation()) { 341 apkWarning("Annotation method ref: " + 342 pubPkgInfo.getName() + "." + classInfo.getName() + 343 "." + nameAndDescr); 344 } else { 345 apkError("Illegal method ref: " + pubPkgInfo.getName() + 346 "." + classInfo.getName() + "." + nameAndDescr); 347 } 348 } else { 349 apkWarning("Possibly covariant method ref: " + 350 pubPkgInfo.getName() + "." + classInfo.getName() + 351 "." + nameAndDescr); 352 } 353 } 354 } 355 356 357 return true; 358 } 359 360 /** 361 * Returns true if the package is in the "ignored" list. 362 */ isIgnorable(PackageInfo pkgInfo)363 static boolean isIgnorable(PackageInfo pkgInfo) { 364 return sIgnorablePackages.contains(pkgInfo.getName()); 365 } 366 367 /** 368 * Prints a warning message about an APK problem. 369 */ apkWarning(String msg)370 public static void apkWarning(String msg) { 371 if (sShowWarnings) { 372 System.out.println("(warn) " + sCurrentApk.getDebugString() + 373 ": " + msg); 374 } 375 sCurrentApk.incrWarnings(); 376 } 377 378 /** 379 * Prints an error message about an APK problem. 380 */ apkError(String msg)381 public static void apkError(String msg) { 382 if (sShowErrors) { 383 System.out.println(sCurrentApk.getDebugString() + ": " + msg); 384 } 385 sCurrentApk.incrErrors(); 386 } 387 388 /** 389 * Recursively dumps the contents of the API. Sort order is not 390 * specified. 391 */ dumpApi(ApiList apiList)392 private static void dumpApi(ApiList apiList) { 393 Iterator<PackageInfo> iter = apiList.getPackageIterator(); 394 while (iter.hasNext()) { 395 PackageInfo pkgInfo = iter.next(); 396 dumpPackage(pkgInfo); 397 } 398 } 399 dumpPackage(PackageInfo pkgInfo)400 private static void dumpPackage(PackageInfo pkgInfo) { 401 Iterator<ClassInfo> iter = pkgInfo.getClassIterator(); 402 System.out.println("PACKAGE " + pkgInfo.getName()); 403 while (iter.hasNext()) { 404 ClassInfo classInfo = iter.next(); 405 dumpClass(classInfo); 406 } 407 } 408 dumpClass(ClassInfo classInfo)409 private static void dumpClass(ClassInfo classInfo) { 410 System.out.println(" CLASS " + classInfo.getName()); 411 Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator(); 412 while (fieldIter.hasNext()) { 413 FieldInfo fieldInfo = fieldIter.next(); 414 dumpField(fieldInfo); 415 } 416 Iterator<MethodInfo> methIter = classInfo.getMethodIterator(); 417 while (methIter.hasNext()) { 418 MethodInfo methInfo = methIter.next(); 419 dumpMethod(methInfo); 420 } 421 } 422 dumpMethod(MethodInfo methInfo)423 private static void dumpMethod(MethodInfo methInfo) { 424 System.out.println(" METHOD " + methInfo.getNameAndDescriptor()); 425 } 426 dumpField(FieldInfo fieldInfo)427 private static void dumpField(FieldInfo fieldInfo) { 428 System.out.println(" FIELD " + fieldInfo.getNameAndType()); 429 } 430 } 431 432