/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.apkcheck; import org.xml.sax.*; import org.xml.sax.helpers.*; import java.io.FileReader; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; /** * Checks an APK's dependencies against the published API specification. * * We need to read two XML files (spec and APK) and perform some operations * on the elements. The file formats are similar but not identical, so * we distill it down to common elements. * * We may also want to read some additional API lists representing * libraries that would be included with a "uses-library" directive. * * For performance we want to allow processing of multiple APKs so * we don't have to re-parse the spec file each time. */ public class ApkCheck { /* keep track of current APK file name, for error messages */ private static ApiList sCurrentApk; /* show warnings? */ private static boolean sShowWarnings = false; /* show errors? */ private static boolean sShowErrors = true; /* names of packages we're allowed to ignore */ private static HashSet<String> sIgnorablePackages = new HashSet<String>(); /** * Program entry point. */ public static void main(String[] args) { ApiList apiDescr = new ApiList("public-api"); if (args.length < 2) { usage(); return; } /* process args */ int idx; for (idx = 0; idx < args.length; idx++) { if (args[idx].equals("--help")) { usage(); return; } else if (args[idx].startsWith("--uses-library=")) { String libName = args[idx].substring(args[idx].indexOf('=')+1); if ("BUILTIN".equals(libName)) { Reader reader = Builtin.getReader(); if (!parseXml(apiDescr, reader, "BUILTIN")) return; } else { if (!parseApiDescr(apiDescr, libName)) return; } } else if (args[idx].startsWith("--ignore-package=")) { String pkgName = args[idx].substring(args[idx].indexOf('=')+1); sIgnorablePackages.add(pkgName); } else if (args[idx].equals("--warn")) { sShowWarnings = true; } else if (args[idx].equals("--no-warn")) { sShowWarnings = false; } else if (args[idx].equals("--error")) { sShowErrors = true; } else if (args[idx].equals("--no-error")) { sShowErrors = false; } else if (args[idx].startsWith("--")) { if (args[idx].equals("--")) { // remainder are filenames, even if they start with "--" idx++; break; } else { // unknown option specified System.err.println("ERROR: unknown option " + args[idx] + " (use \"--help\" for usage info)"); return; } } else { break; } } if (idx > args.length - 2) { usage(); return; } /* parse base API description */ if (!parseApiDescr(apiDescr, args[idx++])) return; /* "flatten" superclasses and interfaces */ sCurrentApk = apiDescr; flattenInherited(apiDescr); /* walk through list of libs we want to scan */ for ( ; idx < args.length; idx++) { ApiList apkDescr = new ApiList(args[idx]); sCurrentApk = apkDescr; boolean success = parseApiDescr(apkDescr, args[idx]); if (!success) { if (idx < args.length-1) System.err.println("Skipping..."); continue; } check(apiDescr, apkDescr); System.out.println(args[idx] + ": summary: " + apkDescr.getErrorCount() + " errors, " + apkDescr.getWarningCount() + " warnings\n"); } } /** * Prints usage statement. */ static void usage() { System.err.println("Android APK checker v1.0"); System.err.println("Copyright (C) 2010 The Android Open Source Project\n"); System.err.println("Usage: apkcheck [options] public-api.xml apk1.xml ...\n"); System.err.println("Options:"); System.err.println(" --help show this message"); System.err.println(" --uses-library=lib.xml load additional public API list"); System.err.println(" --ignore-package=pkg don't show errors for references to this package"); System.err.println(" --[no-]warn enable or disable display of warnings"); System.err.println(" --[no-]error enable or disable display of errors"); } /** * Opens the file and passes it to parseXml. * * TODO: allow '-' as an alias for stdin? */ static boolean parseApiDescr(ApiList apiList, String fileName) { boolean result = false; try { FileReader fileReader = new FileReader(fileName); result = parseXml(apiList, fileReader, fileName); fileReader.close(); } catch (IOException ioe) { System.err.println("Error opening " + fileName); } return result; } /** * Parses an XML file holding an API description. * * @param fileReader Data source. * @param apiList Container to add stuff to. * @param fileName Input file name, only used for debug messages. */ static boolean parseXml(ApiList apiList, Reader reader, String fileName) { //System.out.println("--- parsing " + fileName); try { XMLReader xmlReader = XMLReaderFactory.createXMLReader(); ApiDescrHandler handler = new ApiDescrHandler(apiList); xmlReader.setContentHandler(handler); xmlReader.setErrorHandler(handler); xmlReader.parse(new InputSource(reader)); //System.out.println("--- parsing complete"); //dumpApi(apiList); return true; } catch (SAXParseException ex) { System.err.println("Error parsing " + fileName + " line " + ex.getLineNumber() + ": " + ex.getMessage()); } catch (Exception ex) { System.err.println("Error while reading " + fileName + ": " + ex.getMessage()); ex.printStackTrace(); } // failed return false; } /** * Expands lists of fields and methods to recursively include superclass * and interface entries. * * The API description files have entries for every method a class * declares, even if it's present in the superclass (e.g. toString()). * Removal of one of these methods doesn't constitute an API change, * though, so if we don't find a method in a class we need to hunt * through its superclasses. * * We can walk up the hierarchy while analyzing the target APK, * or we can "flatten" the methods declared by the superclasses and * interfaces before we begin the analysis. Expanding up front can be * beneficial if we're analyzing lots of APKs in one go, but detrimental * to startup time if we just want to look at one small APK. * * It also means filling the field/method hash tables with lots of * entries that never get used, possibly worsening the hash table * hit rate. * * We only need to do this for the public API list. The dexdeps output * doesn't have this sort of information anyway. */ static void flattenInherited(ApiList pubList) { Iterator<PackageInfo> pkgIter = pubList.getPackageIterator(); while (pkgIter.hasNext()) { PackageInfo pubPkgInfo = pkgIter.next(); Iterator<ClassInfo> classIter = pubPkgInfo.getClassIterator(); while (classIter.hasNext()) { ClassInfo pubClassInfo = classIter.next(); pubClassInfo.flattenClass(pubList); } } } /** * Checks the APK against the public API. * * Run through and find the mismatches. * * @return true if all is well */ static boolean check(ApiList pubList, ApiList apkDescr) { Iterator<PackageInfo> pkgIter = apkDescr.getPackageIterator(); while (pkgIter.hasNext()) { PackageInfo apkPkgInfo = pkgIter.next(); PackageInfo pubPkgInfo = pubList.getPackage(apkPkgInfo.getName()); boolean badPackage = false; if (pubPkgInfo == null) { // "illegal package" not a tremendously useful message //apkError("Illegal package ref: " + apkPkgInfo.getName()); badPackage = true; } Iterator<ClassInfo> classIter = apkPkgInfo.getClassIterator(); while (classIter.hasNext()) { ClassInfo apkClassInfo = classIter.next(); if (badPackage) { /* * The package is not present in the public API file, * but simply saying "bad package" isn't all that * useful, so we emit the names of each of the classes. */ if (isIgnorable(apkPkgInfo)) { apkWarning("Ignoring class ref: " + apkPkgInfo.getName() + "." + apkClassInfo.getName()); } else { apkError("Illegal class ref: " + apkPkgInfo.getName() + "." + apkClassInfo.getName()); } } else { checkClass(pubPkgInfo, apkClassInfo); } } } return true; } /** * Checks the class against the public API. We check the class * itself and then any fields and methods. */ static boolean checkClass(PackageInfo pubPkgInfo, ClassInfo classInfo) { ClassInfo pubClassInfo = pubPkgInfo.getClass(classInfo.getName()); if (pubClassInfo == null) { if (isIgnorable(pubPkgInfo)) { apkWarning("Ignoring class ref: " + pubPkgInfo.getName() + "." + classInfo.getName()); } else if (classInfo.hasNoFieldMethod()) { apkWarning("Hidden class referenced: " + pubPkgInfo.getName() + "." + classInfo.getName()); } else { apkError("Illegal class ref: " + pubPkgInfo.getName() + "." + classInfo.getName()); // could list specific fields/methods used } return false; } /* * Check the contents of classInfo against pubClassInfo. */ Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator(); while (fieldIter.hasNext()) { FieldInfo apkFieldInfo = fieldIter.next(); String nameAndType = apkFieldInfo.getNameAndType(); FieldInfo pubFieldInfo = pubClassInfo.getField(nameAndType); if (pubFieldInfo == null) { if (pubClassInfo.isEnum()) { apkWarning("Enum field ref: " + pubPkgInfo.getName() + "." + classInfo.getName() + "." + nameAndType); } else { apkError("Illegal field ref: " + pubPkgInfo.getName() + "." + classInfo.getName() + "." + nameAndType); } } } Iterator<MethodInfo> methodIter = classInfo.getMethodIterator(); while (methodIter.hasNext()) { MethodInfo apkMethodInfo = methodIter.next(); String nameAndDescr = apkMethodInfo.getNameAndDescriptor(); MethodInfo pubMethodInfo = pubClassInfo.getMethod(nameAndDescr); if (pubMethodInfo == null) { pubMethodInfo = pubClassInfo.getMethodIgnoringReturn(nameAndDescr); if (pubMethodInfo == null) { if (pubClassInfo.isAnnotation()) { apkWarning("Annotation method ref: " + pubPkgInfo.getName() + "." + classInfo.getName() + "." + nameAndDescr); } else { apkError("Illegal method ref: " + pubPkgInfo.getName() + "." + classInfo.getName() + "." + nameAndDescr); } } else { apkWarning("Possibly covariant method ref: " + pubPkgInfo.getName() + "." + classInfo.getName() + "." + nameAndDescr); } } } return true; } /** * Returns true if the package is in the "ignored" list. */ static boolean isIgnorable(PackageInfo pkgInfo) { return sIgnorablePackages.contains(pkgInfo.getName()); } /** * Prints a warning message about an APK problem. */ public static void apkWarning(String msg) { if (sShowWarnings) { System.out.println("(warn) " + sCurrentApk.getDebugString() + ": " + msg); } sCurrentApk.incrWarnings(); } /** * Prints an error message about an APK problem. */ public static void apkError(String msg) { if (sShowErrors) { System.out.println(sCurrentApk.getDebugString() + ": " + msg); } sCurrentApk.incrErrors(); } /** * Recursively dumps the contents of the API. Sort order is not * specified. */ private static void dumpApi(ApiList apiList) { Iterator<PackageInfo> iter = apiList.getPackageIterator(); while (iter.hasNext()) { PackageInfo pkgInfo = iter.next(); dumpPackage(pkgInfo); } } private static void dumpPackage(PackageInfo pkgInfo) { Iterator<ClassInfo> iter = pkgInfo.getClassIterator(); System.out.println("PACKAGE " + pkgInfo.getName()); while (iter.hasNext()) { ClassInfo classInfo = iter.next(); dumpClass(classInfo); } } private static void dumpClass(ClassInfo classInfo) { System.out.println(" CLASS " + classInfo.getName()); Iterator<FieldInfo> fieldIter = classInfo.getFieldIterator(); while (fieldIter.hasNext()) { FieldInfo fieldInfo = fieldIter.next(); dumpField(fieldInfo); } Iterator<MethodInfo> methIter = classInfo.getMethodIterator(); while (methIter.hasNext()) { MethodInfo methInfo = methIter.next(); dumpMethod(methInfo); } } private static void dumpMethod(MethodInfo methInfo) { System.out.println(" METHOD " + methInfo.getNameAndDescriptor()); } private static void dumpField(FieldInfo fieldInfo) { System.out.println(" FIELD " + fieldInfo.getNameAndType()); } }