1#[cfg_attr(
43 any(target_os = "ios", target_os = "tvos", target_os = "visionos"),
44 path = "ios.rs"
45)]
46#[cfg_attr(target_os = "macos", path = "macos.rs")]
47#[cfg_attr(target_os = "android", path = "android.rs")]
48#[cfg_attr(target_family = "wasm", path = "wasm.rs")]
49#[cfg_attr(windows, path = "windows.rs")]
50#[cfg_attr(
51 all(
52 unix,
53 not(any(
54 target_os = "ios",
55 target_os = "tvos",
56 target_os = "visionos",
57 target_os = "macos",
58 target_os = "android",
59 target_family = "wasm",
60 windows,
61 )),
62 ),
63 path = "unix.rs"
64)]
65mod os;
66
67#[cfg(any(
68 windows,
69 all(
70 unix,
71 not(any(
72 target_os = "ios",
73 target_os = "tvos",
74 target_os = "visionos",
75 target_os = "macos",
76 target_os = "android",
77 target_family = "wasm",
78 )),
79 ),
80))]
81pub(crate) mod common;
82
83use std::fmt::Display;
84use std::io::{Error, ErrorKind, Result};
85use std::ops::Deref;
86use std::str::FromStr;
87use std::{error, fmt};
88
89#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)]
90pub enum Browser {
92 #[default]
94 Default,
95
96 Firefox,
98
99 InternetExplorer,
101
102 Chrome,
104
105 Opera,
107
108 Safari,
110
111 WebPositive,
113}
114
115impl Browser {
116 pub fn is_available() -> bool {
118 Browser::Default.exists()
119 }
120
121 pub fn exists(&self) -> bool {
123 open_browser_with_options(
124 *self,
125 "https://rootnet.in",
126 BrowserOptions::new().with_dry_run(true),
127 )
128 .is_ok()
129 }
130}
131
132#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
134pub struct ParseBrowserError;
135
136impl fmt::Display for ParseBrowserError {
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 f.write_str("Invalid browser given")
139 }
140}
141
142impl error::Error for ParseBrowserError {
143 fn description(&self) -> &str {
144 "invalid browser"
145 }
146}
147
148impl fmt::Display for Browser {
149 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150 match *self {
151 Browser::Default => f.write_str("Default"),
152 Browser::Firefox => f.write_str("Firefox"),
153 Browser::InternetExplorer => f.write_str("Internet Explorer"),
154 Browser::Chrome => f.write_str("Chrome"),
155 Browser::Opera => f.write_str("Opera"),
156 Browser::Safari => f.write_str("Safari"),
157 Browser::WebPositive => f.write_str("WebPositive"),
158 }
159 }
160}
161
162impl FromStr for Browser {
163 type Err = ParseBrowserError;
164
165 fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
166 match s {
167 "firefox" => Ok(Browser::Firefox),
168 "default" => Ok(Browser::Default),
169 "ie" | "internet explorer" | "internetexplorer" => Ok(Browser::InternetExplorer),
170 "chrome" => Ok(Browser::Chrome),
171 "opera" => Ok(Browser::Opera),
172 "safari" => Ok(Browser::Safari),
173 "webpositive" => Ok(Browser::WebPositive),
174 _ => Err(ParseBrowserError),
175 }
176 }
177}
178
179#[derive(Debug, Eq, PartialEq, Clone, Hash)]
180pub struct BrowserOptions {
185 suppress_output: bool,
186 target_hint: String,
187 dry_run: bool,
188 #[cfg(target_os = "macos")]
189 dont_switch: bool,
190}
191
192impl fmt::Display for BrowserOptions {
193 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
194 f.write_fmt(format_args!(
195 "BrowserOptions(supress_output={}, target_hint={}, dry_run={})",
196 self.suppress_output, self.target_hint, self.dry_run
197 ))
198 }
199}
200
201impl std::default::Default for BrowserOptions {
202 fn default() -> Self {
203 let target_hint = String::from(option_env!("WEBBROWSER_WASM_TARGET").unwrap_or("_blank"));
204 BrowserOptions {
205 suppress_output: true,
206 target_hint,
207 dry_run: false,
208 #[cfg(target_os = "macos")]
209 dont_switch: false,
210 }
211 }
212}
213
214impl BrowserOptions {
215 pub fn new() -> Self {
217 Self::default()
218 }
219
220 pub fn with_suppress_output(&mut self, suppress_output: bool) -> &mut Self {
223 self.suppress_output = suppress_output;
224 self
225 }
226
227 #[allow(clippy::all)]
234 pub fn with_target_hint(&mut self, target_hint: &str) -> &mut Self {
235 self.target_hint = target_hint.to_owned();
236 self
237 }
238
239 pub fn with_dry_run(&mut self, dry_run: bool) -> &mut Self {
242 self.dry_run = dry_run;
243 self
244 }
245
246 #[cfg(target_os = "macos")]
247 pub fn with_dont_switch(&mut self, dont_switch: bool) -> &mut Self {
249 self.dont_switch = dont_switch;
250 self
251 }
252}
253
254pub fn open(url: &str) -> Result<()> {
279 open_browser(Browser::Default, url)
280}
281
282pub fn open_browser(browser: Browser, url: &str) -> Result<()> {
294 open_browser_with_options(browser, url, &BrowserOptions::default())
295}
296
297pub fn open_browser_with_options(
312 browser: Browser,
313 url: &str,
314 options: &BrowserOptions,
315) -> Result<()> {
316 let target = TargetType::try_from(url)?;
317
318 #[cfg(feature = "hardened")]
320 if !target.is_http() {
321 return Err(Error::new(
322 ErrorKind::InvalidInput,
323 "only http/https urls allowed",
324 ));
325 }
326
327 if cfg!(any(
328 target_os = "ios",
329 target_os = "tvos",
330 target_os = "visionos",
331 target_os = "macos",
332 target_os = "android",
333 target_family = "wasm",
334 windows,
335 unix,
336 )) {
337 os::open_browser_internal(browser, &target, options)
338 } else {
339 Err(Error::new(ErrorKind::NotFound, "unsupported platform"))
340 }
341}
342
343struct TargetType(url::Url);
346
347impl TargetType {
348 #[cfg(any(
350 feature = "hardened",
351 target_os = "android",
352 target_os = "ios",
353 target_os = "tvos",
354 target_os = "visionos",
355 target_family = "wasm"
356 ))]
357 fn is_http(&self) -> bool {
358 matches!(self.0.scheme(), "http" | "https")
359 }
360
361 #[cfg(any(
364 target_os = "android",
365 target_os = "ios",
366 target_os = "tvos",
367 target_os = "visionos",
368 target_family = "wasm"
369 ))]
370 fn get_http_url(&self) -> Result<&str> {
371 if self.is_http() {
372 Ok(self.0.as_str())
373 } else {
374 Err(Error::new(ErrorKind::InvalidInput, "not an http url"))
375 }
376 }
377
378 #[cfg(not(target_family = "wasm"))]
379 fn from_file_path(value: &str) -> Result<Self> {
380 let pb = std::path::PathBuf::from(value);
381 let url = url::Url::from_file_path(if pb.is_relative() {
382 std::env::current_dir()?.join(pb)
383 } else {
384 pb
385 })
386 .map_err(|_| Error::new(ErrorKind::InvalidInput, "failed to convert path to url"))?;
387
388 Ok(Self(url))
389 }
390}
391
392impl Deref for TargetType {
393 type Target = str;
394
395 fn deref(&self) -> &Self::Target {
396 self.0.as_str()
397 }
398}
399
400impl Display for TargetType {
401 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402 (self as &str).fmt(f)
403 }
404}
405
406impl std::convert::TryFrom<&str> for TargetType {
407 type Error = Error;
408
409 #[cfg(target_family = "wasm")]
410 fn try_from(value: &str) -> Result<Self> {
411 url::Url::parse(value)
412 .map(|u| Ok(Self(u)))
413 .map_err(|_| Error::new(ErrorKind::InvalidInput, "invalid url for wasm"))?
414 }
415
416 #[cfg(not(target_family = "wasm"))]
417 fn try_from(value: &str) -> Result<Self> {
418 match url::Url::parse(value) {
419 Ok(u) => {
420 if u.scheme().len() == 1 && cfg!(windows) {
421 Self::from_file_path(value)
423 } else {
424 Ok(Self(u))
425 }
426 }
427 Err(_) => Self::from_file_path(value),
428 }
429 }
430}
431
432#[test]
433#[ignore]
434fn test_open_firefox() {
435 assert!(open_browser(Browser::Firefox, "http://github.com").is_ok());
436}
437
438#[test]
439#[ignore]
440fn test_open_chrome() {
441 assert!(open_browser(Browser::Chrome, "http://github.com").is_ok());
442}
443
444#[test]
445#[ignore]
446fn test_open_safari() {
447 assert!(open_browser(Browser::Safari, "http://github.com").is_ok());
448}
449
450#[test]
451#[ignore]
452fn test_open_webpositive() {
453 assert!(open_browser(Browser::WebPositive, "http://github.com").is_ok());
454}