1 /*
<lambda>null2  * Copyright (C) 2019 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 
17 package com.android.protolog.tool
18 
19 import java.util.regex.Pattern
20 
21 class CommandOptions(args: Array<String>) {
22     companion object {
23         const val TRANSFORM_CALLS_CMD = "transform-protolog-calls"
24         const val GENERATE_CONFIG_CMD = "generate-viewer-config"
25         const val READ_LOG_CMD = "read-log"
26         private val commands = setOf(TRANSFORM_CALLS_CMD, GENERATE_CONFIG_CMD, READ_LOG_CMD)
27 
28         // TODO: This is always the same. I don't think it's required
29         private const val PROTOLOG_CLASS_PARAM = "--protolog-class"
30         private const val PROTOLOGGROUP_CLASS_PARAM = "--loggroups-class"
31         private const val PROTOLOGGROUP_JAR_PARAM = "--loggroups-jar"
32         private const val VIEWER_CONFIG_PARAM = "--viewer-config"
33         private const val VIEWER_CONFIG_TYPE_PARAM = "--viewer-config-type"
34         private const val OUTPUT_SOURCE_JAR_PARAM = "--output-srcjar"
35         private const val VIEWER_CONFIG_FILE_PATH_PARAM = "--viewer-config-file-path"
36         // TODO(b/324128613): Remove these legacy options once we fully flip the Perfetto protolog flag
37         private const val LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM = "--legacy-viewer-config-file-path"
38         private const val LEGACY_OUTPUT_FILE_PATH = "--legacy-output-file-path"
39         private val parameters = setOf(PROTOLOG_CLASS_PARAM, PROTOLOGGROUP_CLASS_PARAM,
40             PROTOLOGGROUP_JAR_PARAM, VIEWER_CONFIG_PARAM, VIEWER_CONFIG_TYPE_PARAM,
41             OUTPUT_SOURCE_JAR_PARAM, VIEWER_CONFIG_FILE_PATH_PARAM,
42             LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, LEGACY_OUTPUT_FILE_PATH)
43 
44         val USAGE = """
45             Usage: ${Constants.NAME} <command> [<args>]
46             Available commands:
47 
48             $TRANSFORM_CALLS_CMD $PROTOLOG_CLASS_PARAM <class name>
49                 $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
50                 $OUTPUT_SOURCE_JAR_PARAM <output.srcjar> [<input.java>]
51             - processes java files replacing stub calls with logging code.
52 
53             $GENERATE_CONFIG_CMD $PROTOLOG_CLASS_PARAM <class name>
54                 $PROTOLOGGROUP_CLASS_PARAM <class name> $PROTOLOGGROUP_JAR_PARAM <config.jar>
55                 $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> [<input.java>]
56             - creates viewer config file from given java files.
57 
58             $READ_LOG_CMD $VIEWER_CONFIG_PARAM <viewer.json|viewer.pb> <wm_log.pb>
59             - translates a binary log to a readable format.
60         """.trimIndent()
61 
62         private fun validateClassName(name: String): String {
63             if (!Pattern.matches("^([a-z]+[A-Za-z0-9]*\\.)+([A-Za-z0-9$]+)$", name)) {
64                 throw InvalidCommandException("Invalid class name $name")
65             }
66             return name
67         }
68 
69         private fun getParam(paramName: String, params: Map<String, String>): String {
70             if (!params.containsKey(paramName)) {
71                 throw InvalidCommandException("Param $paramName required")
72             }
73             return params.getValue(paramName)
74         }
75 
76         private fun getOptionalParam(paramName: String, params: Map<String, String>): String? {
77             if (!params.containsKey(paramName)) {
78                 return null
79             }
80             return params.getValue(paramName)
81         }
82 
83         private fun validateNotSpecified(paramName: String, params: Map<String, String>): String {
84             if (params.containsKey(paramName)) {
85                 throw InvalidCommandException("Unsupported param $paramName")
86             }
87             return ""
88         }
89 
90         private fun validateJarName(name: String): String {
91             if (!name.endsWith(".jar")) {
92                 throw InvalidCommandException("Jar file required, got $name instead")
93             }
94             return name
95         }
96 
97         private fun validateSrcJarName(name: String): String {
98             if (!name.endsWith(".srcjar")) {
99                 throw InvalidCommandException("Source jar file required, got $name instead")
100             }
101             return name
102         }
103 
104         private fun validateViewerConfigFilePath(name: String): String {
105             if (!name.endsWith(".pb")) {
106                 throw InvalidCommandException("Proto file (ending with .pb) required, " +
107                         "got $name instead")
108             }
109             return name
110         }
111 
112         private fun validateLegacyViewerConfigFilePath(name: String): String {
113             if (!name.endsWith(".json.gz")) {
114                 throw InvalidCommandException("GZiped Json file (ending with .json.gz) required, " +
115                         "got $name instead")
116             }
117             return name
118         }
119 
120         private fun validateOutputFilePath(name: String): String {
121             if (!name.endsWith(".winscope")) {
122                 throw InvalidCommandException("Winscope file (ending with .winscope) required, " +
123                         "got $name instead")
124             }
125             return name
126         }
127 
128         private fun validateConfigFileName(name: String): String {
129             if (!name.endsWith(".json") && !name.endsWith(".pb")) {
130                 throw InvalidCommandException("Json file (ending with .json) or proto file " +
131                         "(ending with .pb) required, got $name instead")
132             }
133             return name
134         }
135 
136         private fun validateConfigType(name: String): String {
137             val validType = listOf("json", "proto")
138             if (!validType.contains(name)) {
139                 throw InvalidCommandException("Unexpected config file type. " +
140                         "Expected on of [${validType.joinToString()}], but got $name")
141             }
142             return name
143         }
144 
145         private fun validateJavaInputList(list: List<String>): List<String> {
146             if (list.isEmpty()) {
147                 throw InvalidCommandException("No java source input files")
148             }
149             list.forEach { name ->
150                 if (!name.endsWith(".java") && !name.endsWith(".kt")) {
151                     throw InvalidCommandException("Not a java or kotlin source file $name")
152                 }
153             }
154             return list
155         }
156 
157         private fun validateLogInputList(list: List<String>): String {
158             if (list.isEmpty()) {
159                 throw InvalidCommandException("No log input file")
160             }
161             if (list.size > 1) {
162                 throw InvalidCommandException("Only one log input file allowed")
163             }
164             return list[0]
165         }
166     }
167 
168     val protoLogClassNameArg: String
169     val protoLogGroupsClassNameArg: String
170     val protoLogGroupsJarArg: String
171     val viewerConfigFileNameArg: String
172     val viewerConfigTypeArg: String
173     val outputSourceJarArg: String
174     val logProtofileArg: String
175     val viewerConfigFilePathArg: String
176     val legacyViewerConfigFilePathArg: String?
177     val legacyOutputFilePath: String?
178     val javaSourceArgs: List<String>
179     val command: String
180 
181     init {
182         if (args.isEmpty()) {
183             throw InvalidCommandException("No command specified.")
184         }
185         command = args[0]
186         if (command !in commands) {
187             throw InvalidCommandException("Unknown command.")
188         }
189 
190         val params: MutableMap<String, String> = mutableMapOf()
191         val inputFiles: MutableList<String> = mutableListOf()
192 
193         var idx = 1
194         while (idx < args.size) {
195             if (args[idx].startsWith("--")) {
196                 if (idx + 1 >= args.size) {
197                     throw InvalidCommandException("No value for ${args[idx]}")
198                 }
199                 if (args[idx] !in parameters) {
200                     throw InvalidCommandException("Unknown parameter ${args[idx]}")
201                 }
202                 if (args[idx + 1].startsWith("--")) {
203                     throw InvalidCommandException("No value for ${args[idx]}")
204                 }
205                 if (params.containsKey(args[idx])) {
206                     throw InvalidCommandException("Duplicated parameter ${args[idx]}")
207                 }
208                 params[args[idx]] = args[idx + 1]
209                 idx += 2
210             } else {
211                 inputFiles.add(args[idx])
212                 idx += 1
213             }
214         }
215 
216         when (command) {
217             TRANSFORM_CALLS_CMD -> {
218                 protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
219                 protoLogGroupsClassNameArg =
220                     validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
221                 protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
222                 viewerConfigFileNameArg = validateNotSpecified(VIEWER_CONFIG_PARAM, params)
223                 viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
224                 outputSourceJarArg = validateSrcJarName(getParam(OUTPUT_SOURCE_JAR_PARAM, params))
225                 viewerConfigFilePathArg = validateViewerConfigFilePath(
226                     getParam(VIEWER_CONFIG_FILE_PATH_PARAM, params))
227                 legacyViewerConfigFilePathArg =
228                     getOptionalParam(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)?.let {
229                         validateLegacyViewerConfigFilePath(it)
230                     }
231                 legacyOutputFilePath =
232                     getOptionalParam(LEGACY_OUTPUT_FILE_PATH, params)?.let {
233                         validateOutputFilePath(it)
234                     }
235                 javaSourceArgs = validateJavaInputList(inputFiles)
236                 logProtofileArg = ""
237             }
238             GENERATE_CONFIG_CMD -> {
239                 protoLogClassNameArg = validateClassName(getParam(PROTOLOG_CLASS_PARAM, params))
240                 protoLogGroupsClassNameArg =
241                     validateClassName(getParam(PROTOLOGGROUP_CLASS_PARAM, params))
242                 protoLogGroupsJarArg = validateJarName(getParam(PROTOLOGGROUP_JAR_PARAM, params))
243                 viewerConfigFileNameArg =
244                     validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
245                 viewerConfigTypeArg = validateConfigType(getParam(VIEWER_CONFIG_TYPE_PARAM, params))
246                 outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
247                 viewerConfigFilePathArg =
248                     validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
249                 legacyViewerConfigFilePathArg =
250                     validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
251                 legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
252                 javaSourceArgs = validateJavaInputList(inputFiles)
253                 logProtofileArg = ""
254             }
255             READ_LOG_CMD -> {
256                 protoLogClassNameArg = validateNotSpecified(PROTOLOG_CLASS_PARAM, params)
257                 protoLogGroupsClassNameArg = validateNotSpecified(PROTOLOGGROUP_CLASS_PARAM, params)
258                 protoLogGroupsJarArg = validateNotSpecified(PROTOLOGGROUP_JAR_PARAM, params)
259                 viewerConfigFileNameArg =
260                     validateConfigFileName(getParam(VIEWER_CONFIG_PARAM, params))
261                 viewerConfigTypeArg = validateNotSpecified(VIEWER_CONFIG_TYPE_PARAM, params)
262                 outputSourceJarArg = validateNotSpecified(OUTPUT_SOURCE_JAR_PARAM, params)
263                 viewerConfigFilePathArg =
264                     validateNotSpecified(VIEWER_CONFIG_FILE_PATH_PARAM, params)
265                 legacyViewerConfigFilePathArg =
266                     validateNotSpecified(LEGACY_VIEWER_CONFIG_FILE_PATH_PARAM, params)
267                 legacyOutputFilePath = validateNotSpecified(LEGACY_OUTPUT_FILE_PATH, params)
268                 javaSourceArgs = listOf()
269                 logProtofileArg = validateLogInputList(inputFiles)
270             }
271             else -> {
272                 throw InvalidCommandException("Unknown command.")
273             }
274         }
275     }
276 }
277