/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! Argument string parsing and matching functions for Firefox. //! //! Which arguments Firefox accepts and in what style depends on the platform. //! On Windows only, arguments can be prefixed with `/` (slash), such as //! `/screenshot`. Elsewhere, including Windows, arguments may be prefixed //! with both single (`-screenshot`) and double (`--screenshot`) dashes. //! //! An argument's name is determined by a space or an assignment operator (`=`) //! so that for the string `-foo=bar`, `foo` is considered the argument's //! basename. use crate::runner::platform; use std::ffi::{OsStr, OsString}; use std::fmt; /// Parse an argument string into a name and value /// /// Given an argument like `"--arg=value"` this will split it into /// `(Some("arg"), Some("value")). For a case like `"--arg"` it will /// return `(Some("arg"), None)` and where the input doesn't look like /// an argument e.g. `"value"` it will return `(None, Some("value"))` fn parse_arg_name_value(arg: T) -> (Option, Option) where T: AsRef, { let arg_os_str: &OsStr = arg.as_ref(); let arg_str = arg_os_str.to_string_lossy(); let mut name_start = 0; let mut name_end = 0; // Look for an argument name at the start of the // string for (i, c) in arg_str.chars().enumerate() { if i == 0 { if !platform::arg_prefix_char(c) { break; } } else if i == 1 { if name_end_char(c) { break; } else if c != '-' { name_start = i; name_end = name_start + 1; } else { name_start = i + 1; name_end = name_start; } } else { name_end += 1; if name_end_char(c) { name_end -= 1; break; } } } let name = if name_start > 0 && name_end > name_start { Some(arg_str[name_start..name_end].into()) } else { None }; // If there are characters in the string after the argument, read // them as the value, excluding the seperator (e.g. "=") if // present. let mut value_start = name_end; let value_end = arg_str.len(); let value = if value_start < value_end { if let Some(c) = arg_str[value_start..value_end].chars().next() { if name_end_char(c) { value_start += 1; } } Some(arg_str[value_start..value_end].into()) } else { None }; (name, value) } fn name_end_char(c: char) -> bool { c == ' ' || c == '=' } /// Represents a Firefox command-line argument. #[derive(Debug, PartialEq)] pub enum Arg { /// `-foreground` ensures application window gets focus, which is not the /// default on macOS. As such Firefox only supports it on MacOS. Foreground, /// --marionette enables Marionette in the application which is used /// by WebDriver HTTP. Marionette, /// `-no-remote` prevents remote commands to this instance of Firefox, and /// ensure we always start a new instance. NoRemote, /// `-P NAME` starts Firefox with a profile with a given name. NamedProfile, /// `-profile PATH` starts Firefox with the profile at the specified path. Profile, /// `-ProfileManager` starts Firefox with the profile chooser dialogue. ProfileManager, /// All other arguments. Other(String), /// --remote-allow-hosts contains comma-separated values of the Host header /// to allow for incoming WebSocket requests of the Remote Agent. RemoteAllowHosts, /// --remote-allow-origins contains comma-separated values of the Origin header /// to allow for incoming WebSocket requests of the Remote Agent. RemoteAllowOrigins, /// --remote-debugging-port enables the Remote Agent in the application /// which is used for the WebDriver BiDi and CDP remote debugging protocols. RemoteDebuggingPort, /// Not an argument. None, } impl Arg { pub fn new(name: &str) -> Arg { match name { "foreground" => Arg::Foreground, "marionette" => Arg::Marionette, "no-remote" => Arg::NoRemote, "profile" => Arg::Profile, "P" => Arg::NamedProfile, "ProfileManager" => Arg::ProfileManager, "remote-allow-hosts" => Arg::RemoteAllowHosts, "remote-allow-origins" => Arg::RemoteAllowOrigins, "remote-debugging-port" => Arg::RemoteDebuggingPort, _ => Arg::Other(name.into()), } } } impl<'a> From<&'a OsString> for Arg { fn from(arg_str: &OsString) -> Arg { if let (Some(name), _) = parse_arg_name_value(arg_str) { Arg::new(&name) } else { Arg::None } } } impl fmt::Display for Arg { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&match self { Arg::Foreground => "--foreground".to_string(), Arg::Marionette => "--marionette".to_string(), Arg::NamedProfile => "-P".to_string(), Arg::None => "".to_string(), Arg::NoRemote => "--no-remote".to_string(), Arg::Other(x) => format!("--{}", x), Arg::Profile => "--profile".to_string(), Arg::ProfileManager => "--ProfileManager".to_string(), Arg::RemoteAllowHosts => "--remote-allow-hosts".to_string(), Arg::RemoteAllowOrigins => "--remote-allow-origins".to_string(), Arg::RemoteDebuggingPort => "--remote-debugging-port".to_string(), }) } } /// Parse an iterator over arguments into an vector of (name, value) /// tuples /// /// Each entry in the input argument will produce a single item in the /// output. Because we don't know anything about the specific /// arguments, something that doesn't parse as a named argument may /// either be the value of a previous named argument, or may be a /// positional argument. pub fn parse_args<'a>( args: impl Iterator, ) -> Vec<(Option, Option)> { args.map(parse_arg_name_value) .map(|(name, value)| { if let Some(arg_name) = name { (Some(Arg::new(&arg_name)), value) } else { (None, value) } }) .collect() } /// Given an iterator over all arguments, get the value of an argument /// /// This assumes that the argument takes a single value and that is /// either provided as a single argument entry /// (e.g. `["--name=value"]`) or as the following argument /// (e.g. `["--name", "value"]) pub fn get_arg_value<'a>( mut parsed_args: impl Iterator, Option)>, arg: Arg, ) -> Option { let mut found_value = None; for (arg_name, arg_value) in &mut parsed_args { if let (Some(name), value) = (arg_name, arg_value) { if *name == arg { found_value = value.clone(); break; } } } if found_value.is_none() { // If there wasn't a value, check if the following argument is a value if let Some((None, value)) = parsed_args.next() { found_value = value.clone(); } } found_value } #[cfg(test)] mod tests { use super::{get_arg_value, parse_arg_name_value, parse_args, Arg}; use std::ffi::OsString; fn parse(arg: &str, name: Option<&str>) { let (result, _) = parse_arg_name_value(arg); assert_eq!(result, name.map(|x| x.to_string())); } #[test] fn test_parse_arg_name_value() { parse("-p", Some("p")); parse("--p", Some("p")); parse("--profile foo", Some("profile")); parse("--profile", Some("profile")); parse("--", None); parse("", None); parse("-=", None); parse("--=", None); parse("-- foo", None); parse("foo", None); parse("/ foo", None); parse("/- foo", None); parse("/=foo", None); parse("foo", None); parse("-profile", Some("profile")); parse("-profile=foo", Some("profile")); parse("-profile = foo", Some("profile")); parse("-profile abc", Some("profile")); parse("-profile /foo", Some("profile")); } #[cfg(target_os = "windows")] #[test] fn test_parse_arg_name_value_windows() { parse("/profile", Some("profile")); } #[cfg(not(target_os = "windows"))] #[test] fn test_parse_arg_name_value_non_windows() { parse("/profile", None); } #[test] fn test_arg_from_osstring() { assert_eq!(Arg::from(&OsString::from("--foreground")), Arg::Foreground); assert_eq!(Arg::from(&OsString::from("-foreground")), Arg::Foreground); assert_eq!(Arg::from(&OsString::from("--marionette")), Arg::Marionette); assert_eq!(Arg::from(&OsString::from("-marionette")), Arg::Marionette); assert_eq!(Arg::from(&OsString::from("--no-remote")), Arg::NoRemote); assert_eq!(Arg::from(&OsString::from("-no-remote")), Arg::NoRemote); assert_eq!(Arg::from(&OsString::from("-- profile")), Arg::None); assert_eq!(Arg::from(&OsString::from("profile")), Arg::None); assert_eq!(Arg::from(&OsString::from("profile -P")), Arg::None); assert_eq!( Arg::from(&OsString::from("-profiled")), Arg::Other("profiled".into()) ); assert_eq!( Arg::from(&OsString::from("-PROFILEMANAGER")), Arg::Other("PROFILEMANAGER".into()) ); assert_eq!(Arg::from(&OsString::from("--profile")), Arg::Profile); assert_eq!(Arg::from(&OsString::from("-profile foo")), Arg::Profile); assert_eq!( Arg::from(&OsString::from("--ProfileManager")), Arg::ProfileManager ); assert_eq!( Arg::from(&OsString::from("-ProfileManager")), Arg::ProfileManager ); // TODO: -Ptest is valid //assert_eq!(Arg::from(&OsString::from("-Ptest")), Arg::NamedProfile); assert_eq!(Arg::from(&OsString::from("-P")), Arg::NamedProfile); assert_eq!(Arg::from(&OsString::from("-P test")), Arg::NamedProfile); assert_eq!( Arg::from(&OsString::from("--remote-debugging-port")), Arg::RemoteDebuggingPort ); assert_eq!( Arg::from(&OsString::from("-remote-debugging-port")), Arg::RemoteDebuggingPort ); assert_eq!( Arg::from(&OsString::from("--remote-debugging-port 9222")), Arg::RemoteDebuggingPort ); assert_eq!( Arg::from(&OsString::from("--remote-allow-hosts")), Arg::RemoteAllowHosts ); assert_eq!( Arg::from(&OsString::from("-remote-allow-hosts")), Arg::RemoteAllowHosts ); assert_eq!( Arg::from(&OsString::from("--remote-allow-hosts 9222")), Arg::RemoteAllowHosts ); assert_eq!( Arg::from(&OsString::from("--remote-allow-origins")), Arg::RemoteAllowOrigins ); assert_eq!( Arg::from(&OsString::from("-remote-allow-origins")), Arg::RemoteAllowOrigins ); assert_eq!( Arg::from(&OsString::from("--remote-allow-origins http://foo")), Arg::RemoteAllowOrigins ); } #[test] fn test_get_arg_value() { let args = vec!["-P", "ProfileName", "--profile=/path/", "--no-remote"] .iter() .map(|x| OsString::from(x)) .collect::>(); let parsed_args = parse_args(args.iter()); assert_eq!( get_arg_value(parsed_args.iter(), Arg::NamedProfile), Some("ProfileName".into()) ); assert_eq!( get_arg_value(parsed_args.iter(), Arg::Profile), Some("/path/".into()) ); assert_eq!(get_arg_value(parsed_args.iter(), Arg::NoRemote), None); let args = vec!["--profile=", "-P test"] .iter() .map(|x| OsString::from(x)) .collect::>(); let parsed_args = parse_args(args.iter()); assert_eq!( get_arg_value(parsed_args.iter(), Arg::NamedProfile), Some("test".into()) ); assert_eq!( get_arg_value(parsed_args.iter(), Arg::Profile), Some("".into()) ); } }