/* * (C) Copyright Uwe Schindler (Generics Policeman) and others. * Parts of this work are licensed to the Apache Software Foundation (ASF) * under one or more contributor license agreements. * * 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 de.thetaphi.forbiddenapis; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Handle; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; import org.objectweb.asm.commons.Method; final class ClassScanner extends ClassVisitor implements Constants { private final boolean forbidNonPortableRuntime; final RelatedClassLookup lookup; final List<ForbiddenViolation> violations = new ArrayList<ForbiddenViolation>(); // key is the internal name (slashed), followed by \000 and the field name: final Map<String,String> forbiddenFields; // key is the internal name (slashed), followed by \000 and the method signature: final Map<String,String> forbiddenMethods; // key is the internal name (slashed): final Map<String,String> forbiddenClasses; // key is pattern to binary class name: final Iterable<ClassPatternRule> forbiddenClassPatterns; // pattern that matches binary (dotted) class name of all annotations that suppress: final Pattern suppressAnnotations; private String source = null; private boolean isDeprecated = false; private boolean done = false; String internalMainClassName = null; int currentGroupId = 0; // Mapping from a (possible) lambda Method to groupId of declaring method final Map<Method,Integer> lambdas = new HashMap<Method,Integer>(); // all groups that were disabled due to suppressing annotation final BitSet suppressedGroups = new BitSet(); boolean classSuppressed = false; public ClassScanner(RelatedClassLookup lookup, final Map<String,String> forbiddenClasses, final Iterable<ClassPatternRule> forbiddenClassPatterns, final Map<String,String> forbiddenMethods, final Map<String,String> forbiddenFields, final Pattern suppressAnnotations, final boolean forbidNonPortableRuntime) { super(Opcodes.ASM5); this.lookup = lookup; this.forbiddenClasses = forbiddenClasses; this.forbiddenClassPatterns = forbiddenClassPatterns; this.forbiddenMethods = forbiddenMethods; this.forbiddenFields = forbiddenFields; this.suppressAnnotations = suppressAnnotations; this.forbidNonPortableRuntime = forbidNonPortableRuntime; } private void checkDone() { if (done) return; throw new IllegalStateException("Class not fully scanned."); } public List<ForbiddenViolation> getSortedViolations() { checkDone(); return classSuppressed ? Collections.<ForbiddenViolation>emptyList() : Collections.unmodifiableList(violations); } public String getSourceFile() { checkDone(); return source; } String checkClassUse(Type type, String what, boolean deep) { while (type.getSort() == Type.ARRAY) { type = type.getElementType(); } if (type.getSort() != Type.OBJECT) { return null; // we don't know this type, just pass! } final String internalName = type.getInternalName(); final String printout = forbiddenClasses.get(internalName); if (printout != null) { return String.format(Locale.ENGLISH, "Forbidden %s use: %s", what, printout); } final String binaryClassName = type.getClassName(); for (final ClassPatternRule r : forbiddenClassPatterns) { if (r.matches(binaryClassName)) { return String.format(Locale.ENGLISH, "Forbidden %s use: %s", what, r.getPrintout(binaryClassName)); } } if (deep && forbidNonPortableRuntime) { final ClassSignature c = lookup.lookupRelatedClass(internalName); if (c != null && c.isRuntimeClass && !AsmUtils.isPortableRuntimeClass(binaryClassName)) { return String.format(Locale.ENGLISH, "Forbidden %s use: %s [non-portable or internal runtime class]", what, binaryClassName ); } } return null; } String checkClassUse(String internalName, String what) { return checkClassUse(Type.getObjectType(internalName), what, true); } private String checkClassDefinition(String superName, String[] interfaces) { if (superName != null) { String violation = checkClassUse(superName, "class"); if (violation != null) { return violation; } final ClassSignature c = lookup.lookupRelatedClass(superName); if (c != null && (violation = checkClassDefinition(c.superName, c.interfaces)) != null) { return violation; } } if (interfaces != null) { for (String intf : interfaces) { String violation = checkClassUse(intf, "interface"); if (violation != null) { return violation; } final ClassSignature c = lookup.lookupRelatedClass(intf); if (c != null && (violation = checkClassDefinition(c.superName, c.interfaces)) != null) { return violation; } } } return null; } String checkType(Type type) { while (type != null) { String violation; switch (type.getSort()) { case Type.OBJECT: violation = checkClassUse(type, "class/interface", true); if (violation != null) { return violation; } final ClassSignature c = lookup.lookupRelatedClass(type.getInternalName()); if (c == null) return null; return checkClassDefinition(c.superName, c.interfaces); case Type.ARRAY: type = type.getElementType(); break; case Type.METHOD: final ArrayList<String> violations = new ArrayList<String>(); violation = checkType(type.getReturnType()); if (violation != null) { violations.add(violation); } for (final Type t : type.getArgumentTypes()) { violation = checkType(t); if (violation != null) { violations.add(violation); } } if (violations.isEmpty()) { return null; } else if (violations.size() == 1) { return violations.get(0); } else { final StringBuilder sb = new StringBuilder(); boolean nl = false; for (final String v : violations) { if (nl) sb.append(ForbiddenViolation.SEPARATOR); sb.append(v); nl = true; } return sb.toString(); } default: return null; } } return null; } String checkDescriptor(String desc) { return checkType(Type.getType(desc)); } String checkAnnotationDescriptor(Type type, boolean visible) { // for annotations, we don't need to look into super-classes, interfaces,... // -> we just check if its disallowed or internal runtime (only if visible)! return checkClassUse(type, "annotation", visible); } void maybeSuppressCurrentGroup(Type annotation) { if (suppressAnnotations.matcher(annotation.getClassName()).matches()) { suppressedGroups.set(currentGroupId); } } private void reportClassViolation(String violation, String where) { if (violation != null) { violations.add(new ForbiddenViolation(currentGroupId, violation, where, -1)); } } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.internalMainClassName = name; this.isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0; reportClassViolation(checkClassDefinition(superName, interfaces), "class declaration"); if (this.isDeprecated) { classSuppressed |= suppressAnnotations.matcher(DEPRECATED_TYPE.getClassName()).matches(); reportClassViolation(checkType(DEPRECATED_TYPE), "deprecation on class declaration"); } } @Override public void visitSource(String source, String debug) { this.source = source; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (this.isDeprecated && DEPRECATED_DESCRIPTOR.equals(desc)) { // don't report 2 times! return null; } final Type type = Type.getType(desc); classSuppressed |= suppressAnnotations.matcher(type.getClassName()).matches(); reportClassViolation(checkAnnotationDescriptor(type, visible), "annotation on class declaration"); return null; } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { reportClassViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on class declaration"); return null; } @Override public FieldVisitor visitField(final int access, final String name, final String desc, String signature, Object value) { currentGroupId++; if (classSuppressed) { return null; } return new FieldVisitor(Opcodes.ASM5) { final boolean isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0; { // only check signature, if field is not synthetic if ((access & Opcodes.ACC_SYNTHETIC) == 0) { reportFieldViolation(checkDescriptor(desc), "field declaration"); } if (this.isDeprecated) { maybeSuppressCurrentGroup(DEPRECATED_TYPE); reportFieldViolation(checkType(DEPRECATED_TYPE), "deprecation on field declaration"); } } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (this.isDeprecated && DEPRECATED_DESCRIPTOR.equals(desc)) { // don't report 2 times! return null; } final Type type = Type.getType(desc); maybeSuppressCurrentGroup(type); reportFieldViolation(checkAnnotationDescriptor(type, visible), "annotation on field declaration"); return null; } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { reportFieldViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on field declaration"); return null; } private void reportFieldViolation(String violation, String where) { if (violation != null) { violations.add(new ForbiddenViolation(currentGroupId, violation, String.format(Locale.ENGLISH, "%s of '%s'", where, name), -1)); } } }; } @Override public MethodVisitor visitMethod(final int access, final String name, final String desc, String signature, String[] exceptions) { currentGroupId++; if (classSuppressed) { return null; } return new MethodVisitor(Opcodes.ASM5) { private final Method myself = new Method(name, desc); private final boolean isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0; private int lineNo = -1; { // only check signature, if method is not synthetic if ((access & Opcodes.ACC_SYNTHETIC) == 0) { reportMethodViolation(checkDescriptor(desc), "method declaration"); } if (this.isDeprecated) { maybeSuppressCurrentGroup(DEPRECATED_TYPE); reportMethodViolation(checkType(DEPRECATED_TYPE), "deprecation on method declaration"); } } private String checkMethodAccess(String owner, Method method) { String violation = checkClassUse(owner, "class/interface"); if (violation != null) { return violation; } if (CLASS_CONSTRUCTOR_METHOD_NAME.equals(method.getName())) { // we don't check for violations on class constructors return null; } return checkMethodAccessRecursion(owner, method, true); } private String checkMethodAccessRecursion(String owner, Method method, boolean checkClassUse) { String printout = forbiddenMethods.get(owner + '\000' + method); if (printout != null) { return "Forbidden method invocation: " + printout; } final ClassSignature c = lookup.lookupRelatedClass(owner); if (c != null) { if (c.signaturePolymorphicMethods.contains(method.getName())) { // convert the invoked descriptor to a signature polymorphic one for the lookup final Method lookupMethod = new Method(method.getName(), SIGNATURE_POLYMORPHIC_DESCRIPTOR); printout = forbiddenMethods.get(owner + '\000' + lookupMethod); if (printout != null) { return "Forbidden method invocation: " + printout; } } String violation; if (checkClassUse && c.methods.contains(method)) { violation = checkClassUse(owner, "class/interface"); if (violation != null) { return violation; } } if (CONSTRUCTOR_METHOD_NAME.equals(method.getName())) { return null; // don't look into superclasses or interfaces to find constructors! } if (c.superName != null && (violation = checkMethodAccessRecursion(c.superName, method, true)) != null) { return violation; } // JVM spec says: interfaces after superclasses if (c.interfaces != null) { for (String intf : c.interfaces) { // for interfaces we don't check the class use (it is too strict, if just the interface is implemented, but nothing more!): if (intf != null && (violation = checkMethodAccessRecursion(intf, method, false)) != null) { return violation; } } } } return null; } private String checkFieldAccess(String owner, String field) { String violation = checkClassUse(owner, "class/interface"); if (violation != null) { return violation; } final String printout = forbiddenFields.get(owner + '\000' + field); if (printout != null) { return "Forbidden field access: " + printout; } final ClassSignature c = lookup.lookupRelatedClass(owner); // if we have seen the field already, no need to look into superclasses (fields cannot override) if (c != null && !c.fields.contains(field)) { if (c.interfaces != null) { for (String intf : c.interfaces) { if (intf != null && (violation = checkFieldAccess(intf, field)) != null) { return violation; } } } // JVM spec says: superclasses after interfaces if (c.superName != null && (violation = checkFieldAccess(c.superName, field)) != null) { return violation; } } return null; } private String checkHandle(Handle handle, boolean checkLambdaHandle) { switch (handle.getTag()) { case Opcodes.H_GETFIELD: case Opcodes.H_PUTFIELD: case Opcodes.H_GETSTATIC: case Opcodes.H_PUTSTATIC: return checkFieldAccess(handle.getOwner(), handle.getName()); case Opcodes.H_INVOKEVIRTUAL: case Opcodes.H_INVOKESTATIC: case Opcodes.H_INVOKESPECIAL: case Opcodes.H_NEWINVOKESPECIAL: case Opcodes.H_INVOKEINTERFACE: final Method m = new Method(handle.getName(), handle.getDesc()); if (checkLambdaHandle && handle.getOwner().equals(internalMainClassName) && handle.getName().startsWith(LAMBDA_METHOD_NAME_PREFIX)) { // as described in <http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html>, // we will record this metafactory call as "lambda" invokedynamic, // so we can assign the called lambda with the same groupId like *this* method: lambdas.put(m, currentGroupId); } return checkMethodAccess(handle.getOwner(), m); } return null; } private String checkConstant(Object cst, boolean checkLambdaHandle) { if (cst instanceof Type) { return checkType((Type) cst); } else if (cst instanceof Handle) { return checkHandle((Handle) cst, checkLambdaHandle); } return null; } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if (this.isDeprecated && DEPRECATED_DESCRIPTOR.equals(desc)) { // don't report 2 times! return null; } final Type type = Type.getType(desc); maybeSuppressCurrentGroup(type); reportMethodViolation(checkAnnotationDescriptor(type, visible), "annotation on method declaration"); return null; } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "parameter annotation on method declaration"); return null; } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "type annotation on method declaration"); return null; } @Override public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body"); return null; } @Override public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String desc, boolean visible) { reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body"); return null; } @Override public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String desc, boolean visible) { reportMethodViolation(checkAnnotationDescriptor(Type.getType(desc), visible), "annotation in method body"); return null; } @Override public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { reportMethodViolation(checkMethodAccess(owner, new Method(name, desc)), "method body"); } @Override public void visitFieldInsn(int opcode, String owner, String name, String desc) { reportMethodViolation(checkFieldAccess(owner, name), "method body"); } @Override public void visitTypeInsn(int opcode, String type) { if (opcode == Opcodes.ANEWARRAY) { reportMethodViolation(checkType(Type.getObjectType(type)), "method body"); } } @Override public void visitMultiANewArrayInsn(String desc, int dims) { reportMethodViolation(checkDescriptor(desc), "method body"); } @Override public void visitLdcInsn(Object cst) { reportMethodViolation(checkConstant(cst, false), "method body"); } @Override public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) { final boolean isLambdaMetaFactory = LAMBDA_META_FACTORY_INTERNALNAME.equals(bsm.getOwner()); reportMethodViolation(checkHandle(bsm, false), "method body"); for (final Object cst : bsmArgs) { reportMethodViolation(checkConstant(cst, isLambdaMetaFactory), "method body"); } } private String getHumanReadableMethodSignature() { final Type[] args = Type.getType(myself.getDescriptor()).getArgumentTypes(); final StringBuilder sb = new StringBuilder(myself.getName()).append('('); boolean comma = false; for (final Type t : args) { if (comma) sb.append(','); sb.append(t.getClassName()); comma = true; } sb.append(')'); return sb.toString(); } private void reportMethodViolation(String violation, String where) { if (violation != null) { violations.add(new ForbiddenViolation(currentGroupId, myself, violation, String.format(Locale.ENGLISH, "%s of '%s'", where, getHumanReadableMethodSignature()), lineNo)); } } @Override public void visitLineNumber(int lineNo, Label start) { this.lineNo = lineNo; } }; } @Override public void visitEnd() { // fixup lambdas by assigning them the groupId where they were originally declared: for (final ForbiddenViolation v : violations) { if (v.targetMethod != null) { final Integer newGroupId = lambdas.get(v.targetMethod); if (newGroupId != null) { v.setGroupId(newGroupId.intValue()); } } } // filter out suppressed groups if (!suppressedGroups.isEmpty()) { for (final Iterator<ForbiddenViolation> it = violations.iterator(); it.hasNext();) { final ForbiddenViolation v = it.next(); if (suppressedGroups.get(v.getGroupId())) { it.remove(); } } } // sort the violations by group id and later by line number: Collections.sort(violations); done = true; } }