Skip to content

Commit

Permalink
Issue #40: Add initial version of package rules (patterns)
Browse files Browse the repository at this point in the history
  • Loading branch information
uschindler committed Apr 10, 2015
1 parent 0582e33 commit f58dde2
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 57 deletions.
44 changes: 43 additions & 1 deletion src/main/java/de/thetaphi/forbiddenapis/AsmUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public final class AsmUtils {

private AsmUtils() {}

private static final String REGEX_META_CHARS = ".^$+{}[]|()\\";
private static final Pattern INTERNAL_PACKAGE_PATTERN;
static {
final StringBuilder sb = new StringBuilder();
Expand All @@ -35,9 +36,50 @@ private AsmUtils() {}
INTERNAL_PACKAGE_PATTERN = Pattern.compile(sb.append(").*").toString());
}

private static boolean isRegexMeta(char c) {
return REGEX_META_CHARS.indexOf(c) != -1;
}

/** Returns true, if the given binary class name (dotted) is likely a internal class (like sun.misc.Unsafe) */
public static boolean isInternalClass(String className) {
return INTERNAL_PACKAGE_PATTERN.matcher(className).matches();
}


public static boolean isGlob(String s) {
return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
}

/** Returns a regex pattern that matches the glob on class names (e.g., "sun.misc.**") */
public static Pattern glob2Pattern(String glob) {
final StringBuilder regex = new StringBuilder();
int i = 0, len = glob.length();
while (i < len) {
char c = glob.charAt(i++);
switch (c) {
case '*':
if (i < len && glob.charAt(i) == '*') {
// crosses package boundaries
regex.append(".*");
i++;
} else {
// do not cross package boundaries
regex.append("[^.]*");
}
break;

case '?':
// do not cross package boundaries
regex.append("[^.]");
break;

default:
if (isRegexMeta(c)) {
regex.append('\\');
}
regex.append(c);
}
}
return Pattern.compile(regex.toString());
}

}
72 changes: 41 additions & 31 deletions src/main/java/de/thetaphi/forbiddenapis/Checker.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -78,6 +79,8 @@ public abstract class Checker implements RelatedClassLookup {
final Map<String,String> forbiddenMethods = new HashMap<String,String>();
// key is the internal name (slashed):
final Map<String,String> forbiddenClasses = new HashMap<String,String>();
// key is pattern to binary class name:
final Map<Pattern,String> forbiddenClassPatterns = new LinkedHashMap<Pattern,String>();
// descriptors (not internal names) of all annotations that suppress:
final Set<String> suppressAnnotations = new HashSet<String>();

Expand Down Expand Up @@ -283,39 +286,46 @@ private void addSignature(final String line, final String defaultMessage, final
final String printout = (message != null && message.length() > 0) ?
(signature + " [" + message + "]") : signature;
// check class & method/field signature, if it is really existent (in classpath), but we don't really load the class into JVM:
final ClassSignature c;
try {
c = getClassFromClassLoader(clazz);
} catch (ClassNotFoundException cnfe) {
reportParseFailed(failOnUnresolvableSignatures, cnfe.getMessage(), signature);
return;
}
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.className + '\000' + m, printout);
// don't break when found, as there may be more covariant overrides!
}
if (AsmUtils.isGlob(clazz)) {
if (method != null || field != null) {
throw new ParseException(String.format(Locale.ENGLISH, "Class level glob pattern cannot be combined with methods/fields: %s", signature));
}
if (!found) {
reportParseFailed(failOnUnresolvableSignatures, "Method not found", signature);
forbiddenClassPatterns.put(AsmUtils.glob2Pattern(clazz), printout);
} else {
final ClassSignature c;
try {
c = getClassFromClassLoader(clazz);
} catch (ClassNotFoundException cnfe) {
reportParseFailed(failOnUnresolvableSignatures, cnfe.getMessage(), signature);
return;
}
} else if (field != null) {
assert method == null;
if (!c.fields.contains(field)) {
reportParseFailed(failOnUnresolvableSignatures, "Field not found", signature);
return;
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.className + '\000' + m, printout);
// don't break when found, as there may be more covariant overrides!
}
}
if (!found) {
reportParseFailed(failOnUnresolvableSignatures, "Method not found", signature);
return;
}
} else if (field != null) {
assert method == null;
if (!c.fields.contains(field)) {
reportParseFailed(failOnUnresolvableSignatures, "Field not found", signature);
return;
}
forbiddenFields.put(c.className + '\000' + field, printout);
} else {
assert field == null && method == null;
// only add the signature as class name
forbiddenClasses.put(c.className, printout);
}
forbiddenFields.put(c.className + '\000' + field, printout);
} else {
assert field == null && method == null;
// only add the signature as class name
forbiddenClasses.put(c.className, printout);
}
}

Expand Down Expand Up @@ -396,7 +406,7 @@ public final void addClassToCheck(final InputStream in) throws IOException {
}

public final boolean hasNoSignatures() {
return forbiddenMethods.isEmpty() && forbiddenClasses.isEmpty() && forbiddenFields.isEmpty() && (!internalRuntimeForbidden);
return forbiddenMethods.isEmpty() && forbiddenFields.isEmpty() && forbiddenClasses.isEmpty() && forbiddenClassPatterns.isEmpty() && (!internalRuntimeForbidden);
}

/** Adds the given annotation class for suppressing errors. */
Expand All @@ -416,7 +426,7 @@ public final void addSuppressAnnotation(String annoName) {
/** Parses a class and checks for valid method invocations */
private int checkClass(final ClassReader reader) {
final String className = Type.getObjectType(reader.getClassName()).getClassName();
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenMethods, forbiddenFields, suppressAnnotations, internalRuntimeForbidden);
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenClassPatterns, forbiddenMethods, forbiddenFields, suppressAnnotations, internalRuntimeForbidden);
reader.accept(scanner, ClassReader.SKIP_FRAMES);
final List<ForbiddenViolation> violations = scanner.getSortedViolations();
final Pattern splitter = Pattern.compile(Pattern.quote("\n"));
Expand Down
61 changes: 36 additions & 25 deletions src/main/java/de/thetaphi/forbiddenapis/ClassScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
Expand Down Expand Up @@ -56,13 +57,15 @@ final class ClassScanner extends ClassVisitor {
final Map<String,String> forbiddenMethods;
// key is the internal name (slashed):
final Map<String,String> forbiddenClasses;
// key is pattern to binary class name:
final Map<Pattern,String> forbiddenClassPatterns;
// descriptors (not internal names) of all annotations that suppress:
final Set<String> suppressAnnotations;

private String source = null;
private boolean isDeprecated = false;
private boolean done = false;
String internalName = null;
String internalMainClassName = null;
int currentGroupId = 0;

// Mapping from a (possible) lambda Method to groupId of declaring method
Expand All @@ -73,12 +76,14 @@ final class ClassScanner extends ClassVisitor {
boolean classSuppressed = false;

public ClassScanner(RelatedClassLookup lookup,
final Map<String,String> forbiddenClasses, Map<String,String> forbiddenMethods, Map<String,String> forbiddenFields,
final Map<String,String> forbiddenClasses, final Map<Pattern,String> forbiddenClassPatterns,
final Map<String,String> forbiddenMethods, final Map<String,String> forbiddenFields,
final Set<String> suppressAnnotations,
boolean internalRuntimeForbidden) {
final boolean internalRuntimeForbidden) {
super(Opcodes.ASM5);
this.lookup = lookup;
this.forbiddenClasses = forbiddenClasses;
this.forbiddenClassPatterns = forbiddenClassPatterns;
this.forbiddenMethods = forbiddenMethods;
this.forbiddenFields = forbiddenFields;
this.suppressAnnotations = suppressAnnotations;
Expand All @@ -100,26 +105,43 @@ public String getSourceFile() {
return source;
}

String checkClassUse(String internalName, String what) {
String checkClassUse(Type type, String what, boolean deep) {
if (type.getSort() != Type.OBJECT) {
throw new IllegalArgumentException("Type '" + type + "' has wrong sort: " + type.getSort());
}
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);
}
if (internalRuntimeForbidden) {
final String referencedClassName = Type.getObjectType(internalName).getClassName();
if (AsmUtils.isInternalClass(referencedClassName)) {
final String binaryClassName = type.getClassName();
for (final Map.Entry<Pattern,String> pat : forbiddenClassPatterns.entrySet()) {
if (pat.getKey().matcher(binaryClassName).matches()) {
return String.format(Locale.ENGLISH, "Forbidden %s use: %s", what, pat.getValue());
}
}
if (deep) {
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, referencedClassName
what, binaryClassName
);
}
}
}
return null;
}

String checkClassUse(Type type, String what) {
return checkClassUse(type, what, internalRuntimeForbidden);
}

String checkClassUse(String internalName, String what) {
return checkClassUse(Type.getObjectType(internalName), what, internalRuntimeForbidden);
}

private String checkClassDefinition(String superName, String[] interfaces) {
if (superName != null) {
String violation = checkClassUse(superName, "class");
Expand Down Expand Up @@ -151,7 +173,7 @@ String checkType(Type type) {
String violation;
switch (type.getSort()) {
case Type.OBJECT:
violation = checkClassUse(type.getInternalName(), "class/interface");
violation = checkClassUse(type, "class/interface");
if (violation != null) {
return violation;
}
Expand Down Expand Up @@ -204,20 +226,9 @@ String checkAnnotationDescriptor(String desc, boolean visible) {
// should never happen for annotations!
throw new IllegalArgumentException("Annotation descriptor '" + desc + "' has wrong sort: " + type.getSort());
}
if (visible) {
// for visible annotations, we don't need to look into super-classes, interfaces,...
// -> we just check if its disallowed or internal runtime!
return checkClassUse(type.getInternalName(), "annotation");
} else {
// if annotation is not visible at runtime, we don't do deep checks (not
// even internal runtime checks), just lookup in forbidden classes list!
// The reason for this is: They may not be available in classpath at all!!!
final String printout = forbiddenClasses.get(type.getInternalName());
if (printout != null) {
return "Forbidden annotation use: " + printout;
}
}
return null;
// 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 && internalRuntimeForbidden);
}

void maybeSuppressCurrentGroup(String annotationDesc) {
Expand All @@ -234,7 +245,7 @@ private void reportClassViolation(String violation, String where) {

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.internalName = name;
this.internalMainClassName = name;
this.isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0;
reportClassViolation(checkClassDefinition(superName, interfaces), "class declaration");
if (this.isDeprecated) {
Expand Down Expand Up @@ -396,7 +407,7 @@ private String checkHandle(Handle handle, boolean checkLambdaHandle) {
case Opcodes.H_NEWINVOKESPECIAL:
case Opcodes.H_INVOKEINTERFACE:
final Method m = new Method(handle.getName(), handle.getDesc());
if (checkLambdaHandle && handle.getOwner().equals(internalName) && handle.getName().startsWith(LAMBDA_METHOD_NAME_PREFIX)) {
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:
Expand Down
12 changes: 12 additions & 0 deletions src/test/antunit/TestInlineSignatures.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@
<au:assertLogContains level="error" text="java.lang.String [You are crazy that you disallow strings]"/>
</target>

<target name="testForbiddenClassPatternWithMessage">
<au:expectfailure expectedMessage="Check for forbidden API calls failed, see log">
<forbiddenapis classpathref="path.run">
<fileset refid="main.classes"/>
java.util.Array* @ You are crazy that you disallow all Array*
java.lang.** @ You are crazy that you disallow all java.lang
</forbiddenapis>
</au:expectfailure>
<au:assertLogContains level="error" text="java.util.Array* [You are crazy that you disallow all Array*]"/>
<au:assertLogContains level="error" text="java.lang.** [You are crazy that you disallow all java.lang]"/>
</target>

<target name="testForbiddenClassWithDefaultMessage">
<au:expectfailure expectedMessage="Check for forbidden API calls failed, see log">
<forbiddenapis classpathref="path.run">
Expand Down

0 comments on commit f58dde2

Please sign in to comment.