use crate::core::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedUrl {
pub scheme: String,
pub username: Option<String>,
pub password: Option<String>,
pub host: String,
pub port: Option<u16>,
pub path: String,
pub query: Option<String>,
pub fragment: Option<String>,
}
pub struct SafeUrl;
impl SafeUrl {
pub fn parse(url: &str) -> Result<ParsedUrl> {
let url = url.trim();
if url.is_empty() {
return Err(Error::EmptyInput);
}
let (scheme, rest) = url
.split_once("://")
.ok_or_else(|| Error::InvalidFormat("Missing scheme".into()))?;
let (rest, fragment) = match rest.rsplit_once('#') {
Some((r, f)) => (r, Some(f.to_string())),
None => (rest, None),
};
let (rest, query) = match rest.split_once('?') {
Some((r, q)) => (r, Some(q.to_string())),
None => (rest, None),
};
let (authority, path) = match rest.find('/') {
Some(i) => (&rest[..i], rest[i..].to_string()),
None => (rest, "/".to_string()),
};
let (userinfo, hostport) = match authority.rsplit_once('@') {
Some((u, h)) => (Some(u), h),
None => (None, authority),
};
let (username, password) = match userinfo {
Some(u) => match u.split_once(':') {
Some((user, pass)) => (Some(user.to_string()), Some(pass.to_string())),
None => (Some(u.to_string()), None),
},
None => (None, None),
};
let (host, port) = if hostport.starts_with('[') {
match hostport.rfind(']') {
Some(i) => {
let h = &hostport[1..i];
let p = hostport[i + 1..].strip_prefix(':').map(|s| {
s.parse::<u16>()
.map_err(|_| Error::InvalidFormat("Invalid port".into()))
});
match p {
Some(Ok(port)) => (h.to_string(), Some(port)),
Some(Err(e)) => return Err(e),
None => (h.to_string(), None),
}
}
None => return Err(Error::InvalidFormat("Invalid IPv6 address".into())),
}
} else {
match hostport.rsplit_once(':') {
Some((h, p)) => match p.parse::<u16>() {
Ok(port) => (h.to_string(), Some(port)),
Err(_) => (hostport.to_string(), None),
},
None => (hostport.to_string(), None),
}
};
Ok(ParsedUrl {
scheme: scheme.to_lowercase(),
username,
password,
host,
port,
path,
query,
fragment,
})
}
pub fn is_valid(url: &str) -> bool {
Self::parse(url).is_ok()
}
pub fn get_domain(url: &str) -> Result<String> {
let parsed = Self::parse(url)?;
Ok(parsed.host)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple() {
let url = SafeUrl::parse("https://example.com/path").unwrap();
assert_eq!(url.scheme, "https");
assert_eq!(url.host, "example.com");
assert_eq!(url.path, "/path");
}
#[test]
fn test_parse_with_port() {
let url = SafeUrl::parse("http://localhost:8080/api").unwrap();
assert_eq!(url.host, "localhost");
assert_eq!(url.port, Some(8080));
}
#[test]
fn test_parse_with_query() {
let url = SafeUrl::parse("https://example.com/search?q=test").unwrap();
assert_eq!(url.query, Some("q=test".to_string()));
}
}