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