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