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