de.thetaphi.forbiddenapis.ClassScanner.java Source code

Java tutorial

Introduction

Here is the source code for de.thetaphi.forbiddenapis.ClassScanner.java

Source

package de.thetaphi.forbiddenapis;

/*
 * (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.
 */

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 {
    static final Type DEPRECATED_TYPE = Type.getType(Deprecated.class);
    static final String DEPRECATED_DESCRIPTOR = DEPRECATED_TYPE.getDescriptor();

    static final String LAMBDA_META_FACTORY_INTERNALNAME = "java/lang/invoke/LambdaMetafactory";
    static final String LAMBDA_METHOD_NAME_PREFIX = "lambda$";

    private final boolean internalRuntimeForbidden;
    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 supressing 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 internalRuntimeForbidden) {
        super(Opcodes.ASM5);
        this.lookup = lookup;
        this.forbiddenClasses = forbiddenClasses;
        this.forbiddenClassPatterns = forbiddenClassPatterns;
        this.forbiddenMethods = forbiddenMethods;
        this.forbiddenFields = forbiddenFields;
        this.suppressAnnotations = suppressAnnotations;
        this.internalRuntimeForbidden = internalRuntimeForbidden;
    }

    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.printout);
            }
        }
        if (deep && internalRuntimeForbidden) {
            if (AsmUtils.isInternalClass(binaryClassName)) {
                final ClassSignature c = lookup.lookupRelatedClass(internalName);
                if (c == null || c.isRuntimeClass) {
                    return String.format(Locale.ENGLISH, "Forbidden %s use: %s [non-public 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('\n');
                        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;
                }
                final String printout = forbiddenMethods.get(owner + '\000' + method);
                if (printout != null) {
                    return "Forbidden method invocation: " + printout;
                }
                final ClassSignature c = lookup.lookupRelatedClass(owner);
                if (c != null && !c.methods.contains(method)) {
                    if (c.superName != null && (violation = checkMethodAccess(c.superName, method)) != null) {
                        return violation;
                    }
                    // JVM spec says: interfaces after superclasses
                    if (c.interfaces != null) {
                        for (String intf : c.interfaces) {
                            if (intf != null && (violation = checkMethodAccess(intf, method)) != 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 (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;
    }

}