use url::Url;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Query {
pub protocol: String,
pub host: String,
pub path: String,
}
impl Query {
pub fn from_url(url: &Url) -> Self {
let raw_path = url.path().trim_start_matches('/');
Self {
protocol: url.scheme().to_owned(),
host: host_with_port(url),
path: percent_decode(raw_path),
}
}
pub fn without_path(mut self) -> Self {
self.path.clear();
self
}
}
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = (bytes[i + 1] as char).to_digit(16);
let lo = (bytes[i + 2] as char).to_digit(16);
if let (Some(h), Some(l)) = (hi, lo) {
out.push(((h << 4) | l) as u8);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn host_with_port(url: &Url) -> String {
match (url.host_str(), url.port()) {
(Some(h), Some(p)) => format!("{h}:{p}"),
(Some(h), None) => h.to_owned(),
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_url_extracts_protocol_host_path() {
let q =
Query::from_url(&Url::parse("https://git.example.com/foo/bar.git/info/lfs").unwrap());
assert_eq!(q.protocol, "https");
assert_eq!(q.host, "git.example.com");
assert_eq!(q.path, "foo/bar.git/info/lfs");
}
#[test]
fn from_url_includes_port() {
let q = Query::from_url(&Url::parse("http://localhost:8080/lfs").unwrap());
assert_eq!(q.host, "localhost:8080");
}
#[test]
fn without_path_clears_path() {
let q = Query::from_url(&Url::parse("https://h.example/a/b").unwrap()).without_path();
assert!(q.path.is_empty());
}
#[test]
fn from_url_decodes_percent_escapes_in_path() {
let q =
Query::from_url(&Url::parse("https://h.example/test%0aprotect-linefeed.git").unwrap());
assert_eq!(q.path, "test\nprotect-linefeed.git");
}
#[test]
fn from_url_preserves_invalid_percent_sequences() {
let q = Query::from_url(&Url::parse("https://h.example/100%25done").unwrap());
assert_eq!(q.path, "100%done");
}
}