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 //! Argument string parsing and matching functions for Firefox.
6 //!
7 //! Which arguments Firefox accepts and in what style depends on the platform.
8 //! On Windows only, arguments can be prefixed with `/` (slash), such as
9 //! `/foreground`.  Elsewhere, including Windows, arguments may be prefixed
10 //! with both single (`-foreground`) and double (`--foreground`) dashes.
11 //!
12 //! An argument's name is determined by a space or an assignment operator (`=`)
13 //! so that for the string `-foo=bar`, `foo` is considered the argument's
14 //! basename.
15 
16 use std::ffi::{OsStr, OsString};
17 
18 use crate::runner::platform;
19 
20 /// Parse an argument string into a name and value
21 ///
22 /// Given an argument like `"--arg=value"` this will split it into
23 /// `(Some("arg"), Some("value")). For a case like `"--arg"` it will
24 /// return `(Some("arg"), None)` and where the input doesn't look like
25 /// an argument e.g. `"value"` it will return `(None, Some("value"))`
parse_arg_name_value<T>(arg: T) -> (Option<String>, Option<String>) where T: AsRef<OsStr>,26 fn parse_arg_name_value<T>(arg: T) -> (Option<String>, Option<String>)
27 where
28     T: AsRef<OsStr>,
29 {
30     let arg_os_str: &OsStr = arg.as_ref();
31     let arg_str = arg_os_str.to_string_lossy();
32 
33     let mut name_start = 0;
34     let mut name_end = 0;
35 
36     // Look for an argument name at the start of the
37     // string
38     for (i, c) in arg_str.chars().enumerate() {
39         if i == 0 {
40             if !platform::arg_prefix_char(c) {
41                 break;
42             }
43         } else if i == 1 {
44             if name_end_char(c) {
45                 break;
46             } else if c != '-' {
47                 name_start = i;
48                 name_end = name_start + 1;
49             } else {
50                 name_start = i + 1;
51                 name_end = name_start;
52             }
53         } else {
54             name_end += 1;
55             if name_end_char(c) {
56                 name_end -= 1;
57                 break;
58             }
59         }
60     }
61 
62     let name = if name_start > 0 && name_end > name_start {
63         Some(arg_str[name_start..name_end].into())
64     } else {
65         None
66     };
67 
68     // If there are characters in the string after the argument, read
69     // them as the value, excluding the seperator (e.g. "=") if
70     // present.
71     let mut value_start = name_end;
72     let value_end = arg_str.len();
73     let value = if value_start < value_end {
74         if let Some(c) = arg_str[value_start..value_end].chars().next() {
75             if name_end_char(c) {
76                 value_start += 1;
77             }
78         }
79         Some(arg_str[value_start..value_end].into())
80     } else {
81         None
82     };
83     (name, value)
84 }
85 
name_end_char(c: char) -> bool86 fn name_end_char(c: char) -> bool {
87     c == ' ' || c == '='
88 }
89 
90 /// Represents a Firefox command-line argument.
91 #[derive(Debug, PartialEq)]
92 pub enum Arg {
93     /// `-foreground` ensures application window gets focus, which is not the
94     /// default on macOS.
95     Foreground,
96 
97     /// `-no-remote` prevents remote commands to this instance of Firefox, and
98     /// ensure we always start a new instance.
99     NoRemote,
100 
101     /// `-P NAME` starts Firefox with a profile with a given name.
102     NamedProfile,
103 
104     /// `-profile PATH` starts Firefox with the profile at the specified path.
105     Profile,
106 
107     /// `-ProfileManager` starts Firefox with the profile chooser dialogue.
108     ProfileManager,
109 
110     /// All other arguments.
111     Other(String),
112 
113     /// Not an argument.
114     None,
115 }
116 
117 impl Arg {
new(name: &str) -> Arg118     fn new(name: &str) -> Arg {
119         match &*name {
120             "profile" => Arg::Profile,
121             "P" => Arg::NamedProfile,
122             "ProfileManager" => Arg::ProfileManager,
123             "foreground" => Arg::Foreground,
124             "no-remote" => Arg::NoRemote,
125             _ => Arg::Other(name.into()),
126         }
127     }
128 }
129 
130 impl<'a> From<&'a OsString> for Arg {
from(arg_str: &OsString) -> Arg131     fn from(arg_str: &OsString) -> Arg {
132         if let (Some(name), _) = parse_arg_name_value(arg_str) {
133             Arg::new(&name)
134         } else {
135             Arg::None
136         }
137     }
138 }
139 
140 /// Parse an iterator over arguments into an vector of (name, value)
141 /// tuples
142 ///
143 /// Each entry in the input argument will produce a single item in the
144 /// output. Because we don't know anything about the specific
145 /// arguments, something that doesn't parse as a named argument may
146 /// either be the value of a previous named argument, or may be a
147 /// positional argument.
parse_args<'a>( args: impl Iterator<Item = &'a OsString>, ) -> Vec<(Option<Arg>, Option<String>)>148 pub fn parse_args<'a>(
149     args: impl Iterator<Item = &'a OsString>,
150 ) -> Vec<(Option<Arg>, Option<String>)> {
151     args.map(parse_arg_name_value)
152         .map(|(name, value)| {
153             if let Some(arg_name) = name {
154                 (Some(Arg::new(&arg_name)), value)
155             } else {
156                 (None, value)
157             }
158         })
159         .collect()
160 }
161 
162 /// Given an iterator over all arguments, get the value of an argument
163 ///
164 /// This assumes that the argument takes a single value and that is
165 /// either provided as a single argument entry
166 /// (e.g. `["--name=value"]`) or as the following argument
167 /// (e.g. `["--name", "value"])
get_arg_value<'a>( mut parsed_args: impl Iterator<Item = &'a (Option<Arg>, Option<String>)>, arg: Arg, ) -> Option<String>168 pub fn get_arg_value<'a>(
169     mut parsed_args: impl Iterator<Item = &'a (Option<Arg>, Option<String>)>,
170     arg: Arg,
171 ) -> Option<String> {
172     let mut found_value = None;
173     for (arg_name, arg_value) in &mut parsed_args {
174         if let (Some(name), value) = (arg_name, arg_value) {
175             if *name == arg {
176                 found_value = value.clone();
177                 break;
178             }
179         }
180     }
181     if found_value.is_none() {
182         // If there wasn't a value, check if the following argument is a value
183         if let Some((None, value)) = parsed_args.next() {
184             found_value = value.clone();
185         }
186     }
187     found_value
188 }
189 
190 #[cfg(test)]
191 mod tests {
192     use super::{get_arg_value, parse_arg_name_value, parse_args, Arg};
193     use std::ffi::OsString;
194 
parse(arg: &str, name: Option<&str>)195     fn parse(arg: &str, name: Option<&str>) {
196         let (result, _) = parse_arg_name_value(arg);
197         assert_eq!(result, name.map(|x| x.to_string()));
198     }
199 
200     #[test]
test_parse_arg_name_value()201     fn test_parse_arg_name_value() {
202         parse("-p", Some("p"));
203         parse("--p", Some("p"));
204         parse("--profile foo", Some("profile"));
205         parse("--profile", Some("profile"));
206         parse("--", None);
207         parse("", None);
208         parse("-=", None);
209         parse("--=", None);
210         parse("-- foo", None);
211         parse("foo", None);
212         parse("/ foo", None);
213         parse("/- foo", None);
214         parse("/=foo", None);
215         parse("foo", None);
216         parse("-profile", Some("profile"));
217         parse("-profile=foo", Some("profile"));
218         parse("-profile = foo", Some("profile"));
219         parse("-profile abc", Some("profile"));
220         parse("-profile /foo", Some("profile"));
221     }
222 
223     #[cfg(target_os = "windows")]
224     #[test]
test_parse_arg_name_value_windows()225     fn test_parse_arg_name_value_windows() {
226         parse("/profile", Some("profile"));
227     }
228 
229     #[cfg(not(target_os = "windows"))]
230     #[test]
test_parse_arg_name_value_non_windows()231     fn test_parse_arg_name_value_non_windows() {
232         parse("/profile", None);
233     }
234 
235     #[test]
test_arg_from_osstring()236     fn test_arg_from_osstring() {
237         assert_eq!(Arg::from(&OsString::from("-- profile")), Arg::None);
238         assert_eq!(Arg::from(&OsString::from("profile")), Arg::None);
239         assert_eq!(Arg::from(&OsString::from("profile -P")), Arg::None);
240         assert_eq!(
241             Arg::from(&OsString::from("-profiled")),
242             Arg::Other("profiled".into())
243         );
244         assert_eq!(
245             Arg::from(&OsString::from("-PROFILEMANAGER")),
246             Arg::Other("PROFILEMANAGER".into())
247         );
248 
249         assert_eq!(Arg::from(&OsString::from("--profile")), Arg::Profile);
250         assert_eq!(Arg::from(&OsString::from("-profile foo")), Arg::Profile);
251 
252         assert_eq!(
253             Arg::from(&OsString::from("--ProfileManager")),
254             Arg::ProfileManager
255         );
256         assert_eq!(
257             Arg::from(&OsString::from("-ProfileManager")),
258             Arg::ProfileManager
259         );
260 
261         // TODO: -Ptest is valid
262         //assert_eq!(Arg::from(&OsString::from("-Ptest")), Arg::NamedProfile);
263         assert_eq!(Arg::from(&OsString::from("-P")), Arg::NamedProfile);
264         assert_eq!(Arg::from(&OsString::from("-P test")), Arg::NamedProfile);
265 
266         assert_eq!(Arg::from(&OsString::from("--foreground")), Arg::Foreground);
267         assert_eq!(Arg::from(&OsString::from("-foreground")), Arg::Foreground);
268 
269         assert_eq!(Arg::from(&OsString::from("--no-remote")), Arg::NoRemote);
270         assert_eq!(Arg::from(&OsString::from("-no-remote")), Arg::NoRemote);
271     }
272 
273     #[test]
test_get_arg_value()274     fn test_get_arg_value() {
275         let args = vec!["-P", "ProfileName", "--profile=/path/", "--no-remote"]
276             .iter()
277             .map(|x| OsString::from(x))
278             .collect::<Vec<OsString>>();
279         let parsed_args = parse_args(args.iter());
280         assert_eq!(
281             get_arg_value(parsed_args.iter(), Arg::NamedProfile),
282             Some("ProfileName".into())
283         );
284         assert_eq!(
285             get_arg_value(parsed_args.iter(), Arg::Profile),
286             Some("/path/".into())
287         );
288         assert_eq!(get_arg_value(parsed_args.iter(), Arg::NoRemote), None);
289 
290         let args = vec!["--profile=", "-P test"]
291             .iter()
292             .map(|x| OsString::from(x))
293             .collect::<Vec<OsString>>();
294         let parsed_args = parse_args(args.iter());
295         assert_eq!(
296             get_arg_value(parsed_args.iter(), Arg::NamedProfile),
297             Some("test".into())
298         );
299         assert_eq!(
300             get_arg_value(parsed_args.iter(), Arg::Profile),
301             Some("".into())
302         );
303     }
304 }
305