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.has