pub const CREDENTIALS_MASK: &str = "<credentials>";
const SCHEME_SEPARATOR: &[u8] = b"://";
fn ends_scheme(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || byte == b'+' || byte == b'-' || byte == b'.';
}
fn ends_authority(byte: u8) -> bool {
return byte.is_ascii_whitespace()
|| byte.is_ascii_control()
|| matches!(byte, b'/' | b'?' | b'#' | b'\\' | b'"' | b'\'' | b'<' | b'>' | b'`');
}
fn next_separator(text: &[u8], from: usize) -> Option<usize> {
return text
.get(from..)?
.windows(SCHEME_SEPARATOR.len())
.position(|window| {
return window == SCHEME_SEPARATOR;
})
.map(|offset| {
return from + offset;
});
}
pub fn mask_userinfo(text: &[u8]) -> Vec<u8> {
let mut masked: Vec<u8> = Vec::with_capacity(text.len());
let mut copied: usize = 0;
let mut search_from: usize = 0;
while let Some(separator) = next_separator(text, search_from) {
let authority_start: usize = separator + SCHEME_SEPARATOR.len();
let authority_end: usize = text[authority_start..]
.iter()
.position(|byte| {
return ends_authority(*byte);
})
.map_or(text.len(), |offset| {
return authority_start + offset;
});
search_from = authority_end;
let after_scheme: bool = separator > 0 && ends_scheme(text[separator - 1]);
if !after_scheme {
continue;
}
let last_at: Option<usize> = text[authority_start..authority_end].iter().rposition(|byte| {
return *byte == b'@';
});
if let Some(offset) = last_at.filter(|offset| {
return *offset > 0;
}) {
masked.extend_from_slice(&text[copied..authority_start]);
masked.extend_from_slice(CREDENTIALS_MASK.as_bytes());
copied = authority_start + offset;
}
}
masked.extend_from_slice(&text[copied..]);
return masked;
}
pub fn mask_userinfo_text(text: &str) -> String {
return String::from_utf8_lossy(mask_userinfo(text.as_bytes()).as_slice()).into_owned();
}
#[cfg(test)]
#[path = "lfs_output_mask_tests.rs"]
mod tests;