datui_lib/app/
link_open.rs1use std::fmt;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Refused {
15 Scheme,
18 Character,
21 Malformed,
23 NoHost,
25 Credentials,
27}
28
29impl fmt::Display for Refused {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(match self {
32 Refused::Scheme => "not an http or https link",
33 Refused::Character => "a character a link cannot hold",
34 Refused::Malformed => "not a URL",
35 Refused::NoHost => "no host",
36 Refused::Credentials => "a user name or password",
37 })
38 }
39}
40
41pub fn checked_url(raw: &str) -> Result<String, Refused> {
45 if raw
46 .chars()
47 .any(|c| c.is_control() || c.is_whitespace() || c == '\\' || bidi(c))
48 {
49 return Err(Refused::Character);
50 }
51 let Some(rest) = raw
55 .strip_prefix("https://")
56 .or_else(|| raw.strip_prefix("http://"))
57 else {
58 return Err(Refused::Scheme);
59 };
60 if rest.starts_with('/') {
61 return Err(Refused::Malformed);
62 }
63 let url = url::Url::parse(raw).map_err(|_| Refused::Malformed)?;
64 if !matches!(url.scheme(), "http" | "https") {
65 return Err(Refused::Scheme);
66 }
67 if url.host_str().is_none_or(str::is_empty) {
68 return Err(Refused::NoHost);
69 }
70 if !url.username().is_empty() || url.password().is_some() {
71 return Err(Refused::Credentials);
72 }
73 let out = url.as_str().to_string();
74 assert!(
76 !out.starts_with('-'),
77 "a checked URL starts with its scheme"
78 );
79 Ok(out)
80}
81
82fn bidi(c: char) -> bool {
85 matches!(c, '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum Platform {
91 Windows,
92 MacOs,
93 Unix,
95}
96
97impl Platform {
98 pub fn current() -> Self {
99 if cfg!(windows) {
100 Platform::Windows
101 } else if cfg!(target_os = "macos") {
102 Platform::MacOs
103 } else {
104 Platform::Unix
105 }
106 }
107}
108
109pub fn local_desktop(platform: Platform, env: impl Fn(&str) -> Option<String>) -> bool {
113 let set = |name: &str| env(name).is_some_and(|v| !v.trim().is_empty());
114 if set("SSH_CONNECTION") || set("SSH_TTY") {
115 return false;
116 }
117 match platform {
118 Platform::Windows | Platform::MacOs => true,
119 Platform::Unix => set("DISPLAY") || set("WAYLAND_DISPLAY"),
120 }
121}
122
123pub fn opener_argv(platform: Platform, url: &str) -> Vec<String> {
127 let program: &[&str] = match platform {
128 Platform::Windows => &["rundll32", "url.dll,FileProtocolHandler"],
129 Platform::MacOs => &["open"],
130 Platform::Unix => &["xdg-open"],
131 };
132 program
133 .iter()
134 .map(|s| s.to_string())
135 .chain(std::iter::once(url.to_string()))
136 .collect()
137}
138
139pub fn open(url: &str) -> std::io::Result<()> {
141 crate::inspector::external_open::start(&opener_argv(Platform::current(), url))
142}
143
144#[cfg(test)]
145mod tests;