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