1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import android.support.annotation.Nullable;
8 import android.text.TextUtils;
9 import android.util.Log;
10 
11 import org.chromium.base.annotations.MainDex;
12 
13 import java.io.File;
14 import java.io.FileReader;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.HashMap;
19 import java.util.concurrent.atomic.AtomicReference;
20 
21 /**
22  * Java mirror of base/command_line.h.
23  * Android applications don't have command line arguments. Instead, they're "simulated" by reading a
24  * file at a specific location early during startup. Applications each define their own files, e.g.,
25  * ContentShellApplication.COMMAND_LINE_FILE.
26 **/
27 @MainDex
28 public abstract class CommandLine {
29     // Public abstract interface, implemented in derived classes.
30     // All these methods reflect their native-side counterparts.
31     /**
32      *  Returns true if this command line contains the given switch.
33      *  (Switch names ARE case-sensitive).
34      */
35     @VisibleForTesting
hasSwitch(String switchString)36     public abstract boolean hasSwitch(String switchString);
37 
38     /**
39      * Return the value associated with the given switch, or null.
40      * @param switchString The switch key to lookup. It should NOT start with '--' !
41      * @return switch value, or null if the switch is not set or set to empty.
42      */
getSwitchValue(String switchString)43     public abstract String getSwitchValue(String switchString);
44 
45     /**
46      * Return the value associated with the given switch, or {@code defaultValue} if the switch
47      * was not specified.
48      * @param switchString The switch key to lookup. It should NOT start with '--' !
49      * @param defaultValue The default value to return if the switch isn't set.
50      * @return Switch value, or {@code defaultValue} if the switch is not set or set to empty.
51      */
getSwitchValue(String switchString, String defaultValue)52     public String getSwitchValue(String switchString, String defaultValue) {
53         String value = getSwitchValue(switchString);
54         return TextUtils.isEmpty(value) ? defaultValue : value;
55     }
56 
57     /**
58      * Append a switch to the command line.  There is no guarantee
59      * this action happens before the switch is needed.
60      * @param switchString the switch to add.  It should NOT start with '--' !
61      */
62     @VisibleForTesting
appendSwitch(String switchString)63     public abstract void appendSwitch(String switchString);
64 
65     /**
66      * Append a switch and value to the command line.  There is no
67      * guarantee this action happens before the switch is needed.
68      * @param switchString the switch to add.  It should NOT start with '--' !
69      * @param value the value for this switch.
70      * For example, --foo=bar becomes 'foo', 'bar'.
71      */
appendSwitchWithValue(String switchString, String value)72     public abstract void appendSwitchWithValue(String switchString, String value);
73 
74     /**
75      * Append switch/value items in "command line" format (excluding argv[0] program name).
76      * E.g. { '--gofast', '--username=fred' }
77      * @param array an array of switch or switch/value items in command line format.
78      *   Unlike the other append routines, these switches SHOULD start with '--' .
79      *   Unlike init(), this does not include the program name in array[0].
80      */
appendSwitchesAndArguments(String[] array)81     public abstract void appendSwitchesAndArguments(String[] array);
82 
83     /**
84      * Determine if the command line is bound to the native (JNI) implementation.
85      * @return true if the underlying implementation is delegating to the native command line.
86      */
isNativeImplementation()87     public boolean isNativeImplementation() {
88         return false;
89     }
90 
91     /**
92      * Returns the switches and arguments passed into the program, with switches and their
93      * values coming before all of the arguments.
94      */
getCommandLineArguments()95     protected abstract String[] getCommandLineArguments();
96 
97     /**
98      * Destroy the command line. Called when a different instance is set.
99      * @see #setInstance
100      */
destroy()101     protected void destroy() {}
102 
103     private static final AtomicReference<CommandLine> sCommandLine =
104             new AtomicReference<CommandLine>();
105 
106     /**
107      * @return true if the command line has already been initialized.
108      */
isInitialized()109     public static boolean isInitialized() {
110         return sCommandLine.get() != null;
111     }
112 
113     // Equivalent to CommandLine::ForCurrentProcess in C++.
114     @VisibleForTesting
getInstance()115     public static CommandLine getInstance() {
116         CommandLine commandLine = sCommandLine.get();
117         assert commandLine != null;
118         return commandLine;
119     }
120 
121     /**
122      * Initialize the singleton instance, must be called exactly once (either directly or
123      * via one of the convenience wrappers below) before using the static singleton instance.
124      * @param args command line flags in 'argv' format: args[0] is the program name.
125      */
init(@ullable String[] args)126     public static void init(@Nullable String[] args) {
127         setInstance(new JavaCommandLine(args));
128     }
129 
130     /**
131      * Initialize the command line from the command-line file.
132      *
133      * @param file The fully qualified command line file.
134      */
initFromFile(String file)135     public static void initFromFile(String file) {
136         char[] buffer = readFileAsUtf8(file);
137         init(buffer == null ? null : tokenizeQuotedArguments(buffer));
138     }
139 
140     /**
141      * Resets both the java proxy and the native command lines. This allows the entire
142      * command line initialization to be re-run including the call to onJniLoaded.
143      */
144     @VisibleForTesting
reset()145     public static void reset() {
146         setInstance(null);
147     }
148 
149     /**
150      * Parse command line flags from a flat buffer, supporting double-quote enclosed strings
151      * containing whitespace. argv elements are derived by splitting the buffer on whitepace;
152      * double quote characters may enclose tokens containing whitespace; a double-quote literal
153      * may be escaped with back-slash. (Otherwise backslash is taken as a literal).
154      * @param buffer A command line in command line file format as described above.
155      * @return the tokenized arguments, suitable for passing to init().
156      */
157     @VisibleForTesting
tokenizeQuotedArguments(char[] buffer)158     static String[] tokenizeQuotedArguments(char[] buffer) {
159         // Just field trials can take up to 10K of command line.
160         if (buffer.length > 64 * 1024) {
161             // Check that our test runners are setting a reasonable number of flags.
162             throw new RuntimeException("Flags file too big: " + buffer.length);
163         }
164 
165         ArrayList<String> args = new ArrayList<String>();
166         StringBuilder arg = null;
167         final char noQuote = '\0';
168         final char singleQuote = '\'';
169         final char doubleQuote = '"';
170         char currentQuote = noQuote;
171         for (char c : buffer) {
172             // Detect start or end of quote block.
173             if ((currentQuote == noQuote && (c == singleQuote || c == doubleQuote))
174                     || c == currentQuote) {
175                 if (arg != null && arg.length() > 0 && arg.charAt(arg.length() - 1) == '\\') {
176                     // Last char was a backslash; pop it, and treat c as a literal.
177                     arg.setCharAt(arg.length() - 1, c);
178                 } else {
179                     currentQuote = currentQuote == noQuote ? c : noQuote;
180                 }
181             } else if (currentQuote == noQuote && Character.isWhitespace(c)) {
182                 if (arg != null) {
183                     args.add(arg.toString());
184                     arg = null;
185                 }
186             } else {
187                 if (arg == null) arg = new StringBuilder();
188                 arg.append(c);
189             }
190         }
191         if (arg != null) {
192             if (currentQuote != noQuote) {
193                 Log.w(TAG, "Unterminated quoted string: " + arg);
194             }
195             args.add(arg.toString());
196         }
197         return args.toArray(new String[args.size()]);
198     }
199 
200     private static final String TAG = "CommandLine";
201     private static final String SWITCH_PREFIX = "--";
202     private static final String SWITCH_TERMINATOR = SWITCH_PREFIX;
203     private static final String SWITCH_VALUE_SEPARATOR = "=";
204 
enableNativeProxy()205     public static void enableNativeProxy() {
206         // Make a best-effort to ensure we make a clean (atomic) switch over from the old to
207         // the new command line implementation. If another thread is modifying the command line
208         // when this happens, all bets are off. (As per the native CommandLine).
209         sCommandLine.set(new NativeCommandLine(getJavaSwitchesOrNull()));
210     }
211 
212     @Nullable
getJavaSwitchesOrNull()213     public static String[] getJavaSwitchesOrNull() {
214         CommandLine commandLine = sCommandLine.get();
215         if (commandLine != null) {
216             return commandLine.getCommandLineArguments();
217         }
218         return null;
219     }
220 
setInstance(CommandLine commandLine)221     private static void setInstance(CommandLine commandLine) {
222         CommandLine oldCommandLine = sCommandLine.getAndSet(commandLine);
223         if (oldCommandLine != null) {
224             oldCommandLine.destroy();
225         }
226     }
227 
228     /**
229      * @param fileName the file to read in.
230      * @return Array of chars read from the file, or null if the file cannot be read.
231      */
readFileAsUtf8(String fileName)232     private static char[] readFileAsUtf8(String fileName) {
233         File f = new File(fileName);
234         try (FileReader reader = new FileReader(f)) {
235             char[] buffer = new char[(int) f.length()];
236             int charsRead = reader.read(buffer);
237             // charsRead < f.length() in the case of multibyte characters.
238             return Arrays.copyOfRange(buffer, 0, charsRead);
239         } catch (IOException e) {
240             return null; // Most likely file not found.
241         }
242     }
243 
CommandLine()244     private CommandLine() {}
245 
246     private static class JavaCommandLine extends CommandLine {
247         private HashMap<String, String> mSwitches = new HashMap<String, String>();
248         private ArrayList<String> mArgs = new ArrayList<String>();
249 
250         // The arguments begin at index 1, since index 0 contains the executable name.
251         private int mArgsBegin = 1;
252 
JavaCommandLine(@ullable String[] args)253         JavaCommandLine(@Nullable String[] args) {
254             if (args == null || args.length == 0 || args[0] == null) {
255                 mArgs.add("");
256             } else {
257                 mArgs.add(args[0]);
258                 appendSwitchesInternal(args, 1);
259             }
260             // Invariant: we always have the argv[0] program name element.
261             assert mArgs.size() > 0;
262         }
263 
264         @Override
getCommandLineArguments()265         protected String[] getCommandLineArguments() {
266             return mArgs.toArray(new String[mArgs.size()]);
267         }
268 
269         @Override
hasSwitch(String switchString)270         public boolean hasSwitch(String switchString) {
271             return mSwitches.containsKey(switchString);
272         }
273 
274         @Override
getSwitchValue(String switchString)275         public String getSwitchValue(String switchString) {
276             // This is slightly round about, but needed for consistency with the NativeCommandLine
277             // version which does not distinguish empty values from key not present.
278             String value = mSwitches.get(switchString);
279             return value == null || value.isEmpty() ? null : value;
280         }
281 
282         @Override
appendSwitch(String switchString)283         public void appendSwitch(String switchString) {
284             appendSwitchWithValue(switchString, null);
285         }
286 
287         /**
288          * Appends a switch to the current list.
289          * @param switchString the switch to add.  It should NOT start with '--' !
290          * @param value the value for this switch.
291          */
292         @Override
appendSwitchWithValue(String switchString, String value)293         public void appendSwitchWithValue(String switchString, String value) {
294             mSwitches.put(switchString, value == null ? "" : value);
295 
296             // Append the switch and update the switches/arguments divider mArgsBegin.
297             String combinedSwitchString = SWITCH_PREFIX + switchString;
298             if (value != null && !value.isEmpty()) {
299                 combinedSwitchString += SWITCH_VALUE_SEPARATOR + value;
300             }
301 
302             mArgs.add(mArgsBegin++, combinedSwitchString);
303         }
304 
305         @Override
appendSwitchesAndArguments(String[] array)306         public void appendSwitchesAndArguments(String[] array) {
307             appendSwitchesInternal(array, 0);
308         }
309 
310         // Add the specified arguments, but skipping the first |skipCount| elements.
appendSwitchesInternal(String[] array, int skipCount)311         private void appendSwitchesInternal(String[] array, int skipCount) {
312             boolean parseSwitches = true;
313             for (String arg : array) {
314                 if (skipCount > 0) {
315                     --skipCount;
316                     continue;
317                 }
318 
319                 if (arg.equals(SWITCH_TERMINATOR)) {
320                     parseSwitches = false;
321                 }
322 
323                 if (parseSwitches && arg.startsWith(SWITCH_PREFIX)) {
324                     String[] parts = arg.split(SWITCH_VALUE_SEPARATOR, 2);
325                     String value = parts.length > 1 ? parts[1] : null;
326                     appendSwitchWithValue(parts[0].substring(SWITCH_PREFIX.length()), value);
327                 } else {
328                     mArgs.add(arg);
329                 }
330             }
331         }
332     }
333 
334     private static class NativeCommandLine extends CommandLine {
NativeCommandLine(@ullable String[] args)335         public NativeCommandLine(@Nullable String[] args) {
336             nativeInit(args);
337         }
338 
339         @Override
hasSwitch(String switchString)340         public boolean hasSwitch(String switchString) {
341             return nativeHasSwitch(switchString);
342         }
343 
344         @Override
getSwitchValue(String switchString)345         public String getSwitchValue(String switchString) {
346             return nativeGetSwitchValue(switchString);
347         }
348 
349         @Override
appendSwitch(String switchString)350         public void appendSwitch(String switchString) {
351             nativeAppendSwitch(switchString);
352         }
353 
354         @Override
appendSwitchWithValue(String switchString, String value)355         public void appendSwitchWithValue(String switchString, String value) {
356             nativeAppendSwitchWithValue(switchString, value);
357         }
358 
359         @Override
appendSwitchesAndArguments(String[] array)360         public void appendSwitchesAndArguments(String[] array) {
361             nativeAppendSwitchesAndArguments(array);
362         }
363 
364         @Override
isNativeImplementation()365         public boolean isNativeImplementation() {
366             return true;
367         }
368 
369         @Override
getCommandLineArguments()370         protected String[] getCommandLineArguments() {
371             assert false;
372             return null;
373         }
374 
375         @Override
destroy()376         protected void destroy() {
377             // TODO(https://crbug.com/771205): Downgrade this to an assert once we have eliminated
378             // tests that do this.
379             throw new IllegalStateException("Can't destroy native command line after startup");
380         }
381     }
382 
nativeInit(String[] args)383     private static native void nativeInit(String[] args);
nativeHasSwitch(String switchString)384     private static native boolean nativeHasSwitch(String switchString);
nativeGetSwitchValue(String switchString)385     private static native String nativeGetSwitchValue(String switchString);
nativeAppendSwitch(String switchString)386     private static native void nativeAppendSwitch(String switchString);
nativeAppendSwitchWithValue(String switchString, String value)387     private static native void nativeAppendSwitchWithValue(String switchString, String value);
nativeAppendSwitchesAndArguments(String[] array)388     private static native void nativeAppendSwitchesAndArguments(String[] array);
389 }
390