browser_commander/browser/
open_in_user_browser.rs1use anyhow::{anyhow, Result};
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7use crate::utilities::subprocess::{run_command, RunCommandOptions};
8
9const ALLOWED_SCHEMES: [&str; 8] = [
10 "http",
11 "https",
12 "file",
13 "ftp",
14 "about",
15 "chrome",
16 "edge",
17 "view-source",
18];
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct OpenInUserBrowserResult {
23 pub opened: String,
24 pub command: Vec<String>,
25}
26
27pub fn validate_open_url(url: &str) -> Result<&str> {
29 if url.is_empty() {
30 return Err(anyhow!("open_in_user_browser requires a URL string"));
31 }
32 if url.starts_with('-') {
33 return Err(anyhow!(
34 "Refusing to open {url:?}: a URL cannot start with '-'"
35 ));
36 }
37 let parsed = Url::parse(url)
38 .map_err(|_| anyhow!("Refusing to open {url:?}: it is not a valid absolute URL"))?;
39 if !ALLOWED_SCHEMES.contains(&parsed.scheme()) {
40 return Err(anyhow!(
41 "Refusing to open {url:?}: {} is not an allowed web scheme",
42 parsed.scheme()
43 ));
44 }
45 Ok(url)
46}
47
48pub fn build_open_command(url: &str, platform: &str) -> Result<Vec<String>> {
50 validate_open_url(url)?;
51 let mut command = match platform {
52 "linux" => vec!["xdg-open".to_string()],
53 "macos" | "darwin" => vec!["open".to_string()],
54 "windows" | "win32" => vec!["explorer.exe".to_string()],
55 _ => {
56 return Err(anyhow!(
57 "open_in_user_browser is not supported on {platform}"
58 ))
59 }
60 };
61 command.push(url.to_string());
62 Ok(command)
63}
64
65pub async fn open_in_user_browser(url: &str) -> Result<OpenInUserBrowserResult> {
67 let command = build_open_command(url, std::env::consts::OS)?;
68 run_command(&command[0], &command[1..], RunCommandOptions::default()).await?;
69 Ok(OpenInUserBrowserResult {
70 opened: url.to_string(),
71 command,
72 })
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
81 fn builds_each_system_opener_without_a_shell() {
82 assert_eq!(
83 build_open_command("https://example.com/", "linux").unwrap(),
84 ["xdg-open", "https://example.com/"]
85 );
86 assert_eq!(
87 build_open_command("https://example.com/", "darwin").unwrap(),
88 ["open", "https://example.com/"]
89 );
90 assert_eq!(
91 build_open_command("https://example.com/", "win32").unwrap(),
92 ["explorer.exe", "https://example.com/"]
93 );
94 assert_eq!(
95 build_open_command("https://example.com/?a=1&b=2", "win32").unwrap(),
96 ["explorer.exe", "https://example.com/?a=1&b=2"]
97 );
98 }
99
100 #[test]
101 fn rejects_non_web_schemes_and_options() {
102 assert!(validate_open_url("javascript:alert(1)").is_err());
103 assert!(validate_open_url("--version").is_err());
104 assert!(validate_open_url("not a url").is_err());
105 }
106}