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); } } }