use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refused {
Scheme,
Character,
Malformed,
NoHost,
Credentials,
}
impl fmt::Display for Refused {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Refused::Scheme => "not an http or https link",
Refused::Character => "a character a link cannot hold",
Refused::Malformed => "not a URL",
Refused::NoHost => "no host",
Refused::Credentials => "a user name or password",
})
}
}
pub fn checked_url(raw: &str) -> Result<String, Refused> {
if raw
.chars()
.any(|c| c.is_control() || c.is_whitespace() || c == '\\' || bidi(c))
{
return Err(Refused::Character);
}
let Some(rest) = raw
.strip_prefix("https://")
.or_else(|| raw.strip_prefix("http://"))
else {
return Err(Refused::Scheme);
};
if rest.starts_with('/') {
return Err(Refused::Malformed);
}
let url = url::Url::parse(raw).map_err(|_| Refused::Malformed)?;
if !matches!(url.scheme(), "http" | "https") {
return Err(Refused::Scheme);
}
if url.host_str().is_none_or(str::is_empty) {
return Err(Refused::NoHost);
}
if !url.username().is_empty() || url.password().is_some() {
return Err(Refused::Credentials);
}
let out = url.as_str().to_string();
assert!(
!out.starts_with('-'),
"a checked URL starts with its scheme"
);
Ok(out)
}
fn bidi(c: char) -> bool {
matches!(c, '\u{061c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Platform {
Windows,
MacOs,
Unix,
}
impl Platform {
pub fn current() -> Self {
if cfg!(windows) {
Platform::Windows
} else if cfg!(target_os = "macos") {
Platform::MacOs
} else {
Platform::Unix
}
}
}
pub fn local_desktop(platform: Platform, env: impl Fn(&str) -> Option<String>) -> bool {
let set = |name: &str| env(name).is_some_and(|v| !v.trim().is_empty());
if set("SSH_CONNECTION") || set("SSH_TTY") {
return false;
}
match platform {
Platform::Windows | Platform::MacOs => true,
Platform::Unix => set("DISPLAY") || set("WAYLAND_DISPLAY"),
}
}
pub fn opener_argv(platform: Platform, url: &str) -> Vec<String> {
let program: &[&str] = match platform {
Platform::Windows => &["rundll32", "url.dll,FileProtocolHandler"],
Platform::MacOs => &["open"],
Platform::Unix => &["xdg-open"],
};
program
.iter()
.map(|s| s.to_string())
.chain(std::iter::once(url.to_string()))
.collect()
}
pub fn open(url: &str) -> std::io::Result<()> {
crate::inspector::external_open::start(&opener_argv(Platform::current(), url))
}
#[cfg(test)]
mod tests;