/*
* 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());
}
}