#[derive(Clone, Copy)]
enum Sep {
Slash,
Colon,
}
pub(super) fn normalize(input: &str) -> Option<String> {
let input = input.trim();
let input = input.split(['?', '#']).next().unwrap_or(input);
let transport = take_transport(input)?;
let (authority, path) = split_once_char(transport.rest, transport.sep)?;
let host = take_host(authority, transport.keep_port);
let path = take_repo_path(path)?;
if host.is_empty() || host.starts_with(':') {
return None;
}
Some(format!("{}://{host}/{path}", transport.scheme))
}
struct Transport<'a> {
scheme: &'static str,
rest: &'a str,
sep: Sep,
keep_port: bool,
}
fn take_transport(input: &str) -> Option<Transport<'_>> {
if let Some(rest) = strip_prefix_ci(input, "https://") {
Some(Transport {
scheme: "https",
rest,
sep: Sep::Slash,
keep_port: true,
})
} else if let Some(rest) = strip_prefix_ci(input, "http://") {
Some(Transport {
scheme: "http",
rest,
sep: Sep::Slash,
keep_port: true,
})
} else if let Some(rest) = strip_prefix_ci(input, "ssh://") {
Some(Transport {
scheme: "https",
rest,
sep: Sep::Slash,
keep_port: false,
})
} else if is_scp_like(input) {
Some(Transport {
scheme: "https",
rest: input,
sep: Sep::Colon,
keep_port: false,
})
} else {
None
}
}
fn strip_prefix_ci<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
let head = input.get(..prefix.len())?;
head.eq_ignore_ascii_case(prefix)
.then(|| &input[prefix.len()..])
}
fn is_scp_like(input: &str) -> bool {
if input.contains("://") {
return false;
}
match (input.find(':'), input.find('/')) {
(Some(colon), Some(slash)) => colon < slash,
(Some(_), None) => true,
_ => false,
}
}
fn split_once_char(input: &str, sep: Sep) -> Option<(&str, &str)> {
let delim = match sep {
Sep::Slash => '/',
Sep::Colon => ':',
};
input.split_once(delim)
}
fn take_host(authority: &str, keep_port: bool) -> &str {
let after_user = match authority.rsplit_once('@') {
Some((_, host)) => host,
None => authority,
};
if keep_port {
return after_user;
}
match after_user.split_once(':') {
Some((host, _port)) => host,
None => after_user,
}
}
fn take_repo_path(path: &str) -> Option<&str> {
let path = path.trim_matches('/');
let path = path.strip_suffix(".git").unwrap_or(path);
let path = path.trim_end_matches('/');
path.contains('/').then_some(path)
}
#[cfg(test)]
mod tests;