• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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