fn mask_authority_windows(input: &str) -> String {
let mut out = input.to_string();
let mut windows = authority_windows(input);
windows.sort_unstable();
windows.dedup();
for (start, end) in windows.into_iter().rev() {
if let Some(at) = out[start..end].rfind('@') {
out.replace_range(start..start + at, "***");
}
}
out
}
fn authority_windows(input: &str) -> Vec<(usize, usize)> {
let bytes = input.as_bytes();
let mut windows = Vec::new();
let mut i = 0;
while i < bytes.len() {
if !matches!(bytes[i], b'/' | b'\\') {
i += 1;
continue;
}
let run_start = i;
let mut has_backslash = false;
while i < bytes.len() && matches!(bytes[i], b'/' | b'\\') {
has_backslash |= bytes[i] == b'\\';
i += 1;
}
let run_len = i - run_start;
let opens = if has_backslash {
match scheme_prefix_len(input, run_start) {
None => false,
Some(scheme_len) => {
run_len >= 2
|| scheme_len >= 2
|| (scheme_len == 1 && single_backslash_credential_gate(input, i))
}
}
} else {
run_len >= 2
};
if opens {
let end = input[i..]
.find(['/', '?', '#'])
.map_or(input.len(), |offset| i + offset);
let start = i;
windows.push((start, end));
}
}
windows
}
fn scheme_prefix_len(input: &str, run_start: usize) -> Option<usize> {
let bytes = input.as_bytes();
if run_start < 2 || bytes[run_start - 1] != b':' {
return None;
}
let mut start = run_start - 1;
while start > 0
&& (bytes[start - 1].is_ascii_alphanumeric()
|| matches!(bytes[start - 1], b'+' | b'.' | b'-'))
{
start -= 1;
}
let len = run_start - 1 - start;
if len == 0 || !bytes[start].is_ascii_alphabetic() {
return None;
}
Some(len)
}
fn single_backslash_credential_gate(input: &str, content_start: usize) -> bool {
let end = input[content_start..]
.find(['/', '?', '#'])
.map_or(input.len(), |offset| content_start + offset);
let window = &input[content_start..end];
match window.rfind('@') {
Some(at) => window[..at].contains(':'),
None => false,
}
}
fn truncate_utf8_safe(s: &mut String, max: usize) {
if s.len() <= max {
return;
}
let mut cut = max;
while !s.is_char_boundary(cut) {
cut -= 1;
}
s.truncate(cut);
}
fn percent_scan(bytes: &[u8], decode: impl Fn(u8, u8) -> Option<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%'
&& i + 2 < bytes.len()
&& let Some(b) = decode(bytes[i + 1], bytes[i + 2])
{
out.push(b);
i += 3;
continue;
}
out.push(bytes[i]);
i += 1;
}
out
}
fn minimal_decode_pair(pair: &str) -> String {
let decoded = percent_scan(pair.as_bytes(), |hi, lo| match (hi, lo) {
(b'4', b'0') => Some(b'@'),
(b'3', b'a' | b'A') => Some(b':'),
(b'2', b'f' | b'F') => Some(b'/'),
_ => None,
});
String::from_utf8_lossy(&decoded).into_owned()
}
fn is_credential_shaped(decoded: &str) -> bool {
decoded.contains('@') && (decoded.contains(':') || decoded.contains("//"))
}
fn decode_match_key(raw_key: &str) -> String {
let decoded = percent_scan(raw_key.as_bytes(), |hi, lo| {
let hi = (hi as char).to_digit(16)?;
let lo = (lo as char).to_digit(16)?;
Some((hi * 16 + lo) as u8)
});
String::from_utf8_lossy(&decoded).to_lowercase()
}
pub fn redact_url(raw: &str) -> String {
let mut out = mask_authority_windows(raw);
if let Some(i) = out.find(['?', '#']) {
let query_pos = out.find('?');
let fragment_pos = out.find('#');
out.truncate(i);
let sentinel_total = match (query_pos, fragment_pos) {
(Some(_), Some(_)) => 22,
(Some(_), None) | (None, Some(_)) => 11,
(None, None) => 0,
};
if sentinel_total > 0 {
truncate_utf8_safe(&mut out, 256 - sentinel_total);
}
match (query_pos, fragment_pos) {
(Some(q), Some(f)) if f < q => out.push_str("#[redacted]?[redacted]"),
(Some(_), Some(_)) => out.push_str("?[redacted]#[redacted]"),
(Some(_), None) => out.push_str("?[redacted]"),
(None, Some(_)) => out.push_str("#[redacted]"),
(None, None) => {}
}
}
truncate_utf8_safe(&mut out, 256);
out
}
pub fn redact_url_fail_closed(raw: &str) -> String {
if window_has_at_sign(raw) {
"[redacted]".to_string()
} else {
redact_url(raw)
}
}
pub fn redact_url_with_query_allowlist(raw: &str, sensitive_key_substrings: &[&str]) -> String {
let after_userinfo = mask_authority_windows(raw);
let mut out = match after_userinfo.split_once('?') {
Some((base, query)) => {
let redacted: Vec<String> = query
.split('&')
.map(|pair| {
let raw_key = pair.split('=').next().unwrap_or(pair);
let match_key = decode_match_key(raw_key);
if sensitive_key_substrings
.iter()
.any(|s| match_key.contains(s))
{
if is_credential_shaped(&minimal_decode_pair(raw_key)) {
"<redacted>".to_string()
} else {
format!("{raw_key}=<redacted>")
}
} else if is_credential_shaped(&minimal_decode_pair(pair)) {
"<redacted>".to_string()
} else {
pair.to_string()
}
})
.collect();
format!("{base}?{}", redacted.join("&"))
}
None => after_userinfo,
};
if let Some(i) = out.find('#') {
out.truncate(i);
truncate_utf8_safe(&mut out, 256 - 11);
out.push_str("#[redacted]");
}
truncate_utf8_safe(&mut out, 256);
out
}
pub fn window_has_at_sign(raw: &str) -> bool {
authority_windows(raw)
.into_iter()
.any(|(start, end)| raw[start..end].contains('@'))
}
#[cfg(test)]
mod tests {
use super::*;
const JMS_KEYS: &[&str] = &[
"password",
"passwd",
"secret",
"credential",
"token",
"username",
"user",
];
#[test]
fn strict_masks_windows_and_composes_sentinels() {
assert_eq!(
redact_url("https://user:pass@h/p?a=1#t=x"),
"https://***@h/p?[redacted]#[redacted]"
);
assert_eq!(redact_url("https://h//u2:p2@evil/"), "https://h//***@evil/");
}
#[test]
fn fail_closed_suppresses_window_with_at() {
assert_eq!(
redact_url_fail_closed("http://u:secretpw@host:99999/x"),
"[redacted]"
);
}
#[test]
fn sentinel_never_splits_at_cap() {
let mut url = format!("https://host/{}日{}", "x".repeat(220), "x".repeat(80));
url.push_str("?a=1#f");
assert!(url.len() > 300);
let redacted = redact_url(&url);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(
redacted.ends_with("?[redacted]#[redacted]"),
"sentinels must render intact: {redacted}"
);
assert!(std::str::from_utf8(redacted.as_bytes()).is_ok());
assert!(
!redacted.contains('日'),
"straddling char dropped whole: {redacted}"
);
assert!(
redacted.starts_with("https://host/"),
"host kept: {redacted}"
);
}
#[test]
fn idempotent_mask_composition_pin() {
assert_eq!(
redact_url("https://***@host/p?a=1#f"),
"https://***@host/p?[redacted]#[redacted]"
);
}
#[test]
fn allowlist_keeps_benign_and_redacts_sensitive() {
assert_eq!(
redact_url_with_query_allowlist(
"tcp://host:61616?password=p&user=u&keepAlive=true",
JMS_KEYS
),
"tcp://host:61616?password=<redacted>&user=<redacted>&keepAlive=true"
);
}
#[test]
fn allowlist_masks_percent_encoded_credentials_uppercase() {
let redacted = redact_url_with_query_allowlist(
"tcp://h:61616?redirect=http%3A%2F%2Fuser%3Asecret%40host",
JMS_KEYS,
);
assert!(
!redacted.contains("secret"),
"percent-encoded credential leaked: {redacted}"
);
assert!(
!redacted.contains("user%3Asecret%40"),
"encoded userinfo leaked: {redacted}"
);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
}
#[test]
fn allowlist_masks_percent_encoded_credentials_lowercase() {
let redacted = redact_url_with_query_allowlist(
"tcp://h:61616?redirect=http%3a%2f%2fuser%3asecret%40host",
JMS_KEYS,
);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
assert!(
!redacted.contains("secret"),
"percent-encoded credential leaked: {redacted}"
);
}
#[test]
fn allowlist_suppresses_credential_shaped_key_uppercase() {
let redacted =
redact_url_with_query_allowlist("tcp://h:61616?user%3Asecret%40host=1", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
assert!(
!redacted.contains("user%3Asecret%40"),
"credential-shaped key leaked: {redacted}"
);
assert!(
!redacted.contains("secret"),
"key credential bytes leaked: {redacted}"
);
}
#[test]
fn allowlist_suppresses_credential_shaped_key_lowercase() {
let redacted =
redact_url_with_query_allowlist("tcp://h:61616?user%3asecret%40host=1", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
}
#[test]
fn allowlist_suppresses_literal_credential_shaped_key() {
let redacted = redact_url_with_query_allowlist("tcp://h:61616?user:pass@host=1", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
assert!(
!redacted.contains("user:pass"),
"literal key credentials leaked: {redacted}"
);
}
#[test]
fn allowlist_keeps_well_known_key_names_visible() {
let redacted = redact_url_with_query_allowlist(
"tcp://h:61616?password=p&jms.userName=admin&user=u&keepAlive=true",
JMS_KEYS,
);
assert_eq!(
redacted,
"tcp://h:61616?password=<redacted>&jms.userName=<redacted>&user=<redacted>&keepAlive=true"
);
}
#[test]
fn allowlist_keeps_lone_at_key_visible() {
let redacted = redact_url_with_query_allowlist("tcp://h:61616?user@host=1", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?user@host=<redacted>");
}
#[test]
fn allowlist_keeps_encoded_but_benign_shaped_key_visible() {
let redacted = redact_url_with_query_allowlist("tcp://h:61616?pass%77ord=p", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?pass%77ord=<redacted>");
}
#[test]
fn allowlist_masks_literal_at_bypass() {
let redacted =
redact_url_with_query_allowlist("tcp://h:61616?next=%2F%2Fuser:pass@host", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
assert!(
!redacted.contains("pass"),
"literal credential leaked: {redacted}"
);
}
#[test]
fn allowlist_masks_fully_literal_credential_pair() {
let redacted =
redact_url_with_query_allowlist("tcp://h:61616?next=user:pass@host", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
assert!(
!redacted.contains("pass"),
"literal credential leaked: {redacted}"
);
}
#[test]
fn allowlist_keeps_lone_email_value() {
assert_eq!(
redact_url_with_query_allowlist("tcp://h:61616?contact=admin%40corp.example", JMS_KEYS),
"tcp://h:61616?contact=admin%40corp.example"
);
}
#[test]
fn allowlist_masks_credential_shaped_userhostport() {
let redacted =
redact_url_with_query_allowlist("tcp://h:61616?next=user%40host%3Aport", JMS_KEYS);
assert_eq!(redacted, "tcp://h:61616?<redacted>");
}
#[test]
fn allowlist_decodes_percent_encoded_sensitive_key() {
let redacted =
redact_url_with_query_allowlist("tcp://host:61616?pass%77ord=shortsecret", JMS_KEYS);
assert!(
redacted.contains("pass%77ord=<redacted>"),
"encoded key must redact keeping original bytes: {redacted}"
);
assert!(
!redacted.contains("shortsecret"),
"secret value leaked: {redacted}"
);
}
#[test]
fn minimal_decode_preserves_non_ascii_bytes() {
assert_eq!(minimal_decode_pair("café"), "café");
assert_eq!(minimal_decode_pair("café%40x"), "café@x");
assert_eq!(decode_match_key("a%FFb"), "a\u{FFFD}b");
}
#[test]
fn redact_url_keeps_userinfo_mask_shape() {
assert_eq!(
redact_url("redis://user:secret@h:6379"),
"redis://***@h:6379"
);
}
#[test]
fn redact_url_drops_query_secrets() {
assert_eq!(
redact_url("redis://h:6379/0?password=hunter2"),
"redis://h:6379/0?[redacted]"
);
}
#[test]
fn redact_url_drops_fragment() {
assert_eq!(
redact_url("redis://h:6379/0#tok=x"),
"redis://h:6379/0#[redacted]"
);
}
#[test]
fn redact_url_masks_through_last_at() {
assert_eq!(redact_url("redis://user:p@ss@h:6379"), "redis://***@h:6379");
}
#[test]
fn redact_url_slash_run_evader_masked() {
assert_eq!(
redact_url("redis:////user:pass@h:6379/0"),
"redis:////***@h:6379/0"
);
}
#[test]
fn redact_url_at_outside_window_visible() {
assert_eq!(
redact_url("redis://h:6379/0/user@x"),
"redis://h:6379/0/user@x"
);
}
#[test]
fn redact_url_later_window_masked() {
assert_eq!(redact_url("redis://h//user:pass@x/"), "redis://h//***@x/");
}
#[test]
fn redact_url_sentinels_compose_both() {
assert_eq!(
redact_url("redis://h:6379/0?password=x#tok=y"),
"redis://h:6379/0?[redacted]#[redacted]"
);
}
#[test]
fn redact_url_sentinels_compose_fragment_first() {
assert_eq!(
redact_url("redis://h:6379/0#tok=y?password=x"),
"redis://h:6379/0#[redacted]?[redacted]"
);
}
#[test]
fn redact_url_truncates_256_utf8_safe() {
let mut url = format!("redis://{}{}", "x".repeat(246), '日');
url.push_str(&"tail".repeat(20));
assert!(url.len() > 300);
let redacted = redact_url(&url);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(redacted.starts_with("redis://"));
}
#[test]
fn redact_url_keeps_sentinel_intact_under_256_cap() {
let url = format!("http://{}?x=1", "a".repeat(240));
let redacted = redact_url(&url);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(
redacted.ends_with("?[redacted]"),
"sentinel must render intact: {redacted}"
);
}
#[test]
fn redact_broker_url_masks_userinfo_and_sensitive_query() {
let redacted = redact_url_with_query_allowlist(
"tcp://admin:secretpass@broker.example.com:61616",
JMS_KEYS,
);
assert!(
!redacted.contains("secretpass"),
"password masked: {redacted}"
);
assert!(
redacted.contains("broker.example.com"),
"host visible: {redacted}"
);
let redacted = redact_url_with_query_allowlist(
"failover:(tcp://host:61616)?jms.userName=admin&jms.password=secret&keepAlive=true",
JMS_KEYS,
);
assert!(
!redacted.contains("secret"),
"password param masked: {redacted}"
);
assert!(
!redacted.contains("=admin"),
"username param masked: {redacted}"
);
assert!(
redacted.contains("keepAlive=true"),
"benign param kept: {redacted}"
);
assert_eq!(
redact_url_with_query_allowlist("tcp://host:61616", JMS_KEYS),
"tcp://host:61616"
);
}
#[test]
fn redact_exact_userinfo_mask() {
assert_eq!(
redact_url_with_query_allowlist(
"tcp://admin:secretpass@broker.example.com:61616",
JMS_KEYS
),
"tcp://***@broker.example.com:61616"
);
}
#[test]
fn redact_exact_query_join() {
assert_eq!(
redact_url_with_query_allowlist(
"tcp://host:61616?password=p&user=u&keepAlive=true",
JMS_KEYS
),
"tcp://host:61616?password=<redacted>&user=<redacted>&keepAlive=true"
);
}
#[test]
fn redact_exact_bare_at_passthrough() {
assert_eq!(
redact_url_with_query_allowlist("admin@host", JMS_KEYS),
"admin@host"
);
}
#[test]
fn redact_exact_failover_param_boundaries() {
let redacted = redact_url_with_query_allowlist(
"failover:(tcp://host:61616)?jms.userName=admin&jms.password=secret&keepAlive=true",
JMS_KEYS,
);
let (_, query) = redacted.split_once('?').expect("query segment after '?'");
assert_eq!(
query,
"jms.userName=<redacted>&jms.password=<redacted>&keepAlive=true"
);
}
#[test]
fn redact_broker_url_query_at_no_misfire() {
assert_eq!(
redact_url_with_query_allowlist("failover:(tcp://h:61616)?x=a@b", JMS_KEYS),
"failover:(tcp://h:61616)?x=a@b"
);
}
#[test]
fn redact_exact_slash_run_window_composition() {
assert_eq!(
redact_url_with_query_allowlist("tcp:////user:pass@h:61616?keepAlive=true", JMS_KEYS),
"tcp:////***@h:61616?keepAlive=true"
);
}
#[test]
fn redact_exact_last_at_in_window() {
assert_eq!(
redact_url_with_query_allowlist("tcp://u:p@a@h:61616", JMS_KEYS),
"tcp://***@h:61616"
);
}
#[test]
fn redact_broker_url_truncate_multibyte_boundary() {
let mut url = String::from("tcp://broker:61616/");
url.push_str(&"x".repeat(236)); url.push('日'); url.push_str(&"y".repeat(50)); assert!(url.len() > 300);
let redacted = redact_url_with_query_allowlist(&url, JMS_KEYS);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(std::str::from_utf8(redacted.as_bytes()).is_ok());
assert!(
!redacted.contains('日'),
"straddling char dropped whole: {redacted}"
);
}
#[test]
fn redact_broker_url_later_window_masked() {
assert_eq!(
redact_url_with_query_allowlist("tcp://h//user:pass@x/?keepAlive=true", JMS_KEYS),
"tcp://h//***@x/?keepAlive=true"
);
}
#[test]
fn redact_broker_url_drops_fragment() {
assert_eq!(
redact_url_with_query_allowlist("tcp://h:61616?keepAlive=true#tok=x", JMS_KEYS),
"tcp://h:61616?keepAlive=true#[redacted]"
);
}
#[test]
fn redact_broker_url_truncates() {
let mut url = format!("tcp://broker:61616/{}", "x".repeat(300));
url.push('日');
url.push_str(&"tail".repeat(20));
assert!(url.len() > 300);
let redacted = redact_url_with_query_allowlist(&url, JMS_KEYS);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(redacted.starts_with("tcp://broker:61616/"));
}
#[test]
fn redact_url_strips_userinfo_with_password() {
assert_eq!(
redact_url("tcp://admin:s3cret@broker:61616"),
"tcp://***@broker:61616"
);
}
#[test]
fn redact_url_strips_userinfo_without_password() {
assert_eq!(
redact_url("tcp://admin@broker:61616"),
"tcp://***@broker:61616"
);
}
#[test]
fn redact_url_passes_clean_url_unchanged() {
assert_eq!(redact_url("tcp://localhost:61616"), "tcp://localhost:61616");
}
#[test]
fn redact_url_handles_ssl_scheme() {
assert_eq!(
redact_url("ssl://user:pass@secure-broker:61617"),
"ssl://***@secure-broker:61617"
);
}
#[test]
fn redact_url_drops_query_and_fragment() {
assert_eq!(
redact_url("tcp://broker:61616?user=a#tok=x"),
"tcp://broker:61616?[redacted]#[redacted]"
);
}
#[test]
fn jms_redact_url_sentinels_compose_fragment_first() {
assert_eq!(
redact_url("tcp://broker:61616#tok=x?user=a"),
"tcp://broker:61616#[redacted]?[redacted]"
);
}
#[test]
fn jms_redact_url_later_window_masked() {
assert_eq!(redact_url("tcp://h//user:pass@x/"), "tcp://h//***@x/");
}
#[test]
fn redact_url_slash_run_masked() {
let redacted = redact_url("tcp:////user:pass@broker:61616");
assert!(!redacted.contains("user:pass"), "leaked: {redacted}");
assert!(
redacted.contains("***@broker:61616"),
"masked in place: {redacted}"
);
}
#[test]
fn redact_url_at_in_query_not_userinfo_mask() {
assert_eq!(
redact_url("tcp://broker:61616?q=a@b"),
"tcp://broker:61616?[redacted]"
);
}
#[test]
fn redact_url_truncate_multibyte_boundary() {
let mut url = String::from("tcp://broker:61616/");
url.push_str(&"x".repeat(236)); url.push('日'); url.push_str(&"y".repeat(50)); assert!(url.len() > 300);
let redacted = redact_url(&url);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(std::str::from_utf8(redacted.as_bytes()).is_ok());
assert!(
!redacted.contains('日'),
"straddling char dropped whole: {redacted}"
);
}
#[test]
fn redact_url_keeps_sentinels_intact_under_256_cap() {
let url = format!("tcp://{}?x=1", "a".repeat(250));
let redacted = redact_url(&url);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(
redacted.ends_with("?[redacted]"),
"redact_url sentinel must render intact: {redacted}"
);
let broker = format!("tcp://{}?keep=1#frag", "a".repeat(240));
let redacted = redact_url_with_query_allowlist(&broker, JMS_KEYS);
assert!(redacted.len() <= 256, "len={}", redacted.len());
assert!(
redacted.ends_with("#[redacted]"),
"allowlist sentinel must render intact: {redacted}"
);
}
#[test]
fn redact_url_unparseable_fragment_credentials_dropped() {
let raw = "ht tps://app.example/cb#access_token=SECRET";
let redacted = redact_url(raw);
assert!(
!redacted.contains("SECRET"),
"unparseable fragment token leaked: {redacted}"
);
assert!(
!redacted.contains("access_token"),
"unparseable fragment bytes leaked: {redacted}"
);
assert!(
redacted.contains("#[redacted]"),
"unparseable fragment must end in the sentinel: {redacted}"
);
}
#[test]
fn redact_url_empty_host_userinfo_sentinel() {
let redacted = redact_url_fail_closed("scheme://user@");
assert_eq!(
redacted, "[redacted]",
"empty-host userinfo must fail closed: {redacted}"
);
}
#[test]
fn redact_url_unparseable_slash_run_evader_sentinel() {
let raw = "schem e:////user:pass@evil/";
let redacted = redact_url_fail_closed(raw);
assert_eq!(
redacted, "[redacted]",
"unparseable slash-run evader must fail closed: {redacted}"
);
}
#[test]
fn redact_url_unparseable_later_window_userinfo_sentinel() {
let raw = "http://ho st/a//user:pass@evil/";
let redacted = redact_url_fail_closed(raw);
assert_eq!(
redacted, "[redacted]",
"userinfo in a later // window must fail closed: {redacted}"
);
}
#[test]
fn redact_url_truncates_unparseable() {
let long = "x".repeat(1000);
let redacted = redact_url(&long);
assert_eq!(redacted.len(), 256, "unparseable URL must be truncated");
}
#[test]
fn redact_url_suppresses_unparseable_authority_credentials() {
let fixtures = [
"http://u:secretpw@/x",
"http://u:secretpw@host:99999/x",
"http://u:secretpw@host:99999",
"//u:secretpw@h/x",
];
for fixture in fixtures {
assert_eq!(
redact_url_fail_closed(fixture),
"[redacted]",
"credential-bearing authority must be suppressed: {fixture}"
);
}
}
#[test]
fn redact_url_unparseable_query_redacted_short_and_long() {
let short = "http://host:99999/path?token=shortsecret";
let redacted = redact_url(short);
assert_eq!(
redacted, "http://host:99999/path?[redacted]",
"short unparseable query must end with the suffix: {redacted}"
);
let mut long = String::from("http://host:99999/");
long.push_str(&"a".repeat(300));
long.push_str("?token=longsecret");
let redacted = redact_url(&long);
assert!(
!redacted.contains("longsecret"),
"long unparseable query leaked a query byte: {redacted}"
);
assert!(
redacted.len() <= 256,
"long unparseable query must be capped: {} bytes",
redacted.len()
);
}
#[test]
fn redact_url_unparseable_sentinels_compose_both() {
let raw = "ht tp://h.example/p?a=1#tok=x";
assert_eq!(
redact_url(raw),
"ht tp://h.example/p?[redacted]#[redacted]",
"query and fragment sentinels must compose: {raw}"
);
}
#[test]
fn redact_url_unparseable_sentinels_compose_fragment_first() {
let raw = "ht tp://h.example/p#tok=x?a=1";
assert_eq!(
redact_url(raw),
"ht tp://h.example/p#[redacted]?[redacted]",
"sentinels must follow the introducers' first-occurrence order: {raw}"
);
}
#[test]
fn redact_url_unparseable_utf8_straddle_no_panic() {
let fixture = format!("a{}", "é".repeat(200));
let redacted = redact_url(&fixture);
assert!(
redacted.len() <= 256,
"straddle fixture must be capped: {} bytes",
redacted.len()
);
assert!(
redacted.len() >= 253,
"straddle fixture must not over-truncate: {} bytes",
redacted.len()
);
assert!(
fixture.is_char_boundary(redacted.len()),
"cut must land on a UTF-8 char boundary: {} bytes",
redacted.len()
);
}
#[test]
fn redact_url_at_sign_outside_authority_window_visible() {
let at_sign_in_path = "http://host:99999/x@y";
assert_eq!(
redact_url_fail_closed(at_sign_in_path),
at_sign_in_path,
"at-sign in path must not be suppressed"
);
assert_eq!(
redact_url("mailto:user@example.com"),
"mailto:user@example.com",
"at-sign in mailto must round-trip byte-identically"
);
}
#[test]
fn backslash_run_non_special_scheme_masked() {
assert_eq!(
redact_url("foo:\\user:pass@evil/"),
"foo:\\***@evil/",
"non-special-scheme backslash authority must mask userinfo"
);
assert_eq!(
redact_url_fail_closed("foo:\\user:pass@evil/"),
"[redacted]",
"non-special-scheme backslash authority must fail closed"
);
assert_eq!(
redact_url("foo:\\clean/path"),
"foo:\\clean/path",
"clean backslash sibling stays visible"
);
}
#[test]
fn backslash_single_after_multi_char_scheme_masked() {
let redacted = redact_url("http:\\user:pass@evil\\path");
assert!(
!redacted.contains("user:pass"),
"single-backslash authority leaked: {redacted}"
);
assert!(
redacted.contains("***@"),
"single-backslash authority must mask userinfo: {redacted}"
);
}
#[test]
fn backslash_single_after_one_char_scheme_credential_shaped_masked() {
let redacted = redact_url("x:\\user:pass@evil");
assert!(
!redacted.contains("user:pass"),
"one-char-scheme backslash authority leaked: {redacted}"
);
assert!(
redacted.contains("***@"),
"one-char-scheme backslash authority must mask userinfo: {redacted}"
);
}
#[test]
fn drive_path_stays_visible() {
assert_eq!(
redact_url("C:\\Users\\x@corp\\file"),
"C:\\Users\\x@corp\\file"
);
}
#[test]
fn unc_path_stays_visible() {
assert_eq!(redact_url("\\\\server\\x@y"), "\\\\server\\x@y");
}
#[test]
fn window_has_at_sign_sees_backslash_windows() {
assert!(
window_has_at_sign("foo:\\u:p@e/"),
"scheme-prefixed backslash window must carry the at-sign"
);
assert!(
!window_has_at_sign("C:\\Users\\x@corp\\file"),
"drive path must not open a backslash window"
);
}
#[test]
fn backslash_gate_branches_pinned() {
assert_eq!(redact_url("foo:\\\\user:pass@evil/"), "foo:\\\\***@evil/");
assert_eq!(redact_url("a:/\\user:pass@evil/"), "a:/\\***@evil/");
assert_eq!(
redact_url("notscheme%\\user:pass@evil/"),
"notscheme%\\user:pass@evil/"
);
assert_eq!(redact_url("/\\user:pass@evil/"), "/\\user:pass@evil/");
assert_eq!(redact_url("a:\\user@evil"), "a:\\user@evil");
assert!(window_has_at_sign("foo:\\\\u:p@e/"));
assert!(!window_has_at_sign("a:\\user@evil"));
}
}