Refactoring to support more annotation types.
Split the actual annotation handling logic out of AnnotationVisitor,
instead delegating to a new AnnotationHandler interface. The visitor now
simply takes a map of annotation name to handler, and delegate all the
interesting work.
Create a new GreylistAnnotationHandler for dealing with the existing
@UnsupportedAppUsage annotations.
Add support for writing to a whitelist file too, though this is not used
yet in this CL. A new parameter --write-whitelist specifies a filename to
write this to.
Also refactor the tests along similar lines.
Bug: 112186571
Test: atest class2greylisttest
Change-Id: Ic4da92f3499da7f40f9207ad1776ad79d76464a4
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
new file mode 100644
index 0000000..eb54a33
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationContext.java
@@ -0,0 +1,66 @@
+package com.android.class2greylist;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.classfile.FieldOrMethod;
+import org.apache.bcel.classfile.JavaClass;
+
+import java.util.Locale;
+
+/**
+ * Encapsulates context for a single annotation on a class member.
+ */
+public class AnnotationContext {
+
+ public final Status status;
+ public final FieldOrMethod member;
+ public final JavaClass definingClass;
+ public final String signatureFormatString;
+
+ public AnnotationContext(
+ Status status,
+ FieldOrMethod member,
+ JavaClass definingClass,
+ String signatureFormatString) {
+ this.status = status;
+ this.member = member;
+ this.definingClass = definingClass;
+ this.signatureFormatString = signatureFormatString;
+ }
+
+ /**
+ * @return the full descriptor of enclosing class.
+ */
+ public String getClassDescriptor() {
+ // JavaClass.getName() returns the Java-style name (with . not /), so we must fetch
+ // the original class name from the constant pool.
+ return definingClass.getConstantPool().getConstantString(
+ definingClass.getClassNameIndex(), Const.CONSTANT_Class);
+ }
+
+ /**
+ * @return the full descriptor of this member, in the format expected in
+ * the greylist.
+ */
+ public String getMemberDescriptor() {
+ return String.format(Locale.US, signatureFormatString,
+ getClassDescriptor(), member.getName(), member.getSignature());
+ }
+
+ /**
+ * Report an error in this context. The final error message will include
+ * the class and member names, and the source file name.
+ */
+ public void reportError(String message, Object... args) {
+ StringBuilder error = new StringBuilder();
+ error.append(definingClass.getSourceFileName())
+ .append(": ")
+ .append(definingClass.getClassName())
+ .append(".")
+ .append(member.getName())
+ .append(": ")
+ .append(String.format(Locale.US, message, args));
+
+ status.error(error.toString());
+ }
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
new file mode 100644
index 0000000..92d2ab6
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationHandler.java
@@ -0,0 +1,11 @@
+package com.android.class2greylist;
+
+import org.apache.bcel.classfile.AnnotationEntry;
+
+/**
+ * Interface for an annotation handler, which handle individual annotations on
+ * class members.
+ */
+public interface AnnotationHandler {
+ void handleAnnotation(AnnotationEntry annotation, AnnotationContext context);
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
index 1838575..b805b30 100644
--- a/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
+++ b/tools/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
@@ -16,100 +16,40 @@
package com.android.class2greylist;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-
-import org.apache.bcel.Const;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.DescendingVisitor;
-import org.apache.bcel.classfile.ElementValue;
-import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.EmptyVisitor;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.FieldOrMethod;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
-import org.apache.bcel.classfile.SimpleElementValue;
-import java.util.Locale;
-import java.util.Set;
-import java.util.function.Predicate;
+import java.util.Map;
/**
- * Visits a JavaClass instance and pulls out all members annotated with a
- * specific annotation. The signatures of such members are passed to {@link
- * GreylistConsumer#greylistEntry(String, Integer)}. Any errors result in a
- * call to {@link Status#error(String, Object...)}.
- *
- * If the annotation has a property "expectedSignature" the generated signature
- * will be verified against the one specified there. If it differs, an error
- * will be generated.
+ * Visits a JavaClass instance and passes any annotated members to a {@link AnnotationHandler}
+ * according to the map provided.
*/
public class AnnotationVisitor extends EmptyVisitor {
- private static final String EXPECTED_SIGNATURE = "expectedSignature";
- private static final String MAX_TARGET_SDK = "maxTargetSdk";
-
private final JavaClass mClass;
- private final String mAnnotationType;
- private final Predicate<Member> mMemberFilter;
- private final Set<Integer> mValidMaxTargetSdkValues;
- private final GreylistConsumer mConsumer;
private final Status mStatus;
private final DescendingVisitor mDescendingVisitor;
+ private final Map<String, AnnotationHandler> mAnnotationHandlers;
/**
- * Represents a member of a class file (a field or method).
+ * Creates a visitor for a class.
+ *
+ * @param clazz Class to visit
+ * @param status For reporting debug information
+ * @param handlers Map of {@link AnnotationHandler}. The keys should be annotation names, as
+ * class descriptors.
*/
- @VisibleForTesting
- public static class Member {
-
- /**
- * Signature of this member.
- */
- public final String signature;
- /**
- * Indicates if this is a synthetic bridge method.
- */
- public final boolean bridge;
- /**
- * Max target SDK of property this member, if it is set, else null.
- *
- * Note: even though the annotation itself specified a default value,
- * that default value is not encoded into instances of the annotation
- * in class files. So when no value is specified in source, it will
- * result in null appearing in here.
- */
- public final Integer maxTargetSdk;
-
- public Member(String signature, boolean bridge, Integer maxTargetSdk) {
- this.signature = signature;
- this.bridge = bridge;
- this.maxTargetSdk = maxTargetSdk;
- }
- }
-
- public AnnotationVisitor(JavaClass clazz, String annotation, Set<String> publicApis,
- Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer,
- Status status) {
- this(clazz,
- annotation,
- member -> !(member.bridge && publicApis.contains(member.signature)),
- validMaxTargetSdkValues,
- consumer,
- status);
- }
-
- @VisibleForTesting
- public AnnotationVisitor(JavaClass clazz, String annotation, Predicate<Member> memberFilter,
- Set<Integer> validMaxTargetSdkValues, GreylistConsumer consumer,
- Status status) {
+ public AnnotationVisitor(JavaClass clazz, Status status,
+ Map<String, AnnotationHandler> handlers) {
mClass = clazz;
- mAnnotationType = annotation;
- mMemberFilter = memberFilter;
- mValidMaxTargetSdkValues = validMaxTargetSdkValues;
- mConsumer = consumer;
mStatus = status;
+ mAnnotationHandlers = handlers;
mDescendingVisitor = new DescendingVisitor(clazz, this);
}
@@ -118,13 +58,6 @@
mDescendingVisitor.visit();
}
- private static String getClassDescriptor(JavaClass clazz) {
- // JavaClass.getName() returns the Java-style name (with . not /), so we must fetch
- // the original class name from the constant pool.
- return clazz.getConstantPool().getConstantString(
- clazz.getClassNameIndex(), Const.CONSTANT_Class);
- }
-
@Override
public void visitMethod(Method method) {
visitMember(method, "L%s;->%s%s");
@@ -136,80 +69,15 @@
}
private void visitMember(FieldOrMethod member, String signatureFormatString) {
- JavaClass definingClass = (JavaClass) mDescendingVisitor.predecessor();
mStatus.debug("Visit member %s : %s", member.getName(), member.getSignature());
+ AnnotationContext context = new AnnotationContext(mStatus, member,
+ (JavaClass) mDescendingVisitor.predecessor(), signatureFormatString);
for (AnnotationEntry a : member.getAnnotationEntries()) {
- if (mAnnotationType.equals(a.getAnnotationType())) {
- mStatus.debug("Member has annotation %s", mAnnotationType);
- // For fields, the same access flag means volatile, so only check for methods.
- boolean bridge = (member instanceof Method)
- && (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
- if (bridge) {
- mStatus.debug("Member is a bridge", mAnnotationType);
- }
- String signature = String.format(Locale.US, signatureFormatString,
- getClassDescriptor(definingClass), member.getName(), member.getSignature());
- Integer maxTargetSdk = null;
- for (ElementValuePair property : a.getElementValuePairs()) {
- switch (property.getNameString()) {
- case EXPECTED_SIGNATURE:
- verifyExpectedSignature(
- property, signature, definingClass, member, bridge);
- break;
- case MAX_TARGET_SDK:
- maxTargetSdk = verifyAndGetMaxTargetSdk(
- property, definingClass, member);
- break;
- }
- }
- if (mMemberFilter.test(new Member(signature, bridge, maxTargetSdk))) {
- mConsumer.greylistEntry(signature, maxTargetSdk);
- }
+ if (mAnnotationHandlers.containsKey(a.getAnnotationType())) {
+ mStatus.debug("Member has annotation %s for which we have a handler",
+ a.getAnnotationType());
+ mAnnotationHandlers.get(a.getAnnotationType()).handleAnnotation(a, context);
}
}
}
-
- private void verifyExpectedSignature(ElementValuePair property, String signature,
- JavaClass definingClass, FieldOrMethod member, boolean isBridge) {
- String expected = property.getValue().stringifyValue();
- // Don't enforce for bridge methods; they're generated so won't match.
- if (!isBridge && !signature.equals(expected)) {
- error(definingClass, member,
- "Expected signature does not match generated:\n"
- + "Expected: %s\n"
- + "Generated: %s", expected, signature);
- }
- }
-
- private Integer verifyAndGetMaxTargetSdk(
- ElementValuePair property, JavaClass definingClass, FieldOrMethod member) {
- if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) {
- error(definingClass, member, "Expected property %s to be of type int; got %d",
- property.getNameString(), property.getValue().getElementValueType());
- }
- int value = ((SimpleElementValue) property.getValue()).getValueInt();
- if (!mValidMaxTargetSdkValues.contains(value)) {
- error(definingClass, member,
- "Invalid value for %s: got %d, expected one of [%s]",
- property.getNameString(),
- value,
- Joiner.on(",").join(mValidMaxTargetSdkValues));
- return null;
- }
- return value;
- }
-
- private void error(JavaClass clazz, FieldOrMethod member, String message, Object... args) {
- StringBuilder error = new StringBuilder();
- error.append(clazz.getSourceFileName())
- .append(": ")
- .append(clazz.getClassName())
- .append(".")
- .append(member.getName())
- .append(": ")
- .append(String.format(Locale.US, message, args));
-
- mStatus.error(error.toString());
- }
-
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
index 64a0357..c0a3160 100644
--- a/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
+++ b/tools/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -17,6 +17,8 @@
package com.android.class2greylist;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
@@ -44,12 +46,18 @@
*/
public class Class2Greylist {
- private static final String ANNOTATION_TYPE = "Landroid/annotation/UnsupportedAppUsage;";
+ private static final String GREYLIST_ANNOTATION = "Landroid/annotation/UnsupportedAppUsage;";
+ private static final Set<String> WHITELIST_ANNOTATIONS = ImmutableSet.of();
private final Status mStatus;
private final String mPublicApiListFile;
private final String[] mPerSdkOutputFiles;
+ private final String mWhitelistFile;
private final String[] mJarFiles;
+ private final GreylistConsumer mOutput;
+ private final Set<Integer> mAllowedSdkVersions;
+ private final Set<String> mPublicApis;
+
public static void main(String[] args) {
Options options = new Options();
@@ -68,6 +76,11 @@
"no integer is given, members with no maxTargetSdk are written.")
.create("g"));
options.addOption(OptionBuilder
+ .withLongOpt("write-whitelist")
+ .hasArgs(1)
+ .withDescription("Specify file to write whitelist to.")
+ .create('w'));
+ options.addOption(OptionBuilder
.withLongOpt("debug")
.hasArgs(0)
.withDescription("Enable debug")
@@ -100,9 +113,13 @@
}
Status status = new Status(cmd.hasOption('d'));
- Class2Greylist c2gl = new Class2Greylist(
- status, cmd.getOptionValue('p', null), cmd.getOptionValues('g'), jarFiles);
try {
+ Class2Greylist c2gl = new Class2Greylist(
+ status,
+ cmd.getOptionValue('p', null),
+ cmd.getOptionValues('g'),
+ cmd.getOptionValue('w', null),
+ jarFiles);
c2gl.main();
} catch (IOException e) {
status.error(e);
@@ -118,52 +135,58 @@
@VisibleForTesting
Class2Greylist(Status status, String publicApiListFile, String[] perSdkLevelOutputFiles,
- String[] jarFiles) {
+ String whitelistOutputFile, String[] jarFiles) throws IOException {
mStatus = status;
mPublicApiListFile = publicApiListFile;
mPerSdkOutputFiles = perSdkLevelOutputFiles;
+ mWhitelistFile = whitelistOutputFile;
mJarFiles = jarFiles;
- }
-
- private void main() throws IOException {
- GreylistConsumer output;
- Set<Integer> allowedSdkVersions;
if (mPerSdkOutputFiles != null) {
- Map<Integer, String> outputFiles = readGreylistMap(mPerSdkOutputFiles);
- output = new FileWritingGreylistConsumer(mStatus, outputFiles);
- allowedSdkVersions = outputFiles.keySet();
+ Map<Integer, String> outputFiles = readGreylistMap(mStatus, mPerSdkOutputFiles);
+ mOutput = new FileWritingGreylistConsumer(mStatus, outputFiles, mWhitelistFile);
+ mAllowedSdkVersions = outputFiles.keySet();
} else {
// TODO remove this once per-SDK greylist support integrated into the build.
// Right now, mPerSdkOutputFiles is always null as the build never passes the
// corresponding command lind flags. Once the build is updated, can remove this.
- output = new SystemOutGreylistConsumer();
- allowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28));
+ mOutput = new SystemOutGreylistConsumer();
+ mAllowedSdkVersions = new HashSet<>(Arrays.asList(null, 26, 28));
}
- Set<String> publicApis;
if (mPublicApiListFile != null) {
- publicApis = Sets.newHashSet(
+ mPublicApis = Sets.newHashSet(
Files.readLines(new File(mPublicApiListFile), Charset.forName("UTF-8")));
} else {
- publicApis = Collections.emptySet();
+ mPublicApis = Collections.emptySet();
}
+ }
+ private Map<String, AnnotationHandler> createAnnotationHandlers() {
+ return ImmutableMap.<String, AnnotationHandler>builder()
+ .put(GreylistAnnotationHandler.ANNOTATION_NAME,
+ new GreylistAnnotationHandler(
+ mStatus, mOutput, mPublicApis, mAllowedSdkVersions))
+ .build();
+ }
+
+ private void main() throws IOException {
+ Map<String, AnnotationHandler> handlers = createAnnotationHandlers();
for (String jarFile : mJarFiles) {
mStatus.debug("Processing jar file %s", jarFile);
try {
JarReader reader = new JarReader(mStatus, jarFile);
- reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, ANNOTATION_TYPE,
- publicApis, allowedSdkVersions, output, mStatus).visit());
+ reader.stream().forEach(clazz -> new AnnotationVisitor(clazz, mStatus, handlers)
+ .visit());
reader.close();
} catch (IOException e) {
mStatus.error(e);
}
}
- output.close();
+ mOutput.close();
}
@VisibleForTesting
- Map<Integer, String> readGreylistMap(String[] argValues) {
+ static Map<Integer, String> readGreylistMap(Status status, String[] argValues) {
Map<Integer, String> map = new HashMap<>();
for (String sdkFile : argValues) {
Integer maxTargetSdk = null;
@@ -173,12 +196,12 @@
try {
maxTargetSdk = Integer.valueOf(sdkFile.substring(0, colonPos));
} catch (NumberFormatException nfe) {
- mStatus.error("Not a valid integer: %s from argument value '%s'",
+ status.error("Not a valid integer: %s from argument value '%s'",
sdkFile.substring(0, colonPos), sdkFile);
}
filename = sdkFile.substring(colonPos + 1);
if (filename.length() == 0) {
- mStatus.error("Not a valid file name: %s from argument value '%s'",
+ status.error("Not a valid file name: %s from argument value '%s'",
filename, sdkFile);
}
} else {
@@ -186,7 +209,7 @@
filename = sdkFile;
}
if (map.containsKey(maxTargetSdk)) {
- mStatus.error("Multiple output files for maxTargetSdk %s", maxTargetSdk);
+ status.error("Multiple output files for maxTargetSdk %s", maxTargetSdk);
} else {
map.put(maxTargetSdk, filename);
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
index 86eeeff..9f33467 100644
--- a/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/FileWritingGreylistConsumer.java
@@ -11,21 +11,29 @@
private final Status mStatus;
private final Map<Integer, PrintStream> mSdkToPrintStreamMap;
+ private final PrintStream mWhitelistStream;
+
+ private static PrintStream openFile(String filename) throws FileNotFoundException {
+ if (filename == null) {
+ return null;
+ }
+ return new PrintStream(new FileOutputStream(new File(filename)));
+ }
private static Map<Integer, PrintStream> openFiles(
Map<Integer, String> filenames) throws FileNotFoundException {
Map<Integer, PrintStream> streams = new HashMap<>();
for (Map.Entry<Integer, String> entry : filenames.entrySet()) {
- streams.put(entry.getKey(),
- new PrintStream(new FileOutputStream(new File(entry.getValue()))));
+ streams.put(entry.getKey(), openFile(entry.getValue()));
}
return streams;
}
- public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap)
- throws FileNotFoundException {
+ public FileWritingGreylistConsumer(Status status, Map<Integer, String> sdkToFilenameMap,
+ String whitelistFile) throws FileNotFoundException {
mStatus = status;
mSdkToPrintStreamMap = openFiles(sdkToFilenameMap);
+ mWhitelistStream = openFile(whitelistFile);
}
@Override
@@ -40,9 +48,19 @@
}
@Override
+ public void whitelistEntry(String signature) {
+ if (mWhitelistStream != null) {
+ mWhitelistStream.println(signature);
+ }
+ }
+
+ @Override
public void close() {
for (PrintStream p : mSdkToPrintStreamMap.values()) {
p.close();
}
+ if (mWhitelistStream != null) {
+ mWhitelistStream.close();
+ }
}
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
new file mode 100644
index 0000000..460f2c3
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistAnnotationHandler.java
@@ -0,0 +1,146 @@
+package com.android.class2greylist;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.ElementValue;
+import org.apache.bcel.classfile.ElementValuePair;
+import org.apache.bcel.classfile.FieldOrMethod;
+import org.apache.bcel.classfile.Method;
+import org.apache.bcel.classfile.SimpleElementValue;
+
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * Processes {@code UnsupportedAppUsage} annotations to generate greylist
+ * entries.
+ *
+ * Any annotations with a {@link #EXPECTED_SIGNATURE} property will have their
+ * generated signature verified against this, and an error will be reported if
+ * it does not match. Exclusions are made for bridge methods.
+ *
+ * Any {@link #MAX_TARGET_SDK} properties will be validated against the given
+ * set of valid values, then passed through to the greylist consumer.
+ */
+public class GreylistAnnotationHandler implements AnnotationHandler {
+
+ public static final String ANNOTATION_NAME = "Landroid/annotation/UnsupportedAppUsage;";
+
+ // properties of greylist annotations:
+ private static final String EXPECTED_SIGNATURE = "expectedSignature";
+ private static final String MAX_TARGET_SDK = "maxTargetSdk";
+
+ private final Status mStatus;
+ private final Predicate<GreylistMember> mGreylistFilter;
+ private final GreylistConsumer mGreylistConsumer;
+ private final Set<Integer> mValidMaxTargetSdkValues;
+
+ /**
+ * Represents a member of a class file (a field or method).
+ */
+ @VisibleForTesting
+ public static class GreylistMember {
+
+ /**
+ * Signature of this member.
+ */
+ public final String signature;
+ /**
+ * Indicates if this is a synthetic bridge method.
+ */
+ public final boolean bridge;
+ /**
+ * Max target SDK of property this member, if it is set, else null.
+ *
+ * Note: even though the annotation itself specified a default value,
+ * that default value is not encoded into instances of the annotation
+ * in class files. So when no value is specified in source, it will
+ * result in null appearing in here.
+ */
+ public final Integer maxTargetSdk;
+
+ public GreylistMember(String signature, boolean bridge, Integer maxTargetSdk) {
+ this.signature = signature;
+ this.bridge = bridge;
+ this.maxTargetSdk = maxTargetSdk;
+ }
+ }
+
+ public GreylistAnnotationHandler(
+ Status status,
+ GreylistConsumer greylistConsumer,
+ Set<String> publicApis,
+ Set<Integer> validMaxTargetSdkValues) {
+ this(status, greylistConsumer,
+ member -> !(member.bridge && publicApis.contains(member.signature)),
+ validMaxTargetSdkValues);
+ }
+
+ @VisibleForTesting
+ public GreylistAnnotationHandler(
+ Status status,
+ GreylistConsumer greylistConsumer,
+ Predicate<GreylistMember> greylistFilter,
+ Set<Integer> validMaxTargetSdkValues) {
+ mStatus = status;
+ mGreylistConsumer = greylistConsumer;
+ mGreylistFilter = greylistFilter;
+ mValidMaxTargetSdkValues = validMaxTargetSdkValues;
+ }
+
+ @Override
+ public void handleAnnotation(AnnotationEntry annotation, AnnotationContext context) {
+ FieldOrMethod member = context.member;
+ boolean bridge = (member instanceof Method)
+ && (member.getAccessFlags() & Const.ACC_BRIDGE) != 0;
+ if (bridge) {
+ mStatus.debug("Member is a bridge");
+ }
+ String signature = context.getMemberDescriptor();
+ Integer maxTargetSdk = null;
+ for (ElementValuePair property : annotation.getElementValuePairs()) {
+ switch (property.getNameString()) {
+ case EXPECTED_SIGNATURE:
+ verifyExpectedSignature(context, property, signature, bridge);
+ break;
+ case MAX_TARGET_SDK:
+ maxTargetSdk = verifyAndGetMaxTargetSdk(context, property);
+ break;
+ }
+ }
+ if (mGreylistFilter.test(new GreylistMember(signature, bridge, maxTargetSdk))) {
+ mGreylistConsumer.greylistEntry(signature, maxTargetSdk);
+ }
+ }
+
+ private void verifyExpectedSignature(AnnotationContext context, ElementValuePair property,
+ String signature, boolean isBridge) {
+ String expected = property.getValue().stringifyValue();
+ // Don't enforce for bridge methods; they're generated so won't match.
+ if (!isBridge && !signature.equals(expected)) {
+ context.reportError("Expected signature does not match generated:\n"
+ + "Expected: %s\n"
+ + "Generated: %s", expected, signature);
+ }
+ }
+
+ private Integer verifyAndGetMaxTargetSdk(AnnotationContext context, ElementValuePair property) {
+ if (property.getValue().getElementValueType() != ElementValue.PRIMITIVE_INT) {
+ context.reportError("Expected property %s to be of type int; got %d",
+ property.getNameString(), property.getValue().getElementValueType());
+ }
+ int value = ((SimpleElementValue) property.getValue()).getValueInt();
+ if (!mValidMaxTargetSdkValues.contains(value)) {
+ context.reportError("Invalid value for %s: got %d, expected one of [%s]",
+ property.getNameString(),
+ value,
+ Joiner.on(",").join(mValidMaxTargetSdkValues));
+ return null;
+ }
+ return value;
+ }
+
+}
diff --git a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
index debc21d..fd855e8 100644
--- a/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/GreylistConsumer.java
@@ -9,5 +9,12 @@
*/
void greylistEntry(String signature, Integer maxTargetSdk);
+ /**
+ * Handle a new whitelist entry.
+ *
+ * @param signature Signature of the member.
+ */
+ void whitelistEntry(String signature);
+
void close();
}
diff --git a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
index 8e12759..ad5ad70 100644
--- a/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
+++ b/tools/class2greylist/src/com/android/class2greylist/SystemOutGreylistConsumer.java
@@ -7,6 +7,12 @@
}
@Override
+ public void whitelistEntry(String signature) {
+ // Ignore. This class is only used when no grey/white lists are
+ // specified, so we have nowhere to write whitelist entries.
+ }
+
+ @Override
public void close() {
}
}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
new file mode 100644
index 0000000..8f4a76f
--- /dev/null
+++ b/tools/class2greylist/test/src/com/android/class2greylist/AnnotationHandlerTestBase.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * 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 com.android.class2greylist;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
+
+import com.android.javac.Javac;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+import java.io.IOException;
+
+public class AnnotationHandlerTestBase {
+
+ @Rule
+ public TestName mTestName = new TestName();
+
+ protected Javac mJavac;
+ protected GreylistConsumer mConsumer;
+ protected Status mStatus;
+
+ @Before
+ public void baseSetup() throws IOException {
+ System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
+ mTestName.getMethodName()));
+ mConsumer = mock(GreylistConsumer.class);
+ mStatus = mock(Status.class, withSettings().verboseLogging());
+ mJavac = new Javac();
+ }
+
+ protected void assertNoErrors() {
+ verify(mStatus, never()).error(any(Throwable.class));
+ verify(mStatus, never()).error(any(), any());
+ }
+}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
index 979044b..cb75dd3 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/Class2GreylistTest.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * 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 com.android.class2greylist;
import static com.google.common.truth.Truth.assertThat;
@@ -32,41 +48,36 @@
}
@Test
- public void testReadGreylistMap() {
- Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
- Map<Integer, String> map = c2gl.readGreylistMap(
+ public void testReadGreylistMap() throws IOException {
+ Map<Integer, String> map = Class2Greylist.readGreylistMap(mStatus,
new String[]{"noApi", "1:apiOne", "3:apiThree"});
verifyZeroInteractions(mStatus);
assertThat(map).containsExactly(null, "noApi", 1, "apiOne", 3, "apiThree");
}
@Test
- public void testReadGreylistMapDuplicate() {
- Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
- Map<Integer, String> map = c2gl.readGreylistMap(
+ public void testReadGreylistMapDuplicate() throws IOException {
+ Class2Greylist.readGreylistMap(mStatus,
new String[]{"noApi", "1:apiOne", "1:anotherOne"});
verify(mStatus, atLeastOnce()).error(any(), any());
}
@Test
public void testReadGreylistMapDuplicateNoApi() {
- Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
- Map<Integer, String> map = c2gl.readGreylistMap(
+ Class2Greylist.readGreylistMap(mStatus,
new String[]{"noApi", "anotherNoApi", "1:apiOne"});
verify(mStatus, atLeastOnce()).error(any(), any());
}
@Test
- public void testReadGreylistMapInvalidInt() {
- Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
- Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "a:apiOne"});
+ public void testReadGreylistMapInvalidInt() throws IOException {
+ Class2Greylist.readGreylistMap(mStatus, new String[]{"noApi", "a:apiOne"});
verify(mStatus, atLeastOnce()).error(any(), any());
}
@Test
- public void testReadGreylistMapNoFilename() {
- Class2Greylist c2gl = new Class2Greylist(mStatus, null, null, null);
- Map<Integer, String> map = c2gl.readGreylistMap(new String[]{"noApi", "1:"});
+ public void testReadGreylistMapNoFilename() throws IOException {
+ Class2Greylist.readGreylistMap(mStatus, new String[]{"noApi", "1:"});
verify(mStatus, atLeastOnce()).error(any(), any());
}
}
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
similarity index 80%
rename from tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java
rename to tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
index 994fe89..1a4bfb8 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/AnnotationVisitorTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/GreylistAnnotationHandlerTest.java
@@ -19,47 +19,32 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.withSettings;
import static java.util.Collections.emptySet;
-import com.android.javac.Javac;
-
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.TestName;
import org.mockito.ArgumentCaptor;
import java.io.IOException;
+import java.util.Map;
import java.util.Set;
+import java.util.function.Predicate;
-public class AnnotationVisitorTest {
+public class GreylistAnnotationHandlerTest extends AnnotationHandlerTestBase {
private static final String ANNOTATION = "Lannotation/Anno;";
- @Rule
- public TestName mTestName = new TestName();
-
- private Javac mJavac;
- private GreylistConsumer mConsumer;
- private Status mStatus;
-
@Before
public void setup() throws IOException {
- System.out.println(String.format("\n============== STARTING TEST: %s ==============\n",
- mTestName.getMethodName()));
- mConsumer = mock(GreylistConsumer.class);
- mStatus = mock(Status.class, withSettings().verboseLogging());
- mJavac = new Javac();
mJavac.addSource("annotation.Anno", Joiner.on('\n').join(
"package annotation;",
"import static java.lang.annotation.RetentionPolicy.CLASS;",
@@ -71,9 +56,11 @@
"}"));
}
- private void assertNoErrors() {
- verify(mStatus, never()).error(any(Throwable.class));
- verify(mStatus, never()).error(any(), any());
+ private GreylistAnnotationHandler createGreylistHandler(
+ Predicate<GreylistAnnotationHandler.GreylistMember> greylistFilter,
+ Set<Integer> validMaxTargetSdkValues) {
+ return new GreylistAnnotationHandler(
+ mStatus, mConsumer, greylistFilter, validMaxTargetSdkValues);
}
@Test
@@ -87,8 +74,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -107,8 +95,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -127,8 +116,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -147,8 +137,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -167,8 +158,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
verify(mStatus, times(1)).error(any(), any());
}
@@ -186,8 +178,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -204,8 +197,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
verify(mConsumer, never()).greylistEntry(any(String.class), any());
@@ -222,8 +216,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus,
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()))
+ ).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -249,10 +244,10 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -281,10 +276,10 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -317,15 +312,12 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(
- mJavac.getCompiledClass("a.b.Interface"), ANNOTATION, x -> true, emptySet(),
- mConsumer, mStatus).visit();
- new AnnotationVisitor(
- mJavac.getCompiledClass("a.b.Base"), ANNOTATION, x -> true, emptySet(), mConsumer,
- mStatus).visit();
- new AnnotationVisitor(
- mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true, emptySet(), mConsumer,
- mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Interface"), mStatus, handlerMap)
+ .visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -357,10 +349,15 @@
Set<String> publicApis = Sets.newHashSet(
"La/b/Base;->method(Ljava/lang/Object;)V",
"La/b/Class;->method(Ljava/lang/Object;)V");
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), ANNOTATION, publicApis,
- emptySet(), mConsumer, mStatus).visit();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, publicApis,
- emptySet(), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION,
+ new GreylistAnnotationHandler(
+ mStatus,
+ mConsumer,
+ publicApis,
+ emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Base"), mStatus, handlerMap).visit();
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
@@ -380,9 +377,11 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION,
- member -> !member.bridge, // exclude bridge methods
- emptySet(), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(
+ member -> !member.bridge, // exclude bridge methods
+ emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
verify(mConsumer, times(1)).greylistEntry(greylist.capture(), any());
@@ -400,8 +399,9 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- emptySet(), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(x -> true, emptySet()));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
verify(mStatus, times(1)).error(any(), any());
}
@@ -416,8 +416,11 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- ImmutableSet.of(1), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(
+ x -> true,
+ ImmutableSet.of(1)));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
@@ -435,8 +438,11 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- ImmutableSet.of(1), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(
+ x -> true,
+ ImmutableSet.of(1)));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
assertNoErrors();
ArgumentCaptor<Integer> maxTargetSdk = ArgumentCaptor.forClass(Integer.class);
verify(mConsumer, times(1)).greylistEntry(any(), maxTargetSdk.capture());
@@ -454,8 +460,12 @@
"}"));
assertThat(mJavac.compile()).isTrue();
- new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, x -> true,
- ImmutableSet.of(1), mConsumer, mStatus).visit();
+ Map<String, AnnotationHandler> handlerMap =
+ ImmutableMap.of(ANNOTATION, createGreylistHandler(
+ x -> true,
+ ImmutableSet.of(1)));
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), mStatus, handlerMap).visit();
verify(mStatus, times(1)).error(any(), any());
}
+
}