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