| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.base; |
| |
| import android.text.TextUtils; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.jni_zero.JniType; |
| import org.jni_zero.NativeMethods; |
| |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** |
| * Java mirror of base/command_line.h. Android applications don't have command line arguments. |
| * Instead, they're "simulated" by reading a file at a specific location early during startup. |
| * Applications each define their own files, e.g., ContentShellApplication.COMMAND_LINE_FILE. |
| */ |
| public abstract class CommandLine { |
| // Public abstract interface, implemented in derived classes. |
| // All these methods reflect their native-side counterparts. |
| /** |
| * Returns true if this command line contains the given switch. |
| * (Switch names ARE case-sensitive). |
| */ |
| public abstract boolean hasSwitch(String switchString); |
| |
| /** |
| * Return the value associated with the given switch, or null. |
| * |
| * @param switchString The switch key to lookup. It should NOT start with '--' ! |
| * @return switch value, or null if the switch is not set or set to empty. |
| */ |
| public abstract @Nullable String getSwitchValue(String switchString); |
| |
| /** |
| * Return the value associated with the given switch, or {@code defaultValue} if the switch was |
| * not specified. |
| * |
| * @param switchString The switch key to lookup. It should NOT start with '--' ! |
| * @param defaultValue The default value to return if the switch isn't set. |
| * @return Switch value, or {@code defaultValue} if the switch is not set or set to empty. |
| */ |
| public String getSwitchValue(String switchString, String defaultValue) { |
| String value = getSwitchValue(switchString); |
| return TextUtils.isEmpty(value) ? defaultValue : value; |
| } |
| |
| /** Return a copy of all switches, along with their values. */ |
| public abstract Map getSwitches(); |
| |
| /** |
| * Append a switch to the command line. There is no guarantee |
| * this action happens before the switch is needed. |
| * @param switchString the switch to add. It should NOT start with '--' ! |
| */ |
| public abstract void appendSwitch(String switchString); |
| |
| /** |
| * Append a switch and value to the command line. There is no |
| * guarantee this action happens before the switch is needed. |
| * @param switchString the switch to add. It should NOT start with '--' ! |
| * @param value the value for this switch. |
| * For example, --foo=bar becomes 'foo', 'bar'. |
| */ |
| public abstract void appendSwitchWithValue(String switchString, String value); |
| |
| /** |
| * Append switch/value items in "command line" format (excluding argv[0] program name). |
| * E.g. { '--gofast', '--username=fred' } |
| * @param array an array of switch or switch/value items in command line format. |
| * Unlike the other append routines, these switches SHOULD start with '--' . |
| * Unlike init(), this does not include the program name in array[0]. |
| */ |
| public abstract void appendSwitchesAndArguments(String[] array); |
| |
| /** |
| * Remove the switch from the command line. If no such switch is present, this has no effect. |
| * @param switchString The switch key to lookup. It should NOT start with '--' ! |
| */ |
| public abstract void removeSwitch(String switchString); |
| |
| /** |
| * Determine if the command line is bound to the native (JNI) implementation. |
| * @return true if the underlying implementation is delegating to the native command line. |
| */ |
| public boolean isNativeImplementation() { |
| return false; |
| } |
| |
| /** |
| * Returns the switches and arguments passed into the program, with switches and their |
| * values coming before all of the arguments. |
| */ |
| protected abstract String[] getCommandLineArguments(); |
| |
| /** |
| * Destroy the command line. Called when a different instance is set. |
| * @see #setInstance |
| */ |
| protected void destroy() {} |
| |
| private static final AtomicReference<CommandLine> sCommandLine = |
| new AtomicReference<CommandLine>(); |
| |
| /** |
| * @return true if the command line has already been initialized. |
| */ |
| public static boolean isInitialized() { |
| return sCommandLine.get() != null; |
| } |
| |
| // Equivalent to CommandLine::ForCurrentProcess in C++. |
| public static CommandLine getInstance() { |
| CommandLine commandLine = sCommandLine.get(); |
| assert commandLine != null; |
| return commandLine; |
| } |
| |
| /** |
| * Initialize the singleton instance, must be called exactly once (either directly or |
| * via one of the convenience wrappers below) before using the static singleton instance. |
| * @param args command line flags in 'argv' format: args[0] is the program name. |
| */ |
| public static void init(@Nullable String[] args) { |
| setInstance(new JavaCommandLine(args)); |
| } |
| |
| /** |
| * Initialize the command line from the command-line file. |
| * |
| * @param file The fully qualified command line file. |
| */ |
| public static void initFromFile(String file) { |
| char[] buffer = readFileAsUtf8(file); |
| String[] tokenized = buffer == null ? null : tokenizeQuotedArguments(buffer); |
| init(tokenized); |
| // The file existed, which should never be the case under normal operation. |
| // Use a log message to help with debugging if it's the flags that are causing issues. |
| if (tokenized != null) { |
| Log.i(TAG, "COMMAND-LINE FLAGS: %s (from %s)", Arrays.toString(tokenized), file); |
| } |
| } |
| |
| /** |
| * Resets both the java proxy and the native command lines. This allows the entire |
| * command line initialization to be re-run including the call to onJniLoaded. |
| */ |
| static void resetForTesting() { |
| setInstance(null); |
| } |
| |
| /** |
| * Parse command line flags from a flat buffer, supporting double-quote enclosed strings |
| * containing whitespace. argv elements are derived by splitting the buffer on whitepace; |
| * double quote characters may enclose tokens containing whitespace; a double-quote literal |
| * may be escaped with back-slash. (Otherwise backslash is taken as a literal). |
| * @param buffer A command line in command line file format as described above. |
| * @return the tokenized arguments, suitable for passing to init(). |
| */ |
| @VisibleForTesting |
| static String[] tokenizeQuotedArguments(char[] buffer) { |
| // Just field trials can take over 60K of command line. |
| if (buffer.length > 96 * 1024) { |
| // Check that our test runners are setting a reasonable number of flags. |
| throw new RuntimeException("Flags file too big: " + buffer.length); |
| } |
| |
| ArrayList<String> args = new ArrayList<String>(); |
| StringBuilder arg = null; |
| final char noQuote = '\0'; |
| final char singleQuote = '\''; |
| final char doubleQuote = '"'; |
| char currentQuote = noQuote; |
| for (char c : buffer) { |
| // Detect start or end of quote block. |
| if ((currentQuote == noQuote && (c == singleQuote || c == doubleQuote)) |
| || c == currentQuote) { |
| if (arg != null && arg.length() > 0 && arg.charAt(arg.length() - 1) == '\\') { |
| // Last char was a backslash; pop it, and treat c as a literal. |
| arg.setCharAt(arg.length() - 1, c); |
| } else { |
| currentQuote = currentQuote == noQuote ? c : noQuote; |
| } |
| } else if (currentQuote == noQuote && Character.isWhitespace(c)) { |
| if (arg != null) { |
| args.add(arg.toString()); |
| arg = null; |
| } |
| } else { |
| if (arg == null) arg = new StringBuilder(); |
| arg.append(c); |
| } |
| } |
| if (arg != null) { |
| if (currentQuote != noQuote) { |
| Log.w(TAG, "Unterminated quoted string: %s", arg); |
| } |
| args.add(arg.toString()); |
| } |
| return args.toArray(new String[args.size()]); |
| } |
| |
| private static final String TAG = "CommandLine"; |
| private static final String SWITCH_PREFIX = "--"; |
| private static final String SWITCH_TERMINATOR = SWITCH_PREFIX; |
| private static final String SWITCH_VALUE_SEPARATOR = "="; |
| |
| public static void enableNativeProxy() { |
| // Make a best-effort to ensure we make a clean (atomic) switch over from the old to |
| // the new command line implementation. If another thread is modifying the command line |
| // when this happens, all bets are off. (As per the native CommandLine). |
| sCommandLine.set(new NativeCommandLine(getJavaSwitches())); |
| } |
| |
| public static String[] getJavaSwitches() { |
| CommandLine commandLine = sCommandLine.get(); |
| if (commandLine != null) { |
| return commandLine.getCommandLineArguments(); |
| } |
| return new String[0]; |
| } |
| |
| private static void setInstance(CommandLine commandLine) { |
| CommandLine oldCommandLine = sCommandLine.getAndSet(commandLine); |
| if (oldCommandLine != null) { |
| oldCommandLine.destroy(); |
| } |
| } |
| |
| /** |
| * @param fileName the file to read in. |
| * @return Array of chars read from the file, or null if the file cannot be read. |
| */ |
| private static char[] readFileAsUtf8(String fileName) { |
| File f = new File(fileName); |
| try (FileReader reader = new FileReader(f)) { |
| char[] buffer = new char[(int) f.length()]; |
| int charsRead = reader.read(buffer); |
| // charsRead < f.length() in the case of multibyte characters. |
| return Arrays.copyOfRange(buffer, 0, charsRead); |
| } catch (IOException e) { |
| return null; // Most likely file not found. |
| } |
| } |
| |
| private CommandLine() {} |
| |
| @VisibleForTesting |
| static class JavaCommandLine extends CommandLine { |
| private HashMap<String, String> mSwitches = new HashMap<String, String>(); |
| private ArrayList<String> mArgs = new ArrayList<String>(); |
| |
| // The arguments begin at index 1, since index 0 contains the executable name. |
| private int mArgsBegin = 1; |
| |
| JavaCommandLine(@Nullable String[] args) { |
| if (args == null || args.length == 0 || args[0] == null) { |
| mArgs.add(""); |
| } else { |
| mArgs.add(args[0]); |
| appendSwitchesInternal(args, 1); |
| } |
| // Invariant: we always have the argv[0] program name element. |
| assert mArgs.size() > 0; |
| } |
| |
| @Override |
| protected String[] getCommandLineArguments() { |
| return mArgs.toArray(new String[mArgs.size()]); |
| } |
| |
| @Override |
| public boolean hasSwitch(String switchString) { |
| return mSwitches.containsKey(switchString); |
| } |
| |
| @Override |
| public @Nullable String getSwitchValue(String switchString) { |
| // This is slightly round about, but needed for consistency with the NativeCommandLine |
| // version which does not distinguish empty values from key not present. |
| String value = mSwitches.get(switchString); |
| return TextUtils.isEmpty(value) ? null : value; |
| } |
| |
| @Override |
| public Map<String, String> getSwitches() { |
| return new HashMap<>(mSwitches); |
| } |
| |
| @Override |
| public void appendSwitch(String switchString) { |
| appendSwitchWithValue(switchString, null); |
| } |
| |
| /** |
| * Appends a switch to the current list. |
| * @param switchString the switch to add. It should NOT start with '--' ! |
| * @param value the value for this switch. |
| */ |
| @Override |
| public void appendSwitchWithValue(String switchString, String value) { |
| mSwitches.put(switchString, value == null ? "" : value); |
| |
| // Append the switch and update the switches/arguments divider mArgsBegin. |
| String combinedSwitchString = SWITCH_PREFIX + switchString; |
| if (value != null && !value.isEmpty()) { |
| combinedSwitchString += SWITCH_VALUE_SEPARATOR + value; |
| } |
| |
| mArgs.add(mArgsBegin++, combinedSwitchString); |
| } |
| |
| @Override |
| public void appendSwitchesAndArguments(String[] array) { |
| appendSwitchesInternal(array, 0); |
| } |
| |
| // Add the specified arguments, but skipping the first |skipCount| elements. |
| private void appendSwitchesInternal(String[] array, int skipCount) { |
| boolean parseSwitches = true; |
| for (String arg : array) { |
| if (skipCount > 0) { |
| --skipCount; |
| continue; |
| } |
| |
| if (arg.equals(SWITCH_TERMINATOR)) { |
| parseSwitches = false; |
| } |
| |
| if (parseSwitches && arg.startsWith(SWITCH_PREFIX)) { |
| String[] parts = arg.split(SWITCH_VALUE_SEPARATOR, 2); |
| String value = parts.length > 1 ? parts[1] : null; |
| appendSwitchWithValue(parts[0].substring(SWITCH_PREFIX.length()), value); |
| } else { |
| mArgs.add(arg); |
| } |
| } |
| } |
| |
| @Override |
| public void removeSwitch(String switchString) { |
| mSwitches.remove(switchString); |
| String combinedSwitchString = SWITCH_PREFIX + switchString; |
| |
| // Since we permit a switch to be added multiple times, we need to remove all instances |
| // from mArgs. |
| for (int i = mArgsBegin - 1; i > 0; i--) { |
| if (mArgs.get(i).equals(combinedSwitchString) |
| || mArgs.get(i).startsWith(combinedSwitchString + SWITCH_VALUE_SEPARATOR)) { |
| --mArgsBegin; |
| mArgs.remove(i); |
| } |
| } |
| } |
| } |
| |
| private static class NativeCommandLine extends CommandLine { |
| public NativeCommandLine(@Nullable String[] args) { |
| CommandLineJni.get().init(args); |
| } |
| |
| @Override |
| public boolean hasSwitch(String switchString) { |
| return CommandLineJni.get().hasSwitch(switchString); |
| } |
| |
| @Override |
| public @Nullable String getSwitchValue(String switchString) { |
| String ret = CommandLineJni.get().getSwitchValue(switchString); |
| return ret.isEmpty() ? null : ret; |
| } |
| |
| @Override |
| public Map<String, String> getSwitches() { |
| HashMap<String, String> switches = new HashMap<String, String>(); |
| |
| // Iterate 2 array members at a time. JNI doesn't support returning Maps, but because |
| // key & value are both Strings, we can join them into a flattened String array: |
| // [ key1, value1, key2, value2, ... ] |
| String[] keysAndValues = CommandLineJni.get().getSwitchesFlattened(); |
| assert keysAndValues.length % 2 == 0 : "must have same number of keys and values"; |
| for (int i = 0; i < keysAndValues.length; i += 2) { |
| String key = keysAndValues[i]; |
| String value = keysAndValues[i + 1]; |
| switches.put(key, value); |
| } |
| return switches; |
| } |
| |
| @Override |
| public void appendSwitch(String switchString) { |
| CommandLineJni.get().appendSwitch(switchString); |
| } |
| |
| @Override |
| public void appendSwitchWithValue(String switchString, String value) { |
| CommandLineJni.get().appendSwitchWithValue(switchString, value == null ? "" : value); |
| } |
| |
| @Override |
| public void appendSwitchesAndArguments(String[] array) { |
| CommandLineJni.get().appendSwitchesAndArguments(array); |
| } |
| |
| @Override |
| public void removeSwitch(String switchString) { |
| CommandLineJni.get().removeSwitch(switchString); |
| } |
| |
| @Override |
| public boolean isNativeImplementation() { |
| return true; |
| } |
| |
| @Override |
| protected String[] getCommandLineArguments() { |
| assert false; |
| return null; |
| } |
| |
| @Override |
| protected void destroy() { |
| // TODO(crbug.com/40542965): Downgrade this to an assert once we have eliminated |
| // tests that do this. |
| throw new IllegalStateException("Can't destroy native command line after startup"); |
| } |
| } |
| |
| @NativeMethods |
| interface Natives { |
| void init(@JniType("std::vector<std::string>") String[] args); |
| |
| boolean hasSwitch(@JniType("std::string") String switchString); |
| |
| @JniType("std::string") |
| String getSwitchValue(@JniType("std::string") String switchString); |
| |
| @JniType("std::vector<std::string>") |
| String[] getSwitchesFlattened(); |
| |
| void appendSwitch(@JniType("std::string") String switchString); |
| |
| void appendSwitchWithValue( |
| @JniType("std::string") String switchString, @JniType("std::string") String value); |
| |
| void appendSwitchesAndArguments(@JniType("std::vector<std::string>") String[] array); |
| |
| void removeSwitch(@JniType("std::string") String switchString); |
| } |
| } |