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 17 18 import com.android.hoststubgen.asm.ClassNodes 19 import com.android.hoststubgen.dumper.ApiDumper 20 import com.android.hoststubgen.filters.AnnotationBasedFilter 21 import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter 22 import com.android.hoststubgen.filters.ConstantFilter 23 import com.android.hoststubgen.filters.DefaultHookInjectingFilter 24 import com.android.hoststubgen.filters.FilterPolicy 25 import com.android.hoststubgen.filters.ImplicitOutputFilter 26 import com.android.hoststubgen.filters.OutputFilter 27 import com.android.hoststubgen.filters.StubIntersectingFilter 28 import com.android.hoststubgen.filters.createFilterFromTextPolicyFile 29 import com.android.hoststubgen.filters.printAsTextPolicy 30 import com.android.hoststubgen.utils.ClassFilter 31 import com.android.hoststubgen.visitors.BaseAdapter 32 import com.android.hoststubgen.visitors.PackageRedirectRemapper 33 import org.objectweb.asm.ClassReader 34 import org.objectweb.asm.ClassVisitor 35 import org.objectweb.asm.ClassWriter 36 import org.objectweb.asm.util.CheckClassAdapter 37 import java.io.BufferedInputStream 38 import java.io.FileOutputStream 39 import java.io.InputStream 40 import java.io.OutputStream 41 import java.io.PrintWriter 42 import java.util.zip.ZipEntry 43 import java.util.zip.ZipFile 44 import java.util.zip.ZipOutputStream 45 46 /** 47 * Actual main class. 48 */ 49 class HostStubGen(val options: HostStubGenOptions) { 50 fun run() { 51 val errors = HostStubGenErrors() 52 val stats = HostStubGenStats() 53 54 // Load all classes. 55 val allClasses = ClassNodes.loadClassStructures(options.inJar.get) 56 57 // Dump the classes, if specified. 58 options.inputJarDumpFile.ifSet { 59 PrintWriter(it).use { pw -> allClasses.dump(pw) } 60 log.i("Dump file created at $it") 61 } 62 63 options.inputJarAsKeepAllFile.ifSet { 64 PrintWriter(it).use { 65 pw -> allClasses.forEach { 66 classNode -> printAsTextPolicy(pw, classNode) 67 } 68 } 69 log.i("Dump file created at $it") 70 } 71 72 // Build the filters. 73 val filter = buildFilter(errors, allClasses, options) 74 75 // Transform the jar. 76 convert( 77 options.inJar.get, 78 options.outStubJar.get, 79 options.outImplJar.get, 80 filter, 81 options.enableClassChecker.get, 82 allClasses, 83 errors, 84 stats, 85 ) 86 87 // Dump statistics, if specified. 88 options.statsFile.ifSet { 89 PrintWriter(it).use { pw -> stats.dumpOverview(pw) } 90 log.i("Dump file created at $it") 91 } 92 options.apiListFile.ifSet { 93 PrintWriter(it).use { pw -> 94 // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed 95 // framework-minus-apex.jar so that we can dump inherited methods from it. 96 ApiDumper(pw, allClasses, null, filter).dump() 97 } 98 log.i("API list file created at $it") 99 } 100 } 101 102 /** 103 * Build the filter, which decides what classes/methods/fields should be put in stub or impl 104 * jars, and "how". (e.g. with substitution?) 105 */ 106 private fun buildFilter( 107 errors: HostStubGenErrors, 108 allClasses: ClassNodes, 109 options: HostStubGenOptions, 110 ): OutputFilter { 111 // We build a "chain" of multiple filters here. 112 // 113 // The filters are build in from "inside", meaning the first filter created here is 114 // the last filter used, so it has the least precedence. 115 // 116 // So, for example, the "remove" annotation, which is handled by AnnotationBasedFilter, 117 // can override a class-wide annotation, which is handled by 118 // ClassWidePolicyPropagatingFilter, and any annotations can be overridden by the 119 // text-file based filter, which is handled by parseTextFilterPolicyFile. 120 121 // The first filter is for the default policy from the command line options. 122 var filter: OutputFilter = ConstantFilter(options.defaultPolicy.get, "default-by-options") 123 124 // Next, we need a filter that resolves "class-wide" policies. 125 // This is used when a member (methods, fields, nested classes) don't get any polices 126 // from upper filters. e.g. when a method has no annotations, then this filter will apply 127 // the class-wide policy, if any. (if not, we'll fall back to the above filter.) 128 filter = ClassWidePolicyPropagatingFilter(allClasses, filter) 129 130 // Inject default hooks from options. 131 filter = DefaultHookInjectingFilter( 132 options.defaultClassLoadHook.get, 133 options.defaultMethodCallHook.get, 134 filter 135 ) 136 137 val annotationAllowedClassesFilter = options.annotationAllowedClassesFile.get.let { file -> 138 if (file == null) { 139 ClassFilter.newNullFilter(true) // Allow all classes 140 } else { 141 ClassFilter.loadFromFile(file, false) 142 } 143 } 144 145 // Next, Java annotation based filter. 146 filter = AnnotationBasedFilter( 147 errors, 148 allClasses, 149 options.stubAnnotations, 150 options.keepAnnotations, 151 options.stubClassAnnotations, 152 options.keepClassAnnotations, 153 options.throwAnnotations, 154 options.removeAnnotations, 155 options.substituteAnnotations, 156 options.nativeSubstituteAnnotations, 157 options.classLoadHookAnnotations, 158 options.keepStaticInitializerAnnotations, 159 annotationAllowedClassesFilter, 160 filter, 161 ) 162 163 // Next, "text based" filter, which allows to override polices without touching 164 // the target code. 165 options.policyOverrideFile.ifSet { 166 filter = createFilterFromTextPolicyFile(it, allClasses, filter) 167 } 168 169 // If `--intersect-stub-jar` is provided, load from these jar files too. 170 // We use this to restrict stub APIs to public/system/test APIs, 171 // by intersecting with a stub jar file created by metalava. 172 if (options.intersectStubJars.size > 0) { 173 val intersectingJars = loadIntersectingJars(options.intersectStubJars) 174 175 filter = StubIntersectingFilter(errors, intersectingJars, filter) 176 } 177 178 // Apply the implicit filter. 179 filter = ImplicitOutputFilter(errors, allClasses, filter) 180 181 return filter 182 } 183 184 /** 185 * Load jar files specified with "--intersect-stub-jar". 186 */ 187 private fun loadIntersectingJars(filenames: Set<String>): Map<String, ClassNodes> { 188 val intersectingJars = mutableMapOf<String, ClassNodes>() 189 190 filenames.forEach { filename -> 191 intersectingJars[filename] = ClassNodes.loadClassStructures(filename) 192 } 193 return intersectingJars 194 } 195 196 /** 197 * Convert a JAR file into "stub" and "impl" JAR files. 198 */ 199 private fun convert( 200 inJar: String, 201 outStubJar: String?, 202 outImplJar: String?, 203 filter: OutputFilter, 204 enableChecker: Boolean, 205 classes: ClassNodes, 206 errors: HostStubGenErrors, 207 stats: HostStubGenStats, 208 ) { 209 log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar) 210 log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled") 211 212 val start = System.currentTimeMillis() 213 214 val packageRedirector = PackageRedirectRemapper(options.packageRedirects) 215 216 log.withIndent { 217 // Open the input jar file and process each entry. 218 ZipFile(inJar).use { inZip -> 219 maybeWithZipOutputStream(outStubJar) { stubOutStream -> 220 maybeWithZipOutputStream(outImplJar) { implOutStream -> 221 val inEntries = inZip.entries() 222 while (inEntries.hasMoreElements()) { 223 val entry = inEntries.nextElement() 224 convertSingleEntry(inZip, entry, stubOutStream, implOutStream, 225 filter, packageRedirector, enableChecker, classes, errors, 226 stats) 227 } 228 log.i("Converted all entries.") 229 } 230 } 231 outStubJar?.let { log.i("Created stub: $it") } 232 outImplJar?.let { log.i("Created impl: $it") } 233 } 234 } 235 val end = System.currentTimeMillis() 236 log.i("Done transforming the jar in %.1f second(s).", (end - start) / 1000.0) 237 } 238 239 private fun <T> maybeWithZipOutputStream(filename: String?, block: (ZipOutputStream?) -> T): T { 240 if (filename == null) { 241 return block(null) 242 } 243 return ZipOutputStream(FileOutputStream(filename)).use(block) 244 } 245 246 /** 247 * Convert a single ZIP entry, which may or may not be a class file. 248 */ 249 private fun convertSingleEntry( 250 inZip: ZipFile, 251 entry: ZipEntry, 252 stubOutStream: ZipOutputStream?, 253 implOutStream: ZipOutputStream?, 254 filter: OutputFilter, 255 packageRedirector: PackageRedirectRemapper, 256 enableChecker: Boolean, 257 classes: ClassNodes, 258 errors: HostStubGenErrors, 259 stats: HostStubGenStats, 260 ) { 261 log.d("Entry: %s", entry.name) 262 log.withIndent { 263 val name = entry.name 264 265 // Just ignore all the directories. (TODO: make sure it's okay) 266 if (name.endsWith("/")) { 267 return 268 } 269 270 // If it's a class, convert it. 271 if (name.endsWith(".class")) { 272 processSingleClass(inZip, entry, stubOutStream, implOutStream, filter, 273 packageRedirector, enableChecker, classes, errors, stats) 274 return 275 } 276 277 // Handle other file types... 278 279 // - *.uau seems to contain hidden API information. 280 // - *_compat_config.xml is also about compat-framework. 281 if (name.endsWith(".uau") || 282 name.endsWith("_compat_config.xml")) { 283 log.d("Not needed: %s", entry.name) 284 return 285 } 286 287 // Unknown type, we just copy it to both output zip files. 288 // TODO: We probably shouldn't do it for stub jar? 289 log.v("Copying: %s", entry.name) 290 stubOutStream?.let { copyZipEntry(inZip, entry, it) } 291 implOutStream?.let { copyZipEntry(inZip, entry, it) } 292 } 293 } 294 295 /** 296 * Copy a single ZIP entry to the output. 297 */ 298 private fun copyZipEntry( 299 inZip: ZipFile, 300 entry: ZipEntry, 301 out: ZipOutputStream, 302 ) { 303 BufferedInputStream(inZip.getInputStream(entry)).use { bis -> 304 // Copy unknown entries as is to the impl out. (but not to the stub out.) 305 val outEntry = ZipEntry(entry.name) 306 out.putNextEntry(outEntry) 307 while (bis.available() > 0) { 308 out.write(bis.read()) 309 } 310 out.closeEntry() 311 } 312 } 313 314 /** 315 * Convert a single class to "stub" and "impl". 316 */ 317 private fun processSingleClass( 318 inZip: ZipFile, 319 entry: ZipEntry, 320 stubOutStream: ZipOutputStream?, 321 implOutStream: ZipOutputStream?, 322 filter: OutputFilter, 323 packageRedirector: PackageRedirectRemapper, 324 enableChecker: Boolean, 325 classes: ClassNodes, 326 errors: HostStubGenErrors, 327 stats: HostStubGenStats, 328 ) { 329 val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "") 330 val classPolicy = filter.getPolicyForClass(classInternalName) 331 if (classPolicy.policy == FilterPolicy.Remove) { 332 log.d("Removing class: %s %s", classInternalName, classPolicy) 333 return 334 } 335 // Generate stub first. 336 if (stubOutStream != null && classPolicy.policy.needsInStub) { 337 log.v("Creating stub class: %s Policy: %s", classInternalName, classPolicy) 338 log.withIndent { 339 BufferedInputStream(inZip.getInputStream(entry)).use { bis -> 340 val newEntry = ZipEntry(entry.name) 341 stubOutStream.putNextEntry(newEntry) 342 convertClass(classInternalName, /*forImpl=*/false, bis, 343 stubOutStream, filter, packageRedirector, enableChecker, classes, 344 errors, null) 345 stubOutStream.closeEntry() 346 } 347 } 348 } 349 if (implOutStream != null && classPolicy.policy.needsInImpl) { 350 log.v("Creating impl class: %s Policy: %s", classInternalName, classPolicy) 351 log.withIndent { 352 BufferedInputStream(inZip.getInputStream(entry)).use { bis -> 353 val newEntry = ZipEntry(entry.name) 354 implOutStream.putNextEntry(newEntry) 355 convertClass(classInternalName, /*forImpl=*/true, bis, 356 implOutStream, filter, packageRedirector, enableChecker, classes, 357 errors, stats) 358 implOutStream.closeEntry() 359 } 360 } 361 } 362 } 363 364 /** 365 * Convert a single class to either "stub" or "impl". 366 */ 367 private fun convertClass( 368 classInternalName: String, 369 forImpl: Boolean, 370 input: InputStream, 371 out: OutputStream, 372 filter: OutputFilter, 373 packageRedirector: PackageRedirectRemapper, 374 enableChecker: Boolean, 375 classes: ClassNodes, 376 errors: HostStubGenErrors, 377 stats: HostStubGenStats?, 378 ) { 379 val cr = ClassReader(input) 380 381 // COMPUTE_FRAMES wouldn't be happy if code uses 382 val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES 383 val cw = ClassWriter(flags) 384 385 // Connect to the class writer 386 var outVisitor: ClassVisitor = cw 387 if (enableChecker) { 388 outVisitor = CheckClassAdapter(outVisitor) 389 } 390 val visitorOptions = BaseAdapter.Options( 391 enablePreTrace = options.enablePreTrace.get, 392 enablePostTrace = options.enablePostTrace.get, 393 enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get, 394 errors = errors, 395 stats = stats, 396 ) 397 outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter, 398 packageRedirector, forImpl, visitorOptions) 399 400 cr.accept(outVisitor, ClassReader.EXPAND_FRAMES) 401 val data = cw.toByteArray() 402 out.write(data) 403 } 404 } 405