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