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 is_at_sign(byte: &u8) -> bool {
return *byte == b'@';
}
fn next_separator(text: &[u8], from: usize) -> Option<usize> {
for (offset, window) in text.get(from..)?.windows(SCHEME_SEPARATOR.len()).enumerate() {
if window == SCHEME_SEPARATOR {
return Some(from + offset);
}
}
return None;
}
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 mut authority_end: usize = text.len();
for (offset, byte) in text[authority_start..].iter().enumerate() {
if ends_authority(*byte) {
authority_end = authority_start + offset;
break;
}
}
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(is_at_sign);
let userinfo_end: Option<usize> = match last_at {
Some(offset) if offset > 0 => Some(offset),
_ => None,
};
if let Some(offset) = userinfo_end {
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;