const SAFE_SCHEMES: [&str; 3] = ["http", "https", "about"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Refusal {
Empty,
Scheme,
}
pub fn frame_url(raw: &str) -> Result<String, Refusal> {
let cleaned: String = raw
.trim_matches(|c: char| c <= ' ')
.chars()
.filter(|c| !matches!(c, '\t' | '\n' | '\r'))
.collect();
if cleaned.is_empty() {
return Err(Refusal::Empty);
}
if let Some(rest) = cleaned.strip_prefix("//") {
return Ok(format!("https://{rest}"));
}
if cleaned.starts_with(['/', '.', '?', '#']) {
return Ok(cleaned);
}
match scheme(&cleaned) {
Some(scheme) if SAFE_SCHEMES.contains(&scheme.to_ascii_lowercase().as_str()) => Ok(cleaned),
Some(_) => Err(Refusal::Scheme),
None => Ok(format!("https://{cleaned}")),
}
}
#[must_use]
pub fn is_secure(url: &str) -> Option<bool> {
match scheme(url).map(str::to_ascii_lowercase).as_deref() {
Some("https") => Some(true),
Some("http") => Some(false),
_ => None,
}
}
fn scheme(url: &str) -> Option<&str> {
let (candidate, rest) = url.split_once(':')?;
let mut chars = candidate.chars();
let valid = chars.next().is_some_and(|c| c.is_ascii_alphabetic())
&& chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
let port = rest
.split(['/', '?', '#'])
.next()
.is_some_and(|p| !p.is_empty() && p.chars().all(|c| c.is_ascii_digit()));
(valid && !port).then_some(candidate)
}