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 filter for class names.
26  */
27 class ClassFilter private constructor (
28         val defaultResult: Boolean,
29 ) {
30     private data class FilterElement(
31             val allowed: Boolean,
32             val internalName: String,
33             val isPrefix: Boolean,
34     ) {
35         fun matches(classInternalName: String): Boolean {
36             if (isPrefix) {
37                 return classInternalName.startsWith(internalName)
38             } else {
39                 return classInternalName == internalName
40             }
41         }
42     }
43 
44     private val elements: MutableList<FilterElement> = mutableListOf()
45 
46     private val cache: MutableMap<String, Boolean> = mutableMapOf()
47 
48     /**
49      * Takes an internal class name (e.g. "com/android/hoststubgen/ClassName") and returns if
50      * matches the filter or not.
51      */
52     fun matches(classInternalName: String): Boolean {
53         cache[classInternalName]?.let {
54             return it
55         }
56 
57         var result = defaultResult
58         run outer@{
59             elements.forEach { e ->
60                 if (e.matches(classInternalName)) {
61                     result = e.allowed
62                     return@outer // break equivalent.
63                 }
64             }
65         }
66         cache[classInternalName] = result
67 
68         return result
69     }
70 
71     fun getCacheSizeForTest(): Int {
72         return cache.size
73     }
74 
75     companion object {
76         /**
77          * Return a filter that alawys returns true or false.
78          */
79         fun newNullFilter(defaultResult: Boolean): ClassFilter {
80             return ClassFilter(defaultResult)
81         }
82 
83         /** Build a filter from a file. */
84         fun loadFromFile(filename: String, defaultResult: Boolean): ClassFilter {
85             return buildFromString(File(filename).readText(), defaultResult, filename)
86         }
87 
88         /** Build a filter from a string (for unit tests). */
89         fun buildFromString(
90                 filterString: String,
91                 defaultResult: Boolean,
92                 filenameForErrorMessage: String
93         ): ClassFilter {
94             val ret = ClassFilter(defaultResult)
95 
96             var lineNo = 0
97             filterString.split('\n').forEach { s ->
98                 lineNo++
99 
100                 var line = normalizeTextLine(s)
101 
102                 if (line.isEmpty()) {
103                     return@forEach // skip empty lines.
104                 }
105 
106                 line = line.toHumanReadableClassName() // Convert all the slashes to periods.
107 
108                 var allow = true
109                 if (line.startsWith("!")) {
110                     allow = false
111                     line = line.substring(1).trimStart()
112                 }
113 
114                 // Special case -- matches any class names.
115                 if (line == "*") {
116                     ret.elements.add(FilterElement(allow, "", true))
117                     return@forEach
118                 }
119 
120                 // Handle wildcard -- e.g. "package.name.*"
121                 if (line.endsWith(".*")) {
122                     ret.elements.add(FilterElement(
123                             allow, line.substring(0, line.length - 2).toJvmClassName(), true))
124                     return@forEach
125                 }
126 
127                 // Any other uses of "*" would be an error.
128                 if (line.contains('*')) {
129                     throw ParseException(
130                             "Wildcard (*) can only show up as the last element",
131                             filenameForErrorMessage,
132                             lineNo
133                     )
134                 }
135                 ret.elements.add(FilterElement(allow, line.toJvmClassName(), false))
136             }
137 
138             return ret
139         }
140     }
141 }