LUCENE-4206: Allow check-forbidden-apis to also investigate calls to subclasses of forbidden APIs

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1359827 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Uwe Schindler 2012-07-10 18:32:31 +00:00
parent 3042e69056
commit 0446940b7d
2 changed files with 243 additions and 159 deletions

View File

@ -65,11 +65,11 @@ Build
* LUCENE-4115: JAR resolution/ cleanup should be done automatically for ant * LUCENE-4115: JAR resolution/ cleanup should be done automatically for ant
clean/ eclipse/ resolve (Dawid Weiss) clean/ eclipse/ resolve (Dawid Weiss)
* LUCENE-4199, LUCENE-4202: Add a new target "check-forbidden-apis", that * LUCENE-4199, LUCENE-4202, LUCENE-4206: Add a new target "check-forbidden-apis"
parses all generated .class files for use of APIs that use default charset, that parses all generated .class files for use of APIs that use default
default locale, or default timezone and fail build if violations found. This charset, default locale, or default timezone and fail build if violations
ensures, that Lucene / Solr is independent on local configuration options. found. This ensures, that Lucene / Solr is independent on local configuration
(Uwe Schindler, Robert Muir, Dawid Weiss) options. (Uwe Schindler, Robert Muir, Dawid Weiss)
Documentation Documentation

View File

@ -20,13 +20,11 @@ package org.apache.lucene.validation;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Label; import org.objectweb.asm.Label;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes; import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type; import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method; import org.objectweb.asm.commons.Method;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;
import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildException;
@ -50,12 +48,14 @@ import java.io.Reader;
import java.io.File; import java.io.File;
import java.io.StringReader; import java.io.StringReader;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter; import java.util.Formatter;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.HashSet;
import java.util.Set;
/** /**
* Task to check if a set of class files contains calls to forbidden APIs * Task to check if a set of class files contains calls to forbidden APIs
@ -69,32 +69,39 @@ public class ForbiddenApisCheckTask extends Task {
private final Resources classFiles = new Resources(); private final Resources classFiles = new Resources();
private final Resources apiSignatures = new Resources(); private final Resources apiSignatures = new Resources();
private Path classpath = null; private Path classpath = null;
private final Map<String,ClassNode> classCache = new HashMap<String,ClassNode>();
private final Map<String,String> forbiddenFields = new HashMap<String,String>();
private final Map<String,String> forbiddenMethods = new HashMap<String,String>();
private final Map<String,String> forbiddenClasses = new HashMap<String,String>();
/** Reads a class (binary name) from the given {@link ClassLoader}. ClassLoader loader = null;
*/
private ClassReader readClass(final ClassLoader loader, final String clazz) throws BuildException { final Map<String,ClassSignatureLookup> classesToCheck = new HashMap<String,ClassSignatureLookup>();
try { final Map<String,ClassSignatureLookup> classpathClassCache = new HashMap<String,ClassSignatureLookup>();
final InputStream in = loader.getResourceAsStream(clazz.replace('.', '/') + ".class");
if (in == null) { final Map<String,String> forbiddenFields = new HashMap<String,String>();
throw new BuildException("Loading of class " + clazz + " failed: Not found"); final Map<String,String> forbiddenMethods = new HashMap<String,String>();
} final Map<String,String> forbiddenClasses = new HashMap<String,String>();
/** Reads a class (binary name) from the given {@link ClassLoader}. */
ClassSignatureLookup getClassFromClassLoader(final String clazz) throws BuildException {
ClassSignatureLookup c = classpathClassCache.get(clazz);
if (c == null) {
try { try {
return new ClassReader(in); final InputStream in = loader.getResourceAsStream(clazz.replace('.', '/') + ".class");
} finally { if (in == null) {
in.close(); throw new BuildException("Loading of class " + clazz + " failed: Not found");
}
try {
classpathClassCache.put(clazz, c = new ClassSignatureLookup(new ClassReader(in)));
} finally {
in.close();
}
} catch (IOException ioe) {
throw new BuildException("Loading of class " + clazz + " failed.", ioe);
} }
} catch (IOException ioe) {
throw new BuildException("Loading of class " + clazz + " failed.", ioe);
} }
return c;
} }
/** Adds the method signature to the list of disallowed methods. The Signature is checked against the given ClassLoader. */ /** Adds the method signature to the list of disallowed methods. The Signature is checked against the given ClassLoader. */
private void addSignature(final ClassLoader loader, final String signature) throws BuildException { private void addSignature(final String signature) throws BuildException {
final String clazz, field; final String clazz, field;
final Method method; final Method method;
int p = signature.indexOf('#'); int p = signature.indexOf('#');
@ -123,19 +130,15 @@ public class ForbiddenApisCheckTask extends Task {
field = null; field = null;
} }
// check class & method/field signature, if it is really existent (in classpath), but we don't really load the class into JVM: // check class & method/field signature, if it is really existent (in classpath), but we don't really load the class into JVM:
ClassNode c = classCache.get(clazz); final ClassSignatureLookup c = getClassFromClassLoader(clazz);
if (c == null) {
readClass(loader, clazz).accept(c = new ClassNode(Opcodes.ASM4), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
classCache.put(clazz, c);
}
if (method != null) { if (method != null) {
assert field == null; assert field == null;
// list all methods with this signature: // list all methods with this signature:
boolean found = false; boolean found = false;
for (final MethodNode mn : c.methods) { for (final Method m : c.methods) {
if (mn.name.equals(method.getName()) && Arrays.equals(Type.getArgumentTypes(mn.desc), method.getArgumentTypes())) { if (m.getName().equals(method.getName()) && Arrays.equals(m.getArgumentTypes(), method.getArgumentTypes())) {
found = true; found = true;
forbiddenMethods.put(c.name + '\000' + new Method(mn.name, mn.desc), signature); forbiddenMethods.put(c.reader.getClassName() + '\000' + m, signature);
// don't break when found, as there may be more covariant overrides! // don't break when found, as there may be more covariant overrides!
} }
} }
@ -144,27 +147,19 @@ public class ForbiddenApisCheckTask extends Task {
} }
} else if (field != null) { } else if (field != null) {
assert method == null; assert method == null;
// list all fields to find the right one: if (!c.fields.contains(field)) {
boolean found = false;
for (final FieldNode fld : c.fields) {
if (fld.name.equals(field)) {
found = true;
forbiddenFields.put(c.name + '\000' + fld.name, signature);
break;
}
}
if (!found) {
throw new BuildException("No field found with following name: " + signature); throw new BuildException("No field found with following name: " + signature);
} }
forbiddenFields.put(c.reader.getClassName() + '\000' + field, signature);
} else { } else {
assert field == null && method == null; assert field == null && method == null;
// only add the signature as class name // only add the signature as class name
forbiddenClasses.put(c.name, signature); forbiddenClasses.put(c.reader.getClassName(), signature);
} }
} }
/** Reads a list of API signatures. Closes the Reader when done (on Exception, too)! */ /** Reads a list of API signatures. Closes the Reader when done (on Exception, too)! */
private void parseApiFile(ClassLoader loader, Reader reader) throws IOException { private void parseApiFile(Reader reader) throws IOException {
final BufferedReader r = new BufferedReader(reader); final BufferedReader r = new BufferedReader(reader);
try { try {
String line; String line;
@ -172,116 +167,170 @@ public class ForbiddenApisCheckTask extends Task {
line = line.trim(); line = line.trim();
if (line.length() == 0 || line.startsWith("#")) if (line.length() == 0 || line.startsWith("#"))
continue; continue;
addSignature(loader, line); addSignature(line);
} }
} finally { } finally {
r.close(); r.close();
} }
} }
/** Parses a class given as Resource and checks for valid method invocations */ /** Parses a class given as (FileSet) Resource */
private int checkClass(final Resource res) throws IOException { private ClassReader loadClassFromResource(final Resource res) throws BuildException {
final InputStream stream = res.getInputStream();
try { try {
final int[] violations = new int[1]; final InputStream stream = res.getInputStream();
new ClassReader(stream).accept(new ClassVisitor(Opcodes.ASM4) { try {
String className = null, source = null; return new ClassReader(stream);
} finally {
@Override stream.close();
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { }
// save class name in source code format: } catch (IOException ioe) {
this.className = Type.getObjectType(name).getClassName(); throw new BuildException("IO problem while reading class file " + res, ioe);
} }
}
@Override
public void visitSource(String source, String debug) { /** Parses a class given as Resource and checks for valid method invocations */
this.source = source; private int checkClass(final ClassReader reader) {
} final int[] violations = new int[1];
reader.accept(new ClassVisitor(Opcodes.ASM4) {
@Override final String className = Type.getObjectType(reader.getClassName()).getClassName();
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { String source = null;
return new MethodVisitor(Opcodes.ASM4) {
private int lineNo = -1; @Override
public void visitSource(String source, String debug) {
private boolean checkClassUse(String owner) { this.source = source;
final String printout = forbiddenClasses.get(owner); }
if (printout != null) {
log("Forbidden class use: " + printout, Project.MSG_ERR); @Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(Opcodes.ASM4) {
private int lineNo = -1;
private ClassSignatureLookup lookupRelatedClass(String internalName) {
ClassSignatureLookup c = classesToCheck.get(internalName);
if (c == null) try {
c = getClassFromClassLoader(internalName);
} catch (BuildException be) {
// we ignore lookup errors and simply ignore this related class
c = null;
}
return c;
}
private boolean checkClassUse(String owner) {
final String printout = forbiddenClasses.get(owner);
if (printout != null) {
log("Forbidden class use: " + printout, Project.MSG_ERR);
return true;
}
return false;
}
private boolean checkMethodAccess(String owner, Method method) {
if (checkClassUse(owner)) {
return true;
}
final String printout = forbiddenMethods.get(owner + '\000' + method);
if (printout != null) {
log("Forbidden method invocation: " + printout, Project.MSG_ERR);
return true;
}
final ClassSignatureLookup c = lookupRelatedClass(owner);
if (c != null && !c.methods.contains(method)) {
final String superName = c.reader.getSuperName();
if (superName != null && checkMethodAccess(superName, method)) {
return true; return true;
} }
return false; final String[] interfaces = c.reader.getInterfaces();
} if (interfaces != null) {
for (String intf : interfaces) {
private void reportSourceAndLine() { if (intf != null && checkMethodAccess(intf, method)) {
final StringBuilder sb = new StringBuilder(" in ").append(className); return true;
if (source != null && lineNo >= 0) { }
new Formatter(sb, Locale.ROOT).format(" (%s:%d)", source, lineNo).flush();
}
log(sb.toString(), Project.MSG_ERR);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
boolean found = checkClassUse(owner);
if (!found) {
final String printout = forbiddenMethods.get(owner + '\000' + new Method(name, desc));
if (printout != null) {
found = true;
log("Forbidden method invocation: " + printout, Project.MSG_ERR);
} }
} }
if (found) {
violations[0]++;
reportSourceAndLine();
}
} }
return false;
@Override }
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
boolean found = checkClassUse(owner); private boolean checkFieldAccess(String owner, String field) {
if (!found) { if (checkClassUse(owner)) {
final String printout = forbiddenFields.get(owner + '\000' + name); return true;
if (printout != null) { }
found = true; final String printout = forbiddenFields.get(owner + '\000' + field);
log("Forbidden field access: " + printout, Project.MSG_ERR); if (printout != null) {
log("Forbidden field access: " + printout, Project.MSG_ERR);
return true;
}
final ClassSignatureLookup c = lookupRelatedClass(owner);
if (c != null && !c.fields.contains(field)) {
final String superName = c.reader.getSuperName();
if (superName != null && checkFieldAccess(superName, field)) {
return true;
}
final String[] interfaces = c.reader.getInterfaces();
if (interfaces != null) {
for (String intf : interfaces) {
if (intf != null && checkFieldAccess(intf, field)) {
return true;
}
} }
} }
if (found) {
violations[0]++;
reportSourceAndLine();
}
} }
return false;
}
@Override @Override
public void visitLineNumber(int lineNo, Label start) { public void visitMethodInsn(int opcode, String owner, String name, String desc) {
this.lineNo = lineNo; if (checkMethodAccess(owner, new Method(name, desc))) {
violations[0]++;
reportSourceAndLine();
} }
}; }
}
}, ClassReader.SKIP_FRAMES); @Override
return violations[0]; public void visitFieldInsn(int opcode, String owner, String name, String desc) {
} finally { if (checkFieldAccess(owner, name)) {
stream.close(); violations[0]++;
} reportSourceAndLine();
}
}
private void reportSourceAndLine() {
final StringBuilder sb = new StringBuilder(" in ").append(className);
if (source != null && lineNo >= 0) {
new Formatter(sb, Locale.ROOT).format(" (%s:%d)", source, lineNo).flush();
}
log(sb.toString(), Project.MSG_ERR);
}
@Override
public void visitLineNumber(int lineNo, Label start) {
this.lineNo = lineNo;
}
};
}
}, ClassReader.SKIP_FRAMES);
return violations[0];
} }
@Override @Override
public void execute() throws BuildException { public void execute() throws BuildException {
AntClassLoader antLoader = null; AntClassLoader antLoader = null;
try { try {
final ClassLoader loader;
if (classpath != null) { if (classpath != null) {
classpath.setProject(getProject()); classpath.setProject(getProject());
loader = antLoader = getProject().createClassLoader(ClassLoader.getSystemClassLoader(), classpath); this.loader = antLoader = getProject().createClassLoader(ClassLoader.getSystemClassLoader(), classpath);
// force that loading from this class loader is done first, then parent is asked. // force that loading from this class loader is done first, then parent is asked.
// This violates spec, but prevents classes in any system classpath to be used if a local one is available: // This violates spec, but prevents classes in any system classpath to be used if a local one is available:
antLoader.setParentFirst(false); antLoader.setParentFirst(false);
} else { } else {
loader = ClassLoader.getSystemClassLoader(); this.loader = ClassLoader.getSystemClassLoader();
} }
classFiles.setProject(getProject()); classFiles.setProject(getProject());
apiSignatures.setProject(getProject()); apiSignatures.setProject(getProject());
final long start = System.currentTimeMillis();
try { try {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Iterator<Resource> iter = (Iterator<Resource>) apiSignatures.iterator(); Iterator<Resource> iter = (Iterator<Resource>) apiSignatures.iterator();
@ -295,10 +344,10 @@ public class ForbiddenApisCheckTask extends Task {
} }
if (r instanceof StringResource) { if (r instanceof StringResource) {
log("Reading inline API signatures...", Project.MSG_INFO); log("Reading inline API signatures...", Project.MSG_INFO);
parseApiFile(loader, new StringReader(((StringResource) r).getValue())); parseApiFile(new StringReader(((StringResource) r).getValue()));
} else { } else {
log("Reading API signatures: " + r, Project.MSG_INFO); log("Reading API signatures: " + r, Project.MSG_INFO);
parseApiFile(loader, new InputStreamReader(r.getInputStream(), "UTF-8")); parseApiFile(new InputStreamReader(r.getInputStream(), "UTF-8"));
} }
} }
} catch (IOException ioe) { } catch (IOException ioe) {
@ -307,41 +356,48 @@ public class ForbiddenApisCheckTask extends Task {
if (forbiddenMethods.isEmpty() && forbiddenClasses.isEmpty()) { if (forbiddenMethods.isEmpty() && forbiddenClasses.isEmpty()) {
throw new BuildException("No API signatures found; use apiFile=, <apiFileSet/>, or inner text to define those!"); throw new BuildException("No API signatures found; use apiFile=, <apiFileSet/>, or inner text to define those!");
} }
log("Loading classes to check...", Project.MSG_INFO);
@SuppressWarnings("unchecked")
Iterator<Resource> iter = (Iterator<Resource>) classFiles.iterator();
if (!iter.hasNext()) {
throw new BuildException("There is no <fileset/> given or the fileset does not contain any class files to check.");
}
while (iter.hasNext()) {
final Resource r = iter.next();
if (!r.isExists()) {
throw new BuildException("Class file does not exist: " + r);
}
ClassReader reader = loadClassFromResource(r);
classesToCheck.put(reader.getClassName(), new ClassSignatureLookup(reader));
}
log("Scanning for API signatures and dependencies...", Project.MSG_INFO);
int errors = 0;
for (final ClassSignatureLookup c : classesToCheck.values()) {
errors += checkClass(c.reader);
}
log(String.format(Locale.ROOT,
"Scanned %d (and %d related) class file(s) for forbidden API invocations (in %.2fs), %d error(s).",
classesToCheck.size(), classpathClassCache.size(), (System.currentTimeMillis() - start) / 1000.0, errors),
errors > 0 ? Project.MSG_ERR : Project.MSG_INFO);
if (errors > 0) {
throw new BuildException("Check for forbidden API calls failed, see log.");
}
} finally { } finally {
this.loader = null;
if (antLoader != null) antLoader.cleanup(); if (antLoader != null) antLoader.cleanup();
antLoader = null; antLoader = null;
} classesToCheck.clear();
classpathClassCache.clear();
long start = System.currentTimeMillis(); forbiddenFields.clear();
forbiddenMethods.clear();
int checked = 0; forbiddenClasses.clear();
int errors = 0;
@SuppressWarnings("unchecked")
Iterator<Resource> iter = (Iterator<Resource>) classFiles.iterator();
if (!iter.hasNext()) {
throw new BuildException("There is no <fileset/> given or the fileset does not contain any class files to check.");
}
while (iter.hasNext()) {
final Resource r = iter.next();
if (!r.isExists()) {
throw new BuildException("Class file does not exist: " + r);
}
try {
errors += checkClass(r);
} catch (IOException ioe) {
throw new BuildException("IO problem while reading class file " + r, ioe);
}
checked++;
}
log(String.format(Locale.ROOT,
"Scanned %d class file(s) for forbidden API invocations (in %.2fs), %d error(s).",
checked, (System.currentTimeMillis() - start) / 1000.0, errors),
errors > 0 ? Project.MSG_ERR : Project.MSG_INFO);
if (errors > 0) {
throw new BuildException("Check for forbidden API calls failed, see log.");
} }
} }
@ -386,4 +442,32 @@ public class ForbiddenApisCheckTask extends Task {
return this.classpath.createPath(); return this.classpath.createPath();
} }
static final class ClassSignatureLookup {
public final ClassReader reader;
public final Set<Method> methods;
public final Set<String> fields;
public ClassSignatureLookup(final ClassReader reader) {
this.reader = reader;
final Set<Method> methods = new HashSet<Method>();
final Set<String> fields = new HashSet<String>();
reader.accept(new ClassVisitor(Opcodes.ASM4) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
final Method m = new Method(name, desc);
methods.add(m);
return null;
}
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
fields.add(name);
return null;
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
this.methods = Collections.unmodifiableSet(methods);
this.fields = Collections.unmodifiableSet(fields);
}
}
} }