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.filters.FilterPolicy
19 import java.io.BufferedReader
20 import java.io.File
21 import java.io.FileReader
22 
23 /**
24  * A single value that can only set once.
25  */
26 class SetOnce<T>(
27         private var value: T,
28 ) {
29     class SetMoreThanOnceException : Exception()
30 
31     private var set = false
32 
33     fun set(v: T) {
34         if (set) {
35             throw SetMoreThanOnceException()
36         }
37         if (v == null) {
38             throw NullPointerException("This shouldn't happen")
39         }
40         set = true
41         value = v
42     }
43 
44     val get: T
45         get() = this.value
46 
47     val isSet: Boolean
48         get() = this.set
49 
50     fun <R> ifSet(block: (T & Any) -> R): R? {
51         if (isSet) {
52             return block(value!!)
53         }
54         return null
55     }
56 
57     override fun toString(): String {
58         return "$value"
59     }
60 }
61 
62 /**
63  * Options that can be set from command line arguments.
64  */
65 class HostStubGenOptions(
66         /** Input jar file*/
67         var inJar: SetOnce<String> = SetOnce(""),
68 
69         /** Output stub jar file */
70         var outStubJar: SetOnce<String?> = SetOnce(null),
71 
72         /** Output implementation jar file */
73         var outImplJar: SetOnce<String?> = SetOnce(null),
74 
75         var inputJarDumpFile: SetOnce<String?> = SetOnce(null),
76 
77         var inputJarAsKeepAllFile: SetOnce<String?> = SetOnce(null),
78 
79         var stubAnnotations: MutableSet<String> = mutableSetOf(),
80         var keepAnnotations: MutableSet<String> = mutableSetOf(),
81         var throwAnnotations: MutableSet<String> = mutableSetOf(),
82         var removeAnnotations: MutableSet<String> = mutableSetOf(),
83         var stubClassAnnotations: MutableSet<String> = mutableSetOf(),
84         var keepClassAnnotations: MutableSet<String> = mutableSetOf(),
85 
86         var substituteAnnotations: MutableSet<String> = mutableSetOf(),
87         var nativeSubstituteAnnotations: MutableSet<String> = mutableSetOf(),
88         var classLoadHookAnnotations: MutableSet<String> = mutableSetOf(),
89         var keepStaticInitializerAnnotations: MutableSet<String> = mutableSetOf(),
90 
91         var packageRedirects: MutableList<Pair<String, String>> = mutableListOf(),
92 
93         var annotationAllowedClassesFile: SetOnce<String?> = SetOnce(null),
94 
95         var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
96         var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
97 
98         var intersectStubJars: MutableSet<String> = mutableSetOf(),
99 
100         var policyOverrideFile: SetOnce<String?> = SetOnce(null),
101 
102         var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
103 
104         var cleanUpOnError: SetOnce<Boolean> = SetOnce(false),
105 
106         var enableClassChecker: SetOnce<Boolean> = SetOnce(false),
107         var enablePreTrace: SetOnce<Boolean> = SetOnce(false),
108         var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
109 
110         var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false),
111 
112         var statsFile: SetOnce<String?> = SetOnce(null),
113 
114         var apiListFile: SetOnce<String?> = SetOnce(null),
115 ) {
116     companion object {
117 
ensureFileExistsnull118         private fun String.ensureFileExists(): String {
119             if (!File(this).exists()) {
120                 throw InputFileNotFoundException(this)
121             }
122             return this
123         }
124 
parsePackageRedirectnull125         private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
126             val colon = fromColonTo.indexOf(':')
127             if ((colon < 1) || (colon + 1 >= fromColonTo.length)) {
128                 throw ArgumentsException("--package-redirect must be a colon-separated string")
129             }
130             // TODO check for duplicates
131             return Pair(fromColonTo.substring(0, colon), fromColonTo.substring(colon + 1))
132         }
133 
parseArgsnull134         fun parseArgs(args: Array<String>): HostStubGenOptions {
135             val ret = HostStubGenOptions()
136 
137             val ai = ArgIterator(expandAtFiles(args))
138 
139             var allAnnotations = mutableSetOf<String>()
140 
141             fun ensureUniqueAnnotation(name: String): String {
142                 if (!allAnnotations.add(name)) {
143                     throw DuplicateAnnotationException(ai.current)
144                 }
145                 return name
146             }
147 
148             fun setLogFile(level: LogLevel, filename: String) {
149                 log.addFilePrinter(level, filename)
150                 log.i("$level log file: $filename")
151             }
152 
153             while (true) {
154                 val arg = ai.nextArgOptional()
155                 if (arg == null) {
156                     break
157                 }
158 
159                 // Define some shorthands...
160                 fun nextArg(): String = ai.nextArgRequired(arg)
161                 fun SetOnce<String>.setNextStringArg(): String = nextArg().also { this.set(it) }
162                 fun SetOnce<String?>.setNextStringArg(): String = nextArg().also { this.set(it) }
163                 fun MutableSet<String>.addUniqueAnnotationArg(): String =
164                         nextArg().also { this += ensureUniqueAnnotation(it) }
165 
166                 try {
167                     when (arg) {
168                         // TODO: Write help
169                         "-h", "--help" -> TODO("Help is not implemented yet")
170 
171                         "-v", "--verbose" -> log.setConsoleLogLevel(LogLevel.Verbose)
172                         "-d", "--debug" -> log.setConsoleLogLevel(LogLevel.Debug)
173                         "-q", "--quiet" -> log.setConsoleLogLevel(LogLevel.None)
174 
175                         "--in-jar" -> ret.inJar.setNextStringArg().ensureFileExists()
176                         "--out-stub-jar" -> ret.outStubJar.setNextStringArg()
177                         "--out-impl-jar" -> ret.outImplJar.setNextStringArg()
178 
179                         "--policy-override-file" ->
180                             ret.policyOverrideFile.setNextStringArg().ensureFileExists()
181 
182                         "--clean-up-on-error" -> ret.cleanUpOnError.set(true)
183                         "--no-clean-up-on-error" -> ret.cleanUpOnError.set(false)
184 
185                         "--default-remove" -> ret.defaultPolicy.set(FilterPolicy.Remove)
186                         "--default-throw" -> ret.defaultPolicy.set(FilterPolicy.Throw)
187                         "--default-keep" -> ret.defaultPolicy.set(FilterPolicy.Keep)
188                         "--default-stub" -> ret.defaultPolicy.set(FilterPolicy.Stub)
189 
190                         "--stub-annotation" ->
191                             ret.stubAnnotations.addUniqueAnnotationArg()
192 
193                         "--keep-annotation" ->
194                             ret.keepAnnotations.addUniqueAnnotationArg()
195 
196                         "--stub-class-annotation" ->
197                             ret.stubClassAnnotations.addUniqueAnnotationArg()
198 
199                         "--keep-class-annotation" ->
200                             ret.keepClassAnnotations.addUniqueAnnotationArg()
201 
202                         "--throw-annotation" ->
203                             ret.throwAnnotations.addUniqueAnnotationArg()
204 
205                         "--remove-annotation" ->
206                             ret.removeAnnotations.addUniqueAnnotationArg()
207 
208                         "--substitute-annotation" ->
209                             ret.substituteAnnotations.addUniqueAnnotationArg()
210 
211                         "--native-substitute-annotation" ->
212                             ret.nativeSubstituteAnnotations.addUniqueAnnotationArg()
213 
214                         "--class-load-hook-annotation" ->
215                             ret.classLoadHookAnnotations.addUniqueAnnotationArg()
216 
217                         "--keep-static-initializer-annotation" ->
218                             ret.keepStaticInitializerAnnotations.addUniqueAnnotationArg()
219 
220                         "--package-redirect" ->
221                             ret.packageRedirects += parsePackageRedirect(nextArg())
222 
223                         "--annotation-allowed-classes-file" ->
224                             ret.annotationAllowedClassesFile.setNextStringArg()
225 
226                         "--default-class-load-hook" ->
227                             ret.defaultClassLoadHook.setNextStringArg()
228 
229                         "--default-method-call-hook" ->
230                             ret.defaultMethodCallHook.setNextStringArg()
231 
232                         "--intersect-stub-jar" ->
233                             ret.intersectStubJars += nextArg().ensureFileExists()
234 
235                         "--gen-keep-all-file" ->
236                             ret.inputJarAsKeepAllFile.setNextStringArg()
237 
238                         // Following options are for debugging.
239                         "--enable-class-checker" -> ret.enableClassChecker.set(true)
240                         "--no-class-checker" -> ret.enableClassChecker.set(false)
241 
242                         "--enable-pre-trace" -> ret.enablePreTrace.set(true)
243                         "--no-pre-trace" -> ret.enablePreTrace.set(false)
244 
245                         "--enable-post-trace" -> ret.enablePostTrace.set(true)
246                         "--no-post-trace" -> ret.enablePostTrace.set(false)
247 
248                         "--enable-non-stub-method-check" ->
249                             ret.enableNonStubMethodCallDetection.set(true)
250 
251                         "--no-non-stub-method-check" ->
252                             ret.enableNonStubMethodCallDetection.set(false)
253 
254                         "--gen-input-dump-file" -> ret.inputJarDumpFile.setNextStringArg()
255 
256                         "--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
257                         "--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
258 
259                         "--stats-file" -> ret.statsFile.setNextStringArg()
260                         "--supported-api-list-file" -> ret.apiListFile.setNextStringArg()
261 
262                         else -> throw ArgumentsException("Unknown option: $arg")
263                     }
264                 } catch (e: SetOnce.SetMoreThanOnceException) {
265                     throw ArgumentsException("Duplicate or conflicting argument found: $arg")
266                 }
267             }
268 
269             if (!ret.inJar.isSet) {
270                 throw ArgumentsException("Required option missing: --in-jar")
271             }
272             if (!ret.outStubJar.isSet && !ret.outImplJar.isSet) {
273                 log.w("Neither --out-stub-jar nor --out-impl-jar is set." +
274                         " $executableName will not generate jar files.")
275             }
276 
277             if (ret.enableNonStubMethodCallDetection.get) {
278                 log.w("--enable-non-stub-method-check is not fully implemented yet." +
279                     " See the todo in doesMethodNeedNonStubCallCheck().")
280             }
281 
282             return ret
283         }
284 
285         /**
286          * Scan the arguments, and if any of them starts with an `@`, then load from the file
287          * and use its content as arguments.
288          *
289          * In this file, each line is treated as a single argument.
290          *
291          * The file can contain '#' as comments.
292          */
expandAtFilesnull293         private fun expandAtFiles(args: Array<String>): List<String> {
294             val ret = mutableListOf<String>()
295 
296             args.forEach { arg ->
297                 if (!arg.startsWith('@')) {
298                     ret += arg
299                     return@forEach
300                 }
301                 // Read from the file, and add each line to the result.
302                 val filename = arg.substring(1).ensureFileExists()
303 
304                 log.v("Expanding options file $filename")
305 
306                 BufferedReader(FileReader(filename)).use { reader ->
307                     while (true) {
308                         var line = reader.readLine()
309                         if (line == null) {
310                             break // EOF
311                         }
312 
313                         line = normalizeTextLine(line)
314                         if (line.isNotEmpty()) {
315                             ret += line
316                         }
317                     }
318                 }
319             }
320             return ret
321         }
322     }
323 
324     open class ArgumentsException(message: String?) : Exception(message), UserErrorException
325 
326     /** Thrown when the same annotation is used with different annotation arguments. */
327     class DuplicateAnnotationException(annotationName: String?) :
328             ArgumentsException("Duplicate annotation specified: '$annotationName'")
329 
330     /** Thrown when an input file does not exist. */
331     class InputFileNotFoundException(filename: String) :
332             ArgumentsException("File '$filename' not found")
333 
334     private class ArgIterator(
335             private val args: List<String>,
336             private var currentIndex: Int = -1
337     ) {
338         val current: String
339             get() = args.get(currentIndex)
340 
341         /**
342          * Get the next argument, or [null] if there's no more arguments.
343          */
nextArgOptionalnull344         fun nextArgOptional(): String? {
345             if ((currentIndex + 1) >= args.size) {
346                 return null
347             }
348             return args.get(++currentIndex)
349         }
350 
351         /**
352          * Get the next argument, or throw if
353          */
nextArgRequirednull354         fun nextArgRequired(argName: String): String {
355             nextArgOptional().let {
356                 if (it == null) {
357                     throw ArgumentsException("Missing parameter for option $argName")
358                 }
359                 if (it.isEmpty()) {
360                     throw ArgumentsException("Parameter can't be empty for option $argName")
361                 }
362                 return it
363             }
364         }
365     }
366 
toStringnull367     override fun toString(): String {
368         return """
369             HostStubGenOptions{
370               inJar='$inJar',
371               outStubJar='$outStubJar',
372               outImplJar='$outImplJar',
373               inputJarDumpFile=$inputJarDumpFile,
374               inputJarAsKeepAllFile=$inputJarAsKeepAllFile,
375               stubAnnotations=$stubAnnotations,
376               keepAnnotations=$keepAnnotations,
377               throwAnnotations=$throwAnnotations,
378               removeAnnotations=$removeAnnotations,
379               stubClassAnnotations=$stubClassAnnotations,
380               keepClassAnnotations=$keepClassAnnotations,
381               substituteAnnotations=$substituteAnnotations,
382               nativeSubstituteAnnotations=$nativeSubstituteAnnotations,
383               classLoadHookAnnotations=$classLoadHookAnnotations,
384               keepStaticInitializerAnnotations=$keepStaticInitializerAnnotations,
385               packageRedirects=$packageRedirects,
386               $annotationAllowedClassesFile=$annotationAllowedClassesFile,
387               defaultClassLoadHook=$defaultClassLoadHook,
388               defaultMethodCallHook=$defaultMethodCallHook,
389               intersectStubJars=$intersectStubJars,
390               policyOverrideFile=$policyOverrideFile,
391               defaultPolicy=$defaultPolicy,
392               cleanUpOnError=$cleanUpOnError,
393               enableClassChecker=$enableClassChecker,
394               enablePreTrace=$enablePreTrace,
395               enablePostTrace=$enablePostTrace,
396               enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection,
397               statsFile=$statsFile,
398               apiListFile=$apiListFile,
399             }
400             """.trimIndent()
401     }
402 }
403