Home | History | Annotate | Download | only in apkcheck
      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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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 
    423     private static void dumpMethod(MethodInfo methInfo) {
    424         System.out.println("  METHOD " + methInfo.getNameAndDescriptor());
    425     }
    426 
    427     private static void dumpField(FieldInfo fieldInfo) {
    428         System.out.println("  FIELD " + fieldInfo.getNameAndType());
    429     }
    430 }
    431 
    432