use std::borrow::Cow;
use super::key::sanitize_invisible;
#[must_use]
pub fn redact_userinfo(raw: &str) -> String {
redact_userinfo_with_parsed(raw, None)
}
#[expect(
clippy::string_slice,
reason = "#901: authority_start (from find(':') + scheme_separator_end) is an ASCII-byte \
scan, so it always lands on a char boundary"
)]
#[must_use]
pub(super) fn redact_userinfo_with_parsed(raw: &str, parsed: Option<url::Url>) -> String {
debug_assert!(
parsed
.as_ref()
.is_none_or(|u| url::Url::parse(raw).ok().as_ref() == Some(u)),
"parsed must be the result of parsing exactly raw"
);
let parse_result = match parsed {
Some(url) => Ok(url),
None => url::Url::parse(raw),
};
let Ok(mut url) = parse_result else {
return redact_userinfo_unparseable(raw);
};
if url.cannot_be_a_base() {
return redact_userinfo_unparseable(raw);
}
if url.host().is_none() {
return redact_userinfo_opaque_path(raw);
}
if url.username().is_empty() && url.password().is_none() {
let authority_start = raw
.find(':')
.map_or(0, |scheme_end| scheme_separator_end(raw, scheme_end));
let tail = &raw[authority_start..];
if !tail.contains(':') {
return raw.to_string();
}
return format!(
"{}{}",
&raw[..authority_start],
redact_secondary_colon_credential(tail)
);
}
if url.set_username("***").is_err() || url.set_password(None).is_err() {
return "<redacted: index URL contained userinfo>".to_string();
}
redact_authority_url_tail(url.as_str())
}
#[expect(
clippy::string_slice,
reason = "serialized.find(\"***@\") locates an ASCII marker, so tail_start always lands \
on a char boundary"
)]
fn redact_authority_url_tail(serialized: &str) -> String {
let Some(marker_at) = serialized.find("***@") else {
return redact_secondary_colon_credential(serialized);
};
let tail_start = marker_at + "***@".len();
format!(
"{}{}",
&serialized[..tail_start],
redact_secondary_colon_credential(&serialized[tail_start..])
)
}
#[expect(
clippy::string_slice,
reason = "at comes from match_indices('@') on ASCII '@' bytes, so every slice bound is \
always a char boundary"
)]
fn find_credential_at(region: &str) -> Option<usize> {
let mut prev_at = 0;
let mut found = None;
for (at, _) in region.match_indices('@') {
if segment_has_credential_colon(®ion[prev_at..at]) {
found = Some(at);
}
prev_at = at + 1;
}
found
}
const TOKEN_PREFIXES: &[&str] = &[
"ghp_",
"gho_",
"ghu_",
"ghs_",
"ghr_",
"github_pat_",
"glpat-",
"gldt-",
"glrt-",
"glsoat-",
"glptt-",
"gloas-",
"npm_",
"pypi-",
"xoxb-",
"xoxp-",
"xoxa-",
"xoxo-",
"xoxs-",
"xoxr-",
"AKIA",
"ASIA",
"ABIA",
"ACCA",
"sk-",
"sk_live_",
"rk_live_",
"pk_live_",
"AIza",
"ya29.",
"dop_v1_",
"dor_v1_",
"doo_v1_",
"hvs.",
"hvb.",
"shpat_",
"shpss_",
"shpca_",
"shppa_",
"figd_",
"dckr_pat_",
"AKCp",
"glcbt-",
"glft-",
"glimt-",
"glagent-",
"glffct-",
"hf_",
];
#[expect(
clippy::string_slice,
reason = "i/seg_start come from enumerate() over region.bytes() and ASCII byte-literal \
matches against '@'/'/'/'?'/'#'/'\\\\', so every slice bound stays a char boundary"
)]
fn find_token_prefix_at(region: &str) -> Option<usize> {
let mut seg_start = 0;
let mut found = None;
for (i, b) in region.bytes().enumerate() {
match b {
b'@' => {
if TOKEN_PREFIXES
.iter()
.any(|p| region[seg_start..i].starts_with(p))
{
found = Some(i);
}
seg_start = i + 1;
}
b'/' | b'?' | b'#' | b'\\' => seg_start = i + 1,
_ => {}
}
}
found
}
#[expect(
clippy::string_slice,
reason = "at/next come from find('@') on ASCII '@' bytes, so every slice bound is always \
a char boundary"
)]
fn extend_credential_at(region: &str, at: usize) -> usize {
let mut at = at;
loop {
let tail = ®ion[at + 1..];
let Some(next) = tail.find('@') else {
return at;
};
if tail[..next].contains(['/', '?', '#']) {
return at;
}
at += 1 + next;
}
}
#[expect(
clippy::string_slice,
reason = "bracket/colon come from find/starts_with of ASCII [/]/: bytes, so every slice \
bound is always a char boundary"
)]
pub(super) fn segment_has_credential_colon(segment: &str) -> bool {
let mut cursor = 0;
let mut bracket_adjacent = false;
let mut next_bracket = segment.find('[');
let mut next_colon = segment.find(':');
while cursor < segment.len() {
let remaining = &segment[cursor..];
let colon_here = translate_offset(next_colon, cursor, remaining, ':');
next_colon = colon_here.map(|c| cursor + c);
let bracket_here = translate_bracket(next_bracket, cursor, remaining);
next_bracket = bracket_here.map(|b| cursor + b);
if let Some(bracket) = bracket_here {
if let Some(colon) = colon_here.filter(|&c| c < bracket) {
if !colon_is_drive_letter(segment, cursor + colon) {
return true;
}
cursor += colon + 1;
bracket_adjacent = false;
continue;
}
let shape_end = bracket_host_shape_end(remaining, bracket);
bracket_adjacent = remaining.as_bytes().get(shape_end - 1) == Some(&b']');
cursor += shape_end;
continue;
}
let Some(colon) = colon_here else {
return false;
};
let colon_is_bracket_adjacent = bracket_adjacent && colon == 0;
bracket_adjacent = false;
if colon_is_drive_letter(segment, cursor + colon) {
cursor += colon + 1;
continue;
}
if colon_is_bracket_adjacent {
let value_start = colon + 1;
let value_end = remaining[value_start..]
.find(['/', '?', '#'])
.map_or(remaining.len(), |i| value_start + i);
if is_port_like(&remaining[value_start..value_end]) {
cursor += value_end.max(colon + 1);
continue;
}
}
return true;
}
false
}
fn bracket_host_shape_end(text: &str, open: usize) -> usize {
let bytes = text.as_bytes();
let mut i = open + 1;
while let Some(byte) = bytes.get(i) {
match byte {
b']' => return i + 1,
b':' | b'.' | b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => i += 1,
b'[' if i == open + 1 => {
let mut j = i;
while bytes.get(j) == Some(&b'[') {
j += 1;
}
return j - 1;
}
_ => break,
}
}
open + 1
}
fn colon_is_drive_letter(text: &str, colon: usize) -> bool {
let bytes = text.as_bytes();
if bytes.get(colon) != Some(&b':') {
return false;
}
let is_letter = colon
.checked_sub(1)
.and_then(|i| bytes.get(i))
.is_some_and(u8::is_ascii_alphabetic);
let letter_is_standalone = colon == 1
|| colon
.checked_sub(2)
.and_then(|i| bytes.get(i))
.is_some_and(|b| matches!(b, b'/' | b'\\'));
is_letter && letter_is_standalone && matches!(bytes.get(colon + 1), Some(b'/' | b'\\'))
}
fn is_port_like(value: &str) -> bool {
!value.is_empty() && value.len() <= 5 && value.bytes().all(|b| b.is_ascii_digit())
}
#[expect(
clippy::string_slice,
reason = "bracket_host_shape_end's return and colon (from rfind of an ASCII : byte) are \
always char-boundary-safe byte offsets, so every slice bound below is always a \
char boundary"
)]
fn bounded_has_credential_colon(bounded: &str) -> bool {
let bounded = if bounded.starts_with('[') {
&bounded[bracket_host_shape_end(bounded, 0)..]
} else {
bounded
};
let trimmed = bounded.rfind(':').map_or(bounded, |colon| {
if is_port_like(&bounded[colon + 1..]) {
&bounded[..colon]
} else {
bounded
}
});
trimmed.contains(':')
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum RegionKind {
Authority,
OpaquePath,
}
fn host_boundary_scheme_aware(region: &str) -> usize {
let bytes = region.as_bytes();
let mut i = 0;
while let Some(&byte) = bytes.get(i) {
match byte {
b'?' | b'#' => return i,
b'/' => {
if i > 0 && bytes.get(i - 1) == Some(&b':') {
while bytes.get(i) == Some(&b'/') {
i += 1;
}
} else {
return i;
}
}
_ => i += 1,
}
}
bytes.len()
}
#[expect(
clippy::string_slice,
reason = "start is an ASCII byte offset (find(\"://\")/find(':') on raw), so region \
always starts on a char boundary; every further offset comes from find/rfind of \
ASCII tokens on region, so every slice bound stays a char boundary throughout"
)]
fn redact_credential(raw: &str, start: usize, kind: RegionKind) -> String {
let region = &raw[start..];
if kind == RegionKind::Authority {
return format!("{}{}", &raw[..start], redact_authority_suffix(region));
}
let mask_at = |at: usize| {
format!(
"{}***@{}",
&raw[..start],
redact_secondary_colon_credential(®ion[at + 1..])
)
};
match find_credential_at(region).or_else(|| find_token_prefix_at(region)) {
Some(at) => mask_at(extend_credential_at(region, at)),
None => redact_colon_credential(raw, start, region),
}
}
#[expect(
clippy::string_slice,
reason = "every slice bound is derived from find/rfind of ASCII tokens ('@', ':', '/', ...) \
or host_boundary_scheme_aware's own char-boundary-safe byte offsets"
)]
fn redact_authority_suffix(region: &str) -> String {
let has_nested_scheme = region.contains("://");
let mut output = String::new();
let mut base = 0;
let mut window_start = 0;
let mut bracket_before_base = false;
loop {
let host_boundary = host_boundary_scheme_aware(®ion[window_start..]);
let boundary_abs = window_start + host_boundary;
let window = ®ion[window_start..boundary_abs];
if let Some(at) = window.rfind('@').filter(|&at| at != 0) {
let at_abs = window_start + at;
output.push_str(&redact_span_colon_credential(
®ion[base..window_start],
bracket_before_base,
));
output.push_str("***@");
bracket_before_base |= region[base..=at_abs].contains('[');
base = at_abs + 1;
window_start = base;
continue;
}
let colon_evidence = base == 0 && bounded_has_credential_colon(window);
if colon_evidence || (base == 0 && !has_nested_scheme) {
let remaining = ®ion[base..];
if colon_evidence && let Some(at) = find_credential_at(remaining) {
output.push_str("***@");
base = at + 1;
window_start = base;
continue;
}
if let Some(at) = find_token_prefix_at(remaining) {
let at = extend_credential_at(remaining, at);
output.push_str("***@");
let tail = &remaining[at + 1..];
output.push_str(&redact_further_credential(tail, tail.find('[')));
return output;
}
output.push_str(&redact_colon_credential(remaining, 0, remaining));
return output;
}
if boundary_abs < region.len() {
window_start = boundary_abs + 1;
continue;
}
let remaining = ®ion[base..];
output.push_str(&redact_further_credential(remaining, remaining.find('[')));
return output;
}
}
fn redact_secondary_colon_credential(tail: &str) -> String {
redact_colon_credential(tail, 0, tail)
}
fn translate_offset(
known: Option<usize>,
consumed: usize,
remaining: &str,
needle: char,
) -> Option<usize> {
match known {
Some(o) if o >= consumed => Some(o - consumed),
Some(_) => remaining.find(needle),
None => None,
}
}
fn translate_bracket(known: Option<usize>, consumed: usize, remaining: &str) -> Option<usize> {
translate_offset(known, consumed, remaining, '[')
}
fn colon_credential_match(
authority: &str,
next_bracket: Option<usize>,
) -> Option<(String, &str, Option<usize>)> {
colon_credential_match_seeded(authority, next_bracket, false)
}
#[expect(
clippy::string_slice,
reason = "all indices come from find of ASCII tokens (:, ], /, ?, #) or byte-level ASCII \
checks, so every slice bound is always a char boundary"
)]
fn colon_credential_match_seeded(
authority: &str,
next_bracket: Option<usize>,
seed_bracket: bool,
) -> Option<(String, &str, Option<usize>)> {
debug_assert!(
next_bracket.is_none_or(|b| b < authority.len()),
"next_bracket must be an offset into `authority` itself"
);
let mut cursor = 0;
let mut next_bracket = next_bracket;
let mut next_colon = authority.find(':');
let mut bracket_seen = seed_bracket;
loop {
let remaining = &authority[cursor..];
if colon_is_drive_letter(remaining, 1) {
cursor += 2;
continue;
}
let next_colon_here = translate_offset(next_colon, cursor, remaining, ':');
next_colon = next_colon_here.map(|c| cursor + c);
let bracket_here = translate_bracket(next_bracket, cursor, remaining);
next_bracket = bracket_here.map(|b| cursor + b);
if let Some(bracket) = bracket_here {
if next_colon_here.is_none_or(|colon| bracket < colon) {
cursor += bracket_host_shape_end(remaining, bracket);
bracket_seen = true;
continue;
}
}
let colon = next_colon_here?;
let value_start = colon + 1;
let value_end = remaining[value_start..]
.find(['/', '?', '#'])
.map_or(remaining.len(), |i| value_start + i);
let value = &remaining[value_start..value_end];
if value.is_empty() || is_port_like(value) {
cursor += value_end.max(colon + 1);
continue;
}
let end = if bracket_seen {
remaining.len()
} else {
value_end
};
let consumed = cursor + end;
let tail = &authority[consumed..];
let tail_bracket = translate_bracket(next_bracket, consumed, tail);
return Some((
format!("{}:***", &authority[..cursor + colon]),
tail,
tail_bracket,
));
}
}
fn redact_colon_credential(raw: &str, authority_start: usize, authority: &str) -> String {
redact_colon_credential_seeded(raw, authority_start, authority, false)
}
#[expect(
clippy::string_slice,
reason = "authority_start is always caller-derived from find/rfind of ASCII tokens, so it \
always lands on a char boundary"
)]
fn redact_colon_credential_seeded(
raw: &str,
authority_start: usize,
authority: &str,
seed_bracket: bool,
) -> String {
match colon_credential_match_seeded(authority, authority.find('['), seed_bracket) {
None => raw.to_string(),
Some((masked, tail, tail_bracket)) => format!(
"{}{}{}",
&raw[..authority_start],
masked,
redact_further_credential(tail, tail_bracket)
),
}
}
fn redact_span_colon_credential(span: &str, bracket_seen: bool) -> Cow<'_, str> {
if !span.contains(':') {
return Cow::Borrowed(span);
}
Cow::Owned(redact_colon_credential_seeded(span, 0, span, bracket_seen))
}
#[expect(
clippy::string_slice,
reason = "`at` comes from `extend_credential_at`, itself derived from `match_indices('@')` \
on an ASCII byte, so it always lands on a char boundary"
)]
fn redact_further_credential(tail: &str, next_bracket: Option<usize>) -> String {
debug_assert!(
next_bracket.is_none_or(|b| b < tail.len()),
"next_bracket must be an offset into `tail` itself"
);
let mut next_bracket = next_bracket;
let mut at_exhausted = false;
let mut output = String::new();
let mut remaining = tail;
loop {
if !at_exhausted {
match find_credential_at(remaining) {
Some(at) => {
let at = extend_credential_at(remaining, at);
output.push_str("***@");
let consumed = at + 1;
let new_remaining = &remaining[consumed..];
next_bracket = translate_bracket(next_bracket, consumed, new_remaining);
remaining = new_remaining;
continue;
}
None => at_exhausted = true,
}
}
match colon_credential_match(remaining, next_bracket) {
Some((masked, next_tail, tail_bracket)) => {
output.push_str(&masked);
remaining = next_tail;
next_bracket = tail_bracket;
}
None => {
output.push_str(remaining);
return output;
}
}
}
}
#[expect(
clippy::string_slice,
reason = "authority_start comes from find('@') followed by find(\"://\") on ASCII bytes, \
so it always lands on a char boundary"
)]
fn redact_userinfo_unparseable(raw: &str) -> String {
let authority_start = match raw.find('@') {
Some(at) => raw[..at]
.find("://")
.map_or(0, |scheme_end| scheme_separator_end(raw, scheme_end)),
None => raw
.find("://")
.map_or(0, |scheme_end| scheme_separator_end(raw, scheme_end)),
};
redact_credential(raw, authority_start, RegionKind::Authority)
}
fn scheme_separator_end(text: &str, colon: usize) -> usize {
let bytes = text.as_bytes();
let mut i = colon + 1;
while bytes.get(i) == Some(&b'/') {
i += 1;
}
i
}
fn redact_userinfo_opaque_path(raw: &str) -> String {
let path_start = raw.find(':').map_or(0, |scheme_end| scheme_end + 1);
redact_credential(raw, path_start, RegionKind::OpaquePath)
}
const PREFIXED_TOKEN_MIN_BODY_LEN: usize = 16;
const UNPREFIXED_TOKEN_MIN_LEN: usize = 16;
const UNPREFIXED_BASE64URL_TOKEN_MIN_LEN: usize = 20;
const MARKED_HEX_TOKEN_MIN_LEN: usize = 48;
const HEX_TOKEN_MARKER_SEGMENTS: &[&str] = &["priv", "private", "token", "tokens"];
const PATH_PIECE_DELIMITERS: &[u8] = b"/=;,@:";
#[derive(Clone, Copy)]
enum SegmentKind {
HostLabel,
PathPiece,
}
fn is_token_piece_charset(piece: &str) -> bool {
piece
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
fn category_count(body: &str) -> usize {
let mut upper = false;
let mut lower = false;
let mut digit = false;
for b in body.bytes() {
match b {
b'A'..=b'Z' => upper = true,
b'a'..=b'z' => lower = true,
b'0'..=b'9' => digit = true,
_ => {}
}
}
usize::from(upper) + usize::from(lower) + usize::from(digit)
}
fn matching_token_prefix(piece: &str) -> Option<&'static str> {
TOKEN_PREFIXES
.iter()
.copied()
.find(|&prefix| piece.starts_with(prefix))
}
fn prefixed_body_qualifies(prefix: &str, body: &str, kind: SegmentKind) -> bool {
match kind {
SegmentKind::PathPiece => {
body.len() >= PREFIXED_TOKEN_MIN_BODY_LEN
&& (prefix.ends_with('.') || !body.contains('.'))
&& category_count(body) >= 2
}
SegmentKind::HostLabel => {
body.len() >= UNPREFIXED_TOKEN_MIN_LEN
&& body.bytes().all(|b| b.is_ascii_alphanumeric())
&& category_count(body) >= 2
}
}
}
fn piece_should_mask(piece: &str, kind: SegmentKind, preceding: Option<&str>) -> bool {
if piece.is_empty() || !is_token_piece_charset(piece) {
return false;
}
if let Some(prefix) = matching_token_prefix(piece) {
return piece
.get(prefix.len()..)
.is_some_and(|body| prefixed_body_qualifies(prefix, body, kind));
}
match kind {
SegmentKind::PathPiece => {
(piece.len() >= UNPREFIXED_TOKEN_MIN_LEN
&& piece.bytes().all(|b| b.is_ascii_alphanumeric())
&& category_count(piece) == 3)
|| is_unprefixed_base64url_token(piece)
|| is_marked_hex_token(piece, preceding)
}
SegmentKind::HostLabel => false,
}
}
fn is_unprefixed_base64url_token(piece: &str) -> bool {
piece.len() >= UNPREFIXED_BASE64URL_TOKEN_MIN_LEN
&& is_token_piece_charset(piece)
&& !piece.contains('.')
&& piece.bytes().any(|b| matches!(b, b'-' | b'_'))
&& category_count(piece) == 3
}
fn is_marked_hex_token(piece: &str, preceding: Option<&str>) -> bool {
preceding.is_some_and(|segment| HEX_TOKEN_MARKER_SEGMENTS.contains(&segment))
&& piece.len() >= MARKED_HEX_TOKEN_MIN_LEN
&& piece
.bytes()
.all(|b| b.is_ascii_digit() || matches!(b, b'a'..=b'f'))
}
#[expect(
clippy::string_slice,
reason = "piece_start/i come from enumerate() over text.as_bytes() and equality checks \
against single-byte ASCII entries of `delimiters`, so every slice bound is always \
a char boundary"
)]
fn has_maskable_piece(text: &str, delimiters: &[u8], kind: SegmentKind) -> bool {
let mut piece_start = 0;
let mut preceding: Option<&str> = None;
for (i, &byte) in text.as_bytes().iter().enumerate() {
if !delimiters.contains(&byte) {
continue;
}
let piece = &text[piece_start..i];
if piece_should_mask(piece, kind, preceding) {
return true;
}
preceding = Some(piece);
piece_start = i + 1;
}
piece_should_mask(&text[piece_start..], kind, preceding)
}
#[expect(
clippy::string_slice,
reason = "piece_start/i come from enumerate() over text.as_bytes() and equality checks \
against single-byte ASCII entries of `delimiters`, so every slice bound is always \
a char boundary"
)]
fn mask_delimited_pieces<'a>(text: &'a str, delimiters: &[u8], kind: SegmentKind) -> Cow<'a, str> {
if !has_maskable_piece(text, delimiters, kind) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut piece_start = 0;
let mut preceding: Option<&str> = None;
for (i, &byte) in text.as_bytes().iter().enumerate() {
if !delimiters.contains(&byte) {
continue;
}
let piece = &text[piece_start..i];
mask_piece_into(&mut out, piece, kind, preceding);
out.push(char::from(byte));
preceding = Some(piece);
piece_start = i + 1;
}
mask_piece_into(&mut out, &text[piece_start..], kind, preceding);
Cow::Owned(out)
}
fn mask_piece_into(out: &mut String, piece: &str, kind: SegmentKind, preceding: Option<&str>) {
if piece_should_mask(piece, kind, preceding) {
out.push_str("***");
} else {
out.push_str(piece);
}
}
#[expect(
clippy::string_slice,
reason = "colon comes from find(':'); authority_start from scheme_separator_end (ASCII- \
anchored); authority_len from find('/') on the ASCII-safe suffix starting at \
authority_start; host_start from rfind('@') on that same ASCII-safe authority \
slice — every bound here is an ASCII byte offset, so every slice always lands on \
a char boundary"
)]
fn mask_token_segments(redacted: &str) -> Cow<'_, str> {
let Some(colon) = redacted.find(':') else {
return mask_delimited_pieces(redacted, PATH_PIECE_DELIMITERS, SegmentKind::PathPiece);
};
let authority_start = scheme_separator_end(redacted, colon);
let authority_len = redacted[authority_start..]
.find('/')
.unwrap_or(redacted.len() - authority_start);
let authority_end = authority_start + authority_len;
let authority = &redacted[authority_start..authority_end];
let host_start = authority.rfind('@').map_or(0, |at| at + 1);
let host = &authority[host_start..];
let masked_host = mask_delimited_pieces(host, b".", SegmentKind::HostLabel);
let masked_path = mask_delimited_pieces(
&redacted[authority_end..],
PATH_PIECE_DELIMITERS,
SegmentKind::PathPiece,
);
if matches!(masked_host, Cow::Borrowed(_)) && matches!(masked_path, Cow::Borrowed(_)) {
return Cow::Borrowed(redacted);
}
let mut out = String::with_capacity(redacted.len() + 6);
out.push_str(&redacted[..authority_start]);
out.push_str(&authority[..host_start]);
out.push_str(&masked_host);
out.push_str(&masked_path);
Cow::Owned(out)
}
#[must_use]
pub fn url_for_tracing(raw: &str) -> String {
url_for_tracing_with_parsed(raw, None)
}
#[must_use]
#[expect(
clippy::string_slice,
reason = "`end` comes from `find` of ASCII '?'/'#' bytes on the raw input string, so it \
always lands on a valid char boundary"
)]
pub(super) fn url_for_tracing_with_parsed(raw: &str, parsed: Option<url::Url>) -> String {
let end = raw.find(['?', '#']).unwrap_or(raw.len());
let redacted = if end == raw.len() {
redact_userinfo_with_parsed(raw, parsed)
} else {
redact_userinfo(&raw[..end])
};
let masked = match mask_token_segments(&redacted) {
Cow::Borrowed(_) => redacted,
Cow::Owned(masked) => masked,
};
match sanitize_invisible(&masked) {
Cow::Borrowed(_) => masked,
Cow::Owned(sanitized) => sanitized,
}
}
#[must_use]
pub(super) fn is_authority_bearing_url(raw: &str) -> Option<url::Url> {
url::Url::parse(raw).ok().filter(|url| url.host().is_some())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_authority_bearing_url_true_for_real_urls_including_scheme_colon_slash_less_ones() {
assert!(is_authority_bearing_url("https://registry.example/index").is_some());
assert!(is_authority_bearing_url("https:user:hunter2@10.0.0.1/index").is_some());
assert!(is_authority_bearing_url("https:169.254.169.254/index").is_some());
}
#[test]
fn is_authority_bearing_url_false_for_opaque_declaration_key_labels() {
assert!(is_authority_bearing_url("scope:@myorg").is_none());
assert!(is_authority_bearing_url("source:Blocked").is_none());
assert!(is_authority_bearing_url("top-level").is_none());
assert!(is_authority_bearing_url("component-host:10.0.0.1").is_none());
assert!(is_authority_bearing_url("named:internal").is_none());
}
#[test]
fn test_redact_userinfo_noop_cases() {
assert_eq!(
redact_userinfo("https://registry.example/simple"),
"https://registry.example/simple"
);
assert_eq!(redact_userinfo("not-a-valid-url"), "not-a-valid-url");
}
#[test]
fn test_redact_userinfo_strips_username_and_password() {
let redacted = redact_userinfo("https://user:hunter2@registry.example/simple");
assert!(!redacted.contains("hunter2"));
assert!(!redacted.contains("user:"));
assert_eq!(redacted, "https://***@registry.example/simple");
}
#[test]
fn test_redact_userinfo_redacts_unparseable_url_with_userinfo() {
let redacted = redact_userinfo("https://user:hunter2@registry.example:99999/simple");
assert!(!redacted.contains("hunter2"));
assert!(!redacted.contains("user:"));
assert_eq!(redacted, "https://***@registry.example:99999/simple");
}
#[test]
fn test_redact_userinfo_redacts_schemeless_userinfo() {
let redacted = redact_userinfo("user:hunter2@registry.example/simple");
assert!(!redacted.contains("hunter2"));
assert!(!redacted.contains("user:"));
assert_eq!(redacted, "***@registry.example/simple");
}
#[test]
fn test_redact_userinfo_unparseable_no_userinfo_is_noop() {
assert_eq!(
redact_userinfo("https://registry.example:99999/simple"),
"https://registry.example:99999/simple"
);
}
#[test]
fn test_url_for_tracing_redacts_colon_credential_no_at_sign() {
let cases = [
("oauth2:glpat-SECRET", "oauth2:***"),
("gitlab-ci-token:JOBTOKEN", "gitlab-ci-token:***"),
("gitlab.corp/user:hunter2@evil", "gitlab.corp/user:***"),
(
"//registry.npmjs.org/:_authToken=npm_SECRET",
"//registry.npmjs.org/:***",
),
];
for (raw, expected) in cases {
assert_eq!(url_for_tracing(raw), expected, "raw={raw:?}");
}
}
#[test]
fn test_url_for_tracing_colon_credential_after_port_carve_out_is_still_redacted() {
let cases = [
(
"gitlab.corp:8443/user:hunter2@evil",
"gitlab.corp:8443/user:***",
),
("gitlab.corp:8443/user:hunter2", "gitlab.corp:8443/user:***"),
(
"//registry.npmjs.org:443/:_authToken=npm_SECRET",
"//registry.npmjs.org:443/:***",
),
("[::1]:8443/user:hunter2", "[::1]:8443/user:***"),
];
for (raw, expected) in cases {
let redacted = url_for_tracing(raw);
assert_eq!(redacted, expected, "raw={raw:?}");
assert!(
!redacted.contains("SECRET"),
"raw={raw:?} redacted={redacted:?}"
);
}
}
#[test]
fn test_url_for_tracing_colon_credential_after_drive_letter_carve_out_is_still_redacted() {
assert_eq!(url_for_tracing(r"C:\x:hunter2"), r"C:\x:***");
}
#[test]
fn test_url_for_tracing_colon_credential_documented_collisions() {
assert_eq!(url_for_tracing("oauth2:12345"), "oauth2:12345");
}
#[test]
fn test_url_for_tracing_single_slash_scheme_path_redacted() {
assert_eq!(url_for_tracing("c:/user:hunter2@evil"), "c:***@evil");
let redacted = url_for_tracing("token:/hunter2:secret");
assert!(!redacted.contains("secret"), "redacted={redacted:?}");
assert_eq!(redacted, "token:/hunter2:***");
}
#[test]
fn test_url_for_tracing_special_scheme_single_slash_still_redacted() {
assert_eq!(
url_for_tracing("https:/user:hunter2@evil"),
"https://***@evil/"
);
}
#[test]
fn test_url_for_tracing_colon_credential_false_positive_carve_outs() {
let unchanged = [
"gitlab.corp:8443",
"gitlab.corp:8443/api",
"git.corp:8443/*",
"https://gitlab.corp:8443/api/v4",
"https://host:99999/x",
"[::1]:8443",
"https://[::1]:8443/x",
"https://[:::1]:8443/x",
"c:/packages/feed",
r"C:\Users\x\.npmrc",
r"\\server\share\feed",
"file:///Users/x/.npmrc",
"@types/node",
"1.2.3",
"^1.2.3",
"registry.example",
"*.corp.example.com",
"token:",
];
for raw in unchanged {
assert_eq!(url_for_tracing(raw), raw, "raw={raw:?}");
}
}
#[test]
fn test_url_for_tracing_colon_credential_edge_cases() {
assert_eq!(url_for_tracing(":secret"), ":***");
assert_eq!(url_for_tracing("a:b:c"), "a:***");
let redacted = url_for_tracing("польз:секрет@хост");
assert!(!redacted.contains("секрет"), "redacted={redacted:?}");
}
#[test]
fn test_url_for_tracing_no_double_redaction_with_ipv6_port() {
assert_eq!(
url_for_tracing("https://user:pass@[::1]:8443/x"),
"https://***@[::1]:8443/x"
);
}
#[test]
fn test_url_for_tracing_non_numeric_port_like_segment_redacted() {
assert_eq!(url_for_tracing("https://host:abc/x"), "https://host:***/x");
}
#[test]
fn test_redact_userinfo_leading_at_with_no_scheme_is_noop() {
assert_eq!(redact_userinfo("@types/node"), "@types/node");
assert_eq!(redact_userinfo("@angular/core"), "@angular/core");
}
#[test]
fn test_redact_userinfo_non_empty_userinfo_before_scoped_looking_path_still_redacted() {
let redacted = redact_userinfo("user:hunter2@types/node");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "***@types/node");
}
#[test]
fn test_redact_userinfo_redacts_single_slash_scheme_path_credential() {
let redacted = redact_userinfo("c:/user:hunter2@evil");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "c:***@evil");
}
#[test]
fn test_redact_userinfo_redacts_single_slash_scheme_path_credential_other_scheme() {
let redacted = redact_userinfo("oauth2:/a:b@c/d");
assert!(!redacted.contains("a:b"));
assert_eq!(redacted, "oauth2:***@c/d");
}
#[test]
fn test_redact_userinfo_normalizes_special_scheme_single_slash_to_authority() {
let redacted = redact_userinfo("https:/user:hunter2@evil");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "https://***@evil/");
}
#[test]
fn test_redact_userinfo_empty_authority_double_slash_form_is_noop() {
assert_eq!(
redact_userinfo("file:///home/user@example/file"),
"file:///home/user@example/file"
);
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_without_at_is_noop() {
assert_eq!(redact_userinfo("c:/simple/path"), "c:/simple/path");
}
#[test]
fn test_redact_userinfo_redacts_triple_slash_scheme_path_credential() {
let redacted = redact_userinfo("c:///user:hunter2@evil");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "c:***@evil");
}
#[test]
fn test_redact_userinfo_redacts_triple_slash_scheme_path_credential_compound_scheme() {
let redacted = redact_userinfo("git+ssh:///user:hunter2@evil");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "git+ssh:***@evil");
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_at_in_directory_name_is_noop() {
assert_eq!(
redact_userinfo("C:/Users/john.doe@corp/project"),
"C:/Users/john.doe@corp/project"
);
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_scoped_package_version_is_noop() {
assert_eq!(
redact_userinfo("npm:/@scope/pkg@1.0.0"),
"npm:/@scope/pkg@1.0.0"
);
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_scoped_package_name_is_noop() {
assert_eq!(
redact_userinfo("c:/repo/@types/node"),
"c:/repo/@types/node"
);
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_versioned_filename_is_noop() {
assert_eq!(
redact_userinfo("c:/path/to/file@v1.2.3"),
"c:/path/to/file@v1.2.3"
);
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_bare_at_is_noop() {
assert_eq!(redact_userinfo("c:/@only"), "c:/@only");
}
#[test]
fn test_redact_userinfo_redacts_credential_followed_by_unrelated_at_segment() {
let redacted = redact_userinfo("c:///user:hunter2@evil/@scope/pkg");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "c:***@evil/@scope/pkg");
}
#[test]
fn test_redact_userinfo_redacts_credential_followed_by_versioned_path_segment() {
let redacted = redact_userinfo("nexus:/user:hunter2@host/repo/lib@2.0.0");
assert!(!redacted.contains("hunter2"));
assert_eq!(redacted, "nexus:***@host/repo/lib@2.0.0");
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_no_at_credential_is_redacted() {
let redacted = redact_userinfo("token:/hunter2:secret");
assert!(!redacted.contains("secret"), "redacted={redacted:?}");
assert_eq!(redacted, "token:/hunter2:***");
}
#[test]
fn test_redact_userinfo_single_slash_scheme_path_password_with_slash_is_redacted() {
let redacted = redact_userinfo("c:/user:pa/ss@evil");
assert!(!redacted.contains("pa/ss"), "redacted={redacted:?}");
assert_eq!(redacted, "c:***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_password_with_slash_is_redacted() {
let redacted = redact_userinfo("user:pa/ss@evil");
assert!(!redacted.contains("pa/ss"), "redacted={redacted:?}");
assert_eq!(redacted, "***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_question_mark_in_password_is_redacted() {
let redacted = redact_userinfo("https://user:pa?ss@evil");
assert!(!redacted.contains("pa?ss"), "redacted={redacted:?}");
assert_eq!(redacted, "https://***@evil");
let redacted = redact_userinfo("user:pa?ss@evil");
assert!(!redacted.contains("pa?ss"), "redacted={redacted:?}");
assert_eq!(redacted, "***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_hash_in_password_is_redacted() {
assert_eq!(
redact_userinfo("https://user:pa#ss@evil"),
"https://***@evil"
);
assert_eq!(redact_userinfo("user:pa#ss@evil"), "***@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_question_mark_and_hash_in_password_is_redacted() {
assert_eq!(redact_userinfo("c:/user:pa?ss@evil"), "c:***@evil");
assert_eq!(redact_userinfo("c:/user:pa#ss@evil"), "c:***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_widen_does_not_fire_without_leading_colon() {
assert_eq!(
url_for_tracing("gitlab.corp/user:hunter2@evil"),
"gitlab.corp/user:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_username_only_userinfo_is_redacted() {
assert_eq!(redact_userinfo("ghp_TOKEN@github.com"), "***@github.com");
assert_eq!(
redact_userinfo("x-access-token@github.com/repo.git"),
"***@github.com/repo.git"
);
assert_eq!(
redact_userinfo("https://ghp_TOKEN@github.com:99999/x"),
"https://***@github.com:99999/x"
);
let redacted = redact_userinfo("https://glpat-SECRET@gitlab.corp:notaport/x");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "https://***@gitlab.corp:***/x");
}
#[test]
fn test_redact_userinfo_unparseable_leading_at_scope_does_not_hide_later_credential() {
let redacted = redact_userinfo("@scope/user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
}
#[test]
fn test_redact_userinfo_unparseable_leading_at_scope_with_no_later_credential_is_noop() {
assert_eq!(redact_userinfo("@scope/pkg"), "@scope/pkg");
}
#[test]
fn test_redact_userinfo_unparseable_at_sign_inside_password_is_fully_redacted() {
let redacted = redact_userinfo("user:pa@ss@evil");
assert!(!redacted.contains("pa@ss"), "redacted={redacted:?}");
assert_eq!(redacted, "***@evil");
let redacted = redact_userinfo("user:hunter2@evil@host:notaport/x");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@host:***/x");
}
#[test]
fn test_redact_userinfo_opaque_path_username_only_credential_is_redacted() {
assert_eq!(redact_userinfo("c:/ghp_TOKEN@evil"), "c:***@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_credential_is_redacted_nuget() {
assert_eq!(
redact_userinfo("nuget:///glpat-SECRET@feed.corp/v3"),
"nuget:***@feed.corp/v3"
);
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_credential_is_redacted_file() {
assert_eq!(redact_userinfo("file:///ghp_TOKEN@evil"), "file:***@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_no_op_windows_path() {
assert_eq!(
redact_userinfo("file:///C:/Users/john.doe@corp/project"),
"file:///C:/Users/john.doe@corp/project"
);
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_no_op_bare_scope() {
assert_eq!(redact_userinfo("c:///@scope/pkg"), "c:///@scope/pkg");
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_no_op_unprefixed_username() {
assert_eq!(
redact_userinfo("file:///home/user@example/file"),
"file:///home/user@example/file"
);
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_requires_segment_start() {
assert_eq!(redact_userinfo("c:/xghp_TOKEN@evil"), "c:/xghp_TOKEN@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_no_at_sign_is_noop() {
assert_eq!(redact_userinfo("c:/ghp_TOKEN"), "c:/ghp_TOKEN");
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_last_match_wins() {
assert_eq!(
redact_userinfo("c:/ghp_AAA@evil/ghp_BBB@evil2"),
"c:***@evil2"
);
}
#[test]
fn test_redact_userinfo_opaque_path_token_prefix_not_at_segment_start_is_noop() {
assert_eq!(
redact_userinfo("c:/a;ghp_TOKEN@evil"),
"c:/a;ghp_TOKEN@evil"
);
}
#[test]
fn test_redact_userinfo_authority_widen_branch_token_prefix_credential_is_redacted() {
assert_eq!(
redact_userinfo("https://[/ghp_TOKEN@evil"),
"https://***@evil"
);
assert_eq!(redact_userinfo("c://[/ghp_TOKEN@evil"), "c://***@evil");
assert_eq!(redact_userinfo("[/ghp_TOKEN@evil"), "***@evil");
}
#[test]
fn test_redact_userinfo_authority_widen_branch_anti_naive_continue_guard() {
for raw in [
"h/ghp_X@a/oauth2:SECRET/b@c",
"h/ghp_X@[::1]:abc/oauth2:SECRET/b@c",
] {
let redacted = redact_userinfo(raw);
assert!(
!redacted.contains("SECRET"),
"raw={raw:?} redacted={redacted:?}"
);
assert!(
!redacted.contains("ghp_X"),
"raw={raw:?} redacted={redacted:?}"
);
}
assert_eq!(redact_userinfo("h/ghp_X@a/oauth2:SECRET/b@c"), "***@***@c");
assert_eq!(
redact_userinfo("h/ghp_X@[::1]:abc/oauth2:SECRET/b@c"),
"***@***@c"
);
}
#[test]
fn test_redact_userinfo_authority_widen_branch_bracket_sticky_credential() {
let redacted = redact_userinfo("[/::1/x]:abc/ghp_X@h/user:SECRET");
assert!(!redacted.contains("SECRET"), "redacted={redacted:?}");
assert!(!redacted.contains("ghp_X"), "redacted={redacted:?}");
assert_eq!(redacted, "***@h/user:***");
}
#[test]
fn test_redact_userinfo_authority_widen_branch_narrows_bracket_sticky_over_redaction() {
let redacted = redact_userinfo("[/ghp_X@a/user:hunter2@evil/[::1]AKIAX@s3");
assert!(!redacted.contains("ghp_X"), "redacted={redacted:?}");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@***@evil/[::1]AKIAX@s3");
}
#[test]
fn test_redact_userinfo_authority_widen_branch_token_prefix_over_redaction_is_accepted() {
assert_eq!(redact_userinfo("docs.rs/sk-lang@1.2.3"), "***@1.2.3");
assert_eq!(redact_userinfo("unpkg.com/npm_thing@3"), "***@3");
assert_eq!(redact_userinfo("host/pypi-mirror@2.0.0"), "***@2.0.0");
assert_eq!(
redact_userinfo("nexus.corp:8081/repo/sk-lib@2.0.0"),
"***@2.0.0"
);
assert_eq!(
redact_userinfo("https://docs.rs/sk-lang@1.2.3"),
"https://docs.rs/sk-lang@1.2.3"
);
}
#[test]
fn test_redact_userinfo_authority_widen_branch_no_token_prefix_is_unaffected() {
for raw in [
"nexus.corp:8081/repo/lib@2.0.0",
"[::1]:8443/pkg@1.0.0",
"registry.example/@scope/pkg",
"@types/node",
"file:///C:/Users/x@corp/project",
] {
assert_eq!(redact_userinfo(raw), raw, "raw={raw:?}");
}
assert_eq!(
redact_userinfo("gitlab.corp/user:hunter2@evil"),
"gitlab.corp/user:***"
);
}
#[test]
fn test_redact_userinfo_opaque_path_at_sign_inside_password_is_fully_redacted() {
assert_eq!(redact_userinfo("c:/user:pa@ss@evil"), "c:***@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_multiple_at_signs_inside_password_is_fully_redacted() {
assert_eq!(redact_userinfo("c:/user:pa@ss@wo@rd@evil"), "c:***@evil");
}
#[test]
fn test_redact_userinfo_opaque_path_at_sign_inside_password_stops_at_slash_boundary() {
assert_eq!(
redact_userinfo("c:/user:pa@ss@evil/@scope/pkg"),
"c:***@evil/@scope/pkg"
);
}
#[test]
fn test_redact_userinfo_opaque_path_at_sign_inside_password_stops_at_query_and_fragment_boundary()
{
let leaked =
url_for_tracing("c:/user:pw@feed.corp?email=john@corp.com&token=glpat-SUPERSECRET");
assert!(!leaked.contains("SUPERSECRET"), "leaked={leaked:?}");
assert_eq!(leaked, "c:***@feed.corp");
let leaked =
url_for_tracing("c:/user:pw@feed.corp#email=john@corp.com&token=glpat-SUPERSECRET");
assert!(!leaked.contains("SUPERSECRET"), "leaked={leaked:?}");
assert_eq!(leaked, "c:***@feed.corp");
}
#[test]
fn test_redact_userinfo_unparseable_straddle_fallthrough_is_not_widened() {
assert_eq!(
redact_userinfo("https://user:hunter2?x@host@corp"),
"https://***@***@corp"
);
}
#[test]
fn test_redact_userinfo_unparseable_credential_before_later_unrelated_scheme_is_redacted() {
assert_eq!(redact_userinfo("user:hunter2@evil://x"), "***@evil://x");
assert_eq!(redact_userinfo("ghp_TOKEN@evil://x"), "***@evil://x");
assert_eq!(redact_userinfo("a:user:hunter2@evil://x"), "***@evil://x");
assert_eq!(redact_userinfo("://@evil"), "://@evil");
assert_eq!(redact_userinfo("://"), "://");
assert_eq!(redact_userinfo("@"), "@");
assert_eq!(redact_userinfo(""), "");
assert_eq!(redact_userinfo("üser:hünter2@évil://x"), "***@évil://x");
assert_eq!(redact_userinfo("user:hunter2@evil"), "***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_credential_before_and_after_unrelated_at_or_scheme() {
assert_eq!(redact_userinfo("user:hunter2@evil://x@y"), "***@y");
assert_eq!(
redact_userinfo(
"https://user:hunter2@registry.example:99999/redirect?to=http://evil@x"
),
"https://***@registry.example:99999/redirect?***@x"
);
assert_eq!(
redact_userinfo("https://user:hunter2@registry.example:99999/a/b://c@d"),
"https://***@registry.example:99999/a/***@d"
);
}
#[test]
fn test_redact_userinfo_unparseable_leading_credential_shaped_at_before_nested_scheme_is_redacted()
{
assert_eq!(
redact_userinfo("mailto:a@b://user:hunter2@evil"),
"***@evil"
);
assert_eq!(redact_userinfo("a@b://c@d://user:hunter2@evil"), "***@evil");
assert_eq!(
redact_userinfo("mailto:a@b://user:hunter2@evil://x@y"),
"***@y"
);
assert_eq!(redact_userinfo("a@b:///user:hunter2@evil"), "***@evil");
assert_eq!(redact_userinfo("a@b:////user:hunter2@evil"), "***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_leading_at_before_genuine_path_separator_then_nested_scheme_is_redacted()
{
let redacted = redact_userinfo("a@b/c://user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@b/***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_well_anchored_scheme_is_not_a_safe_reason_to_stop_scanning()
{
let redacted = redact_userinfo("https://x@y:notaport/a@b/c://user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "https://***@y:***/***@b/***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_leading_at_before_query_or_fragment_boundary_then_nested_scheme_is_redacted()
{
let redacted = redact_userinfo("a@b?c://user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@b?***@evil");
let redacted = redact_userinfo("a@b#c://user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@b#***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_deeply_chained_fake_credentials_never_leak_the_real_one() {
assert_eq!(
redact_userinfo("a@b/c@d/e://user:hunter2@evil"),
"***@b/***@d/***@evil"
);
assert_eq!(
redact_userinfo("a@b/c/d/e://user:hunter2@evil"),
"***@b/c/d/***@evil"
);
assert_eq!(
redact_userinfo("a@b/c://user:hunter2@evil/d@e"),
"***@b/***@evil/***@e"
);
assert_eq!(
redact_userinfo("a@b/c://d@e/user:hunter2@evil"),
"***@b/***@e/***@evil"
);
}
#[test]
fn test_redact_userinfo_unparseable_decoy_colon_before_nested_scheme_does_not_hide_later_credential()
{
let cases = [
"host/x@y:notaport/c://user:hunter2@evil",
"host/d:e@f/c://user:hunter2@evil",
"/x@y:notaport/c://user:hunter2@evil",
"#d:e@f/c://user:hunter2@evil",
];
for raw in cases {
let redacted = redact_userinfo(raw);
assert!(
!redacted.contains("hunter2"),
"raw={raw:?} redacted={redacted:?}"
);
}
}
#[test]
fn test_redact_userinfo_unparseable_username_only_credential_behind_decoy_at_sign_is_redacted()
{
let cases = [
"npmjs.org/pkg@1.0.0/https://ghp_TOKEN@evil",
"//registry.npmjs.org/@scope/-/x/https://glpat-TOKEN@evil",
"github.com/owner/repo@v1.2.3/x://ghp_TOKEN@evil",
"registry.corp/proxy@v2/https://ghp_TOKEN@evil",
"host/d@e/c://ghp_TOKEN@evil",
"/d@e://ghp_TOKEN@evil",
"@/c://ghp_TOKEN@evil",
"host/x@y:notaport/c://ghp_TOKEN@evil",
];
for raw in cases {
let redacted = redact_userinfo(raw);
assert!(
!redacted.contains("ghp_TOKEN") && !redacted.contains("glpat-TOKEN"),
"raw={raw:?} redacted={redacted:?}"
);
}
assert_eq!(
redact_userinfo("host/d@e/c://ghp_TOKEN@evil"),
"host/***@e/***@evil"
);
}
#[test]
fn test_redact_userinfo_unparseable_anchor_inside_multi_slash_scheme_separator_is_redacted() {
let redacted = redact_userinfo("_://_hostv1:///ghp_TOKEN@h/");
assert!(!redacted.contains("ghp_TOKEN"), "redacted={redacted:?}");
}
#[test]
fn test_redact_userinfo_unparseable_no_over_redaction_without_nested_scheme() {
let cases = [
"nexus.corp:8081/repo/lib@2.0.0",
"registry.corp:4873/@scope/pkg",
"localhost:8080/@types/node",
"gitlab.corp:8443/repo/pkg@1.0.0",
"[::1]:8443/pkg@1.0.0",
];
for raw in cases {
assert_eq!(redact_userinfo(raw), raw, "raw={raw:?}");
}
}
#[test]
fn test_redact_userinfo_unparseable_colon_only_credential_behind_decoy_at_is_redacted() {
assert_eq!(
redact_userinfo("npmjs.org/pkg@1.0.0/https://oauth2:glpat-SECRET"),
"npmjs.org/***@1.0.0/https://oauth2:***"
);
assert_eq!(
redact_userinfo("registry.corp/proxy@v2/https://gitlab-ci-token:JOBTOKEN"),
"registry.corp/***@v2/https://gitlab-ci-token:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_colon_less_credential_behind_colon_evidenced_decoy_is_redacted()
{
assert_eq!(
redact_userinfo("host:x/user1:pass1@evil1/decoy/ghp_TOKEN@evil2"),
"***@evil1/decoy/***@evil2"
);
}
#[test]
fn test_redact_userinfo_unparseable_second_colon_only_credential_behind_decoy_colon_is_redacted()
{
assert_eq!(
redact_userinfo("y:notaport/oauth2:glpat-SECRET"),
"y:***/oauth2:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_credential_past_anchor_consumed_scheme_is_redacted() {
assert_eq!(redact_userinfo("://://?ghp_TOKEN@"), "://***@");
}
#[test]
fn test_redact_userinfo_unparseable_multi_at_no_nested_scheme_over_redacts_every_at() {
assert_eq!(
redact_userinfo("a@b/c@d/host:8081/lib@2.0.0"),
"***@b/***@d/host:8081/***@2.0.0"
);
}
#[test]
fn test_redact_userinfo_unparseable_colonless_credential_after_decoy_at_no_scheme_is_redacted()
{
assert_eq!(redact_userinfo("a@b/ghp_TOKEN@evil"), "***@b/***@evil");
}
#[test]
fn test_redact_userinfo_unparseable_contiguous_slash_run_is_linear_time() {
let n = 400_000;
let raw = format!("a@b{}y", "/".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert_eq!(redacted, format!("***@b{}y", "/".repeat(n)));
assert!(
elapsed < std::time::Duration::from_secs(2),
"took {elapsed:?} for a {n}-byte contiguous slash run — \
host_boundary_scheme_aware may have regressed to quadratic behavior"
);
}
#[test]
fn test_redact_userinfo_unparseable_many_small_undecided_windows_is_linear_time() {
let n = 200_000;
let raw = format!("{}a@b://ghp_TOKEN@evil", "/x".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
!redacted.contains("ghp_TOKEN"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for n={n} small undecided windows — bounded_has_credential_colon \
may have regressed to scanning a growing accumulated span"
);
}
#[test]
fn test_redact_further_credential_colon_dense_chain_is_linear_time() {
let k = 400_000;
let raw = format!("u:SECRET@h/{}z", "a:b/".repeat(k));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
!redacted.contains("SECRET"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for a {k}-segment colon-dense chain — redact_further_credential \
may have regressed to quadratic behavior"
);
}
#[test]
fn test_redact_further_credential_colon_dense_chain_with_trailing_bracket_is_linear_time() {
let k = 400_000;
let raw = format!("u:SECRET@h/{}[::1]", "a:b/".repeat(k));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
!redacted.contains("SECRET"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for a {k}-segment colon-dense chain with a trailing bracket — \
colon_credential_match may have regressed to quadratic behavior on a bracket-present \
value"
);
}
#[test]
fn test_redact_userinfo_consecutive_bracket_run_is_linear_time() {
let n = 200_000;
let input = format!("a@{}", "[".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&input);
let elapsed = start.elapsed();
assert_eq!(
redacted,
format!("***@{}", "[".repeat(n)),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_millis(300),
"took {elapsed:?} for n={n} consecutive '[' — bracket_host_shape_end may have \
regressed to quadratic behavior"
);
}
#[test]
fn test_redact_userinfo_alternating_bracket_run_is_linear_time() {
let n = 200_000;
let input = format!("a@{}", "[0".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&input);
let elapsed = start.elapsed();
assert_eq!(
redacted,
format!("***@{}", "[0".repeat(n)),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_millis(300),
"took {elapsed:?} for n={n} alternating '[0' pairs — colon_credential_match_seeded \
may have regressed to quadratic behavior on the colon rescan"
);
}
#[test]
fn test_redact_userinfo_bracket_run_then_real_host_is_still_redacted() {
let redacted = redact_userinfo("[[[::1]:glpat-SECRET");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "[[[::1]:***");
}
#[test]
fn test_redact_userinfo_bracket_run_not_anchored_at_open_does_not_leak_credential() {
assert_eq!(redact_userinfo("[:[ghp_SECRET"), "[:***");
assert_eq!(redact_userinfo("[0:0[ghp_SECRET"), "[0:***");
assert_eq!(redact_userinfo("[::1:0[glpat-SECRET"), "[:***");
assert_eq!(redact_userinfo("[1.2:0[ghp_SECRET"), "[1.2:***");
}
#[test]
fn test_bounded_has_credential_colon_multi_bracket_run_prefix_is_unchanged() {
assert!(bounded_has_credential_colon("[[[::1]:glpat-SECRET"));
assert!(bounded_has_credential_colon("[[[::1]:8443"));
}
#[test]
fn test_redact_authority_suffix_large_slash_chain_is_iterative_not_recursive() {
let n = 200_000;
let raw = format!("a@b{}user:hunter2@evil", "/x".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
!redacted.contains("hunter2"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(10),
"took {elapsed:?} for n={n} segments — may have regressed to recursive/quadratic \
behavior"
);
}
#[test]
fn test_redact_authority_suffix_mask_site_colon_credential_between_two_at_signs_is_redacted() {
let redacted = redact_userinfo("a@b/oauth2:SECRET/c@d");
assert!(!redacted.contains("SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "***@b/oauth2:***/***@d");
}
#[test]
fn test_redact_authority_suffix_mask_site_colon_credential_between_many_at_signs_is_redacted() {
let redacted = redact_userinfo("a@b/oauth2:SECRET/c@d/gitlab-ci-token:JOBTOKEN/e@f");
assert!(!redacted.contains("SECRET"), "redacted={redacted:?}");
assert!(!redacted.contains("JOBTOKEN"), "redacted={redacted:?}");
assert_eq!(redacted, "***@b/oauth2:***/***@d/gitlab-ci-token:***/***@f");
}
#[test]
fn test_redact_authority_suffix_mask_site_colon_dense_streak_is_linear_time() {
let k = 400_000;
let raw = format!("x@{}oauth2:SECRET@y", "p:q/".repeat(k));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
!redacted.contains("SECRET"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for a {k}-segment colon-dense mask-site streak — \
redact_span_colon_credential may have regressed to quadratic behavior"
);
}
#[test]
fn test_redact_authority_suffix_colon_dense_undecided_streak_after_single_at_sign_is_linear_time()
{
let n = 200_000;
let raw = format!("a@{}", "/:".repeat(n));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert!(
redacted.starts_with("***@"),
"redacted len={}",
redacted.len()
);
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for n={n} `/:`repeats after a single `@` — may have regressed to \
quadratic behavior"
);
}
#[test]
fn test_redact_userinfo_opaque_path_file_scheme_drive_letter_is_noop() {
assert_eq!(
redact_userinfo("file:///C:/Users/john.doe@corp/project"),
"file:///C:/Users/john.doe@corp/project"
);
assert_eq!(
redact_userinfo("file:///C:/feeds/pkg@1.0.0.nupkg"),
"file:///C:/feeds/pkg@1.0.0.nupkg"
);
assert_eq!(
redact_userinfo("nuget:///C:/Users/john.doe@corp/feed"),
"nuget:///C:/Users/john.doe@corp/feed"
);
}
#[test]
fn test_redact_userinfo_bracketed_ipv6_well_formed_port_is_noop() {
assert_eq!(
redact_userinfo("[::1]:8443/pkg@1.0.0"),
"[::1]:8443/pkg@1.0.0"
);
assert_eq!(
redact_userinfo("c:/[::1]:8443/pkg@1.0.0"),
"c:/[::1]:8443/pkg@1.0.0"
);
}
#[test]
fn test_redact_userinfo_bracketed_ipv6_well_formed_port_with_real_credential_elsewhere() {
let redacted = redact_userinfo("user:hunter2@[::1]:8443/pkg");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "***@[::1]:8443/pkg");
}
#[test]
fn test_redact_userinfo_bracket_not_at_scan_start_is_still_recognized() {
let redacted = redact_userinfo("x/[::1]:glpat-SECRET");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "x/[::1]:***");
}
#[test]
fn test_redact_userinfo_bracket_adjacent_non_port_value_is_redacted() {
let redacted = redact_userinfo("[::1]:glpat-SECRET");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "[::1]:***");
}
#[test]
fn test_redact_userinfo_doubled_bracket_prefix_is_still_redacted() {
let redacted = redact_userinfo("[[::1]:glpat-SECRET");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "[[::1]:***");
}
#[test]
fn test_redact_userinfo_unclosed_bracket_does_not_leak_later_credential() {
assert_eq!(redact_userinfo("[::1/user:hunter2@evil"), "***@evil");
}
#[test]
fn test_redact_userinfo_bracket_with_no_closing_bracket_anywhere_is_still_redacted() {
assert_eq!(redact_userinfo("[::1:glpat-SECRET"), "[:***");
}
#[test]
fn test_redact_userinfo_bracket_adjacent_false_positive_does_not_leak_later_credential() {
let redacted = redact_userinfo("[::1]:abc/user:glpat-SECRET");
assert!(!redacted.contains("glpat-SECRET"), "redacted={redacted:?}");
assert_eq!(redacted, "[::1]:***");
}
#[test]
fn test_redact_userinfo_orphaned_close_bracket_in_value_does_not_leak_later_credential() {
assert_eq!(redact_userinfo("[/::1]:abc/user:glpat-SECRET"), "[/:***");
}
#[test]
fn test_redact_userinfo_sticky_bracket_seen_catches_farther_orphaned_bracket() {
assert_eq!(redact_userinfo("[/::1/x]:abc/user:glpat-SECRET"), "[/:***");
}
#[test]
fn test_redact_userinfo_unclosed_bracket_before_port_like_value_is_not_exempted() {
assert_eq!(redact_userinfo("c:/[:12345@evil"), "c:***@evil");
}
#[test]
fn test_redact_userinfo_bare_unclosed_bracket_digit_gap_is_documented_collision() {
assert_eq!(redact_userinfo("[:12345"), "[:12345");
}
#[test]
fn test_redact_userinfo_opaque_path_unrelated_at_does_not_hide_later_credential() {
assert_eq!(
redact_userinfo("c:/user@host/token:glpat-SECRET"),
"c:/user@host/token:***"
);
}
#[test]
fn test_redact_userinfo_opaque_path_unrelated_at_gitlab_ci_token_is_redacted() {
let redacted = redact_userinfo("file:///home/u@corp/gitlab-ci-token:JOBTOKEN");
assert!(!redacted.contains("JOBTOKEN"), "redacted={redacted:?}");
assert_eq!(redacted, "file:///home/u@corp/gitlab-ci-token:***");
}
#[test]
fn test_redact_userinfo_opaque_path_bracket_host_stays_noop_without_guard() {
assert_eq!(
redact_userinfo("c:/[::1]:8443/pkg@1.0.0"),
"c:/[::1]:8443/pkg@1.0.0"
);
}
#[test]
fn test_redact_userinfo_opaque_path_unrelated_bracket_does_not_hide_credential() {
assert_eq!(
redact_userinfo("c:/[]/token:glpat-SECRET"),
"c:/[]/token:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_digit_prefixed_password_is_accepted_gap() {
for raw in [
"user:12345/rest@evil",
"user:12345?rest@evil",
"user:12345#rest@evil",
] {
assert_eq!(redact_userinfo(raw), raw, "raw={raw:?}");
}
}
#[test]
fn test_redact_userinfo_https_digit_after_colon_before_slash_is_not_a_credential() {
assert_eq!(
redact_userinfo("https://user:1234/secret@evil"),
"https://user:1234/secret@evil"
);
}
#[test]
fn test_redact_userinfo_unparseable_host_port_with_trailing_credential_unaffected_by_s3_fix() {
assert_eq!(
url_for_tracing("gitlab.corp:8443/user:hunter2@evil"),
"gitlab.corp:8443/user:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_host_port_registry_strings_are_not_over_redacted() {
for raw in [
"nexus.corp:8081/repo/lib@2.0.0",
"registry.corp:4873/@scope/pkg",
"localhost:8080/@types/node",
"gitlab.corp:8443/repo/pkg@1.0.0",
] {
assert_eq!(redact_userinfo(raw), raw, "raw={raw:?}");
}
}
#[test]
fn test_redact_userinfo_unparseable_ipv6_malformed_port_is_redacted() {
assert_eq!(
redact_userinfo("https://[::1]:notaport/x@y"),
"https://***@y"
);
}
#[test]
fn test_redact_userinfo_opaque_path_single_letter_username_matches_drive_letter_shape() {
assert_eq!(redact_userinfo("c:/a:/b@evil"), "c:/a:/b@evil");
}
#[test]
fn test_url_for_tracing_strips_query_string_token() {
let safe = url_for_tracing("https://npm.internal/pkg?token=super-secret-value");
assert!(!safe.contains("super-secret-value"));
assert_eq!(safe, "https://npm.internal/pkg");
}
#[test]
fn test_url_for_tracing_strips_fragment() {
assert_eq!(
url_for_tracing("https://registry.example/simple#token=x"),
"https://registry.example/simple"
);
}
#[test]
fn test_url_for_tracing_strips_both_userinfo_and_query() {
assert_eq!(
url_for_tracing("https://user:hunter2@registry.example/simple?token=x"),
"https://***@registry.example/simple"
);
}
#[test]
fn test_url_for_tracing_noop_for_plain_url() {
assert_eq!(
url_for_tracing("https://registry.example/simple"),
"https://registry.example/simple"
);
}
#[test]
fn test_url_for_tracing_unparseable_with_no_redactable_shape_is_unchanged() {
assert_eq!(url_for_tracing("not-a-url-at-all"), "not-a-url-at-all");
}
#[test]
fn test_url_for_tracing_unparseable_still_truncates_query_and_redacts_userinfo() {
assert_eq!(
url_for_tracing("not-a-url?token=super-secret-value"),
"not-a-url"
);
assert_eq!(
url_for_tracing("user:hunter2@registry.example/simple?token=x"),
"***@registry.example/simple"
);
}
#[test]
fn test_redact_userinfo_secondary_colon_credential_in_tail_after_at_sign() {
assert_eq!(
redact_userinfo("https://user:hunter2@evil/tok:SECRET"),
"https://***@evil/tok:***"
);
}
#[test]
fn test_redact_userinfo_secondary_colon_credential_in_tail_mask_at_fallbacks() {
assert_eq!(
redact_userinfo("c:/user:hunter2@evil/tok:SECRET"),
"c:***@evil/tok:***"
);
assert_eq!(
redact_userinfo("c:///user:hunter2@evil/tok:SECRET"),
"c:***@evil/tok:***"
);
}
#[test]
fn test_redact_userinfo_unparseable_colon_only_credential_reachable_with_no_scheme_prefix_is_redacted()
{
assert_eq!(
redact_userinfo("not-a-url:user:hunter2@evil/tok:SECRET"),
"***@evil/tok:***"
);
}
#[test]
fn test_url_for_tracing_truncates_before_redacting_query_credential_at_sign() {
assert_eq!(
url_for_tracing("c:/path?u=user:pw@a&token=SUPERSECRET"),
"c:/path"
);
assert_eq!(
url_for_tracing("c:/path#u=user:pw@a&token=SUPERSECRET"),
"c:/path"
);
}
#[test]
fn test_url_for_tracing_masks_token_shaped_host_and_path_pieces() {
assert_eq!(
url_for_tracing("https://npm-proxy.fury.io/AbCdEf1234567890xyz/acme/"),
"https://npm-proxy.fury.io/***/acme/"
);
assert_eq!(
url_for_tracing("https://dl.cloudsmith.io/Xy9KqP2mZr8Lw4Tn/repo/deb/"),
"https://dl.cloudsmith.io/***/repo/deb/"
);
assert_eq!(
url_for_tracing("https://registry.example/ghp_1a2b3c4d5e6f7g8h9i/simple"),
"https://registry.example/***/simple"
);
assert_eq!(
url_for_tracing("https://gitlab.example/glpat-1a2b3c4d5e6f7g8h9i/simple"),
"https://gitlab.example/***/simple"
);
assert_eq!(
url_for_tracing(
"https://registry.npmjs.org/_authToken=npm_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6/"
),
"https://registry.npmjs.org/_authToken=***/"
);
assert_eq!(
url_for_tracing("https://registry.example/tok;glpat-1a2b3c4d5e6f7g8h9i/simple"),
"https://registry.example/tok;***/simple"
);
assert_eq!(
url_for_tracing("https://ghp_1a2b3c4d5e6f7g8h9i.registry.example/"),
"https://***.registry.example/"
);
}
#[test]
fn test_url_for_tracing_masks_1432_token_shapes() {
assert_eq!(
url_for_tracing(
"https://packagecloud.io/priv/0123456789abcdef0123456789abcdef0123456789abcdef/acme/repo/pypi/simple"
),
"https://packagecloud.io/priv/***/acme/repo/pypi/simple"
);
assert_eq!(
url_for_tracing(
"https://dl.cloudsmith.io/Xy9-KqP2_mZr8Lw4Tn6Q/acme/repo/python/simple/"
),
"https://dl.cloudsmith.io/***/acme/repo/python/simple/"
);
assert_eq!(
url_for_tracing("https://gitlab.example.com/glcbt-AbCdEfGh1234567890xyzWQR/x"),
"https://gitlab.example.com/***/x"
);
}
#[test]
fn test_url_for_tracing_does_not_mask_short_dashed_package_names() {
assert_eq!(
url_for_tracing("https://pypi.org/simple/PyQtWebEngine-Qt5/"),
"https://pypi.org/simple/PyQtWebEngine-Qt5/"
);
assert_eq!(
url_for_tracing("https://pypi.org/pypi/PyQt6-Charts-Qt6/json"),
"https://pypi.org/pypi/PyQt6-Charts-Qt6/json"
);
}
#[test]
fn test_url_for_tracing_does_not_mask_hex_path_piece_below_marker_floor() {
assert_eq!(
url_for_tracing(
"https://github.example/repo/commit/abcdef0123456789abcdef0123456789abcdef01"
),
"https://github.example/repo/commit/abcdef0123456789abcdef0123456789abcdef01"
);
assert_eq!(
url_for_tracing(
"https://registry.example/token/abcdef0123456789abcdef0123456789abcdef01"
),
"https://registry.example/token/abcdef0123456789abcdef0123456789abcdef01"
);
}
#[test]
fn test_url_for_tracing_masks_host_label_token_prefix() {
assert_eq!(
url_for_tracing("https://u:p@ghp_1a2b3c4d5e6f7g8h9i.host/"),
"https://***@***.host/"
);
}
#[test]
fn test_url_for_tracing_uppercase_host_label_token_is_accepted_miss() {
assert_eq!(
url_for_tracing("https://GHP_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r.host/"),
"https://GHP_1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r.host/"
);
}
#[test]
fn test_url_for_tracing_masks_host_label_token_in_query_truncated_branch() {
assert_eq!(
url_for_tracing("https://ghp_1a2b3c4d5e6f7g8h9i.host/?q=1"),
"https://***.host/"
);
}
#[test]
fn test_url_for_tracing_public_registry_urls_are_noop() {
let urls = [
"https://registry.npmjs.org/lodash",
"https://pypi.org/simple/requests/",
"https://api.nuget.org/v3/index.json",
"https://proxy.golang.org/github.com/pkg/errors/@v/list",
"https://repo1.maven.org/maven2/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar",
"https://index.crates.io/se/rd/serde",
"https://api.github.com/repos/rust-lang/rust",
"https://gitlab.com/api/v4/projects",
"https://api.osv.dev/v1/query",
"https://deps.dev/_/s/npm/p/lodash",
"https://files.pythonhosted.org/packages/9d/be/10918a2eac4ae9f02f6cfe6414b7a155ccd8f7f9d4380d62c95dd6ddaca/requests-2.31.0.tar.gz",
"https://asia-east1-docker.pkg.dev/my-project/npm-registry/pkg",
"https://asia-northeast1-npm.pkg.dev/my-project/npm-registry/",
"https://acca-mirror1.example.org/simple",
"https://sk-mirror01.example.com/simple",
"https://static.crates.io/crates/sk-lang/sk-lang-1.2.3.crate",
"https://registry.npmjs.org/sk-foo/-/sk-foo-1.2.3.tgz",
"https://files.pythonhosted.org/packages/aa/bb/pypi-simple-1.4.0.tar.gz",
"https://registry.npmjs.org/npm_package2/-/npm_package2-10.0.0.tgz",
];
for url in urls {
assert_eq!(url_for_tracing(url), url, "url={url:?}");
}
}
#[test]
fn test_url_for_tracing_package_identifiers_are_noop() {
assert_eq!(
url_for_tracing("https://docs.rs/sk-lang@1.2.3"),
"https://docs.rs/sk-lang@1.2.3"
);
assert_eq!(url_for_tracing("@types/node"), "@types/node");
}
#[test]
fn test_url_for_tracing_neutralizes_control_characters() {
for forged in [
"lodash\nERROR deps_lsp: FORGED\x1b[31mRED",
"ok\r\nWARN forged2",
] {
let rendered = url_for_tracing(forged);
assert!(
!rendered.contains('\n'),
"{rendered:?} must not contain \\n"
);
assert!(
!rendered.contains('\r'),
"{rendered:?} must not contain \\r"
);
assert!(
!rendered.contains('\x1b'),
"{rendered:?} must not contain ESC"
);
}
}
#[test]
fn test_url_for_tracing_neutralizes_control_characters_alongside_credential_redaction() {
let rendered =
url_for_tracing("https://user:hunter2@registry.example/simple\n\x1binjected");
assert!(!rendered.contains('\n'));
assert!(!rendered.contains('\x1b'));
assert!(!rendered.contains("hunter2"));
}
#[test]
fn test_url_for_tracing_bracketed_ipv6_and_port_host_are_noop() {
assert_eq!(
url_for_tracing("https://[::1]:8443/x"),
"https://[::1]:8443/x"
);
assert_eq!(
url_for_tracing("https://host:8443/x"),
"https://host:8443/x"
);
}
#[test]
fn test_redact_userinfo_drive_letter_dense_streak_is_linear_time() {
let k = 400_000;
let raw = format!("{}@x", "c:/".repeat(k));
let start = std::time::Instant::now();
let redacted = redact_userinfo(&raw);
let elapsed = start.elapsed();
assert_eq!(redacted, raw, "redacted len={}", redacted.len());
assert!(
elapsed < std::time::Duration::from_secs(5),
"took {elapsed:?} for a {k}-segment drive-letter-dense run before a single `@` — \
segment_has_credential_colon may have regressed to quadratic behavior"
);
}
#[test]
fn test_redact_userinfo_dense_drive_letter_run_before_unrelated_at_is_noop() {
let raw = format!("{}@x", "c:/".repeat(50));
assert_eq!(redact_userinfo(&raw), raw);
}
#[test]
fn test_redact_userinfo_drive_letter_run_then_bracket_is_noop() {
assert_eq!(
redact_userinfo("c:/c:/c:/[::1]:8443/pkg@1.0.0"),
"c:/c:/c:/[::1]:8443/pkg@1.0.0"
);
}
#[test]
fn test_redact_userinfo_drive_letter_run_then_real_credential_is_redacted() {
let redacted = redact_userinfo("c:/c:/user:hunter2@evil");
assert!(!redacted.contains("hunter2"), "redacted={redacted:?}");
assert_eq!(redacted, "c:***@evil");
}
#[test]
fn test_redact_userinfo_no_colon_no_bracket_segment_is_noop() {
assert_eq!(redact_userinfo("path/to/pkg@1.0.0"), "path/to/pkg@1.0.0");
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_c_drive_path_credential_is_redacted() {
assert_eq!(
redact_userinfo("https://c:/c:/user:glpat-SECRET@h/p"),
"https://c:/c:/user:***/p"
);
assert_eq!(
redact_userinfo("https://c:/[::1:glpat-SECRET@h/p"),
"https://c:/[:***"
);
assert_eq!(
redact_userinfo("https://c:/[[[[:glpat-SECRET@h/p"),
"https://c:/[[[[:***"
);
assert_eq!(
redact_userinfo("https://c:/[/::1]:abc/user:SECRET@h/p"),
"https://c:/[/:***"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_ordinary_host_path_credential_is_redacted()
{
assert_eq!(
redact_userinfo("https://example.com/api/user:glpat-SECRET@internal"),
"https://example.com/api/user:***"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_no_credential_shape_is_noop() {
assert_eq!(
redact_userinfo("https://docs.rs/sk-lang@1.2.3"),
"https://docs.rs/sk-lang@1.2.3"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_no_colon_in_path_is_noop() {
for raw in [
"https://example.com/no/colon/at/all",
"https://example.com",
"https://example.com/",
] {
assert_eq!(redact_userinfo(raw), raw, "raw={raw:?}");
}
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_query_param_without_colon_is_noop() {
assert_eq!(
redact_userinfo("https://example.com/search?token=glpat-SECRET"),
"https://example.com/search?token=glpat-SECRET"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_colon_credential_in_query_is_redacted() {
assert_eq!(
redact_userinfo("https://example.com/path?token:SECRET@foo"),
"https://example.com/path?token:***"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_slashless_scheme_credential_before_later_scheme_is_redacted()
{
assert_eq!(
redact_userinfo("https:example.com/user:glpat-SECRET@h/x?r=http://y"),
"https:example.com/user:***/x?r=http://y"
);
assert_eq!(
redact_userinfo("https:h/oauth2:glpat-SECRET/x?r=http://y"),
"https:h/oauth2:***/x?r=http://y"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_slashless_scheme_no_credential_is_noop() {
assert_eq!(redact_userinfo("https:example.com"), "https:example.com");
assert_eq!(
redact_userinfo("https:example.com/pkg"),
"https:example.com/pkg"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_port_and_ipv6_host_is_noop() {
assert_eq!(
redact_userinfo("https://host:8443/x"),
"https://host:8443/x"
);
assert_eq!(
redact_userinfo("https://[::1]:8443/x"),
"https://[::1]:8443/x"
);
}
#[test]
fn test_redact_userinfo_host_present_empty_userinfo_colon_credential_in_fragment_is_redacted() {
assert_eq!(
redact_userinfo("https://example.com/x#u=user:SECRET@evil"),
"https://example.com/x#u=user:***"
);
}
}