blob: 9f45df5117822580658cd77b50a276fedea5cb63 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.bytecode;
import org.objectweb.asm.ClassReader;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.Consumer;
/**
* Checks classpaths (given as ClassLoaders) by reading the constant pool of the class file and
* attempting to load every referenced class. If there are some that are unable to be found, it
* stores a helpful error message if it knows where it might find them, and exits the program if it
* can't find the class with any given classpath.
*/
public class ClassPathValidator {
// Number of warnings to print.
private static final int MAX_MISSING_CLASS_WARNINGS = 10;
// Number of missing classes to show per missing jar.
private static final int MAX_ERRORS_PER_JAR = 2;
// Map of missing .jar -> Missing class -> Classes that failed.
// TreeMap so that error messages have sorted list of jars.
private final Map<String, Map<String, Set<String>>> mDirectErrors =
Collections.synchronizedMap(new TreeMap<>());
// Missing classes we only track the first one for each jar.
// Map of missingClass -> srcClass.
private final Map<String, String> mMissingClasses =
Collections.synchronizedMap(new TreeMap<>());
static class ClassNotLoadedException extends ClassNotFoundException {
private final String mClassName;
ClassNotLoadedException(String className, Throwable ex) {
super("Couldn't load " + className, ex);
mClassName = className;
}
public String getClassName() {
return mClassName;
}
}
private static void validateClass(ClassLoader classLoader, String className)
throws ClassNotLoadedException {
if (className.startsWith("[")) {
// Dealing with an array type which isn't encoded nicely in the constant pool.
// For example, [[Lorg/chromium/Class$1;
className = className.substring(className.lastIndexOf('[') + 1);
if (className.charAt(0) == 'L' && className.endsWith(";")) {
className = className.substring(1, className.length() - 1);
} else {
// Bailing out if we have an non-class array type.
// This could be something like [B
return;
}
}
if (className.matches(".*\\bR(\\$\\w+)?$")) {
// Resources in R.java files are not expected to be valid at this stage in the build.
return;
}
if (className.matches("^libcore\\b.*")) {
// libcore exists on devices, but is not included in the Android sdk as it is a private
// API.
return;
}
try {
classLoader.loadClass(className.replace('/', '.'));
} catch (ClassNotFoundException e) {
throw new ClassNotLoadedException(className, e);
} catch (NoClassDefFoundError e) {
// We assume that this is caused by another class that is not going to able to be
// loaded, so we will skip this and let that class fail with ClassNotFoundException.
}
}
/**
* Given a .class file, see if every class referenced in the main class' constant pool can be
* loaded by the given ClassLoader.
*
* @param classReader .class file interface for reading the constant pool.
* @param classLoader classpath you wish to validate.
* @param errorConsumer Called for each missing class.
*/
private static void validateClassPath(ClassReader classReader, ClassLoader classLoader,
Consumer<ClassNotLoadedException> errorConsumer) {
char[] charBuffer = new char[classReader.getMaxStringLength()];
// According to the Java spec, the constant pool is indexed from 1 to constant_pool_count -
// 1. See https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4
for (int i = 1; i < classReader.getItemCount(); i++) {
int offset = classReader.getItem(i);
// Class entries correspond to 7 in the constant pool
// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4
if (offset > 0 && classReader.readByte(offset - 1) == 7) {
try {
validateClass(classLoader, classReader.readUTF8(offset, charBuffer));
} catch (ClassNotLoadedException e) {
errorConsumer.accept(e);
}
}
}
}
public void validateFullClassPath(ClassReader classReader, ClassLoader fullClassLoader,
Set<String> missingClassAllowlist) {
// Prebuilts only need transitive dependencies checked, not direct dependencies.
validateClassPath(classReader, fullClassLoader, (e) -> {
if (!missingClassAllowlist.contains(e.getClassName())) {
addMissingError(classReader.getClassName(), e.getClassName());
}
});
}
public void validateDirectClassPath(ClassReader classReader, ClassLoader directClassLoader,
ClassLoader fullClassLoader, Collection<String> jarsOnlyInFullClassPath,
Set<String> missingClassAllowlist, boolean verbose) {
validateClassPath(classReader, directClassLoader, (e) -> {
try {
validateClass(fullClassLoader, e.getClassName());
} catch (ClassNotLoadedException d) {
if (!missingClassAllowlist.contains(e.getClassName())) {
addMissingError(classReader.getClassName(), e.getClassName());
}
return;
}
if (verbose) {
System.err.println("Class \"" + e.getClassName()
+ "\" not found in direct dependencies,"
+ " but found in indirect dependiences.");
}
// Iterating through all jars that are in the full classpath but not the direct
// classpath to find which one provides the class we are looking for.
for (String jarPath : jarsOnlyInFullClassPath) {
try {
ClassLoader smallLoader =
ByteCodeProcessor.loadJars(Collections.singletonList(jarPath));
validateClass(smallLoader, e.getClassName());
addDirectError(jarPath, classReader.getClassName(), e.getClassName());
break;
} catch (ClassNotLoadedException f) {
}
}
});
}
private void addMissingError(String srcClass, String missingClass) {
mMissingClasses.put(missingClass, srcClass);
}
private void addDirectError(String jarPath, String srcClass, String missingClass) {
synchronized (mDirectErrors) {
Map<String, Set<String>> failedClassesByMissingClass = mDirectErrors.get(jarPath);
if (failedClassesByMissingClass == null) {
// TreeMap so that error messages have sorted list of classes.
failedClassesByMissingClass = new TreeMap<>();
mDirectErrors.put(jarPath, failedClassesByMissingClass);
}
Set<String> failedClasses = failedClassesByMissingClass.get(missingClass);
if (failedClasses == null) {
failedClasses = new TreeSet<>();
failedClassesByMissingClass.put(missingClass, failedClasses);
}
failedClasses.add(srcClass);
}
}
public boolean hasErrors() {
return !mDirectErrors.isEmpty() || !mMissingClasses.isEmpty();
}
private static void printValidationError(
PrintStream out, String gnTarget, Map<String, Set<String>> missingClasses) {
out.print(" * ");
out.println(gnTarget);
int i = 0;
// The list of missing classes is non-exhaustive because each class that fails to validate
// reports only the first missing class.
for (Map.Entry<String, Set<String>> entry : missingClasses.entrySet()) {
String missingClass = entry.getKey();
Set<String> filesThatNeededIt = entry.getValue();
out.print(" * ");
if (i == MAX_ERRORS_PER_JAR) {
out.print(String.format(
"And %d more...", missingClasses.size() - MAX_ERRORS_PER_JAR));
break;
}
out.print(missingClass.replace('/', '.'));
out.print(" (needed by ");
out.print(filesThatNeededIt.iterator().next().replace('/', '.'));
if (filesThatNeededIt.size() > 1) {
out.print(String.format(" and %d more", filesThatNeededIt.size() - 1));
}
out.println(")");
i++;
}
}
public void printAll(String gnTarget, Map<String, String> jarToGnTarget) {
String streamer = "=============================";
System.err.println();
System.err.println(streamer + " Dependency Checks Failed " + streamer);
System.err.println("Target: " + gnTarget);
if (!mMissingClasses.isEmpty()) {
int i = 0;
for (Map.Entry<String, String> entry : mMissingClasses.entrySet()) {
if (++i > MAX_MISSING_CLASS_WARNINGS) {
System.err.println(String.format("... and %d more.",
mMissingClasses.size() - MAX_MISSING_CLASS_WARNINGS));
break;
}
System.err.println(String.format(
"Class \"%s\" not found on any classpath. Used by class \"%s\"",
entry.getKey(), entry.getValue()));
}
System.err.println();
}
if (!mDirectErrors.isEmpty()) {
System.err.println("Direct classpath is incomplete. To fix, add deps on:");
for (Map.Entry<String, Map<String, Set<String>>> entry : mDirectErrors.entrySet()) {
printValidationError(
System.err, jarToGnTarget.get(entry.getKey()), entry.getValue());
}
System.err.println();
}
}
}