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.hoststubgen.utils
17
18 import com.android.hoststubgen.ArgumentsException
19 import com.android.hoststubgen.InputFileNotFoundException
20 import com.android.hoststubgen.JarResourceNotFoundException
21 import com.android.hoststubgen.log
22 import com.android.hoststubgen.normalizeTextLine
23 import java.io.File
24 import java.io.FileReader
25 import java.io.InputStreamReader
26 import java.io.Reader
27
28 const val JAR_RESOURCE_PREFIX = "jar:"
29
30 /**
31 * Base class for parsing arguments from commandline.
32 */
33 abstract class BaseOptions {
34 /**
35 * Parse all arguments.
36 *
37 * This method should remain final. For customization in subclasses, override [parseOption].
38 */
39 fun parseArgs(args: List<String>) {
40 val ai = ArgIterator.withAtFiles(args)
41 while (true) {
42 val arg = ai.nextArgOptional() ?: break
43
44 if (log.maybeHandleCommandLineArg(arg) { ai.nextArgRequired(arg) }) {
45 continue
46 }
47 try {
48 if (!parseOption(arg, ai)) {
49 throw ArgumentsException("Unknown option: $arg")
50 }
51 } catch (e: SetOnce.SetMoreThanOnceException) {
52 throw ArgumentsException("Duplicate or conflicting argument found: $arg")
53 }
54 }
55
56 checkArgs()
57 }
58
59 /**
60 * Print out all fields in this class.
61 *
62 * This method should remain final. For customization in subclasses, override [dumpFields].
63 */
64 final override fun toString(): String {
65 val fields = dumpFields().prependIndent(" ")
66 return "${this::class.simpleName} {\n$fields\n}"
67 }
68
69 /**
70 * Check whether the parsed options are in a correct state.
71 *
72 * This method is called as the last step in [parseArgs].
73 */
74 open fun checkArgs() {}
75
76 /**
77 * Parse a single option. Return true if the option is accepted, otherwise return false.
78 *
79 * Subclasses override/extend this method to support more options.
80 */
81 abstract fun parseOption(option: String, args: ArgIterator): Boolean
82
83 abstract fun dumpFields(): String
84 }
85
86 class ArgIterator(
87 private val args: List<String>,
88 private var currentIndex: Int = -1
89 ) {
90 val current: String
91 get() = args[currentIndex]
92
93 /**
94 * Get the next argument, or [null] if there's no more arguments.
95 */
nextArgOptionalnull96 fun nextArgOptional(): String? {
97 if ((currentIndex + 1) >= args.size) {
98 return null
99 }
100 return args[++currentIndex]
101 }
102
103 /**
104 * Get the next argument, or throw if
105 */
nextArgRequirednull106 fun nextArgRequired(argName: String): String {
107 nextArgOptional().let {
108 if (it == null) {
109 throw ArgumentsException("Missing parameter for option $argName")
110 }
111 if (it.isEmpty()) {
112 throw ArgumentsException("Parameter can't be empty for option $argName")
113 }
114 return it
115 }
116 }
117
118 companion object {
withAtFilesnull119 fun withAtFiles(args: List<String>): ArgIterator {
120 val expanded = mutableListOf<String>()
121 expandAtFiles(args.asSequence(), expanded)
122 return ArgIterator(expanded)
123 }
124
125 /**
126 * Scan the arguments, and if any of them starts with an `@`, then load from the file
127 * and use its content as arguments.
128 *
129 * In order to pass an argument that starts with an '@', use '@@' instead.
130 *
131 * In this file, each line is treated as a single argument.
132 *
133 * The file can contain '#' as comments.
134 */
expandAtFilesnull135 private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) {
136 args.forEach { arg ->
137 if (arg.startsWith("@@")) {
138 out.add(arg.substring(1))
139 return@forEach
140 } else if (!arg.startsWith('@')) {
141 out.add(arg)
142 return@forEach
143 }
144
145 // Read from the file, and add each line to the result.
146 val file = FileOrResource(arg.substring(1))
147
148 log.v("Expanding options file ${file.path}")
149
150 val fileArgs = file
151 .open()
152 .buffered()
153 .lineSequence()
154 .map(::normalizeTextLine)
155 .filter(CharSequence::isNotEmpty)
156
157 expandAtFiles(fileArgs, out)
158 }
159 }
160 }
161 }
162
163 /**
164 * A single value that can only set once.
165 */
166 open class SetOnce<T>(private var value: T) {
167 class SetMoreThanOnceException : Exception()
168
169 private var set = false
170
setnull171 fun set(v: T): T {
172 if (set) {
173 throw SetMoreThanOnceException()
174 }
175 if (v == null) {
176 throw NullPointerException("This shouldn't happen")
177 }
178 set = true
179 value = v
180 return v
181 }
182
183 val get: T
184 get() = this.value
185
186 val isSet: Boolean
187 get() = this.set
188
189 fun <R> ifSet(block: (T & Any) -> R): R? {
190 if (isSet) {
191 return block(value!!)
192 }
193 return null
194 }
195
toStringnull196 override fun toString(): String {
197 return "$value"
198 }
199 }
200
201 class IntSetOnce(value: Int) : SetOnce<Int>(value) {
setnull202 fun set(v: String): Int {
203 try {
204 return this.set(v.toInt())
205 } catch (e: NumberFormatException) {
206 throw ArgumentsException("Invalid integer $v")
207 }
208 }
209 }
210
211 /**
212 * A path either points to a file in filesystem, or an entry in the JAR.
213 */
214 class FileOrResource(val path: String) {
215 init {
216 path.ensureFileExists()
217 }
218
219 /**
220 * Either read from filesystem, or read from JAR resources.
221 */
opennull222 fun open(): Reader {
223 return if (path.startsWith(JAR_RESOURCE_PREFIX)) {
224 val path = path.removePrefix(JAR_RESOURCE_PREFIX)
225 InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!)
226 } else {
227 FileReader(path)
228 }
229 }
230 }
231
ensureFileExistsnull232 fun String.ensureFileExists(): String {
233 if (this.startsWith(JAR_RESOURCE_PREFIX)) {
234 val cl = FileOrResource::class.java.classLoader
235 val path = this.removePrefix(JAR_RESOURCE_PREFIX)
236 if (cl.getResource(path) == null) {
237 throw JarResourceNotFoundException(path)
238 }
239 } else if (!File(this).exists()) {
240 throw InputFileNotFoundException(this)
241 }
242 return this
243 }
244