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