use std::borrow::Cow;
pub fn ends_url(c: char) -> bool {
matches!(c, '\'' | '"' | '<' | '>' | '`') || c.is_whitespace()
}
pub fn ends_authority(c: char) -> bool {
matches!(c, '/' | '?' | '#') || ends_url(c)
}
pub fn userinfo_end(tail: &str) -> Option<usize> {
let end = tail.find(ends_authority).unwrap_or(tail.len());
let authority = &tail[..end];
if let Some(at) = authority.rfind('@') {
return Some(at);
}
let colon = authority.find(':')?;
let url_end = tail.find(ends_url).unwrap_or(tail.len());
tail.get(colon..url_end)?.rfind('@').map(|i| colon + i)
}
fn stored_userinfo_end(tail: &str) -> Option<usize> {
let end = tail.find(ends_authority).unwrap_or(tail.len());
let authority = &tail[..end];
if !authority.contains('@') && is_host_port(authority) {
return None;
}
userinfo_end(tail)
}
fn is_host_port(authority: &str) -> bool {
let after_host = match authority.strip_prefix('[') {
Some(v6) => match v6.find(']') {
Some(close) => &v6[close + 1..],
None => return false,
},
None => authority.find(':').map_or("", |colon| &authority[colon..]),
};
after_host.is_empty()
|| after_host
.strip_prefix(':')
.is_some_and(|port| port.bytes().all(|b| b.is_ascii_digit()))
}
pub fn strip_userinfo(url: &str) -> Cow<'_, str> {
if let Some(at) = url.find("://") {
let tail = &url[at + 3..];
return match userinfo_end(tail) {
Some(cut) => Cow::Owned(format!("{}{}", &url[..at + 3], &tail[cut + 1..])),
None => Cow::Borrowed(url),
};
}
match scp_userinfo_end(url) {
Some(at) => Cow::Borrowed(&url[at + 1..]),
None => Cow::Borrowed(url),
}
}
pub fn scp_userinfo_end(url: &str) -> Option<usize> {
if url.contains("://") {
return None;
}
let head = &url[..url.find('/').unwrap_or(url.len())];
head.rfind('@').filter(|&at| head[at..].contains(':'))
}
pub fn strip_url_secret(url: &str) -> Cow<'_, str> {
let (prefix, userinfo, rest, http) = if let Some(at) = url.find("://") {
let tail = &url[at + 3..];
let Some(cut) = stored_userinfo_end(tail) else {
return Cow::Borrowed(url);
};
let scheme = url[..at].rsplit('+').next().unwrap_or_default();
let http = scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https");
(&url[..at + 3], &tail[..cut], &tail[cut + 1..], http)
} else {
let Some(at) = scp_userinfo_end(url) else {
return Cow::Borrowed(url);
};
("", &url[..at], &url[at + 1..], false)
};
let user = match userinfo.split_once(':') {
_ if http => "",
Some((user, _)) => user,
None => return Cow::Borrowed(url),
};
let at = if user.is_empty() { "" } else { "@" };
Cow::Owned(format!("{prefix}{user}{at}{rest}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_userinfo_table() {
for (url, want) in [
("https://user:TOK@host/x.git", "https://host/x.git"),
("https://user:TOK@host/", "https://host/"),
("https://TOK@github.com/o/r", "https://github.com/o/r"),
(
"https://x-access-token:T@github.com/o/r.git",
"https://github.com/o/r.git",
),
("https://u:pa/ss@host/o/r", "https://host/o/r"),
("https://u:pa?ss@host/o/r", "https://host/o/r"),
("https://u:p@ss@host/o/r", "https://host/o/r"),
("ssh://git@host:22/o/r", "ssh://host:22/o/r"),
("u:TOK@host:o/r", "host:o/r"),
("git@github.com:o/r.git", "github.com:o/r.git"),
("https://github.com/o/r", "https://github.com/o/r"),
("https://host:8080/o/r", "https://host:8080/o/r"),
("/srv/dir@x/repo", "/srv/dir@x/repo"),
("dir@x/repo", "dir@x/repo"),
("", ""),
] {
assert_eq!(strip_userinfo(url), want, "{url:?}");
}
}
#[test]
fn strip_url_secret_table() {
for (url, want) in [
("git@github.com:o/r.git", "git@github.com:o/r.git"),
("ssh://git@h:2222/t/r", "ssh://git@h:2222/t/r"),
("ssh://u:TOK@h:2222/t/r", "ssh://u@h:2222/t/r"),
("ssh://:TOK@h/t/r", "ssh://h/t/r"),
("git+ssh://git@h/t/r", "git+ssh://git@h/t/r"),
("https://TOK@github.com/o/r", "https://github.com/o/r"),
("https://u:TOK@github.com/o/r", "https://github.com/o/r"),
("HTTPS://u:TOK@github.com/o/r", "HTTPS://github.com/o/r"),
(
"git+https://TOK@github.com/o/r",
"git+https://github.com/o/r",
),
("http://u:pa/ss@host/o/r", "http://host/o/r"),
("u:TOK@host:o/r", "u@host:o/r"),
("u:p@ss@host:o/r", "u@host:o/r"),
("https://github.com/o/r", "https://github.com/o/r"),
("/srv/dir@x/repo", "/srv/dir@x/repo"),
("https://u:T@[::1]:8080/x", "https://[::1]:8080/x"),
(
"https://host:8080/@scope/pkg",
"https://host:8080/@scope/pkg",
),
("ssh://h:2222/o/r@x", "ssh://h:2222/o/r@x"),
("https://host:8443/p?who=a@b", "https://host:8443/p?who=a@b"),
("https://[::1]:8080/o/r@x", "https://[::1]:8080/o/r@x"),
("https://[::1]/o/r?who=a@b", "https://[::1]/o/r?who=a@b"),
("https://u:1234/x@host/", "https://u:1234/x@host/"),
("https://u:/x@host/", "https://u:/x@host/"),
("", ""),
] {
assert_eq!(strip_url_secret(url), want, "{url:?}");
}
}
#[test]
fn userinfo_end_stops_at_free_text() {
assert_eq!(userinfo_end("host/o/r and mail@x"), None);
assert_eq!(userinfo_end("u:T@host/o/r"), Some(3));
assert_eq!(userinfo_end("host:8080/o/r 'a@b'"), None);
}
}