1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.hoststubgen.utils 17 18 import com.android.hoststubgen.ParseException 19 import com.android.hoststubgen.asm.toHumanReadableClassName 20 import com.android.hoststubgen.asm.toJvmClassName 21 import com.android.hoststubgen.normalizeTextLine 22 import java.io.File 23 24 /** 25 * General purpose class "selector", which returns a boolean for a given class name. 26 * 27 * (It's used to check if a class is in the "annotations allowed classes" allowlist.) 28 */ 29 class ClassPredicate private constructor( 30 private val defaultResult: Boolean, 31 ) { 32 private enum class MatchType { 33 Full, 34 Prefix, 35 Suffix, 36 } 37 38 private class FilterElement( 39 val allowed: Boolean, 40 val internalName: String, 41 val matchType: MatchType, 42 ) { 43 fun matches(classInternalName: String): Boolean { 44 return when (matchType) { 45 MatchType.Full -> classInternalName == internalName 46 MatchType.Prefix -> classInternalName.startsWith(internalName) 47 MatchType.Suffix -> classInternalName.endsWith(internalName) 48 } 49 } 50 } 51 52 private val elements: MutableList<FilterElement> = mutableListOf() 53 54 private val cache: MutableMap<String, Boolean> = mutableMapOf() 55 56 /** 57 * Takes an internal class name (e.g. "com/android/hoststubgen/ClassName") and returns if 58 * matches the filter or not. 59 */ 60 fun matches(classInternalName: String): Boolean { 61 cache[classInternalName]?.let { 62 return it 63 } 64 65 val testClasses = sequence { 66 // Yield itself and its outer class(es) one by one 67 var idx = classInternalName.length 68 while (idx > 0) { 69 yield(classInternalName.substring(0, idx)) 70 idx = classInternalName.lastIndexOf('$', idx - 1) 71 } 72 } 73 74 val result = elements.find { testClasses.any(it::matches) }?.allowed ?: defaultResult 75 cache[classInternalName] = result 76 77 return result 78 } 79 80 fun getCacheSizeForTest(): Int { 81 return cache.size 82 } 83 84 companion object { 85 /** 86 * Return a filter that always returns true or false. 87 */ 88 fun newConstantPredicate(defaultResult: Boolean): ClassPredicate { 89 return ClassPredicate(defaultResult) 90 } 91 92 /** Build a filter from a file. */ 93 fun loadFromFile(filename: String, defaultResult: Boolean): ClassPredicate { 94 return buildFromString(File(filename).readText(), defaultResult, filename) 95 } 96 97 /** Build a filter from a string (for unit tests). */ 98 fun buildFromString( 99 filterString: String, 100 defaultResult: Boolean, 101 filenameForErrorMessage: String 102 ): ClassPredicate { 103 val ret = ClassPredicate(defaultResult) 104 105 var lineNo = 0 106 filterString.split('\n').forEach { s -> 107 lineNo++ 108 109 var line = normalizeTextLine(s) 110 111 if (line.isEmpty()) { 112 return@forEach // skip empty lines. 113 } 114 115 line = line.toHumanReadableClassName() // Convert all the slashes to periods. 116 117 var allow = true 118 if (line.startsWith("!")) { 119 allow = false 120 line = line.substring(1).trimStart() 121 } 122 123 // Special case -- matches any class names. 124 if (line == "*") { 125 ret.elements.add(FilterElement(allow, "", MatchType.Prefix)) 126 return@forEach 127 } 128 129 // Handle prefix match -- e.g. "package.name.*" 130 if (line.endsWith(".*")) { 131 ret.elements.add( 132 FilterElement( 133 allow, 134 line.substring(0, line.length - 2).toJvmClassName() + "/", 135 MatchType.Prefix 136 ) 137 ) 138 return@forEach 139 } 140 141 // Handle suffix match -- e.g. "*.Flags" 142 if (line.startsWith("*.")) { 143 ret.elements.add( 144 FilterElement( 145 allow, 146 "/" + line.substring(2, line.length).toJvmClassName(), 147 MatchType.Suffix 148 ) 149 ) 150 return@forEach 151 } 152 153 // Any other uses of "*" would be an error. 154 if (line.contains('*')) { 155 throw ParseException( 156 "Wildcard (*) can only show up as the last element", 157 filenameForErrorMessage, 158 lineNo 159 ) 160 } 161 ret.elements.add(FilterElement(allow, line.toJvmClassName(), MatchType.Suffix)) 162 } 163 164 return ret 165 } 166 } 167 } 168