1 /* <lambda>null2 * Copyright (C) 2025 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.platform.test.ravenwood.ravenhelper.policytoannot 17 18 import com.android.hoststubgen.LogLevel 19 import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME 20 import com.android.hoststubgen.asm.toJvmClassName 21 import com.android.hoststubgen.filters.FilterPolicyWithReason 22 import com.android.hoststubgen.filters.MethodCallReplaceSpec 23 import com.android.hoststubgen.filters.PolicyFileProcessor 24 import com.android.hoststubgen.filters.SpecialClass 25 import com.android.hoststubgen.filters.TextFileFilterPolicyParser 26 import com.android.hoststubgen.log 27 import com.android.hoststubgen.utils.ClassPredicate 28 import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler 29 import com.android.platform.test.ravenwood.ravenhelper.psi.createUastEnvironment 30 import com.android.platform.test.ravenwood.ravenhelper.sourcemap.AllClassInfo 31 import com.android.platform.test.ravenwood.ravenhelper.sourcemap.ClassInfo 32 import com.android.platform.test.ravenwood.ravenhelper.sourcemap.MethodInfo 33 import com.android.platform.test.ravenwood.ravenhelper.sourcemap.SourceLoader 34 import java.io.FileReader 35 import java.util.regex.Pattern 36 37 /** 38 * This is the main routine of the "pta" -- policy-to-annotation -- subcommands. 39 */ 40 class PtaProcessor : SubcommandHandler { 41 override fun handle(args: List<String>) { 42 val options = PtaOptions().apply { parseArgs(args) } 43 44 log.v("Options: $options") 45 46 val converter = TextPolicyToAnnotationConverter( 47 options.policyOverrideFiles, 48 options.sourceFilesOrDirectories, 49 options.annotationAllowedClassesFile.get, 50 Annotations(), 51 options.dumpOperations.get || log.isEnabled(LogLevel.Debug), 52 ) 53 converter.process() 54 55 createShellScript(converter.resultOperations, options.outputScriptFile.get) 56 } 57 } 58 59 /** 60 * This class implements the actual logic. 61 */ 62 private class TextPolicyToAnnotationConverter( 63 val policyFiles: List<String>, 64 val sourceFilesOrDirectories: List<String>, 65 val annotationAllowedClassesFile: String?, 66 val annotations: Annotations, 67 val dumpOperations: Boolean, 68 ) { filenull69 private val annotationAllowedClasses: ClassPredicate = annotationAllowedClassesFile.let { file -> 70 if (file == null) { 71 ClassPredicate.newConstantPredicate(true) // Allow all classes 72 } else { 73 ClassPredicate.loadFromFile(file, false) 74 } 75 } 76 77 val resultOperations = SourceOperations() 78 private val classes = AllClassInfo() 79 private val policyParser = TextFileFilterPolicyParser() 80 private val annotationNeedingClasses = mutableSetOf<String>() 81 82 /** 83 * Entry point. 84 */ processnull85 fun process() { 86 // First, load 87 val env = createUastEnvironment() 88 try { 89 loadSources() 90 91 processPolicies() 92 93 addToAnnotationsAllowedListFile() 94 95 if (dumpOperations) { 96 log.withIndent { 97 resultOperations.getOperations().toSortedMap().forEach { (file, ops) -> 98 log.i("ops: $file") 99 ops.forEach { op -> 100 log.i(" line: ${op.lineNumber}: ${op.type}: \"${op.text}\" " + 101 "(${op.description})") 102 } 103 } 104 } 105 } 106 } finally { 107 env.dispose() 108 } 109 } 110 111 /** 112 * Load all the java source files into [classes]. 113 */ loadSourcesnull114 private fun loadSources() { 115 val env = createUastEnvironment() 116 try { 117 val loader = SourceLoader(env) 118 loader.load(sourceFilesOrDirectories, classes) 119 } finally { 120 env.dispose() 121 } 122 } 123 addToAnnotationsAllowedListFilenull124 private fun addToAnnotationsAllowedListFile() { 125 log.i("Generating operations to update annotation allowlist file...") 126 log.withIndent { 127 annotationNeedingClasses.sorted().forEach { className -> 128 if (!annotationAllowedClasses.matches(className.toJvmClassName())) { 129 resultOperations.add( 130 SourceOperation( 131 annotationAllowedClassesFile!!, 132 -1, // add to the end 133 SourceOperationType.Insert, 134 className, 135 "add to annotation allowlist" 136 )) 137 } 138 } 139 } 140 } 141 142 /** 143 * Process the policy files with [Processor]. 144 */ processPoliciesnull145 private fun processPolicies() { 146 log.i("Loading the policy files and generating operations...") 147 log.withIndent { 148 policyFiles.forEach { policyFile -> 149 log.i("Parsing $policyFile ...") 150 log.withIndent { 151 policyParser.parse(FileReader(policyFile), policyFile, Processor()) 152 } 153 } 154 } 155 } 156 157 private inner class Processor : PolicyFileProcessor { 158 159 var classPolicyText = "" 160 var classPolicyLine = -1 161 162 // Whether the current class has a skip marker, in which case we ignore all members. 163 // Applicable only within a "simple class" 164 var classSkipping = false 165 166 var classLineConverted = false 167 var classHasMember = false 168 currentLineHasSkipMarkernull169 private fun currentLineHasSkipMarker(): Boolean { 170 val ret = policyParser.currentLineText.contains("no-pta") 171 172 if (ret) { 173 log.forVerbose { 174 log.v("Current line has a skip marker: ${policyParser.currentLineText}") 175 } 176 } 177 178 return ret 179 } 180 shouldSkipCurrentLinenull181 private fun shouldSkipCurrentLine(): Boolean { 182 // If a line contains a special marker "no-pta", we'll skip it. 183 return classSkipping || currentLineHasSkipMarker() 184 } 185 186 /** Print a warning about an unsupported policy directive. */ warnOnPolicynull187 private fun warnOnPolicy(message: String, policyLine: String, lineNumber: Int) { 188 log.w("Warning: $message") 189 log.w(" policy: \"$policyLine\"") 190 log.w(" at ${policyParser.filename}:$lineNumber") 191 } 192 193 /** Print a warning about an unsupported policy directive. */ warnOnCurrentPolicynull194 private fun warnOnCurrentPolicy(message: String) { 195 warnOnPolicy(message, policyParser.currentLineText, policyParser.lineNumber) 196 } 197 198 /** Print a warning about an unsupported policy directive on the class line. */ warnOnClassPolicynull199 private fun warnOnClassPolicy(message: String) { 200 warnOnPolicy(message, classPolicyText, classPolicyLine) 201 } 202 onPackagenull203 override fun onPackage(name: String, policy: FilterPolicyWithReason) { 204 warnOnCurrentPolicy("'package' directive isn't supported (yet).") 205 } 206 onRenamenull207 override fun onRename(pattern: Pattern, prefix: String) { 208 // Rename will never be supported, so don't show a warning. 209 } 210 addOperationnull211 private fun addOperation(op: SourceOperation) { 212 resultOperations.add(op) 213 } 214 commentOutPolicynull215 private fun commentOutPolicy(lineNumber: Int, description: String) { 216 addOperation( 217 SourceOperation( 218 policyParser.filename, 219 lineNumber, 220 SourceOperationType.Prepend, 221 "#[PTA]: ", // comment out. 222 description, 223 ) 224 ) 225 } 226 onClassStartnull227 override fun onClassStart(className: String) { 228 classSkipping = currentLineHasSkipMarker() 229 classLineConverted = false 230 classHasMember = false 231 classPolicyLine = policyParser.lineNumber 232 classPolicyText = policyParser.currentLineText 233 } 234 onClassEndnull235 override fun onClassEnd(className: String) { 236 if (classSkipping) { 237 classSkipping = false 238 return 239 } 240 if (!classLineConverted) { 241 // Class line is still needed in the policy file. 242 // (Because the source file wasn't found.) 243 return 244 } 245 if (!classHasMember) { 246 commentOutPolicy(classPolicyLine, "remove class policy on $className") 247 } else { 248 warnOnClassPolicy( 249 "Class policy on $className can't be removed because it still has members.") 250 } 251 } 252 findClassnull253 private fun findClass(className: String): ClassInfo? { 254 val ci = classes.findClass(className) 255 if (ci == null) { 256 warnOnCurrentPolicy("Class not found: $className") 257 } 258 return ci 259 } 260 addClassAnnotationnull261 private fun addClassAnnotation( 262 className: String, 263 annotation: String, 264 ): Boolean { 265 val ci = findClass(className) ?: return false 266 267 // Add the annotation to the source file. 268 addOperation( 269 SourceOperation( 270 ci.location.file, 271 ci.location.line, 272 SourceOperationType.Insert, 273 ci.location.getIndent() + annotation, 274 "add class annotation to $className" 275 ) 276 ) 277 annotationNeedingClasses.add(className) 278 return true 279 } 280 onSimpleClassPolicynull281 override fun onSimpleClassPolicy(className: String, policy: FilterPolicyWithReason) { 282 if (shouldSkipCurrentLine()) { 283 return 284 } 285 log.v("Found simple class policy: $className - ${policy.policy}") 286 287 val annot = annotations.get(policy.policy, Annotations.Target.Class)!! 288 if (addClassAnnotation(className, annot)) { 289 classLineConverted = true 290 } 291 } 292 onSubClassPolicynull293 override fun onSubClassPolicy(superClassName: String, policy: FilterPolicyWithReason) { 294 warnOnCurrentPolicy("Subclass policies isn't supported (yet).") 295 } 296 onRedirectionClassnull297 override fun onRedirectionClass(fromClassName: String, toClassName: String) { 298 if (shouldSkipCurrentLine()) { 299 return 300 } 301 302 log.v("Found class redirection: $fromClassName - $toClassName") 303 304 if (addClassAnnotation( 305 fromClassName, 306 annotations.getRedirectionClassAnnotation(toClassName), 307 )) { 308 commentOutPolicy(policyParser.lineNumber, 309 "remove class redirection policy on $fromClassName") 310 } 311 } 312 onClassLoadHooknull313 override fun onClassLoadHook(className: String, callback: String) { 314 if (shouldSkipCurrentLine()) { 315 return 316 } 317 318 log.v("Found class load hook: $className - $callback") 319 320 if (addClassAnnotation( 321 className, 322 annotations.getClassLoadHookAnnotation(callback), 323 )) { 324 commentOutPolicy(policyParser.lineNumber, 325 "remove class load hook policy on $className") 326 } 327 } 328 onSpecialClassPolicynull329 override fun onSpecialClassPolicy(type: SpecialClass, policy: FilterPolicyWithReason) { 330 // This can't be converted to an annotation, so don't show a warning. 331 } 332 onFieldnull333 override fun onField(className: String, fieldName: String, policy: FilterPolicyWithReason) { 334 if (shouldSkipCurrentLine()) { 335 return 336 } 337 338 log.v("Found field policy: $className.$fieldName - ${policy.policy}") 339 340 val ci = findClass(className) ?: return 341 342 ci.findField(fieldName)?.let { fi -> 343 val annot = annotations.get(policy.policy, Annotations.Target.Field)!! 344 345 addOperation( 346 SourceOperation( 347 fi.location.file, 348 fi.location.line, 349 SourceOperationType.Insert, 350 fi.location.getIndent() + annot, 351 "add annotation to field $className.$fieldName", 352 ) 353 ) 354 commentOutPolicy(policyParser.lineNumber, 355 "remove field policy $className.$fieldName") 356 357 annotationNeedingClasses.add(className) 358 } ?: { 359 warnOnCurrentPolicy("Field not found: $className.$fieldName") 360 } 361 } 362 onSimpleMethodPolicynull363 override fun onSimpleMethodPolicy( 364 className: String, 365 methodName: String, 366 methodDesc: String, 367 policy: FilterPolicyWithReason 368 ) { 369 if (shouldSkipCurrentLine()) { 370 return 371 } 372 val readableName = "$className.$methodName$methodDesc" 373 log.v("Found simple method policy: $readableName - ${policy.policy}") 374 375 376 // Inner method to get the matching methods for this policy. 377 // 378 // If this policy can't be converted for any reason, it'll return null. 379 // Otherwise, it'll return a pair of method list and the annotation string. 380 fun getMethods(): Pair<List<MethodInfo>, String>? { 381 if (methodName == CLASS_INITIALIZER_NAME) { 382 warnOnClassPolicy("Policy for class initializers not supported.") 383 return null 384 } 385 val ci = findClass(className) ?: return null 386 val methods = ci.findMethods(methodName, methodDesc) 387 if (methods == null) { 388 warnOnCurrentPolicy("Method not found: $readableName") 389 return null 390 } 391 392 // If the policy is "ignore", we can't convert it to an annotation, in which case 393 // annotations.get() will return null. 394 val annot = annotations.get(policy.policy, Annotations.Target.Method) 395 if (annot == null) { 396 warnOnCurrentPolicy("Annotation for policy '${policy.policy}' isn't available") 397 return null 398 } 399 return Pair(methods, annot) 400 } 401 402 val methodsAndAnnot = getMethods() 403 404 if (methodsAndAnnot == null) { 405 classHasMember = true 406 return // This policy can't be converted. 407 } 408 val methods = methodsAndAnnot.first 409 val annot = methodsAndAnnot.second 410 411 var found = false 412 methods.forEach { mi -> 413 found = true 414 addOperation( 415 SourceOperation( 416 mi.location.file, 417 mi.location.line, 418 SourceOperationType.Insert, 419 mi.location.getIndent() + annot, 420 "add annotation to method $readableName", 421 ) 422 ) 423 } 424 if (found) { 425 commentOutPolicy( 426 policyParser.lineNumber, 427 "remove method policy $readableName" 428 ) 429 430 annotationNeedingClasses.add(className) 431 } else { 432 warnOnCurrentPolicy("Method not found: $readableName") 433 } 434 } 435 onMethodInClassReplacenull436 override fun onMethodInClassReplace( 437 className: String, 438 methodName: String, 439 methodDesc: String, 440 targetName: String, 441 policy: FilterPolicyWithReason 442 ) { 443 warnOnCurrentPolicy("Found method replace but it's not supported yet: " 444 + "$className.$methodName$methodDesc - $targetName") 445 } 446 onMethodOutClassReplacenull447 override fun onMethodOutClassReplace( 448 className: String, 449 methodName: String, 450 methodDesc: String, 451 replaceSpec: MethodCallReplaceSpec, 452 ) { 453 // This can't be converted to an annotation. 454 classHasMember = true 455 } 456 } 457 } 458