1 /*
<lambda>null2 * Copyright (C) 2024 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
17 package com.android.xts.apimapper.adapter
18
19 import com.android.xts.apimapper.asm.ClassNodes
20 import com.android.xts.apimapper.config.Configuration
21 import org.objectweb.asm.Opcodes
22 import org.objectweb.asm.Type
23 import org.objectweb.asm.tree.AnnotationNode
24 import org.objectweb.asm.tree.ClassNode
25
26 private const val TEST_CLASS_ACCESS_MASK = Opcodes.ACC_STATIC or Opcodes.ACC_PRIVATE or
27 Opcodes.ACC_PROTECTED or Opcodes.ACC_INTERFACE or Opcodes.ACC_ABSTRACT
28 private val JUNIT3_TEST_CLASS_NAMES = setOf(
29 "junit/framework/TestCase",
30 "android/test/AndroidTestCase",
31 "android/test/InstrumentationTestCase",
32 "android/test/ActivityInstrumentationTestCase2",
33 )
34 private val JUNIT4_ANNOTATION_PREFIXED = listOf("org.junit")
35
36 private const val API_MAPPER_CLASS_PREFIX = "com/android/xts/apimapper/"
37
38 // Assume only classes (android.* or com.android.*) or jetpack libs will call android APIs.
39 // TODO(slotus): Use more general rule.
40 private val API_CALLER_CLASS_PREFIXES = listOf(
41 "android/",
42 "com/android/",
43 "com/google/",
44 "com/google/android/",
45 "androidx/",
46 "com/androidx/",
47 )
48
49 // Ignore classes that could introduce a lot of meaningless logs.
50 private val MEANINGLESS_API_CALLER_CLASS_PREFIXES = listOf(
51 "androidx/test/",
52 "androidx/tracing/Trace",
53 "com/android/tradefed",
54 )
55
56 // Potential Android API classes.
57 private val API_CLASS_PREFIXES = listOf(
58 "android/",
59 "com/android/"
60 )
61
62 // Android API classes that are meaningless to log.
63 private val MEANINGLESS_API_CLASS_PREFIXES = listOf(
64 "org/junit/",
65 "junit/",
66 "org/mockito/",
67 "kotlin/",
68 "kotlinx/",
69 "android/test/AndroidTestCase",
70 "android/test/InstrumentationTestCase"
71 )
72
73 /** Decide whether the given class should be injected. */
74 fun shouldProcessClass(
75 className: String,
76 jarFile: String,
77 configuration: Configuration
78 ): Boolean {
79 configuration.getInjectRules().forEach({
80 rule ->
81 if (jarFile.matches(rule.pattern)) {
82 return className.mayAndroidApiCallerClass(rule.callerPrefixes)
83 }
84 })
85 return className.mayAndroidApiCallerClass(API_CLASS_PREFIXES)
86 }
87
88 /** Decide whether the class is a test class. */
ClassNodenull89 fun ClassNode.isTestClass(classNodes: ClassNodes): Boolean {
90 if (this.access and TEST_CLASS_ACCESS_MASK != 0) {
91 return false
92 }
93 return isJunit3Test(classNodes) ||
94 hasJunit4Annotation(classNodes)
95 }
96
97 /** Decide whether the class is a Junit3 test class. */
ClassNodenull98 fun ClassNode.isJunit3Test(classNodes: ClassNodes): Boolean {
99 var currentClassNode = classNodes.findClass(this.name)
100 while (currentClassNode != null) {
101 val superName = currentClassNode.superName
102 if (superName in JUNIT3_TEST_CLASS_NAMES) {
103 return true
104 }
105 currentClassNode = classNodes.findClass(superName)
106 }
107 return false
108 }
109
110 /** An interface used to decide whether a method call should be logged. */
111 interface HookSettings {
shouldInjectHooknull112 fun shouldInjectHook(
113 classNodes: ClassNodes,
114 callerClass: String,
115 opcode: Int,
116 apiClass: String,
117 apiMethod: String,
118 apiDesc: String
119 ): Boolean
120 }
121
122 class AndroidApiInjectionSettings(private val configuration: Configuration) : HookSettings {
123
124 override fun shouldInjectHook(
125 classNodes: ClassNodes,
126 callerClass: String,
127 opcode: Int,
128 apiClass: String,
129 apiMethod: String,
130 apiDesc: String,
131 ): Boolean {
132 if (classNodes.findMethod(apiClass, apiMethod, apiDesc) != null) {
133 return false
134 }
135 // Don't call methods from Object.
136 // This is because handling it correctly would be painful when calling into, e.g. clone(),
137 // onto an array class.
138 if (apiClass == "java/lang/Object") {
139 return false
140 }
141 // Never hook array methods.
142 if (apiClass.startsWith("[")) {
143 return false
144 }
145 if (apiClass.isMeaninglessAndroidApiClass()) {
146 return false
147 }
148 configuration.getInjectRules().forEach({
149 rule ->
150 if (classNodes.sourceJarFile.matches(rule.pattern)) {
151 return apiClass.mayAndroidApiClass(rule.apiPrefixes) &&
152 callerClass.mayAndroidApiCallerClass(rule.callerPrefixes)
153 }
154 })
155 return apiClass.mayAndroidApiClass(API_CLASS_PREFIXES) &&
156 callerClass.mayAndroidApiCallerClass(API_CALLER_CLASS_PREFIXES)
157 }
158 }
159
Stringnull160 private fun String.startsWithAny(prefixes: List<String>): Boolean {
161 prefixes.forEach {
162 if (this.startsWith(it)) {
163 return true
164 }
165 }
166 return false
167 }
168
isApiMapperClassnull169 private fun String.isApiMapperClass(): Boolean {
170 return this.startsWith(API_MAPPER_CLASS_PREFIX)
171 }
172
mayAndroidApiCallerClassnull173 private fun String.mayAndroidApiCallerClass(prefixes: List<String>): Boolean {
174 // Hooking in this package could cause unexpected errors.
175 if (this.startsWithAny(MEANINGLESS_API_CALLER_CLASS_PREFIXES)) {
176 return false
177 }
178 // Always ignore ApiMapper classes.
179 if (this.isApiMapperClass()) {
180 return false
181 }
182 return prefixes.isEmpty() || this.startsWithAny(prefixes)
183 }
184
Stringnull185 private fun String.mayAndroidApiClass(prefixes: List<String>): Boolean {
186 if (this.isApiMapperClass()) {
187 return false
188 }
189 return prefixes.isEmpty() || this.startsWithAny(prefixes)
190 }
191
isMeaninglessAndroidApiClassnull192 private fun String.isMeaninglessAndroidApiClass(): Boolean {
193 return this.startsWithAny(MEANINGLESS_API_CLASS_PREFIXES)
194 }
195
anyJunit4Annotationnull196 private fun anyJunit4Annotation(annotations: List<AnnotationNode>?): Boolean {
197 if (annotations == null) {
198 return false
199 }
200 annotations.forEach {
201 val type = Type.getType(it.desc).className
202 if (type.startsWithAny(JUNIT4_ANNOTATION_PREFIXED)) {
203 return true
204 }
205 }
206 return false
207 }
208
209 /** Check whether the class or its super classes is using Junit4 annotations. */
ClassNodenull210 private fun ClassNode.hasJunit4Annotation(classNodes: ClassNodes): Boolean {
211 var currentClassNode = classNodes.findClass(this.name)
212 while (currentClassNode != null) {
213 if (anyJunit4Annotation(currentClassNode.visibleAnnotations)) {
214 return true
215 }
216 currentClassNode.methods.forEach {
217 if (anyJunit4Annotation(it.visibleAnnotations)) {
218 return true
219 }
220 }
221 val superName = currentClassNode.superName
222 currentClassNode = classNodes.findClass(superName)
223 }
224 return false
225 }
226