Add option to limit what classes can have annotations

Bug: 292141694
Test: run-all-tests.sh
Test: atest --no-bazel-mode CtsUtilTestCasesRavenwood

Change-Id: I492206e4b14e02fb3d563c3bc5cd2f0b4907317a
diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING
index 9703626..e02492d 100644
--- a/tools/hoststubgen/TEST_MAPPING
+++ b/tools/hoststubgen/TEST_MAPPING
@@ -1,6 +1,8 @@
 {
     // TODO: Change to presubmit.
     "postsubmit": [
-        { "name": "tiny-framework-dump-test" }
+        { "name": "tiny-framework-dump-test" },
+        { "name": "hoststubgentest" },
+        { "name": "hoststubgen-invoke-test" }
     ]
 }
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index 182940e..fd4ec8b 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -99,6 +99,18 @@
     visibility: ["//visibility:public"],
 }
 
+java_test_host {
+    name: "hoststubgentest",
+    // main_class: "com.android.hoststubgen.Main",
+    srcs: ["test/**/*.kt"],
+    static_libs: [
+        "hoststubgen",
+        "truth",
+    ],
+    test_suites: ["general-tests"],
+    visibility: ["//visibility:private"],
+}
+
 // File that contains the standard command line argumetns to hoststubgen.
 // This is only for the prototype. The productionized version is "ravenwood-standard-options".
 filegroup {
diff --git a/tools/hoststubgen/hoststubgen/invoketest/Android.bp b/tools/hoststubgen/hoststubgen/invoketest/Android.bp
new file mode 100644
index 0000000..7e90e42
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/invoketest/Android.bp
@@ -0,0 +1,20 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+sh_test_host {
+    name: "hoststubgen-invoke-test",
+    src: "hoststubgen-invoke-test.sh",
+    test_suites: ["general-tests"],
+
+    // Note: java_data: ["hoststubgen"] will only install the jar file, but not the command wrapper.
+    java_data: [
+        "hoststubgen",
+        "hoststubgen-test-tiny-framework",
+    ],
+}
diff --git a/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
new file mode 100755
index 0000000..34b2145
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/invoketest/hoststubgen-invoke-test.sh
@@ -0,0 +1,163 @@
+#!/bin/bash
+# Copyright (C) 2023 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.
+
+set -e # Exit when any command files
+
+# This script runs HostStubGen directly with various arguments and make sure
+# the tool behaves in the expected way.
+
+
+echo "# Listing files in the test environment"
+ls -lR
+
+echo "# Dumping the environment variables"
+env
+
+# Set up the constants and variables
+
+export TEMP=$TEST_TMPDIR
+
+JAR=hoststubgen-test-tiny-framework.jar
+STUB=$TEMP/stub.jar
+IMPL=$TEMP/impl.jar
+
+ANNOTATION_FILTER=$TEMP/annotation-filter.txt
+
+HOSTSTUBGEN_OUT=$TEMP/output.txt
+
+# Because of `set -e`, we can't return non-zero from functions, so we store
+# HostStubGen result in it.
+HOSTSTUBGEN_RC=0
+
+# Define the functions to
+
+
+# Note, because the build rule will only install hoststubgen.jar, but not the wrapper script,
+# we need to execute it manually with the java command.
+hoststubgen() {
+  java -jar ./hoststubgen.jar "$@"
+}
+
+run_hoststubgen() {
+  local test_name="$1"
+  local annotation_filter="$2"
+
+  echo "# Test: $test_name"
+
+  rm -f $HOSTSTUBGEN_OUT
+
+  local filter_arg=""
+
+  if [[ "$annotation_filter" != "" ]] ; then
+    echo "$annotation_filter" > $ANNOTATION_FILTER
+    filter_arg="--annotation-allowed-classes-file $ANNOTATION_FILTER"
+    echo "=== filter ==="
+    cat $ANNOTATION_FILTER
+  fi
+
+  hoststubgen \
+      --debug \
+      --in-jar $JAR \
+      --out-stub-jar $STUB \
+      --out-impl-jar $IMPL \
+      $filter_arg \
+      |& tee $HOSTSTUBGEN_OUT
+  HOSTSTUBGEN_RC=${PIPESTATUS[0]}
+  echo "HostStubGen exited with $HOSTSTUBGEN_RC"
+  return 0
+}
+
+run_hoststubgen_for_success() {
+  run_hoststubgen "$@"
+
+  if (( $HOSTSTUBGEN_RC != 0 )) ; then
+    echo "HostStubGen expected to finish successfully, but failed with $rc"
+    return 1
+  fi
+}
+
+run_hoststubgen_for_failure() {
+  local test_name="$1"
+  local expected_error_message="$2"
+  shift 2
+
+  run_hoststubgen "$test_name" "$@"
+
+  if (( $HOSTSTUBGEN_RC == 0 )) ; then
+    echo "HostStubGen expected to fail, but it didn't fail"
+    return 1
+  fi
+
+  # The output should contain the expected message. (note we se fgrep here.)
+  grep -Fq "$expected_error_message" $HOSTSTUBGEN_OUT
+}
+
+# Start the tests...
+
+# Pass "" as a filter to _not_ add `--annotation-allowed-classes-file`.
+run_hoststubgen_for_success "No annotation filter" ""
+
+# Now, we use " ", so we do add `--annotation-allowed-classes-file`.
+run_hoststubgen_for_failure "No classes are allowed to have annotations" \
+    "not allowed to have Ravenwood annotations" \
+    " "
+
+run_hoststubgen_for_success "All classes allowed (wildcard)" \
+    "
+* # Allow all classes
+"
+
+run_hoststubgen_for_failure "All classes disallowed (wildcard)" \
+    "not allowed to have Ravenwood annotations" \
+    "
+!* # Disallow all classes
+"
+
+run_hoststubgen_for_failure "Some classes not allowed (1)" \
+    "not allowed to have Ravenwood annotations" \
+    "
+android.hosttest.*
+com.android.hoststubgen.*
+com.supported.*
+"
+
+run_hoststubgen_for_failure "Some classes not allowed (2)" \
+    "not allowed to have Ravenwood annotations" \
+    "
+android.hosttest.*
+com.android.hoststubgen.*
+com.unsupported.*
+"
+
+run_hoststubgen_for_success "All classes allowed (package wildcard)" \
+    "
+android.hosttest.*
+com.android.hoststubgen.*
+com.supported.*
+com.unsupported.*
+"
+
+
+run_hoststubgen_for_failure "One specific class disallowed" \
+    "TinyFrameworkClassAnnotations is not allowed to have Ravenwood annotations" \
+    "
+!com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnotations
+* # All other classes allowed
+"
+
+
+
+echo "All tests passed"
+exit 0
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 872d568..f32dc72 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -27,6 +27,7 @@
 import com.android.hoststubgen.filters.StubIntersectingFilter
 import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
 import com.android.hoststubgen.filters.printAsTextPolicy
+import com.android.hoststubgen.utils.ClassFilter
 import com.android.hoststubgen.visitors.BaseAdapter
 import com.android.hoststubgen.visitors.PackageRedirectRemapper
 import org.objectweb.asm.ClassReader
@@ -167,6 +168,14 @@
             filter
         )
 
+        val annotationAllowedClassesFilter = options.annotationAllowedClassesFile.let { filename ->
+            if (filename == null) {
+                ClassFilter.newNullFilter(true) // Allow all classes
+            } else {
+                ClassFilter.loadFromFile(filename, false)
+            }
+        }
+
         // Next, Java annotation based filter.
         filter = AnnotationBasedFilter(
             errors,
@@ -181,7 +190,8 @@
             options.nativeSubstituteAnnotations,
             options.classLoadHookAnnotations,
             options.stubStaticInitializerAnnotations,
-            filter
+            annotationAllowedClassesFilter,
+            filter,
         )
 
         // Next, "text based" filter, which allows to override polices without touching
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index d74612d..aab02b8 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -51,6 +51,8 @@
 
         var packageRedirects: MutableList<Pair<String, String>> = mutableListOf(),
 
+        var annotationAllowedClassesFile: String? = null,
+
         var defaultClassLoadHook: String? = null,
         var defaultMethodCallHook: String? = null,
 
@@ -171,6 +173,9 @@
                     "--package-redirect" ->
                         ret.packageRedirects += parsePackageRedirect(ai.nextArgRequired(arg))
 
+                    "--annotation-allowed-classes-file" ->
+                        ret.annotationAllowedClassesFile = ai.nextArgRequired(arg)
+
                     "--default-class-load-hook" ->
                         ret.defaultClassLoadHook = ai.nextArgRequired(arg)
 
@@ -314,6 +319,7 @@
               nativeSubstituteAnnotations=$nativeSubstituteAnnotations,
               classLoadHookAnnotations=$classLoadHookAnnotations,
               packageRedirects=$packageRedirects,
+              $annotationAllowedClassesFile=$annotationAllowedClassesFile,
               defaultClassLoadHook=$defaultClassLoadHook,
               defaultMethodCallHook=$defaultMethodCallHook,
               intersectStubJars=$intersectStubJars,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt
index f75062b..937e56c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/Utils.kt
@@ -58,4 +58,29 @@
         return listOf(b)
     }
     return a + b
-}
\ No newline at end of file
+}
+
+
+/**
+ * Exception for a parse error in a file
+ */
+class ParseException : Exception, UserErrorException {
+    val hasSourceInfo: Boolean
+
+    constructor(message: String) : super(message) {
+        hasSourceInfo = false
+    }
+
+    constructor(message: String, file: String, line: Int) :
+            super("$message in file $file line $line") {
+        hasSourceInfo = true
+    }
+
+    fun withSourceInfo(filename: String, lineNo: Int): ParseException {
+        if (hasSourceInfo) {
+            return this // Already has source information.
+        } else {
+            return ParseException(this.message ?: "", filename, lineNo)
+        }
+    }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
index 9f3ec4d..9bb5381e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AnnotationBasedFilter.kt
@@ -25,9 +25,11 @@
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.asm.findAnnotationValueAsString
 import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.toHumanReadableClassName
 import com.android.hoststubgen.asm.toHumanReadableMethodName
 import com.android.hoststubgen.asm.toJvmClassName
 import com.android.hoststubgen.log
+import com.android.hoststubgen.utils.ClassFilter
 import org.objectweb.asm.tree.AnnotationNode
 import org.objectweb.asm.tree.ClassNode
 
@@ -51,6 +53,7 @@
         nativeSubstituteAnnotations_: Set<String>,
         classLoadHookAnnotations_: Set<String>,
         stubStaticInitializerAnnotations_: Set<String>,
+        private val annotationAllowedClassesFilter: ClassFilter,
         fallback: OutputFilter,
 ) : DelegatingFilter(fallback) {
     private var stubAnnotations = convertToInternalNames(stubAnnotations_)
@@ -62,7 +65,8 @@
     private var substituteAnnotations = convertToInternalNames(substituteAnnotations_)
     private var nativeSubstituteAnnotations = convertToInternalNames(nativeSubstituteAnnotations_)
     private var classLoadHookAnnotations = convertToInternalNames(classLoadHookAnnotations_)
-    private var stubStaticInitializerAnnotations = convertToInternalNames(stubStaticInitializerAnnotations_)
+    private var stubStaticInitializerAnnotations =
+            convertToInternalNames(stubStaticInitializerAnnotations_)
 
     /** Annotations that control API visibility. */
     private var visibilityAnnotations: Set<String> = convertToInternalNames(
@@ -135,15 +139,22 @@
      * name1 - 4 are only used in exception messages.
      */
     private fun findAnnotation(
-        visibles: List<AnnotationNode>?,
-        invisibles: List<AnnotationNode>?,
-        type: String,
-        name1: String,
-        name2: String = "",
-        name3: String = "",
+            className: String,
+            visibles: List<AnnotationNode>?,
+            invisibles: List<AnnotationNode>?,
+            type: String,
+            name1: String,
+            name2: String = "",
+            name3: String = "",
     ): FilterPolicyWithReason? {
         detectInvalidAnnotations(visibles, invisibles, type, name1, name2, name3)
 
+        if (!annotationAllowedClassesFilter.matches(className)) {
+            throw InvalidAnnotationException(
+                    "Class ${className.toHumanReadableClassName()} is not allowed to have " +
+                    "Ravenwood annotations. Contact g/ravenwood for more details.")
+        }
+
         findAnyAnnotation(stubAnnotations, visibles, invisibles)?.let {
             return FilterPolicy.Stub.withReason(reasonAnnotation)
         }
@@ -170,6 +181,7 @@
         val cn = classes.getClass(className)
 
         findAnnotation(
+            cn.name,
             cn.visibleAnnotations,
             cn.invisibleAnnotations,
             "class",
@@ -193,6 +205,7 @@
 
         cn.fields?.firstOrNull { it.name == fieldName }?.let {fn ->
             findAnnotation(
+                cn.name,
                 fn.visibleAnnotations,
                 fn.invisibleAnnotations,
                 "field",
@@ -229,6 +242,7 @@
 
             // If there's no substitution, then we check the annotation.
             findAnnotation(
+                cn.name,
                 mn.visibleAnnotations,
                 mn.invisibleAnnotations,
                 "method",
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 46546e8..416f085 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -15,7 +15,7 @@
  */
 package com.android.hoststubgen.filters
 
-import com.android.hoststubgen.UserErrorException
+import com.android.hoststubgen.ParseException
 import com.android.hoststubgen.asm.ClassNodes
 import com.android.hoststubgen.log
 import com.android.hoststubgen.normalizeTextLine
@@ -46,30 +46,6 @@
     return (access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) != 0
 }
 
-/**
- * Exception for a parse error.
- */
-private class ParseException : Exception, UserErrorException {
-    val hasSourceInfo: Boolean
-
-    constructor(message: String) : super(message) {
-        hasSourceInfo = false
-    }
-
-    constructor(message: String, file: String, line: Int) :
-            super("$message in file $file line $line") {
-        hasSourceInfo = true
-    }
-
-    fun withSourceInfo(filename: String, lineNo: Int): ParseException {
-        if (hasSourceInfo) {
-            return this // Already has source information.
-        } else {
-            return ParseException(this.message ?: "", filename, lineNo)
-        }
-    }
-}
-
 private const val FILTER_REASON = "file-override"
 
 /**
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
new file mode 100644
index 0000000..01a7ab3
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/utils/ClassFilter.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 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.hoststubgen.utils
+
+import com.android.hoststubgen.ParseException
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.asm.toJvmClassName
+import com.android.hoststubgen.normalizeTextLine
+import java.io.File
+
+/**
+ * General purpose filter for class names.
+ */
+class ClassFilter private constructor (
+        val defaultResult: Boolean,
+) {
+    private data class FilterElement(
+            val allowed: Boolean,
+            val internalName: String,
+            val isPrefix: Boolean,
+    ) {
+        fun matches(classInternalName: String): Boolean {
+            if (isPrefix) {
+                return classInternalName.startsWith(internalName)
+            } else {
+                return classInternalName == internalName
+            }
+        }
+    }
+
+    private val elements: MutableList<FilterElement> = mutableListOf()
+
+    private val cache: MutableMap<String, Boolean> = mutableMapOf()
+
+    /**
+     * Takes an internal class name (e.g. "com/android/hoststubgen/ClassName") and returns if
+     * matches the filter or not.
+     */
+    fun matches(classInternalName: String): Boolean {
+        cache[classInternalName]?.let {
+            return it
+        }
+
+        var result = defaultResult
+        run outer@{
+            elements.forEach { e ->
+                if (e.matches(classInternalName)) {
+                    result = e.allowed
+                    return@outer // break equivalent.
+                }
+            }
+        }
+        cache[classInternalName] = result
+
+        return result
+    }
+
+    fun getCacheSizeForTest(): Int {
+        return cache.size
+    }
+
+    companion object {
+        /**
+         * Return a filter that alawys returns true or false.
+         */
+        fun newNullFilter(defaultResult: Boolean): ClassFilter {
+            return ClassFilter(defaultResult)
+        }
+
+        /** Build a filter from a file. */
+        fun loadFromFile(filename: String, defaultResult: Boolean): ClassFilter {
+            return buildFromString(File(filename).readText(), defaultResult, filename)
+        }
+
+        /** Build a filter from a string (for unit tests). */
+        fun buildFromString(
+                filterString: String,
+                defaultResult: Boolean,
+                filenameForErrorMessage: String
+        ): ClassFilter {
+            val ret = ClassFilter(defaultResult)
+
+            var lineNo = 0
+            filterString.split('\n').forEach { s ->
+                lineNo++
+
+                var line = normalizeTextLine(s)
+
+                if (line.isEmpty()) {
+                    return@forEach // skip empty lines.
+                }
+
+                line = line.toHumanReadableClassName() // Convert all the slashes to periods.
+
+                var allow = true
+                if (line.startsWith("!")) {
+                    allow = false
+                    line = line.substring(1).trimStart()
+                }
+
+                // Special case -- matches any class names.
+                if (line == "*") {
+                    ret.elements.add(FilterElement(allow, "", true))
+                    return@forEach
+                }
+
+                // Handle wildcard -- e.g. "package.name.*"
+                if (line.endsWith(".*")) {
+                    ret.elements.add(FilterElement(
+                            allow, line.substring(0, line.length - 2).toJvmClassName(), true))
+                    return@forEach
+                }
+
+                // Any other uses of "*" would be an error.
+                if (line.contains('*')) {
+                    throw ParseException(
+                            "Wildcard (*) can only show up as the last element",
+                            filenameForErrorMessage,
+                            lineNo
+                    )
+                }
+                ret.elements.add(FilterElement(allow, line.toJvmClassName(), false))
+            }
+
+            return ret
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp b/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
index 3dc6da3..e7873d6 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/Android.bp
@@ -16,6 +16,7 @@
     static_libs: [
         "hoststubgen-annotations",
     ],
+    visibility: ["//frameworks/base/tools/hoststubgen:__subpackages__"],
 }
 
 // Create stub/impl jars from "hoststubgen-test-tiny-framework", using the following 3 rules.
@@ -30,6 +31,7 @@
         ":hoststubgen-test-tiny-framework",
         "policy-override-tiny-framework.txt",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_genrule_host {
@@ -41,6 +43,7 @@
     out: [
         "host_stub.jar",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_genrule_host {
@@ -52,6 +55,7 @@
     out: [
         "host_impl.jar",
     ],
+    visibility: ["//visibility:private"],
 }
 
 // Same as "hoststubgen-test-tiny-framework-host", but with more options, to test more hoststubgen
@@ -71,6 +75,7 @@
         ":hoststubgen-test-tiny-framework",
         "policy-override-tiny-framework.txt",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_genrule_host {
@@ -82,6 +87,7 @@
     out: [
         "host_stub.jar",
     ],
+    visibility: ["//visibility:private"],
 }
 
 java_genrule_host {
@@ -93,6 +99,7 @@
     out: [
         "host_impl.jar",
     ],
+    visibility: ["//visibility:private"],
 }
 
 // Compile the test jar, using 2 rules.
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/annotation-allowed-classes-tiny-framework.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/annotation-allowed-classes-tiny-framework.txt
new file mode 100644
index 0000000..bd9e85e
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/annotation-allowed-classes-tiny-framework.txt
@@ -0,0 +1,29 @@
+# Only classes listed here can use the hoststubgen annotations.
+
+# For each class, we check each item in this file, and when a match is found, we
+# either allow it if the line doesn't have a !, or disallow if the line has a !.
+# All the lines after the matching line will be ignored.
+
+
+# To allow a specific class to use annotations:
+# com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnotations
+
+# To disallow a specific class to use annotations:
+# !com.android.hoststubgen.test.tinyframework.TinyFrameworkClassAnnotations
+
+# To allow a specific package to use annotations:
+# com.android.hoststubgen.test.*
+
+# To disallow a specific package to use annotations:
+# !com.android.hoststubgen.test.*
+
+
+com.android.hoststubgen.test.tinyframework.*
+com.supported.*
+com.unsupported.*
+
+# Use this to allow all packages
+# *
+
+# Use this to allow all packages
+# !*
\ No newline at end of file
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh b/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
index e212890..872bbf8 100755
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/run-test-manually.sh
@@ -93,6 +93,7 @@
     --gen-keep-all-file out/tiny-framework_keep_all.txt \
     --gen-input-dump-file out/tiny-framework_dump.txt \
     --package-redirect com.unsupported:com.supported \
+    --annotation-allowed-classes-file annotation-allowed-classes-tiny-framework.txt \
     $HOSTSTUBGEN_OPTS
 
 # Extract the jar files, so we can look into them.
diff --git a/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
new file mode 100644
index 0000000..f651514
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/test/com/android/hoststubgen/utils/ClassFilterTest.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2023 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.hoststubgen.utils
+
+import com.android.hoststubgen.ParseException
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Test
+
+class ClassFilterTest {
+    @Test
+    fun testDefaultTrue() {
+        val f = ClassFilter.newNullFilter(true)
+        assertThat(f.matches("a/b/c")).isEqualTo(true)
+    }
+
+    @Test
+    fun testDefaultFalse() {
+        val f = ClassFilter.newNullFilter(false)
+        assertThat(f.matches("a/b/c")).isEqualTo(false)
+    }
+
+    @Test
+    fun testComplex1() {
+        val f = ClassFilter.buildFromString("""
+            # ** this is a comment **
+            a.b.c       # allow
+            !a.b.d      # disallow
+            *           # allow all
+            """.trimIndent(), false, "X")
+        assertThat(f.getCacheSizeForTest()).isEqualTo(0)
+
+        assertThat(f.matches("a/b/c")).isEqualTo(true)
+        assertThat(f.getCacheSizeForTest()).isEqualTo(1)
+
+        assertThat(f.matches("a/b/d")).isEqualTo(false)
+        assertThat(f.matches("x")).isEqualTo(true)
+
+        assertThat(f.getCacheSizeForTest()).isEqualTo(3)
+
+        // Make sure the cache is working
+        assertThat(f.matches("x")).isEqualTo(true)
+    }
+
+    @Test
+    fun testComplex2() {
+        val f = ClassFilter.buildFromString("""
+            a.b.c       # allow
+            !a.*        # disallow everything else in package "a".
+            !d.e.f      # disallow d.e.f.
+
+            # everything else is allowed by default
+            """.trimIndent(), true, "X")
+        assertThat(f.matches("a/b/c")).isEqualTo(true)
+        assertThat(f.matches("a/x")).isEqualTo(false)
+        assertThat(f.matches("d/e/f")).isEqualTo(false)
+        assertThat(f.matches("d/e/f/g")).isEqualTo(true)
+        assertThat(f.matches("x")).isEqualTo(true)
+    }
+
+    @Test
+    fun testBadFilter1() {
+        try {
+            ClassFilter.buildFromString("""
+                a*
+                """.trimIndent(), true, "FILENAME")
+            fail("ParseException didn't happen")
+        } catch (e: ParseException) {
+            assertThat(e.message).contains("Wildcard")
+            assertThat(e.message).contains("FILENAME")
+            assertThat(e.message).contains("line 1")
+        }
+    }
+}
\ No newline at end of file
diff --git a/tools/hoststubgen/scripts/run-all-tests.sh b/tools/hoststubgen/scripts/run-all-tests.sh
index ba1d404..4afa2d7 100755
--- a/tools/hoststubgen/scripts/run-all-tests.sh
+++ b/tools/hoststubgen/scripts/run-all-tests.sh
@@ -33,6 +33,9 @@
 # First, build all the test / etc modules. This shouldn't fail.
 run m "${MUST_BUILD_MODULES[@]}"
 
+# Run the hoststubgen unittests / etc
+run atest hoststubgentest hoststubgen-invoke-test
+
 # Next, run the golden check. This should always pass too.
 # The following scripts _should_ pass too, but they depend on the internal paths to soong generated
 # files, and they may fail when something changes in the build system.