tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

capabilities.rs (52375B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 use crate::command::LogOptions;
      6 use crate::logging::Level;
      7 use crate::marionette::MarionetteSettings;
      8 use base64::prelude::BASE64_STANDARD;
      9 use base64::Engine;
     10 use mozdevice::AndroidStorageInput;
     11 use mozprofile::preferences::Pref;
     12 use mozprofile::profile::Profile;
     13 use mozrunner::firefox_args::{get_arg_value, parse_args, Arg};
     14 use mozrunner::runner::platform::firefox_default_path;
     15 use mozversion::{firefox_binary_version, firefox_version, Version};
     16 use regex::bytes::Regex;
     17 use serde_json::{Map, Value};
     18 use std::collections::BTreeMap;
     19 use std::default::Default;
     20 use std::ffi::OsString;
     21 use std::fs;
     22 use std::io;
     23 use std::io::BufWriter;
     24 use std::io::Cursor;
     25 use std::path::{Path, PathBuf};
     26 use std::str::{self, FromStr};
     27 use thiserror::Error;
     28 use webdriver::capabilities::{BrowserCapabilities, Capabilities};
     29 use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
     30 
     31 #[derive(Clone, Debug, Error)]
     32 enum VersionError {
     33    #[error(transparent)]
     34    VersionError(#[from] mozversion::Error),
     35    #[error("No binary provided")]
     36    MissingBinary,
     37 }
     38 
     39 impl From<VersionError> for WebDriverError {
     40    fn from(err: VersionError) -> WebDriverError {
     41        WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
     42    }
     43 }
     44 
     45 /// Provides matching of `moz:firefoxOptions` and resolutionnized  of which Firefox
     46 /// binary to use.
     47 ///
     48 /// `FirefoxCapabilities` is constructed with the fallback binary, should
     49 /// `moz:firefoxOptions` not contain a binary entry.  This may either be the
     50 /// system Firefox installation or an override, for example given to the
     51 /// `--binary` flag of geckodriver.
     52 pub struct FirefoxCapabilities<'a> {
     53    pub chosen_binary: Option<PathBuf>,
     54    fallback_binary: Option<&'a PathBuf>,
     55    version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>,
     56 }
     57 
     58 impl<'a> FirefoxCapabilities<'a> {
     59    pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
     60        FirefoxCapabilities {
     61            chosen_binary: None,
     62            fallback_binary,
     63            version_cache: BTreeMap::new(),
     64        }
     65    }
     66 
     67    fn set_binary(&mut self, capabilities: &Map<String, Value>) {
     68        self.chosen_binary = capabilities
     69            .get("moz:firefoxOptions")
     70            .and_then(|x| x.get("binary"))
     71            .and_then(|x| x.as_str())
     72            .map(PathBuf::from)
     73            .or_else(|| self.fallback_binary.cloned())
     74            .or_else(firefox_default_path);
     75    }
     76 
     77    fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> {
     78        if let Some(binary) = binary {
     79            if let Some(cache_value) = self.version_cache.get(binary) {
     80                return cache_value.clone();
     81            }
     82            let rv = self
     83                .version_from_ini(binary)
     84                .or_else(|_| self.version_from_binary(binary));
     85            if let Ok(ref version) = rv {
     86                debug!("Found version {}", version);
     87            } else {
     88                debug!("Failed to get binary version");
     89            }
     90            self.version_cache.insert(binary.to_path_buf(), rv.clone());
     91            rv
     92        } else {
     93            Err(VersionError::MissingBinary)
     94        }
     95    }
     96 
     97    fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> {
     98        debug!("Trying to read firefox version from ini files");
     99        let version = firefox_version(binary)?;
    100        if let Some(version_string) = version.version_string {
    101            Version::from_str(&version_string).map_err(|err| err.into())
    102        } else {
    103            Err(VersionError::VersionError(
    104                mozversion::Error::MetadataError("Missing version string".into()),
    105            ))
    106        }
    107    }
    108 
    109    fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> {
    110        debug!("Trying to read firefox version from binary");
    111        Ok(firefox_binary_version(binary)?)
    112    }
    113 }
    114 
    115 impl BrowserCapabilities for FirefoxCapabilities<'_> {
    116    fn init(&mut self, capabilities: &Capabilities) {
    117        self.set_binary(capabilities);
    118    }
    119 
    120    fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
    121        Ok(Some("firefox".into()))
    122    }
    123 
    124    fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
    125        let binary = self.chosen_binary.clone();
    126        self.version(binary.as_ref().map(|x| x.as_ref()))
    127            .map_err(|err| err.into())
    128            .map(|x| Some(x.to_string()))
    129    }
    130 
    131    fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
    132        Ok(if cfg!(target_os = "windows") {
    133            Some("windows".into())
    134        } else if cfg!(target_os = "macos") {
    135            Some("mac".into())
    136        } else if cfg!(target_os = "linux") {
    137            Some("linux".into())
    138        } else {
    139            None
    140        })
    141    }
    142 
    143    fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    144        Ok(true)
    145    }
    146 
    147    fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> {
    148        Ok(true)
    149    }
    150 
    151    fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    152        Ok(true)
    153    }
    154 
    155    fn compare_browser_version(
    156        &mut self,
    157        version: &str,
    158        comparison: &str,
    159    ) -> WebDriverResult<bool> {
    160        Version::from_str(version)
    161            .map_err(VersionError::from)?
    162            .matches(comparison)
    163            .map_err(|err| VersionError::from(err).into())
    164    }
    165 
    166    fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    167        Ok(true)
    168    }
    169 
    170    fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    171        Ok(true)
    172    }
    173 
    174    fn webauthn_virtual_authenticators(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    175        Ok(true)
    176    }
    177 
    178    fn webauthn_extension_uvm(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    179        Ok(false)
    180    }
    181 
    182    fn webauthn_extension_prf(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    183        Ok(false)
    184    }
    185 
    186    fn webauthn_extension_large_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    187        Ok(false)
    188    }
    189 
    190    fn webauthn_extension_cred_blob(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
    191        Ok(false)
    192    }
    193 
    194    fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> {
    195        if !name.starts_with("moz:") {
    196            return Ok(());
    197        }
    198        match name {
    199            "moz:firefoxOptions" => {
    200                let data = try_opt!(
    201                    value.as_object(),
    202                    ErrorStatus::InvalidArgument,
    203                    "moz:firefoxOptions is not an object"
    204                );
    205                for (key, value) in data.iter() {
    206                    match &**key {
    207                        "androidActivity"
    208                        | "androidDeviceSerial"
    209                        | "androidPackage"
    210                        | "profile" => {
    211                            if !value.is_string() {
    212                                return Err(WebDriverError::new(
    213                                    ErrorStatus::InvalidArgument,
    214                                    format!("{} is not a string", &**key),
    215                                ));
    216                            }
    217                        }
    218                        "androidIntentArguments" | "args" => {
    219                            if !try_opt!(
    220                                value.as_array(),
    221                                ErrorStatus::InvalidArgument,
    222                                format!("{} is not an array", &**key)
    223                            )
    224                            .iter()
    225                            .all(|value| value.is_string())
    226                            {
    227                                return Err(WebDriverError::new(
    228                                    ErrorStatus::InvalidArgument,
    229                                    format!("{} entry is not a string", &**key),
    230                                ));
    231                            }
    232                        }
    233                        "binary" => {
    234                            if let Some(binary) = value.as_str() {
    235                                if !data.contains_key("androidPackage")
    236                                    && self.version(Some(Path::new(binary))).is_err()
    237                                {
    238                                    return Err(WebDriverError::new(
    239                                        ErrorStatus::InvalidArgument,
    240                                        format!("{} is not a Firefox executable", &**key),
    241                                    ));
    242                                }
    243                            } else {
    244                                return Err(WebDriverError::new(
    245                                    ErrorStatus::InvalidArgument,
    246                                    format!("{} is not a string", &**key),
    247                                ));
    248                            }
    249                        }
    250                        "env" => {
    251                            let env_data = try_opt!(
    252                                value.as_object(),
    253                                ErrorStatus::InvalidArgument,
    254                                "env value is not an object"
    255                            );
    256                            if !env_data.values().all(Value::is_string) {
    257                                return Err(WebDriverError::new(
    258                                    ErrorStatus::InvalidArgument,
    259                                    "Environment values were not all strings",
    260                                ));
    261                            }
    262                        }
    263                        "log" => {
    264                            let log_data = try_opt!(
    265                                value.as_object(),
    266                                ErrorStatus::InvalidArgument,
    267                                "log value is not an object"
    268                            );
    269                            for (log_key, log_value) in log_data.iter() {
    270                                match &**log_key {
    271                                    "level" => {
    272                                        let level = try_opt!(
    273                                            log_value.as_str(),
    274                                            ErrorStatus::InvalidArgument,
    275                                            "log level is not a string"
    276                                        );
    277                                        if Level::from_str(level).is_err() {
    278                                            return Err(WebDriverError::new(
    279                                                ErrorStatus::InvalidArgument,
    280                                                format!("Not a valid log level: {}", level),
    281                                            ));
    282                                        }
    283                                    }
    284                                    x => {
    285                                        return Err(WebDriverError::new(
    286                                            ErrorStatus::InvalidArgument,
    287                                            format!("Invalid log field {}", x),
    288                                        ))
    289                                    }
    290                                }
    291                            }
    292                        }
    293                        "prefs" => {
    294                            let prefs_data = try_opt!(
    295                                value.as_object(),
    296                                ErrorStatus::InvalidArgument,
    297                                "prefs value is not an object"
    298                            );
    299                            let is_pref_value_type = |x: &Value| {
    300                                x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean()
    301                            };
    302                            if !prefs_data.values().all(is_pref_value_type) {
    303                                return Err(WebDriverError::new(
    304                                    ErrorStatus::InvalidArgument,
    305                                    "Preference values not all string or integer or boolean",
    306                                ));
    307                            }
    308                        }
    309                        x => {
    310                            return Err(WebDriverError::new(
    311                                ErrorStatus::InvalidArgument,
    312                                format!("Invalid moz:firefoxOptions field {}", x),
    313                            ))
    314                        }
    315                    }
    316                }
    317            }
    318            "moz:webdriverClick" => {
    319                if !value.is_boolean() {
    320                    return Err(WebDriverError::new(
    321                        ErrorStatus::InvalidArgument,
    322                        "moz:webdriverClick is not a boolean",
    323                    ));
    324                }
    325            }
    326            // Bug 1967916: Remove when Firefox 140 is no longer supported
    327            "moz:debuggerAddress" => {
    328                if !value.is_boolean() {
    329                    return Err(WebDriverError::new(
    330                        ErrorStatus::InvalidArgument,
    331                        "moz:debuggerAddress is not a boolean",
    332                    ));
    333                }
    334            }
    335            _ => {
    336                return Err(WebDriverError::new(
    337                    ErrorStatus::InvalidArgument,
    338                    format!("Unrecognised option {}", name),
    339                ))
    340            }
    341        }
    342        Ok(())
    343    }
    344 
    345    fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> {
    346        Ok(true)
    347    }
    348 }
    349 
    350 /// Android-specific options in the `moz:firefoxOptions` struct.
    351 /// These map to "androidCamelCase", following [chromedriver's Android-specific
    352 /// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android).
    353 #[derive(Default, Clone, Debug, PartialEq)]
    354 pub struct AndroidOptions {
    355    pub activity: Option<String>,
    356    pub device_serial: Option<String>,
    357    pub intent_arguments: Option<Vec<String>>,
    358    pub package: String,
    359    pub storage: AndroidStorageInput,
    360 }
    361 
    362 impl AndroidOptions {
    363    pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions {
    364        AndroidOptions {
    365            package,
    366            storage,
    367            ..Default::default()
    368        }
    369    }
    370 }
    371 
    372 #[derive(Debug, Default, PartialEq)]
    373 pub enum ProfileType {
    374    Path(Profile),
    375    Named,
    376    #[default]
    377    Temporary,
    378 }
    379 
    380 /// Rust representation of `moz:firefoxOptions`.
    381 ///
    382 /// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
    383 /// the encoded profile, the binary arguments, log settings, and additional
    384 /// preferences to be checked and unmarshaled from the `moz:firefoxOptions`
    385 /// JSON Object into a Rust representation.
    386 #[derive(Default, Debug)]
    387 pub struct FirefoxOptions {
    388    pub binary: Option<PathBuf>,
    389    pub profile: ProfileType,
    390    pub args: Option<Vec<String>>,
    391    pub env: Option<Vec<(String, String)>>,
    392    pub log: LogOptions,
    393    pub prefs: Vec<(String, Pref)>,
    394    pub android: Option<AndroidOptions>,
    395    pub use_websocket: bool,
    396 }
    397 
    398 impl FirefoxOptions {
    399    pub fn new() -> FirefoxOptions {
    400        Default::default()
    401    }
    402 
    403    pub(crate) fn from_capabilities(
    404        binary_path: Option<PathBuf>,
    405        settings: &MarionetteSettings,
    406        matched: &mut Capabilities,
    407    ) -> WebDriverResult<FirefoxOptions> {
    408        let mut rv = FirefoxOptions::new();
    409        rv.binary = binary_path;
    410 
    411        if let Some(json) = matched.remove("moz:firefoxOptions") {
    412            let options = json.as_object().ok_or_else(|| {
    413                WebDriverError::new(
    414                    ErrorStatus::InvalidArgument,
    415                    "'moz:firefoxOptions' \
    416                 capability is not an object",
    417                )
    418            })?;
    419 
    420            if options.get("androidPackage").is_some() && options.get("binary").is_some() {
    421                return Err(WebDriverError::new(
    422                    ErrorStatus::InvalidArgument,
    423                    "androidPackage and binary are mutual exclusive",
    424                ));
    425            }
    426 
    427            rv.android = FirefoxOptions::load_android(settings.android_storage, options)?;
    428            rv.args = FirefoxOptions::load_args(options)?;
    429            rv.env = FirefoxOptions::load_env(options)?;
    430            rv.log = FirefoxOptions::load_log(options)?;
    431            rv.prefs = FirefoxOptions::load_prefs(options)?;
    432            if let Some(profile) =
    433                FirefoxOptions::load_profile(settings.profile_root.as_deref(), options)?
    434            {
    435                rv.profile = ProfileType::Path(profile);
    436            }
    437        }
    438 
    439        if let Some(args) = rv.args.as_ref() {
    440            let os_args = parse_args(args.iter().map(OsString::from).collect::<Vec<_>>().iter());
    441 
    442            if let Some(path) = get_arg_value(os_args.iter(), Arg::Profile) {
    443                if let ProfileType::Path(_) = rv.profile {
    444                    return Err(WebDriverError::new(
    445                        ErrorStatus::InvalidArgument,
    446                        "Can't provide both a --profile argument and a profile",
    447                    ));
    448                }
    449                let path_buf = PathBuf::from(path);
    450                rv.profile = ProfileType::Path(Profile::new_from_path(&path_buf)?);
    451            }
    452 
    453            if get_arg_value(os_args.iter(), Arg::NamedProfile).is_some() {
    454                if let ProfileType::Path(_) = rv.profile {
    455                    return Err(WebDriverError::new(
    456                        ErrorStatus::InvalidArgument,
    457                        "Can't provide both a -P argument and a profile",
    458                    ));
    459                }
    460                // See bug 1757720
    461                warn!("Firefox was configured to use a named profile (`-P <name>`). \
    462                       Support for named profiles will be removed in a future geckodriver release. \
    463                       Please instead use the `--profile <path>` Firefox argument to start with an existing profile");
    464                rv.profile = ProfileType::Named;
    465            }
    466 
    467            // Block these Firefox command line arguments that should not be settable
    468            // via session capabilities.
    469            if let Some(arg) = os_args
    470                .iter()
    471                .filter_map(|(opt_arg, _)| opt_arg.as_ref())
    472                .find(|arg| {
    473                    matches!(
    474                        arg,
    475                        Arg::Marionette
    476                            | Arg::RemoteAllowHosts
    477                            | Arg::RemoteAllowOrigins
    478                            | Arg::RemoteDebuggingPort
    479                    )
    480                })
    481            {
    482                return Err(WebDriverError::new(
    483                    ErrorStatus::InvalidArgument,
    484                    format!("Argument {} can't be set via capabilities", arg),
    485                ));
    486            };
    487        }
    488 
    489        let has_web_socket_url = matched
    490            .get("webSocketUrl")
    491            .and_then(|x| x.as_bool())
    492            .unwrap_or(false);
    493 
    494        let has_debugger_address = matched
    495            .remove("moz:debuggerAddress")
    496            .and_then(|x| x.as_bool())
    497            .unwrap_or(false);
    498 
    499        // Set a command line provided port for the Remote Agent for now.
    500        // It needs to be the same on the host and the Android device.
    501        if has_web_socket_url || has_debugger_address {
    502            rv.use_websocket = true;
    503 
    504            // Bug 1722863: Setting of command line arguments would be
    505            // better suited in the individual Browser implementations.
    506            let mut remote_args = Vec::new();
    507            remote_args.push("--remote-debugging-port".to_owned());
    508            remote_args.push(settings.websocket_port.to_string());
    509 
    510            // Handle additional hosts for WebDriver BiDi WebSocket connections
    511            if !settings.allow_hosts.is_empty() {
    512                remote_args.push("--remote-allow-hosts".to_owned());
    513                remote_args.push(
    514                    settings
    515                        .allow_hosts
    516                        .iter()
    517                        .map(|host| host.to_string())
    518                        .collect::<Vec<String>>()
    519                        .join(","),
    520                );
    521            }
    522 
    523            // Handle additional origins for WebDriver BiDi WebSocket connections
    524            if !settings.allow_origins.is_empty() {
    525                remote_args.push("--remote-allow-origins".to_owned());
    526                remote_args.push(
    527                    settings
    528                        .allow_origins
    529                        .iter()
    530                        .map(|origin| origin.to_string())
    531                        .collect::<Vec<String>>()
    532                        .join(","),
    533                );
    534            }
    535 
    536            if let Some(ref mut args) = rv.args {
    537                args.append(&mut remote_args);
    538            } else {
    539                rv.args = Some(remote_args);
    540            }
    541        }
    542 
    543        Ok(rv)
    544    }
    545 
    546    fn load_profile(
    547        profile_root: Option<&Path>,
    548        options: &Capabilities,
    549    ) -> WebDriverResult<Option<Profile>> {
    550        if let Some(profile_json) = options.get("profile") {
    551            let profile_base64 = profile_json.as_str().ok_or_else(|| {
    552                WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
    553            })?;
    554            let profile_zip = &*BASE64_STANDARD.decode(profile_base64)?;
    555 
    556            // Create an emtpy profile directory
    557            let profile = Profile::new(profile_root)?;
    558            unzip_buffer(
    559                profile_zip,
    560                profile
    561                    .temp_dir
    562                    .as_ref()
    563                    .expect("Profile doesn't have a path")
    564                    .path(),
    565            )?;
    566 
    567            Ok(Some(profile))
    568        } else {
    569            Ok(None)
    570        }
    571    }
    572 
    573    fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> {
    574        if let Some(args_json) = options.get("args") {
    575            let args_array = args_json.as_array().ok_or_else(|| {
    576                WebDriverError::new(ErrorStatus::InvalidArgument, "Arguments were not an array")
    577            })?;
    578            let args = args_array
    579                .iter()
    580                .map(|x| x.as_str().map(|x| x.to_owned()))
    581                .collect::<Option<Vec<String>>>()
    582                .ok_or_else(|| {
    583                    WebDriverError::new(
    584                        ErrorStatus::InvalidArgument,
    585                        "Arguments entries were not all strings",
    586                    )
    587                })?;
    588 
    589            Ok(Some(args))
    590        } else {
    591            Ok(None)
    592        }
    593    }
    594 
    595    pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> {
    596        if let Some(env_data) = options.get("env") {
    597            let env = env_data.as_object().ok_or_else(|| {
    598                WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object")
    599            })?;
    600            let mut rv = Vec::with_capacity(env.len());
    601            for (key, value) in env.iter() {
    602                rv.push((
    603                    key.clone(),
    604                    value
    605                        .as_str()
    606                        .ok_or_else(|| {
    607                            WebDriverError::new(
    608                                ErrorStatus::InvalidArgument,
    609                                "Env value is not a string",
    610                            )
    611                        })?
    612                        .to_string(),
    613                ));
    614            }
    615            Ok(Some(rv))
    616        } else {
    617            Ok(None)
    618        }
    619    }
    620 
    621    fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> {
    622        if let Some(json) = options.get("log") {
    623            let log = json.as_object().ok_or_else(|| {
    624                WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object")
    625            })?;
    626 
    627            let level = match log.get("level") {
    628                Some(json) => {
    629                    let s = json.as_str().ok_or_else(|| {
    630                        WebDriverError::new(
    631                            ErrorStatus::InvalidArgument,
    632                            "Log level is not a string",
    633                        )
    634                    })?;
    635                    Some(Level::from_str(s).ok().ok_or_else(|| {
    636                        WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown")
    637                    })?)
    638                }
    639                None => None,
    640            };
    641 
    642            Ok(LogOptions { level })
    643        } else {
    644            Ok(Default::default())
    645        }
    646    }
    647 
    648    pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> {
    649        if let Some(prefs_data) = options.get("prefs") {
    650            let prefs = prefs_data.as_object().ok_or_else(|| {
    651                WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object")
    652            })?;
    653            let mut rv = Vec::with_capacity(prefs.len());
    654            for (key, value) in prefs.iter() {
    655                rv.push((key.clone(), pref_from_json(value)?));
    656            }
    657            Ok(rv)
    658        } else {
    659            Ok(vec![])
    660        }
    661    }
    662 
    663    pub fn load_android(
    664        storage: AndroidStorageInput,
    665        options: &Capabilities,
    666    ) -> WebDriverResult<Option<AndroidOptions>> {
    667        if let Some(package_json) = options.get("androidPackage") {
    668            let package = package_json
    669                .as_str()
    670                .ok_or_else(|| {
    671                    WebDriverError::new(
    672                        ErrorStatus::InvalidArgument,
    673                        "androidPackage is not a string",
    674                    )
    675                })?
    676                .to_owned();
    677 
    678            // https://developer.android.com/studio/build/application-id
    679            let package_regexp =
    680                Regex::new(r"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$").unwrap();
    681            if !package_regexp.is_match(package.as_bytes()) {
    682                return Err(WebDriverError::new(
    683                    ErrorStatus::InvalidArgument,
    684                    "Not a valid androidPackage name",
    685                ));
    686            }
    687 
    688            let mut android = AndroidOptions::new(package.clone(), storage);
    689 
    690            android.activity = match options.get("androidActivity") {
    691                Some(json) => {
    692                    let activity = json
    693                        .as_str()
    694                        .ok_or_else(|| {
    695                            WebDriverError::new(
    696                                ErrorStatus::InvalidArgument,
    697                                "androidActivity is not a string",
    698                            )
    699                        })?
    700                        .to_owned();
    701 
    702                    if activity.contains('/') {
    703                        return Err(WebDriverError::new(
    704                            ErrorStatus::InvalidArgument,
    705                            "androidActivity should not contain '/",
    706                        ));
    707                    }
    708 
    709                    Some(activity)
    710                }
    711                None => {
    712                    match package.as_str() {
    713                        "org.mozilla.firefox"
    714                        | "org.mozilla.firefox_beta"
    715                        | "org.mozilla.fenix"
    716                        | "org.mozilla.fenix.debug"
    717                        | "org.mozilla.reference.browser" => {
    718                            Some("org.mozilla.fenix.IntentReceiverActivity".to_string())
    719                        }
    720                        "org.mozilla.focus"
    721                        | "org.mozilla.focus.debug"
    722                        | "org.mozilla.klar"
    723                        | "org.mozilla.klar.debug" => {
    724                            Some("org.mozilla.focus.activity.IntentReceiverActivity".to_string())
    725                        }
    726                        // For all other applications fallback to auto-detection.
    727                        _ => None,
    728                    }
    729                }
    730            };
    731 
    732            android.device_serial = match options.get("androidDeviceSerial") {
    733                Some(json) => Some(
    734                    json.as_str()
    735                        .ok_or_else(|| {
    736                            WebDriverError::new(
    737                                ErrorStatus::InvalidArgument,
    738                                "androidDeviceSerial is not a string",
    739                            )
    740                        })?
    741                        .to_owned(),
    742                ),
    743                None => None,
    744            };
    745 
    746            android.intent_arguments = match options.get("androidIntentArguments") {
    747                Some(json) => {
    748                    let args_array = json.as_array().ok_or_else(|| {
    749                        WebDriverError::new(
    750                            ErrorStatus::InvalidArgument,
    751                            "androidIntentArguments is not an array",
    752                        )
    753                    })?;
    754                    let args = args_array
    755                        .iter()
    756                        .map(|x| x.as_str().map(|x| x.to_owned()))
    757                        .collect::<Option<Vec<String>>>()
    758                        .ok_or_else(|| {
    759                            WebDriverError::new(
    760                                ErrorStatus::InvalidArgument,
    761                                "androidIntentArguments entries are not all strings",
    762                            )
    763                        })?;
    764 
    765                    Some(args)
    766                }
    767                None => {
    768                    // All GeckoView based applications support this view,
    769                    // and allow to open a blank page in a Gecko window.
    770                    Some(vec![
    771                        "-a".to_string(),
    772                        "android.intent.action.VIEW".to_string(),
    773                        "-d".to_string(),
    774                        "about:blank".to_string(),
    775                    ])
    776                }
    777            };
    778 
    779            Ok(Some(android))
    780        } else {
    781            Ok(None)
    782        }
    783    }
    784 }
    785 
    786 fn pref_from_json(value: &Value) -> WebDriverResult<Pref> {
    787    match *value {
    788        Value::String(ref x) => Ok(Pref::new(x.clone())),
    789        Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())),
    790        Value::Bool(x) => Ok(Pref::new(x)),
    791        _ => Err(WebDriverError::new(
    792            ErrorStatus::UnknownError,
    793            "Could not convert pref value to string, boolean, or integer",
    794        )),
    795    }
    796 }
    797 
    798 fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
    799    let reader = Cursor::new(buf);
    800    let mut zip = zip::ZipArchive::new(reader)
    801        .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?;
    802 
    803    for i in 0..zip.len() {
    804        let mut file = zip.by_index(i).map_err(|_| {
    805            WebDriverError::new(
    806                ErrorStatus::UnknownError,
    807                "Processing profile zip file failed",
    808            )
    809        })?;
    810        let unzip_path = {
    811            let name = file.name();
    812            let is_dir = name.ends_with('/');
    813            let rel_path = Path::new(name);
    814            let dest_path = dest_dir.join(rel_path);
    815 
    816            {
    817                let create_dir = if is_dir {
    818                    Some(dest_path.as_path())
    819                } else {
    820                    dest_path.parent()
    821                };
    822                if let Some(dir) = create_dir {
    823                    if !dir.exists() {
    824                        debug!("Creating profile directory tree {}", dir.to_string_lossy());
    825                        fs::create_dir_all(dir)?;
    826                    }
    827                }
    828            }
    829 
    830            if is_dir {
    831                None
    832            } else {
    833                Some(dest_path)
    834            }
    835        };
    836 
    837        if let Some(unzip_path) = unzip_path {
    838            debug!("Extracting profile to {}", unzip_path.to_string_lossy());
    839            let dest = fs::File::create(unzip_path)?;
    840            if file.size() > 0 {
    841                let mut writer = BufWriter::new(dest);
    842                io::copy(&mut file, &mut writer)?;
    843            }
    844        }
    845    }
    846 
    847    Ok(())
    848 }
    849 
    850 #[cfg(test)]
    851 mod tests {
    852    extern crate mozprofile;
    853 
    854    use self::mozprofile::preferences::Pref;
    855    use super::*;
    856    use serde_json::{json, Map, Value};
    857    use std::fs::File;
    858    use std::io::Read;
    859    use url::{Host, Url};
    860    use webdriver::capabilities::Capabilities;
    861 
    862    fn example_profile() -> Value {
    863        let mut profile_data = Vec::with_capacity(1024);
    864        let mut profile = File::open("src/tests/profile.zip").unwrap();
    865        profile.read_to_end(&mut profile_data).unwrap();
    866        Value::String(BASE64_STANDARD.encode(&profile_data))
    867    }
    868 
    869    fn make_options(
    870        firefox_opts: Capabilities,
    871        marionette_settings: Option<MarionetteSettings>,
    872    ) -> WebDriverResult<FirefoxOptions> {
    873        let mut caps = Capabilities::new();
    874        caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
    875 
    876        FirefoxOptions::from_capabilities(None, &marionette_settings.unwrap_or_default(), &mut caps)
    877    }
    878 
    879    #[test]
    880    fn fx_options_default() {
    881        let opts: FirefoxOptions = Default::default();
    882        assert_eq!(opts.android, None);
    883        assert_eq!(opts.args, None);
    884        assert_eq!(opts.binary, None);
    885        assert_eq!(opts.log, LogOptions { level: None });
    886        assert_eq!(opts.prefs, vec![]);
    887        // Profile doesn't support PartialEq
    888        // assert_eq!(opts.profile, None);
    889    }
    890 
    891    #[test]
    892    fn fx_options_from_capabilities_no_binary_and_empty_caps() {
    893        let mut caps = Capabilities::new();
    894 
    895        let marionette_settings = Default::default();
    896        let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
    897            .expect("valid firefox options");
    898        assert_eq!(opts.android, None);
    899        assert_eq!(opts.args, None);
    900        assert_eq!(opts.binary, None);
    901        assert_eq!(opts.log, LogOptions { level: None });
    902        assert_eq!(opts.prefs, vec![]);
    903    }
    904 
    905    #[test]
    906    fn fx_options_from_capabilities_with_binary_and_caps() {
    907        let mut caps = Capabilities::new();
    908        caps.insert(
    909            "moz:firefoxOptions".into(),
    910            Value::Object(Capabilities::new()),
    911        );
    912 
    913        let binary = PathBuf::from("foo");
    914        let marionette_settings = Default::default();
    915 
    916        let opts = FirefoxOptions::from_capabilities(
    917            Some(binary.clone()),
    918            &marionette_settings,
    919            &mut caps,
    920        )
    921        .expect("valid firefox options");
    922        assert_eq!(opts.android, None);
    923        assert_eq!(opts.args, None);
    924        assert_eq!(opts.binary, Some(binary));
    925        assert_eq!(opts.log, LogOptions { level: None });
    926        assert_eq!(opts.prefs, vec![]);
    927    }
    928 
    929    #[test]
    930    fn fx_options_from_capabilities_with_blocked_firefox_arguments() {
    931        let blocked_args = vec![
    932            "--marionette",
    933            "--remote-allow-hosts",
    934            "--remote-allow-origins",
    935            "--remote-debugging-port",
    936        ];
    937 
    938        for arg in blocked_args {
    939            let mut firefox_opts = Capabilities::new();
    940            firefox_opts.insert("args".into(), json!([arg]));
    941 
    942            make_options(firefox_opts, None).expect_err("invalid firefox options");
    943        }
    944    }
    945 
    946    #[test]
    947    fn fx_options_from_capabilities_with_websocket_url_not_set() {
    948        let mut caps = Capabilities::new();
    949 
    950        let marionette_settings = Default::default();
    951        let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
    952            .expect("Valid Firefox options");
    953 
    954        assert!(
    955            opts.args.is_none(),
    956            "CLI arguments for Firefox unexpectedly found"
    957        );
    958    }
    959 
    960    #[test]
    961    fn fx_options_from_capabilities_with_websocket_url_false() {
    962        let mut caps = Capabilities::new();
    963        caps.insert("webSocketUrl".into(), json!(false));
    964 
    965        let marionette_settings = Default::default();
    966        let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
    967            .expect("Valid Firefox options");
    968 
    969        assert!(
    970            opts.args.is_none(),
    971            "CLI arguments for Firefox unexpectedly found"
    972        );
    973    }
    974 
    975    #[test]
    976    fn fx_options_from_capabilities_with_websocket_url_true() {
    977        let mut caps = Capabilities::new();
    978        caps.insert("webSocketUrl".into(), json!(true));
    979 
    980        let settings = MarionetteSettings {
    981            websocket_port: 1234,
    982            ..Default::default()
    983        };
    984        let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
    985            .expect("Valid Firefox options");
    986 
    987        if let Some(args) = opts.args {
    988            let mut iter = args.iter();
    989            assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
    990            assert_eq!(iter.next(), Some(&"1234".to_owned()));
    991        } else {
    992            panic!("CLI arguments for Firefox not found");
    993        }
    994    }
    995 
    996    #[test]
    997    fn fx_options_from_capabilities_with_websocket_and_allow_hosts() {
    998        let mut caps = Capabilities::new();
    999        caps.insert("webSocketUrl".into(), json!(true));
   1000 
   1001        let mut marionette_settings: MarionetteSettings = Default::default();
   1002        marionette_settings.allow_hosts = vec![
   1003            Host::parse("foo").expect("host"),
   1004            Host::parse("bar").expect("host"),
   1005        ];
   1006        let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
   1007            .expect("Valid Firefox options");
   1008 
   1009        if let Some(args) = opts.args {
   1010            let mut iter = args.iter();
   1011            assert!(iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
   1012            assert_eq!(iter.next(), Some(&"foo,bar".to_owned()));
   1013            assert!(!iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
   1014        } else {
   1015            panic!("CLI arguments for Firefox not found");
   1016        }
   1017    }
   1018 
   1019    #[test]
   1020    fn fx_options_from_capabilities_with_websocket_and_allow_origins() {
   1021        let mut caps = Capabilities::new();
   1022        caps.insert("webSocketUrl".into(), json!(true));
   1023 
   1024        let mut marionette_settings: MarionetteSettings = Default::default();
   1025        marionette_settings.allow_origins = vec![
   1026            Url::parse("http://foo/").expect("url"),
   1027            Url::parse("http://bar/").expect("url"),
   1028        ];
   1029        let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
   1030            .expect("Valid Firefox options");
   1031 
   1032        if let Some(args) = opts.args {
   1033            let mut iter = args.iter();
   1034            assert!(iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
   1035            assert_eq!(iter.next(), Some(&"http://foo/,http://bar/".to_owned()));
   1036            assert!(!iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
   1037        } else {
   1038            panic!("CLI arguments for Firefox not found");
   1039        }
   1040    }
   1041 
   1042    #[test]
   1043    fn fx_options_from_capabilities_with_debugger_address_not_set() {
   1044        let caps = Capabilities::new();
   1045 
   1046        let opts = make_options(caps, None).expect("valid firefox options");
   1047        assert!(
   1048            opts.args.is_none(),
   1049            "CLI arguments for Firefox unexpectedly found"
   1050        );
   1051    }
   1052 
   1053    #[test]
   1054    fn fx_options_from_capabilities_with_debugger_address_false() {
   1055        let mut caps = Capabilities::new();
   1056        caps.insert("moz:debuggerAddress".into(), json!(false));
   1057 
   1058        let opts = make_options(caps, None).expect("valid firefox options");
   1059        assert!(
   1060            opts.args.is_none(),
   1061            "CLI arguments for Firefox unexpectedly found"
   1062        );
   1063    }
   1064 
   1065    #[test]
   1066    fn fx_options_from_capabilities_with_debugger_address_true() {
   1067        let mut caps = Capabilities::new();
   1068        caps.insert("moz:debuggerAddress".into(), json!(true));
   1069 
   1070        let settings = MarionetteSettings {
   1071            websocket_port: 1234,
   1072            ..Default::default()
   1073        };
   1074        let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
   1075            .expect("Valid Firefox options");
   1076 
   1077        if let Some(args) = opts.args {
   1078            let mut iter = args.iter();
   1079            assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
   1080            assert_eq!(iter.next(), Some(&"1234".to_owned()));
   1081        } else {
   1082            panic!("CLI arguments for Firefox not found");
   1083        }
   1084    }
   1085 
   1086    #[test]
   1087    fn fx_options_from_capabilities_with_invalid_caps() {
   1088        let mut caps = Capabilities::new();
   1089        caps.insert("moz:firefoxOptions".into(), json!(42));
   1090 
   1091        let marionette_settings = Default::default();
   1092        FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
   1093            .expect_err("Firefox options need to be of type object");
   1094    }
   1095 
   1096    #[test]
   1097    fn fx_options_android_package_and_binary() {
   1098        let mut firefox_opts = Capabilities::new();
   1099        firefox_opts.insert("androidPackage".into(), json!("foo"));
   1100        firefox_opts.insert("binary".into(), json!("bar"));
   1101 
   1102        make_options(firefox_opts, None)
   1103            .expect_err("androidPackage and binary are mutual exclusive");
   1104    }
   1105 
   1106    #[test]
   1107    fn fx_options_android_no_package() {
   1108        let mut firefox_opts = Capabilities::new();
   1109        firefox_opts.insert("androidAvtivity".into(), json!("foo"));
   1110 
   1111        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1112        assert_eq!(opts.android, None);
   1113    }
   1114 
   1115    #[test]
   1116    fn fx_options_android_package_valid_value() {
   1117        for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() {
   1118            let mut firefox_opts = Capabilities::new();
   1119            firefox_opts.insert("androidPackage".into(), json!(value));
   1120 
   1121            let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1122            assert_eq!(opts.android.unwrap().package, value.to_string());
   1123        }
   1124    }
   1125 
   1126    #[test]
   1127    fn fx_options_android_package_invalid_type() {
   1128        let mut firefox_opts = Capabilities::new();
   1129        firefox_opts.insert("androidPackage".into(), json!(42));
   1130 
   1131        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1132    }
   1133 
   1134    #[test]
   1135    fn fx_options_android_package_invalid_value() {
   1136        for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() {
   1137            let mut firefox_opts = Capabilities::new();
   1138            firefox_opts.insert("androidPackage".into(), json!(value));
   1139            make_options(firefox_opts, None).expect_err("invalid firefox options");
   1140        }
   1141    }
   1142 
   1143    #[test]
   1144    fn fx_options_android_activity_default_known_apps() {
   1145        let packages = vec![
   1146            "org.mozilla.firefox",
   1147            "org.mozilla.firefox_beta",
   1148            "org.mozilla.fenix",
   1149            "org.mozilla.fenix.debug",
   1150            "org.mozilla.focus",
   1151            "org.mozilla.focus.debug",
   1152            "org.mozilla.klar",
   1153            "org.mozilla.klar.debug",
   1154            "org.mozilla.reference.browser",
   1155        ];
   1156 
   1157        for package in packages {
   1158            let mut firefox_opts = Capabilities::new();
   1159            firefox_opts.insert("androidPackage".into(), json!(package));
   1160 
   1161            let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1162            assert!(opts
   1163                .android
   1164                .unwrap()
   1165                .activity
   1166                .unwrap()
   1167                .contains("IntentReceiverActivity"));
   1168        }
   1169    }
   1170 
   1171    #[test]
   1172    fn fx_options_android_activity_default_unknown_apps() {
   1173        let packages = vec!["org.mozilla.geckoview_example", "com.some.other.app"];
   1174 
   1175        for package in packages {
   1176            let mut firefox_opts = Capabilities::new();
   1177            firefox_opts.insert("androidPackage".into(), json!(package));
   1178 
   1179            let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1180            assert_eq!(opts.android.unwrap().activity, None);
   1181        }
   1182 
   1183        let mut firefox_opts = Capabilities::new();
   1184        firefox_opts.insert(
   1185            "androidPackage".into(),
   1186            json!("org.mozilla.geckoview_example"),
   1187        );
   1188 
   1189        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1190        assert_eq!(opts.android.unwrap().activity, None);
   1191    }
   1192 
   1193    #[test]
   1194    fn fx_options_android_activity_override() {
   1195        let mut firefox_opts = Capabilities::new();
   1196        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1197        firefox_opts.insert("androidActivity".into(), json!("foo"));
   1198 
   1199        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1200        assert_eq!(opts.android.unwrap().activity, Some("foo".to_string()));
   1201    }
   1202 
   1203    #[test]
   1204    fn fx_options_android_activity_invalid_type() {
   1205        let mut firefox_opts = Capabilities::new();
   1206        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1207        firefox_opts.insert("androidActivity".into(), json!(42));
   1208 
   1209        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1210    }
   1211 
   1212    #[test]
   1213    fn fx_options_android_activity_invalid_value() {
   1214        let mut firefox_opts = Capabilities::new();
   1215        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1216        firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese"));
   1217 
   1218        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1219    }
   1220 
   1221    #[test]
   1222    fn fx_options_android_device_serial() {
   1223        let mut firefox_opts = Capabilities::new();
   1224        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1225        firefox_opts.insert("androidDeviceSerial".into(), json!("cheese"));
   1226 
   1227        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1228        assert_eq!(
   1229            opts.android.unwrap().device_serial,
   1230            Some("cheese".to_string())
   1231        );
   1232    }
   1233 
   1234    #[test]
   1235    fn fx_options_android_device_serial_invalid() {
   1236        let mut firefox_opts = Capabilities::new();
   1237        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1238        firefox_opts.insert("androidDeviceSerial".into(), json!(42));
   1239 
   1240        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1241    }
   1242 
   1243    #[test]
   1244    fn fx_options_android_intent_arguments_defaults() {
   1245        let packages = vec![
   1246            "org.mozilla.firefox",
   1247            "org.mozilla.firefox_beta",
   1248            "org.mozilla.fenix",
   1249            "org.mozilla.fenix.debug",
   1250            "org.mozilla.geckoview_example",
   1251            "org.mozilla.reference.browser",
   1252            "com.some.other.app",
   1253        ];
   1254 
   1255        for package in packages {
   1256            let mut firefox_opts = Capabilities::new();
   1257            firefox_opts.insert("androidPackage".into(), json!(package));
   1258 
   1259            let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1260            assert_eq!(
   1261                opts.android.unwrap().intent_arguments,
   1262                Some(vec![
   1263                    "-a".to_string(),
   1264                    "android.intent.action.VIEW".to_string(),
   1265                    "-d".to_string(),
   1266                    "about:blank".to_string(),
   1267                ])
   1268            );
   1269        }
   1270    }
   1271 
   1272    #[test]
   1273    fn fx_options_android_intent_arguments_override() {
   1274        let mut firefox_opts = Capabilities::new();
   1275        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1276        firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"]));
   1277 
   1278        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1279        assert_eq!(
   1280            opts.android.unwrap().intent_arguments,
   1281            Some(vec!["lorem".to_string(), "ipsum".to_string()])
   1282        );
   1283    }
   1284 
   1285    #[test]
   1286    fn fx_options_android_intent_arguments_no_array() {
   1287        let mut firefox_opts = Capabilities::new();
   1288        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1289        firefox_opts.insert("androidIntentArguments".into(), json!(42));
   1290 
   1291        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1292    }
   1293 
   1294    #[test]
   1295    fn fx_options_android_intent_arguments_invalid_value() {
   1296        let mut firefox_opts = Capabilities::new();
   1297        firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
   1298        firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42]));
   1299 
   1300        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1301    }
   1302 
   1303    #[test]
   1304    fn fx_options_env() {
   1305        let mut env: Map<String, Value> = Map::new();
   1306        env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into()));
   1307        env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into()));
   1308 
   1309        let mut firefox_opts = Capabilities::new();
   1310        firefox_opts.insert("env".into(), env.into());
   1311 
   1312        let mut opts = make_options(firefox_opts, None).expect("valid firefox options");
   1313        for sorted in opts.env.iter_mut() {
   1314            sorted.sort()
   1315        }
   1316        assert_eq!(
   1317            opts.env,
   1318            Some(vec![
   1319                ("TEST_KEY_A".into(), "test_value_a".into()),
   1320                ("TEST_KEY_B".into(), "test_value_b".into()),
   1321            ])
   1322        );
   1323    }
   1324 
   1325    #[test]
   1326    fn fx_options_env_invalid_container() {
   1327        let env = Value::Number(1.into());
   1328 
   1329        let mut firefox_opts = Capabilities::new();
   1330        firefox_opts.insert("env".into(), env);
   1331 
   1332        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1333    }
   1334 
   1335    #[test]
   1336    fn fx_options_env_invalid_value() {
   1337        let mut env: Map<String, Value> = Map::new();
   1338        env.insert("TEST_KEY".into(), Value::Number(1.into()));
   1339 
   1340        let mut firefox_opts = Capabilities::new();
   1341        firefox_opts.insert("env".into(), env.into());
   1342 
   1343        make_options(firefox_opts, None).expect_err("invalid firefox options");
   1344    }
   1345 
   1346    #[test]
   1347    fn test_profile() {
   1348        let encoded_profile = example_profile();
   1349        let mut firefox_opts = Capabilities::new();
   1350        firefox_opts.insert("profile".into(), encoded_profile);
   1351 
   1352        let opts = make_options(firefox_opts, None).expect("valid firefox options");
   1353        let mut profile = match opts.profile {
   1354            ProfileType::Path(profile) => profile,
   1355            _ => panic!("Expected ProfileType::Path"),
   1356        };
   1357        let prefs = profile.user_prefs().expect("valid preferences");
   1358 
   1359        println!("{:#?}", prefs.prefs);
   1360 
   1361        assert_eq!(
   1362            prefs.get("startup.homepage_welcome_url"),
   1363            Some(&Pref::new("data:text/html,PASS"))
   1364        );
   1365    }
   1366 
   1367    #[test]
   1368    fn fx_options_args_profile() {
   1369        let mut firefox_opts = Capabilities::new();
   1370        firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
   1371 
   1372        let options = make_options(firefox_opts, None).expect("Valid args");
   1373        assert!(matches!(options.profile, ProfileType::Path(_)));
   1374    }
   1375 
   1376    #[test]
   1377    fn fx_options_args_named_profile() {
   1378        let mut firefox_opts = Capabilities::new();
   1379        firefox_opts.insert("args".into(), json!(["-P", "foo"]));
   1380 
   1381        let options = make_options(firefox_opts, None).expect("Valid args");
   1382        assert!(matches!(options.profile, ProfileType::Named));
   1383    }
   1384 
   1385    #[test]
   1386    fn fx_options_args_no_profile() {
   1387        let mut firefox_opts = Capabilities::new();
   1388        firefox_opts.insert("args".into(), json!(["--headless"]));
   1389 
   1390        let options = make_options(firefox_opts, None).expect("Valid args");
   1391        assert!(matches!(options.profile, ProfileType::Temporary));
   1392    }
   1393 
   1394    #[test]
   1395    fn fx_options_args_profile_and_profile() {
   1396        let mut firefox_opts = Capabilities::new();
   1397        firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
   1398        firefox_opts.insert("profile".into(), json!("foo"));
   1399 
   1400        make_options(firefox_opts, None).expect_err("Invalid args");
   1401    }
   1402 
   1403    #[test]
   1404    fn fx_options_args_p_and_profile() {
   1405        let mut firefox_opts = Capabilities::new();
   1406        firefox_opts.insert("args".into(), json!(["-P"]));
   1407        firefox_opts.insert("profile".into(), json!("foo"));
   1408 
   1409        make_options(firefox_opts, None).expect_err("Invalid args");
   1410    }
   1411 }