• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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