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