summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathew Inwood <mathewi@google.com>2018-07-23 15:51:06 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2018-07-23 15:51:06 +0000
commit7f0aa734a3f4b3d1f7842b8546543668275f8ddb (patch)
treee1422a4d3becc0736afc7c985c501e3e7a5691c5
parent3e3a6e4760e3e8f3b7ff30aac4aa1a64c13d52dc (diff)
parent6395690ec99bd13214c0530cac54d33b1f8e601b (diff)
Merge "Add new "class2greylist" tool."
-rw-r--r--tools/hiddenapi/class2greylist/Android.bp33
-rw-r--r--tools/hiddenapi/class2greylist/src/class2greylist.mf1
-rw-r--r--tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java118
-rw-r--r--tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java99
-rw-r--r--tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java65
-rw-r--r--tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java58
-rw-r--r--tools/hiddenapi/class2greylist/test/Android.mk32
-rw-r--r--tools/hiddenapi/class2greylist/test/AndroidTest.xml21
-rw-r--r--tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java202
-rw-r--r--tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java103
10 files changed, 732 insertions, 0 deletions
diff --git a/tools/hiddenapi/class2greylist/Android.bp b/tools/hiddenapi/class2greylist/Android.bp
new file mode 100644
index 000000000000..7b1233bb8535
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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.
+//
+
+java_library_host {
+ name: "class2greylistlib",
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "commons-cli-1.2",
+ "apache-bcel",
+ ],
+}
+
+java_binary_host {
+ name: "class2greylist",
+ manifest: "src/class2greylist.mf",
+ static_libs: [
+ "class2greylistlib",
+ ],
+}
+
diff --git a/tools/hiddenapi/class2greylist/src/class2greylist.mf b/tools/hiddenapi/class2greylist/src/class2greylist.mf
new file mode 100644
index 000000000000..ea3a3d9153a3
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/src/class2greylist.mf
@@ -0,0 +1 @@
+Main-Class: com.android.class2greylist.Class2Greylist
diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
new file mode 100644
index 000000000000..66857525aa0c
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/AnnotationVisitor.java
@@ -0,0 +1,118 @@
+/*
+ * 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 org.apache.bcel.Const;
+import org.apache.bcel.classfile.AnnotationEntry;
+import org.apache.bcel.classfile.DescendingVisitor;
+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 java.util.Locale;
+
+/**
+ * Visits a JavaClass instance and pulls out all members annotated with a
+ * specific annotation. The signatures of such members are passed to {@link
+ * Status#greylistEntry(String)}. Any errors result in a call to {@link
+ * Status#error(String)}.
+ *
+ * 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.
+ */
+public class AnnotationVisitor extends EmptyVisitor {
+
+ private static final String EXPECTED_SIGNATURE = "expectedSignature";
+
+ private final JavaClass mClass;
+ private final String mAnnotationType;
+ private final Status mStatus;
+ private final DescendingVisitor mDescendingVisitor;
+
+ public AnnotationVisitor(JavaClass clazz, String annotation, Status d) {
+ mClass = clazz;
+ mAnnotationType = annotation;
+ mStatus = d;
+ mDescendingVisitor = new DescendingVisitor(clazz, this);
+ }
+
+ public void visit() {
+ mStatus.debug("Visit class %s", mClass.getClassName());
+ 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");
+ }
+
+ @Override
+ public void visitField(Field field) {
+ visitMember(field, "L%s;->%s:%s");
+ }
+
+ private void visitMember(FieldOrMethod member, String signatureFormatString) {
+ JavaClass definingClass = (JavaClass) mDescendingVisitor.predecessor();
+ mStatus.debug("Visit member %s : %s", member.getName(), member.getSignature());
+ for (AnnotationEntry a : member.getAnnotationEntries()) {
+ if (mAnnotationType.equals(a.getAnnotationType())) {
+ mStatus.debug("Method has annotation %s", mAnnotationType);
+ String signature = String.format(Locale.US, signatureFormatString,
+ getClassDescriptor(definingClass), member.getName(), member.getSignature());
+ for (ElementValuePair property : a.getElementValuePairs()) {
+ switch (property.getNameString()) {
+ case EXPECTED_SIGNATURE:
+ String expected = property.getValue().stringifyValue();
+ if (!signature.equals(expected)) {
+ error(definingClass, member,
+ "Expected signature does not match generated:\n"
+ + "Expected: %s\n"
+ + "Generated: %s", expected, signature);
+ }
+ break;
+ }
+ }
+ mStatus.greylistEntry(signature);
+ }
+ }
+ }
+
+ 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/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java
new file mode 100644
index 000000000000..bcccf4aa18a9
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Class2Greylist.java
@@ -0,0 +1,99 @@
+/*
+ * 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 org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.cli.PatternOptionBuilder;
+
+import java.io.IOException;
+
+/**
+ * Build time tool for extracting a list of members from jar files that have the @UsedByApps
+ * annotation, for building the greylist.
+ */
+public class Class2Greylist {
+
+ private static final String ANNOTATION_TYPE = "Landroid/annotation/UsedByApps;";
+
+ public static void main(String[] args) {
+ Options options = new Options();
+ options.addOption(OptionBuilder
+ .withLongOpt("debug")
+ .hasArgs(0)
+ .withDescription("Enable debug")
+ .create("d"));
+ options.addOption(OptionBuilder
+ .withLongOpt("help")
+ .hasArgs(0)
+ .withDescription("Show this help")
+ .create("h"));
+
+ CommandLineParser parser = new GnuParser();
+ CommandLine cmd;
+
+ try {
+ cmd = parser.parse(options, args);
+ } catch (ParseException e) {
+ System.err.println(e.getMessage());
+ help(options);
+ return;
+ }
+ if (cmd.hasOption('h')) {
+ help(options);
+ }
+
+ String[] jarFiles = cmd.getArgs();
+ if (jarFiles.length == 0) {
+ System.err.println("Error: no jar files specified.");
+ help(options);
+ }
+
+ Status status = new Status(cmd.hasOption('d'));
+
+ for (String jarFile : jarFiles) {
+ status.debug("Processing jar file %s", jarFile);
+ try {
+ JarReader reader = new JarReader(status, jarFile);
+ reader.stream().forEach(clazz -> new AnnotationVisitor(
+ clazz, ANNOTATION_TYPE, status).visit());
+ reader.close();
+ } catch (IOException e) {
+ status.error(e);
+ }
+ }
+ if (status.ok()) {
+ System.exit(0);
+ } else {
+ System.exit(1);
+ }
+
+ }
+
+ private static void help(Options options) {
+ new HelpFormatter().printHelp(
+ "class2greylist path/to/classes.jar [classes2.jar ...]",
+ "Extracts greylist entries from classes jar files given",
+ options, null, true);
+ System.exit(1);
+ }
+}
diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java
new file mode 100644
index 000000000000..f3a9d0b92e6c
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/JarReader.java
@@ -0,0 +1,65 @@
+/*
+ * 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 org.apache.bcel.classfile.ClassParser;
+import org.apache.bcel.classfile.JavaClass;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Reads {@link JavaClass} members from a zip/jar file, providing a stream of them for processing.
+ * Any errors are reported via {@link Status#error(Throwable)}.
+ */
+public class JarReader {
+
+ private final Status mStatus;
+ private final String mFileName;
+ private final ZipFile mZipFile;
+
+ public JarReader(Status s, String filename) throws IOException {
+ mStatus = s;
+ mFileName = filename;
+ mZipFile = new ZipFile(mFileName);
+ }
+
+ private JavaClass openZipEntry(ZipEntry e) {
+ try {
+ mStatus.debug("Reading %s from %s", e.getName(), mFileName);
+ return new ClassParser(mZipFile.getInputStream(e), e.getName()).parse();
+ } catch (IOException ioe) {
+ mStatus.error(ioe);
+ return null;
+ }
+ }
+
+
+ public Stream<JavaClass> stream() {
+ return mZipFile.stream()
+ .filter(zipEntry -> zipEntry.getName().endsWith(".class"))
+ .map(zipEntry -> openZipEntry(zipEntry))
+ .filter(Objects::nonNull);
+ }
+
+ public void close() throws IOException {
+ mZipFile.close();
+ }
+}
diff --git a/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java
new file mode 100644
index 000000000000..d7078986d9cd
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/src/com/android/class2greylist/Status.java
@@ -0,0 +1,58 @@
+/*
+ * 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 java.util.Locale;
+
+public class Status {
+
+ // Highlight "Error:" in red.
+ private static final String ERROR = "\u001B[31mError: \u001B[0m";
+
+ private final boolean mDebug;
+ private boolean mHasErrors;
+
+ public Status(boolean debug) {
+ mDebug = debug;
+ }
+
+ public void debug(String msg, Object... args) {
+ if (mDebug) {
+ System.err.println(String.format(Locale.US, msg, args));
+ }
+ }
+
+ public void error(Throwable t) {
+ System.err.print(ERROR);
+ t.printStackTrace(System.err);
+ mHasErrors = true;
+ }
+
+ public void error(String message) {
+ System.err.print(ERROR);
+ System.err.println(message);
+ mHasErrors = true;
+ }
+
+ public void greylistEntry(String signature) {
+ System.out.println(signature);
+ }
+
+ public boolean ok() {
+ return !mHasErrors;
+ }
+}
diff --git a/tools/hiddenapi/class2greylist/test/Android.mk b/tools/hiddenapi/class2greylist/test/Android.mk
new file mode 100644
index 000000000000..23f4156f6d03
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/test/Android.mk
@@ -0,0 +1,32 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := class2greylisttest
+
+LOCAL_STATIC_JAVA_LIBRARIES := class2greylistlib truth-host-prebuilt mockito-host junit-host
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := general-tests
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH)) \ No newline at end of file
diff --git a/tools/hiddenapi/class2greylist/test/AndroidTest.xml b/tools/hiddenapi/class2greylist/test/AndroidTest.xml
new file mode 100644
index 000000000000..66bb63446f71
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/test/AndroidTest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="class2greylist tests">
+ <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+ <option name="jar" value="class2greylisttest.jar" />
+ <option name="runtime-hint" value="1m" />
+ </test>
+</configuration> \ No newline at end of file
diff --git a/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java b/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java
new file mode 100644
index 000000000000..2d9721803cac
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/test/src/com/android/javac/AnnotationVisitorTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.javac;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.android.class2greylist.Status;
+import com.android.class2greylist.AnnotationVisitor;
+
+import com.google.common.base.Joiner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.io.IOException;
+
+public class AnnotationVisitorTest {
+
+ private static final String ANNOTATION = "Lannotation/Anno;";
+
+ private Javac mJavac;
+ @Mock
+ private Status mStatus;
+
+ @Before
+ public void setup() throws IOException {
+ initMocks(this);
+ mJavac = new Javac();
+ mJavac.addSource("annotation.Anno", Joiner.on('\n').join(
+ "package annotation;",
+ "import static java.lang.annotation.RetentionPolicy.CLASS;",
+ "import java.lang.annotation.Retention;",
+ "import java.lang.annotation.Target;",
+ "@Retention(CLASS)",
+ "public @interface Anno {",
+ " String expectedSignature() default \"\";",
+ "}"));
+ }
+
+ private void assertNoErrors() {
+ verify(mStatus, never()).error(any(Throwable.class));
+ verify(mStatus, never()).error(any(String.class));
+ }
+
+ @Test
+ public void testGreylistMethod() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " @Anno",
+ " public void method() {}",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ assertNoErrors();
+ ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
+ verify(mStatus, times(1)).greylistEntry(greylist.capture());
+ assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
+ }
+
+ @Test
+ public void testGreylistConstructor() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " @Anno",
+ " public Class() {}",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ assertNoErrors();
+ ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
+ verify(mStatus, times(1)).greylistEntry(greylist.capture());
+ assertThat(greylist.getValue()).isEqualTo("La/b/Class;-><init>()V");
+ }
+
+ @Test
+ public void testGreylistField() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " @Anno",
+ " public int i;",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ assertNoErrors();
+ ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
+ verify(mStatus, times(1)).greylistEntry(greylist.capture());
+ assertThat(greylist.getValue()).isEqualTo("La/b/Class;->i:I");
+ }
+
+ @Test
+ public void testGreylistMethodExpectedSignature() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " @Anno(expectedSignature=\"La/b/Class;->method()V\")",
+ " public void method() {}",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ assertNoErrors();
+ ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
+ verify(mStatus, times(1)).greylistEntry(greylist.capture());
+ assertThat(greylist.getValue()).isEqualTo("La/b/Class;->method()V");
+ }
+
+ @Test
+ public void testGreylistMethodExpectedSignatureWrong() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " @Anno(expectedSignature=\"La/b/Class;->nomethod()V\")",
+ " public void method() {}",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ verify(mStatus, times(1)).error(any(String.class));
+ }
+
+ @Test
+ public void testGreylistInnerClassMethod() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "import annotation.Anno;",
+ "public class Class {",
+ " public class Inner {",
+ " @Anno",
+ " public void method() {}",
+ " }",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class$Inner"), ANNOTATION,
+ mStatus).visit();
+
+ assertNoErrors();
+ ArgumentCaptor<String> greylist = ArgumentCaptor.forClass(String.class);
+ verify(mStatus, times(1)).greylistEntry(greylist.capture());
+ assertThat(greylist.getValue()).isEqualTo("La/b/Class$Inner;->method()V");
+ }
+
+ @Test
+ public void testMethodNotGreylisted() throws IOException {
+ mJavac.addSource("a.b.Class", Joiner.on('\n').join(
+ "package a.b;",
+ "public class Class {",
+ " public void method() {}",
+ "}"));
+ assertThat(mJavac.compile()).isTrue();
+
+ new AnnotationVisitor(mJavac.getCompiledClass("a.b.Class"), ANNOTATION, mStatus)
+ .visit();
+
+ assertNoErrors();
+ verify(mStatus, never()).greylistEntry(any(String.class));
+ }
+
+}
diff --git a/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java b/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java
new file mode 100644
index 000000000000..202f4121fc60
--- /dev/null
+++ b/tools/hiddenapi/class2greylist/test/src/com/android/javac/Javac.java
@@ -0,0 +1,103 @@
+/*
+ * 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.javac;
+
+import com.google.common.io.Files;
+
+import org.apache.bcel.classfile.ClassParser;
+import org.apache.bcel.classfile.JavaClass;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+/**
+ * Helper class for compiling snippets of Java source and providing access to the resulting class
+ * files.
+ */
+public class Javac {
+
+ private final JavaCompiler mJavac;
+ private final StandardJavaFileManager mFileMan;
+ private final List<JavaFileObject> mCompilationUnits;
+ private final File mClassOutDir;
+
+ public Javac() throws IOException {
+ mJavac = ToolProvider.getSystemJavaCompiler();
+ mFileMan = mJavac.getStandardFileManager(null, Locale.US, null);
+ mClassOutDir = Files.createTempDir();
+ mFileMan.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(mClassOutDir));
+ mFileMan.setLocation(StandardLocation.CLASS_PATH, Arrays.asList(mClassOutDir));
+ mCompilationUnits = new ArrayList<>();
+ }
+
+ private String classToFileName(String classname) {
+ return classname.replace('.', '/');
+ }
+
+ public Javac addSource(String classname, String contents) {
+ JavaFileObject java = new SimpleJavaFileObject(URI.create(
+ String.format("string:///%s.java", classToFileName(classname))),
+ JavaFileObject.Kind.SOURCE
+ ){
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
+ return contents;
+ }
+ };
+ mCompilationUnits.add(java);
+ return this;
+ }
+
+ public boolean compile() {
+ JavaCompiler.CompilationTask task = mJavac.getTask(
+ null,
+ mFileMan,
+ null,
+ null,
+ null,
+ mCompilationUnits);
+ return task.call();
+ }
+
+ public InputStream getClassFile(String classname) throws IOException {
+ Iterable<? extends JavaFileObject> objs = mFileMan.getJavaFileObjects(
+ new File(mClassOutDir, String.format("%s.class", classToFileName(classname))));
+ if (!objs.iterator().hasNext()) {
+ return null;
+ }
+ return objs.iterator().next().openInputStream();
+ }
+
+ public JavaClass getCompiledClass(String classname) throws IOException {
+ return new ClassParser(getClassFile(classname),
+ String.format("%s.class", classToFileName(classname))).parse();
+ }
+}