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
17package com.android.apkcheck;
18
19import org.xml.sax.*;
20import org.xml.sax.helpers.*;
21import java.io.FileReader;
22import java.io.IOException;
23import java.io.Reader;
24import java.util.ArrayList;
25import java.util.HashSet;
26import 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 */
42public 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