use anyhow::{Context, Result, bail};
use std::process::Command;
pub fn open(url: &str) -> Result<()> {
check(url)?;
launch(url)
}
fn check(url: &str) -> Result<()> {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
.with_context(|| {
format!("{url:?} is not an http(s) address; sefy only opens web addresses")
})?;
if rest.is_empty() {
bail!("{url:?} has no host");
}
if let Some(bad) = rest.chars().find(|c| c.is_control() || c.is_whitespace()) {
bail!("{url:?} contains {bad:?}, which cannot appear in a web address");
}
Ok(())
}
#[cfg(windows)]
fn launch(url: &str) -> Result<()> {
Command::new("rundll32")
.args(["url.dll,FileProtocolHandler", url])
.status()
.context("cannot reach the browser through rundll32")?;
Ok(())
}
#[cfg(target_os = "macos")]
fn launch(url: &str) -> Result<()> {
Command::new("open")
.arg(url)
.status()
.context("cannot run `open` to reach the browser")?;
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn launch(url: &str) -> Result<()> {
Command::new("xdg-open")
.arg(url)
.status()
.context("cannot run `xdg-open`; is xdg-utils installed?")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ordinary_web_addresses_pass() {
for url in [
"https://example.com",
"http://example.com",
"https://user.example.com:8443/a/b#fragment",
"https://example.com/search?q=1&page=2",
"https://example.com/a%20b",
] {
assert!(
check(url).is_ok(),
"{url} should be allowed: {:?}",
check(url)
);
}
}
#[test]
fn anything_that_is_not_http_is_refused() {
for url in [
"file:///C:/Windows/System32/calc.exe",
"javascript:alert(1)",
"ms-settings:privacy",
"ftp://example.com",
"example.com",
"",
] {
assert!(check(url).is_err(), "{url:?} should be refused");
}
}
#[test]
fn a_scheme_with_no_host_is_refused() {
assert!(check("https://").is_err());
assert!(check("http://").is_err());
}
#[test]
fn whitespace_and_control_characters_are_refused() {
assert!(check("https://exam ple.com").is_err());
assert!(check("https://example.com\nstart calc").is_err());
assert!(check("https://example.com\tx").is_err());
}
}