package org.apache.lucene.validation;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Label;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.types.ResourceCollection;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.Resources;
import org.apache.tools.ant.types.resources.FileResource;
import org.apache.tools.ant.types.resources.StringResource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.Reader;
import java.io.File;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
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
* from a given classpath and list of API signatures (either inline or as pointer to files).
* In contrast to other ANT tasks, this tool does only visit the given classpath
* and the system classloader. It uses the local classpath in preference to the system classpath
* (which violates the spec).
*/
public class ForbiddenApisCheckTask extends Task {
private final Resources classFiles = new Resources();
private final Resources apiSignatures = new Resources();
private Path classpath = null;
ClassLoader loader = null;
final Map<String,ClassSignatureLookup> classesToCheck = new HashMap<String,ClassSignatureLookup>();
final Map<String,ClassSignatureLookup> classpathClassCache = new HashMap<String,ClassSignatureLookup>();
final Map<String,String> forbiddenFields = new HashMap<String,String>();
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 {
final InputStream in = loader.getResourceAsStream(clazz.replace('.', '/') + ".class");
if (in == null) {
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);
}
}
return c;
}
/** Adds the method signature to the list of disallowed methods. The Signature is checked against the given ClassLoader. */
private void addSignature(final String signature) throws BuildException {
final String clazz, field;
final Method method;
int p = signature.indexOf('#');
if (p >= 0) {
clazz = signature.substring(0, p);
final String s = signature.substring(p + 1);
p = s.indexOf('(');
if (p >= 0) {
if (p == 0) {
throw new BuildException("Invalid method signature (method name missing): " + signature);
}
// we ignore the return type, its just to match easier (so return type is void):
try {
method = Method.getMethod("void " + s, true);
} catch (IllegalArgumentException iae) {
throw new BuildException("Invalid method signature: " + signature);
}
field = null;
} else {
field = s;
method = null;
}
} else {
clazz = signature;
method = 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:
final ClassSignatureLookup c = getClassFromClassLoader(clazz);
if (method != null) {
assert field == null;
// list all methods with this signature:
boolean found = false;
for (final Method m : c.methods) {
if (m.getName().equals(method.getName()) && Arrays.equals(m.getArgumentTypes(), method.getArgumentTypes())) {
found = true;
forbiddenMethods.put(c.reader.getClassName() + '\000' + m, signature);
// don't break when found, as there may be more covariant overrides!
}
}
if (!found) {
throw new BuildException("No method found with following signature: " + signature);
}
} else if (field != null) {
assert method == null;
if (!c.fields.contains(field)) {
throw new BuildException("No field found with following name: " + signature);
}
forbiddenFields.put(c.reader.getClassName() + '\000' + field, signature);
} else {
assert field == null && method == null;
// only add the signature as class name
forbiddenClasses.put(c.reader.getClassName(), signature);
}
}
/** Reads a list of API signatures. Closes the Reader when done (on Exception, too)! */
private void parseApiFile(Reader reader) throws IOException {
final BufferedReader r = new BufferedReader(reader);
try {
String line;
while ((line = r.readLine()) != null) {
line = line.trim();
if (line.length() == 0 || line.startsWith("#"))
continue;
addSignature(line);
}
} finally {
r.close();
}
}
/** Parses a class given as (FileSet) Resource */
private ClassReader loadClassFromResource(final Resource res) throws BuildException {
try {
final InputStream stream = res.getInputStream();
try {
return new ClassReader(stream);
} finally {
stream.close();
}
} catch (IOException ioe) {
throw new BuildException("IO problem while reading class file " + res, ioe);
}
}
/** Parses a class given as Resource and checks for valid method invocations */
private int checkClass(final ClassReader reader) {
final int[] violations = new int[1];
reader.accept(new ClassVisitor(Opcodes.ASM4) {
final String className = Type.getObjectType(reader.getClassName()).getClassName();
String source = null;
@Override
public void visitSource(String source, String debug) {
this.source = source;
}
@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;
}
final String[] interfaces = c.reader.getInterfaces();
if (interfaces != null) {
for (String intf : interfaces) {
if (intf != null && checkMethodAccess(intf, method)) {
return true;
}
}
}
}
return false;
}
private boolean checkFieldAccess(String owner, String field) {
if (checkClassUse(owner)) {
return true;
}
final String printout = forbiddenFields.get(owner + '\000' + field);
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;
}
}
}
}
return false;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
if (checkMethodAccess(owner, new Method(name, desc))) {
violations[0]++;
reportSourceAndLine();
}
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String desc) {
if (checkFieldAccess(owner, name)) {
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
public void execute() throws BuildException {
AntClassLoader antLoader = null;
try {
if (classpath != null) {
classpath.setProject(getProject());
this.loader = antLoader = getProject().createClassLoader(ClassLoader.getSystemClassLoader(), classpath);
// 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:
antLoader.setParentFirst(false);
} else {
this.loader = ClassLoader.getSystemClassLoader();
}
classFiles.setProject(getProject());
apiSignatures.setProject(getProject());
final long start = System.currentTimeMillis();
try {
@SuppressWarnings("unchecked")
Iterator<Resource> iter = (Iterator<Resource>) apiSignatures.iterator();
if (!iter.hasNext()) {
throw new BuildException("You need to supply at least one API signature definition through apiFile=, <apiFileSet/>, or inner text.");
}
while (iter.hasNext()) {
final Resource r = iter.next();
if (!r.isExists()) {
throw new BuildException("Resource does not exist: " + r);
}
if (r instanceof StringResource) {
final String s = ((StringResource) r).getValue();
if (s != null && s.trim().length() > 0) {
log("Reading inline API signatures...", Project.MSG_INFO);
parseApiFile(new StringReader(s));
}
} else {
log("Reading API signatures: " + r, Project.MSG_INFO);
parseApiFile(new InputStreamReader(r.getInputStream(), "UTF-8"));
}
}
} catch (IOException ioe) {
throw new BuildException("IO problem while reading files with API signatures.", ioe);
}
if (forbiddenMethods.isEmpty() && forbiddenClasses.isEmpty()) {
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 {
this.loader = null;
if (antLoader != null) antLoader.cleanup();
antLoader = null;
classesToCheck.clear();
classpathClassCache.clear();
forbiddenFields.clear();
forbiddenMethods.clear();
forbiddenClasses.clear();
}
}
/** Set of class files to check */
public void add(ResourceCollection rc) {
classFiles.add(rc);
}
/** A file with API signatures apiFile= attribute */
public void setApiFile(File file) {
apiSignatures.add(new FileResource(getProject(), file));
}
/** Set of files with API signatures as <apiFileSet/> nested element */
public FileSet createApiFileSet() {
final FileSet fs = new FileSet();
fs.setProject(getProject());
apiSignatures.add(fs);
return fs;
}
/** Support for API signatures list as nested text */
public void addText(String text) {
apiSignatures.add(new StringResource(getProject(), text));
}
/** Classpath as classpath= attribute */
public void setClasspath(Path classpath) {
createClasspath().append(classpath);
}
/** Classpath as classpathRef= attribute */
public void setClasspathRef(Reference r) {
createClasspath().setRefid(r);
}
/** Classpath as <classpath/> nested element */
public Path createClasspath() {
if (this.classpath == null) {
this.classpath = new Path(getProject());
}
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);
}
}
}