use std::borrow::Cow;
use std::marker::PhantomData;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HostClass {
Loopback,
LinkLocal,
CloudMetadata,
PrivateV4,
Cgnat,
UniqueLocalV6,
Unspecified,
InternalName,
Global,
}
impl HostClass {
#[must_use]
pub const fn never_a_registry(self) -> bool {
matches!(
self,
Self::Loopback | Self::LinkLocal | Self::CloudMetadata | Self::Unspecified
)
}
}
impl std::fmt::Display for HostClass {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Loopback => "loopback",
Self::LinkLocal => "link-local",
Self::CloudMetadata => "cloud metadata",
Self::PrivateV4 => "private (RFC1918)",
Self::Cgnat => "carrier-grade NAT",
Self::UniqueLocalV6 => "unique-local IPv6",
Self::Unspecified => "unspecified",
Self::InternalName => "internal name",
Self::Global => "global",
})
}
}
fn unwrap_mapped_v4(addr: IpAddr) -> IpAddr {
match addr {
IpAddr::V6(v6) => v6
.to_ipv4_mapped()
.or_else(|| nat64_embedded_v4(v6))
.map_or(addr, IpAddr::V4),
IpAddr::V4(_) => addr,
}
}
fn nat64_embedded_v4(v6: Ipv6Addr) -> Option<Ipv4Addr> {
let segments = v6.segments();
if segments[0] == 0x0064 && segments[1] == 0xff9b && segments[2..6] == [0, 0, 0, 0] {
let [a, b] = segments[6].to_be_bytes();
let [c, d] = segments[7].to_be_bytes();
Some(Ipv4Addr::new(a, b, c, d))
} else {
None
}
}
fn classify_ip(addr: IpAddr) -> HostClass {
match addr {
IpAddr::V4(v4) => {
if v4.is_loopback() {
HostClass::Loopback
} else if v4 == Ipv4Addr::new(169, 254, 169, 254) {
HostClass::CloudMetadata
} else if v4.is_link_local() {
HostClass::LinkLocal
} else if v4.is_unspecified() {
HostClass::Unspecified
} else if v4.is_private() {
HostClass::PrivateV4
} else if v4.octets()[0] == 100 && (v4.octets()[1] & 0b1100_0000) == 0b0100_0000 {
HostClass::Cgnat
} else {
HostClass::Global
}
}
IpAddr::V6(v6) => {
if v6.is_loopback() {
HostClass::Loopback
} else if v6.segments() == [0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x254] {
HostClass::CloudMetadata
} else if (v6.segments()[0] & 0xffc0) == 0xfe80 {
HostClass::LinkLocal
} else if v6.is_unspecified() {
HostClass::Unspecified
} else if (v6.segments()[0] & 0xfe00) == 0xfc00 {
HostClass::UniqueLocalV6
} else {
HostClass::Global
}
}
}
}
fn classify_name(host: &str) -> HostClass {
let lower = host.to_ascii_lowercase();
let lower = lower.trim_end_matches('.');
if lower == "localhost" || lower.ends_with(".localhost") {
return HostClass::Loopback;
}
if lower == "metadata.google.internal" || lower == "metadata.goog" {
return HostClass::CloudMetadata;
}
if lower.ends_with(".internal") || lower.ends_with(".local") || lower.ends_with(".home.arpa") {
return HostClass::InternalName;
}
if !lower.contains('.') {
return HostClass::InternalName;
}
HostClass::Global
}
#[must_use]
pub fn classify_addr(addr: IpAddr) -> HostClass {
classify_ip(unwrap_mapped_v4(addr))
}
#[must_use]
pub fn classify_host(url: &url::Url) -> HostClass {
match url.host() {
Some(url::Host::Ipv4(v4)) => classify_ip(unwrap_mapped_v4(IpAddr::V4(v4))),
Some(url::Host::Ipv6(v6)) => classify_ip(unwrap_mapped_v4(IpAddr::V6(v6))),
Some(url::Host::Domain(name)) => classify_name(name),
None => HostClass::InternalName,
}
}
#[must_use]
pub fn is_trusted_prefix(candidate: &url::Url, trusted: &url::Url) -> bool {
candidate.origin() == trusted.origin() && path_under_prefix(candidate.path(), trusted.path())
}
fn path_under_prefix(path: &str, prefix: &str) -> bool {
let prefix = prefix.trim_end_matches('/');
path == prefix
|| path
.strip_prefix(prefix)
.is_some_and(|rest| rest.starts_with('/'))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum WorkspaceRegistryAccess {
Off,
#[default]
PublicOnly,
All,
}
impl WorkspaceRegistryAccess {
#[must_use]
pub const fn allows(self, class: HostClass) -> bool {
match self {
Self::Off => false,
Self::PublicOnly => matches!(class, HostClass::Global),
Self::All => true,
}
}
pub(crate) const fn to_u8(self) -> u8 {
match self {
Self::Off => 0,
Self::PublicOnly => 1,
Self::All => 2,
}
}
const fn from_u8(value: u8) -> Self {
match value {
0 => Self::Off,
2 => Self::All,
_ => Self::PublicOnly,
}
}
}
#[derive(Debug)]
pub struct RegistryAccessPolicy(AtomicU8);
impl RegistryAccessPolicy {
#[must_use]
pub fn new(initial: WorkspaceRegistryAccess) -> Self {
Self(AtomicU8::new(initial.to_u8()))
}
#[must_use]
pub fn get(&self) -> WorkspaceRegistryAccess {
WorkspaceRegistryAccess::from_u8(self.0.load(Ordering::Relaxed))
}
pub fn set(&self, value: WorkspaceRegistryAccess) {
self.0.store(value.to_u8(), Ordering::Relaxed);
}
}
impl Default for RegistryAccessPolicy {
fn default() -> Self {
Self::new(WorkspaceRegistryAccess::default())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IndexUrlError {
#[error("not a valid URL: {0}")]
InvalidUrl(RedactedUrl),
#[error("registry index must use https, got scheme {0:?}")]
NotHttps(String),
#[error("registry index URL must not carry userinfo")]
UserInfoPresent,
#[error("registry index host class {class} blocked by registries.workspace_registries policy")]
BlockedHost {
class: HostClass,
},
}
#[derive(Debug, Clone, Copy)]
pub enum PolicyGate<'a> {
Skip,
Enforce(&'a RegistryAccessPolicy),
}
#[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 fn redact_userinfo(raw: &str) -> String {
let Ok(mut url) = url::Url::parse(raw) 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",
];
#[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"
)]
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)
}
#[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 fn url_for_tracing(raw: &str) -> String {
let end = raw.find(['?', '#']).unwrap_or(raw.len());
redact_userinfo(&raw[..end])
}
#[must_use]
pub fn is_authority_bearing_url(raw: &str) -> bool {
url::Url::parse(raw).is_ok_and(|url| url.host().is_some())
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct RedactedUrl(String);
impl RedactedUrl {
#[must_use]
pub fn new(raw: &str) -> Self {
Self(url_for_tracing(raw))
}
}
impl From<&str> for RedactedUrl {
fn from(raw: &str) -> Self {
Self::new(raw)
}
}
impl From<String> for RedactedUrl {
fn from(raw: String) -> Self {
Self::new(&raw)
}
}
impl std::fmt::Display for RedactedUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Debug for RedactedUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.0, f)
}
}
impl AsRef<str> for RedactedUrl {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(any(test, feature = "test-util"))]
fn is_loopback_url(url: &url::Url) -> bool {
url.scheme() == "http" && matches!(url.host_str(), Some("127.0.0.1" | "localhost" | "::1"))
}
pub fn validate_index_url(
candidate: &str,
raw_for_log: &str,
ecosystem: &'static str,
gate: PolicyGate<'_>,
) -> Result<url::Url, IndexUrlError> {
let url = url::Url::parse(candidate)
.map_err(|_| IndexUrlError::InvalidUrl(RedactedUrl::new(raw_for_log)))?;
let is_https = url.scheme() == "https";
#[cfg(any(test, feature = "test-util"))]
let is_https = is_https || is_loopback_url(&url);
if !is_https {
return Err(IndexUrlError::NotHttps(url.scheme().to_string()));
}
if !url.username().is_empty() || url.password().is_some() {
return Err(IndexUrlError::UserInfoPresent);
}
if let PolicyGate::Enforce(policy) = gate {
let class = classify_host(&url);
if !policy.get().allows(class) {
tracing::warn!(
url = %RedactedUrl::new(raw_for_log),
?class,
ecosystem,
"workspace-declared registry index host blocked by registries.workspace_registries policy"
);
return Err(IndexUrlError::BlockedHost { class });
}
}
Ok(url)
}
#[doc(hidden)]
pub mod private {
pub trait Sealed {}
}
pub trait RegistryUrlKind: private::Sealed {
const ECOSYSTEM: &'static str;
const REJECT_QUERY_FRAGMENT: bool;
type Error: From<IndexUrlError>;
}
pub trait TrustedConstantRegistryUrl: RegistryUrlKind {}
pub struct ValidatedRegistryUrl<K> {
normalized: String,
kind: PhantomData<fn() -> K>,
}
impl<K> Clone for ValidatedRegistryUrl<K> {
fn clone(&self) -> Self {
Self {
normalized: self.normalized.clone(),
kind: PhantomData,
}
}
}
impl<K: RegistryUrlKind> std::fmt::Debug for ValidatedRegistryUrl<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple(K::ECOSYSTEM).field(&self.normalized).finish()
}
}
impl<K> PartialEq for ValidatedRegistryUrl<K> {
fn eq(&self, other: &Self) -> bool {
self.normalized == other.normalized
}
}
impl<K> Eq for ValidatedRegistryUrl<K> {}
impl<K> std::hash::Hash for ValidatedRegistryUrl<K> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.normalized.hash(state);
}
}
impl<K> std::fmt::Display for ValidatedRegistryUrl<K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.normalized)
}
}
impl<K: RegistryUrlKind> ValidatedRegistryUrl<K> {
pub fn new(raw: &str, policy: &RegistryAccessPolicy) -> Result<Self, K::Error> {
Self::new_with_raw_for_log(raw, raw, policy)
}
pub fn new_with_raw_for_log(
candidate: &str,
raw_for_log: &str,
policy: &RegistryAccessPolicy,
) -> Result<Self, K::Error> {
Self::build(candidate, raw_for_log, PolicyGate::Enforce(policy))
}
fn build(candidate: &str, raw_for_log: &str, gate: PolicyGate<'_>) -> Result<Self, K::Error> {
let url = validate_index_url(candidate, raw_for_log, K::ECOSYSTEM, gate)?;
if K::REJECT_QUERY_FRAGMENT && (url.query().is_some() || url.fragment().is_some()) {
return Err(IndexUrlError::InvalidUrl(RedactedUrl::new(raw_for_log)).into());
}
let normalized = url.as_str().trim_end_matches('/').to_string();
Ok(Self {
normalized,
kind: PhantomData,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.normalized
}
}
impl<K: TrustedConstantRegistryUrl> ValidatedRegistryUrl<K> {
pub fn new_trusted_constant(raw: &'static str) -> Result<Self, K::Error> {
Self::build(raw, raw, PolicyGate::Skip)
}
}
pub trait BlockedHostReason {
fn blocked_host_class(&self) -> Option<HostClass>;
}
impl BlockedHostReason for IndexUrlError {
fn blocked_host_class(&self) -> Option<HostClass> {
match self {
Self::BlockedHost { class } => Some(*class),
_ => None,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct InvalidEntry<E = IndexUrlError> {
pub raw: RedactedUrl,
pub reason: E,
}
impl<E> InvalidEntry<E> {
#[must_use]
pub fn new(raw: RedactedUrl, reason: E) -> Self {
Self { raw, reason }
}
pub fn logged(raw: &str, reason: E, ecosystem: &'static str, message: &'static str) -> Self
where
E: std::fmt::Display,
{
let redacted = RedactedUrl::new(raw);
tracing::warn!(raw = %redacted, %reason, ecosystem, "{}", message);
Self::new(redacted, reason)
}
}
impl<E: BlockedHostReason> InvalidEntry<E> {
#[must_use]
pub fn blocked_class(&self) -> Option<(HostClass, String)> {
self.reason
.blocked_host_class()
.map(|class| (class, self.raw.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::assert_matches;
#[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"));
assert!(is_authority_bearing_url(
"https:user:hunter2@10.0.0.1/index"
));
assert!(is_authority_bearing_url("https:169.254.169.254/index"));
}
#[test]
fn is_authority_bearing_url_false_for_opaque_declaration_key_labels() {
assert!(!is_authority_bearing_url("scope:@myorg"));
assert!(!is_authority_bearing_url("source:Blocked"));
assert!(!is_authority_bearing_url("top-level"));
assert!(!is_authority_bearing_url("component-host:10.0.0.1"));
assert!(!is_authority_bearing_url("named:internal"));
}
fn host_class(url: &str) -> HostClass {
classify_host(&url::Url::parse(url).unwrap())
}
fn trusted_prefix(candidate: &str, trusted: &str) -> bool {
is_trusted_prefix(
&url::Url::parse(candidate).unwrap(),
&url::Url::parse(trusted).unwrap(),
)
}
#[test]
fn test_is_trusted_prefix_rejects_sibling_path_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index";
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/index-public/steal",
trusted
));
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/indexEVIL",
trusted
));
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/index.evil/x",
trusted
));
}
#[test]
fn test_is_trusted_prefix_accepts_self_and_child_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index";
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index",
trusted
));
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index/se/rd/serde",
trusted
));
}
#[test]
fn test_is_trusted_prefix_trailing_slash_equivalent_to_no_trailing_slash() {
let trusted = "https://artifacts.corp/cargo/index/";
assert!(!trusted_prefix(
"https://artifacts.corp/cargo/indexEVIL",
trusted
));
assert!(trusted_prefix(
"https://artifacts.corp/cargo/index/se/rd/serde",
trusted
));
}
#[test]
fn test_is_trusted_prefix_doubled_trailing_slash_still_trusts_children() {
let trusted = "https://artifacts.corp/v3-flatcontainer//";
assert!(trusted_prefix(
"https://artifacts.corp/v3-flatcontainer/serde/index.json",
trusted
));
}
#[test]
fn test_is_trusted_prefix_rejects_origin_mismatch() {
assert!(!trusted_prefix(
"https://artifacts.corp.evil.com/cargo/index",
"https://artifacts.corp/cargo/index"
));
}
#[test]
fn test_is_trusted_prefix_root_path_trusts_everything_on_origin() {
assert!(trusted_prefix(
"https://registry.example/anything/at/all",
"https://registry.example/"
));
}
#[test]
fn test_classify_cloud_metadata_ipv4() {
assert_eq!(
host_class("https://169.254.169.254/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_ipv4_mapped_v6_bypass() {
assert_eq!(
host_class("https://[::ffff:169.254.169.254]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_nat64_bypass() {
assert_eq!(
host_class("https://[64:ff9b::a9fe:a9fe]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_cloud_metadata_ec2_ipv6() {
assert_eq!(
host_class("https://[fd00:ec2::254]/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_link_local_ipv6() {
assert_eq!(host_class("https://[fe80::1]/"), HostClass::LinkLocal);
}
#[test]
fn test_classify_private_v4() {
assert_eq!(host_class("https://10.0.0.1/"), HostClass::PrivateV4);
}
#[test]
fn test_classify_cgnat() {
assert_eq!(host_class("https://100.64.0.1/"), HostClass::Cgnat);
}
#[test]
fn test_classify_private_v4_192_168() {
assert_eq!(host_class("https://192.168.1.1/"), HostClass::PrivateV4);
}
#[test]
fn test_classify_unique_local_v6() {
assert_eq!(host_class("https://[fc00::1]/"), HostClass::UniqueLocalV6);
}
#[test]
fn test_classify_unspecified_v4() {
assert_eq!(host_class("https://0.0.0.0/"), HostClass::Unspecified);
}
#[test]
fn test_classify_localhost_name() {
assert_eq!(host_class("https://localhost/"), HostClass::Loopback);
}
#[test]
fn test_classify_localhost_subdomain() {
assert_eq!(host_class("https://foo.localhost/"), HostClass::Loopback);
}
#[test]
fn test_classify_google_metadata_name() {
assert_eq!(
host_class("https://metadata.google.internal/"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_internal_suffix_name() {
assert_eq!(
host_class("https://registry.internal/"),
HostClass::InternalName
);
}
#[test]
fn test_classify_single_label_name() {
assert_eq!(host_class("https://single-label/"), HostClass::InternalName);
}
#[test]
fn test_classify_localhost_trailing_dot() {
assert_eq!(host_class("https://localhost./"), HostClass::Loopback);
}
#[test]
fn test_classify_google_metadata_trailing_dot() {
assert_eq!(
host_class("https://metadata.google.internal./"),
HostClass::CloudMetadata
);
}
#[test]
fn test_classify_internal_suffix_trailing_dot() {
assert_eq!(
host_class("https://registry.internal./"),
HostClass::InternalName
);
}
#[test]
fn test_classify_localhost_double_trailing_dot() {
assert_eq!(host_class("https://localhost../"), HostClass::Loopback);
}
#[test]
fn test_classify_global_public_name() {
assert_eq!(host_class("https://index.crates.io/"), HostClass::Global);
}
#[test]
fn test_classify_addr_cloud_metadata() {
let addr: IpAddr = "169.254.169.254".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_private_v4() {
let addr: IpAddr = "10.0.0.1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::PrivateV4);
}
#[test]
fn test_classify_addr_unwraps_mapped_v4() {
let addr: IpAddr = "::ffff:169.254.169.254".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_unwraps_nat64_cloud_metadata() {
let addr: IpAddr = "64:ff9b::a9fe:a9fe".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::CloudMetadata);
}
#[test]
fn test_classify_addr_unwraps_nat64_loopback() {
let addr: IpAddr = "64:ff9b::7f00:1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::Loopback);
}
#[test]
fn test_classify_addr_global() {
let addr: IpAddr = "1.1.1.1".parse().unwrap();
assert_eq!(classify_addr(addr), HostClass::Global);
}
#[test]
fn test_never_a_registry_classes() {
assert!(HostClass::Loopback.never_a_registry());
assert!(HostClass::LinkLocal.never_a_registry());
assert!(HostClass::CloudMetadata.never_a_registry());
assert!(HostClass::Unspecified.never_a_registry());
assert!(!HostClass::PrivateV4.never_a_registry());
assert!(!HostClass::Cgnat.never_a_registry());
assert!(!HostClass::UniqueLocalV6.never_a_registry());
assert!(!HostClass::InternalName.never_a_registry());
assert!(!HostClass::Global.never_a_registry());
}
#[test]
fn test_workspace_registry_access_off_blocks_everything() {
let policy = WorkspaceRegistryAccess::Off;
assert!(!policy.allows(HostClass::Global));
assert!(!policy.allows(HostClass::PrivateV4));
assert!(!policy.allows(HostClass::Loopback));
}
#[test]
fn test_workspace_registry_access_public_only_allows_global_only() {
let policy = WorkspaceRegistryAccess::PublicOnly;
assert!(policy.allows(HostClass::Global));
assert!(!policy.allows(HostClass::PrivateV4));
assert!(!policy.allows(HostClass::CloudMetadata));
}
#[test]
fn test_workspace_registry_access_all_allows_everything() {
let policy = WorkspaceRegistryAccess::All;
assert!(policy.allows(HostClass::Global));
assert!(policy.allows(HostClass::PrivateV4));
assert!(policy.allows(HostClass::Loopback));
}
#[test]
fn test_registry_access_policy_default_is_public_only() {
let policy = RegistryAccessPolicy::default();
assert_eq!(policy.get(), WorkspaceRegistryAccess::PublicOnly);
}
#[test]
fn test_registry_access_policy_live_update() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::All);
assert_eq!(policy.get(), WorkspaceRegistryAccess::All);
policy.set(WorkspaceRegistryAccess::Off);
assert_eq!(policy.get(), WorkspaceRegistryAccess::Off);
}
#[test]
fn test_validate_index_url_userinfo_rejected_before_policy_gate() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
let result = validate_index_url(
"https://user:pass@169.254.169.254/",
"https://user:pass@169.254.169.254/",
"cargo",
PolicyGate::Enforce(&policy),
);
assert_eq!(result, Err(IndexUrlError::UserInfoPresent));
}
#[test]
fn test_validate_index_url_policy_gate_skip_vs_enforce() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
assert!(
validate_index_url(
"https://169.254.169.254/",
"https://169.254.169.254/",
"cargo",
PolicyGate::Skip
)
.is_ok()
);
assert_matches!(
validate_index_url(
"https://169.254.169.254/",
"https://169.254.169.254/",
"cargo",
PolicyGate::Enforce(&policy)
),
Err(IndexUrlError::BlockedHost { .. })
);
}
#[cfg(feature = "test-util")]
#[test]
fn test_validate_index_url_blocked_host_log_redacts_query_string() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::Off);
let raw = "https://169.254.169.254/?ApiKey=super-secret-value";
let log = crate::test_util::capture_tracing_output(|| {
let result = validate_index_url(raw, raw, "cargo", PolicyGate::Enforce(&policy));
assert_matches!(result, Err(IndexUrlError::BlockedHost { .. }));
});
assert!(!log.contains("super-secret-value"), "log: {log}");
}
#[test]
fn test_redacted_url_matches_url_for_tracing_byte_for_byte() {
let inputs = [
"https://npm.internal/pkg?token=super-secret-value",
"https://user:hunter2@registry.example/simple?token=x",
"https://registry.example/simple",
"not-a-url-at-all",
"@types/node",
"c:/user:hunter2@evil",
"c:///user:hunter2@evil",
];
for raw in inputs {
assert_eq!(RedactedUrl::new(raw).to_string(), url_for_tracing(raw));
}
}
#[test]
fn test_redacted_url_display_and_as_ref_never_expose_raw_credential() {
let raw = "https://user:hunter2@registry.example/simple?token=super-secret-value";
let redacted = RedactedUrl::new(raw);
assert!(!redacted.to_string().contains("hunter2"));
assert!(!redacted.to_string().contains("super-secret-value"));
assert!(!redacted.as_ref().contains("hunter2"));
assert!(!redacted.as_ref().contains("super-secret-value"));
assert_eq!(redacted.to_string(), "https://***@registry.example/simple");
}
#[test]
fn test_redacted_url_debug_forwards_to_inner_string_and_redacts() {
let redacted = RedactedUrl::new("https://user:hunter2@registry.example/simple");
assert_eq!(
format!("{redacted:?}"),
"\"https://***@registry.example/simple\""
);
}
#[test]
fn test_redacted_url_noop_for_scoped_package_name() {
assert_eq!(RedactedUrl::new("@types/node").to_string(), "@types/node");
}
#[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_validate_index_url_redacts_userinfo_in_invalid_url_error() {
let raw = "https://user:hunter2@registry.example:99999/simple";
let err = validate_index_url(raw, raw, "cargo", PolicyGate::Skip).unwrap_err();
let IndexUrlError::InvalidUrl(redacted) = &err else {
panic!("expected InvalidUrl, got {err:?}");
};
assert!(
!redacted.as_ref().contains("hunter2"),
"redacted: {redacted}"
);
assert!(!err.to_string().contains("hunter2"), "Display: {err}");
}
#[test]
fn test_validate_index_url_invalid_url_redacts_query_string() {
let raw = "https://user:hunter2@registry.example:99999/simple?token=super-secret-value";
for ecosystem in ["cargo", "npm"] {
let err = validate_index_url(raw, raw, ecosystem, PolicyGate::Skip).unwrap_err();
let IndexUrlError::InvalidUrl(redacted) = &err else {
panic!("expected InvalidUrl, got {err:?}");
};
assert!(
!redacted.as_ref().contains("hunter2"),
"redacted: {redacted}"
);
assert!(
!redacted.as_ref().contains("super-secret-value"),
"redacted: {redacted}"
);
assert!(
!err.to_string().contains("super-secret-value"),
"Display: {err}"
);
}
}
#[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:***"
);
}
enum PermissiveTestKind {}
impl private::Sealed for PermissiveTestKind {}
impl RegistryUrlKind for PermissiveTestKind {
const ECOSYSTEM: &'static str = "test-permissive";
const REJECT_QUERY_FRAGMENT: bool = false;
type Error = IndexUrlError;
}
enum RejectingTestKind {}
impl private::Sealed for RejectingTestKind {}
impl RegistryUrlKind for RejectingTestKind {
const ECOSYSTEM: &'static str = "test-rejecting";
const REJECT_QUERY_FRAGMENT: bool = true;
type Error = IndexUrlError;
}
impl TrustedConstantRegistryUrl for PermissiveTestKind {}
impl TrustedConstantRegistryUrl for RejectingTestKind {}
fn permissive_policy() -> RegistryAccessPolicy {
RegistryAccessPolicy::new(WorkspaceRegistryAccess::All)
}
#[test]
fn test_validated_registry_url_accepts_query_string_when_kind_opts_out() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com/path?token=abc",
&permissive_policy(),
)
.unwrap();
assert_eq!(url.as_str(), "https://example.com/path?token=abc");
}
#[test]
fn test_validated_registry_url_accepts_fragment_when_kind_opts_out() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com/path#frag",
&permissive_policy(),
)
.unwrap();
assert_eq!(url.as_str(), "https://example.com/path#frag");
}
#[test]
fn test_validated_registry_url_rejects_query_string_when_kind_opts_in() {
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new(
"https://example.com/path?token=abc",
&permissive_policy(),
),
Err(IndexUrlError::InvalidUrl(_))
);
}
#[test]
fn test_validated_registry_url_rejects_fragment_when_kind_opts_in() {
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new(
"https://example.com/path#frag",
&permissive_policy(),
),
Err(IndexUrlError::InvalidUrl(_))
);
}
#[test]
fn test_validated_registry_url_debug_names_ecosystem() {
let url = ValidatedRegistryUrl::<PermissiveTestKind>::new(
"https://example.com",
&permissive_policy(),
)
.unwrap();
assert_eq!(
format!("{url:?}"),
"test-permissive(\"https://example.com\")"
);
}
#[test]
fn test_validated_registry_url_send_sync_unconditional_regardless_of_kind() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ValidatedRegistryUrl<PermissiveTestKind>>();
}
#[test]
fn test_validated_registry_url_new_trusted_constant_skips_policy() {
let url =
ValidatedRegistryUrl::<PermissiveTestKind>::new_trusted_constant("https://example.com")
.unwrap();
assert_eq!(url.as_str(), "https://example.com");
}
#[test]
fn test_validated_registry_url_new_trusted_constant_bypasses_blocked_host_policy() {
let policy = RegistryAccessPolicy::new(WorkspaceRegistryAccess::PublicOnly);
assert_matches!(
ValidatedRegistryUrl::<RejectingTestKind>::new("https://127.0.0.1:9999", &policy),
Err(IndexUrlError::BlockedHost { .. })
);
let url = ValidatedRegistryUrl::<RejectingTestKind>::new_trusted_constant(
"https://127.0.0.1:9999",
)
.unwrap();
assert_eq!(url.as_str(), "https://127.0.0.1:9999");
}
#[test]
fn test_invalid_entry_blocked_class_some_for_blocked_host() {
let entry = InvalidEntry::new(
RedactedUrl::new("https://127.0.0.1:9999"),
IndexUrlError::BlockedHost {
class: HostClass::Loopback,
},
);
assert_eq!(
entry.blocked_class(),
Some((HostClass::Loopback, "https://127.0.0.1:9999".to_string()))
);
}
#[test]
fn test_invalid_entry_blocked_class_none_for_other_reason() {
let entry = InvalidEntry::new(
RedactedUrl::new("not-a-url"),
IndexUrlError::InvalidUrl(RedactedUrl::new("not-a-url")),
);
assert_eq!(entry.blocked_class(), None);
}
}