1 /*
2  * Copyright (C) 2016 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.apksigner;
18 
19 import java.util.Arrays;
20 
21 /**
22  * Parser of command-line options/switches/flags.
23  *
24  * <p>Supported option formats:
25  * <ul>
26  * <li>{@code --name value}</li>
27  * <li>{@code --name=value}</li>
28  * <li>{@code -name value}</li>
29  * <li>{@code --name} (boolean options only)</li>
30  * </ul>
31  *
32  * <p>To use the parser, create an instance, providing it with the command-line parameters, then
33  * iterate over options by invoking {@link #nextOption()} until it returns {@code null}.
34  */
35 class OptionsParser {
36     private final String[] mParams;
37     private int mIndex;
38     private String mLastOptionValue;
39     private String mLastOptionOriginalForm;
40 
41     /**
42      * Constructs a new {@code OptionsParser} initialized with the provided command-line.
43      */
OptionsParser(String[] params)44     public OptionsParser(String[] params) {
45         mParams = params.clone();
46     }
47 
48     /**
49      * Returns the name (without leading dashes) of the next option (starting with the very first
50      * option) or {@code null} if there are no options left.
51      *
52      * <p>The value of this option can be obtained via {@link #getRequiredValue(String)},
53      * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}.
54      */
nextOption()55     public String nextOption() {
56         if (mIndex >= mParams.length) {
57             // No more parameters left
58             return null;
59         }
60         String param = mParams[mIndex];
61         if (!param.startsWith("-")) {
62             // Not an option
63             return null;
64         }
65 
66         mIndex++;
67         mLastOptionOriginalForm = param;
68         mLastOptionValue = null;
69         if (param.startsWith("--")) {
70             // FORMAT: --name value OR --name=value
71             if ("--".equals(param)) {
72                 // End of options marker
73                 return null;
74             }
75             int valueDelimiterIndex = param.indexOf('=');
76             if (valueDelimiterIndex != -1) {
77                 mLastOptionValue = param.substring(valueDelimiterIndex + 1);
78                 mLastOptionOriginalForm = param.substring(0, valueDelimiterIndex);
79                 return param.substring("--".length(), valueDelimiterIndex);
80             } else {
81                 return param.substring("--".length());
82             }
83         } else {
84             // FORMAT: -name value
85             return param.substring("-".length());
86         }
87     }
88 
89     /**
90      * Returns the original form of the current option. The original form includes the leading dash
91      * or dashes. This is intended to be used for referencing the option in error messages.
92      */
getOptionOriginalForm()93     public String getOptionOriginalForm() {
94         return mLastOptionOriginalForm;
95     }
96 
97     /**
98      * Returns the value of the current option, throwing an exception if the value is missing.
99      */
getRequiredValue(String valueDescription)100     public String getRequiredValue(String valueDescription) throws OptionsException {
101         if (mLastOptionValue != null) {
102             String result = mLastOptionValue;
103             mLastOptionValue = null;
104             return result;
105         }
106         if (mIndex >= mParams.length) {
107             // No more parameters left
108             throw new OptionsException(
109                     valueDescription + " missing after " + mLastOptionOriginalForm);
110         }
111         String param = mParams[mIndex];
112         if ("--".equals(param)) {
113             // End of options marker
114             throw new OptionsException(
115                     valueDescription + " missing after " + mLastOptionOriginalForm);
116         }
117         mIndex++;
118         return param;
119     }
120 
121     /**
122      * Returns the value of the current numeric option, throwing an exception if the value is
123      * missing or is not numeric.
124      */
getRequiredIntValue(String valueDescription)125     public int getRequiredIntValue(String valueDescription) throws OptionsException {
126         String value = getRequiredValue(valueDescription);
127         try {
128             return Integer.parseInt(value);
129         } catch (NumberFormatException e) {
130             throw new OptionsException(
131                     valueDescription + " (" + mLastOptionOriginalForm
132                             + ") must be a decimal number: " + value);
133         }
134     }
135 
136     /**
137      * Gets the value of the current boolean option. Boolean options are not required to have
138      * explicitly specified values.
139      */
getOptionalBooleanValue(boolean defaultValue)140     public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException {
141         if (mLastOptionValue != null) {
142             // --option=value form
143             String stringValue = mLastOptionValue;
144             mLastOptionValue = null;
145             if ("true".equals(stringValue)) {
146                 return true;
147             } else if ("false".equals(stringValue)) {
148                 return false;
149             }
150             throw new OptionsException(
151                     "Unsupported value for " + mLastOptionOriginalForm + ": " + stringValue
152                             + ". Only true or false supported.");
153         }
154 
155         // --option (true|false) form OR just --option
156         if (mIndex >= mParams.length) {
157             return defaultValue;
158         }
159 
160         String stringValue = mParams[mIndex];
161         if ("true".equals(stringValue)) {
162             mIndex++;
163             return true;
164         } else if ("false".equals(stringValue)) {
165             mIndex++;
166             return false;
167         } else {
168             return defaultValue;
169         }
170     }
171 
172     /**
173      * Returns the remaining command-line parameters. This is intended to be invoked once
174      * {@link #nextOption()} returns {@code null}.
175      */
getRemainingParams()176     public String[] getRemainingParams() {
177         if (mIndex >= mParams.length) {
178             return new String[0];
179         }
180         String param = mParams[mIndex];
181         if ("--".equals(param)) {
182             // Skip end of options marker
183             return Arrays.copyOfRange(mParams, mIndex + 1, mParams.length);
184         } else {
185             return Arrays.copyOfRange(mParams, mIndex, mParams.length);
186         }
187     }
188 
189     /**
190      * Indicates that an error was encountered while parsing command-line options.
191      */
192     public static class OptionsException extends Exception {
193         private static final long serialVersionUID = 1L;
194 
OptionsException(String message)195         public OptionsException(String message) {
196             super(message);
197         }
198     }
199 }
200