Use ref-cast for DisplaySafeUrl (#13696)

By default, Rust does not support safe cast from `&U` to `&T` for
`#[repr(transparent)] T(U)` even if the newtype opts in. The dtolnay
ref-cast crate fills this gap, allowing to remove `DisplaySafeUrlRef`.
This commit is contained in:
konsti 2025-05-28 13:28:28 +02:00 committed by GitHub
parent 410dc33574
commit de64f1dfa8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 97 additions and 137 deletions

39
Cargo.lock generated
View file

@ -728,7 +728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1094,7 +1094,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1920,7 +1920,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1980,7 +1980,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2854,7 +2854,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2966,6 +2966,26 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ref-cast"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "reflink-copy"
version = "0.1.26"
@ -3283,7 +3303,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3296,7 +3316,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.9.2",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3864,7 +3884,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix 1.0.7",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -5577,6 +5597,7 @@ dependencies = [
name = "uv-redacted"
version = "0.0.1"
dependencies = [
"ref-cast",
"schemars",
"serde",
"url",
@ -6187,7 +6208,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View file

@ -138,6 +138,7 @@ procfs = { version = "0.17.0", default-features = false, features = ["flate2"] }
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
quote = { version = "1.0.37" }
rayon = { version = "1.10.0" }
ref-cast = { version = "1.0.24" }
reflink-copy = { version = "0.1.19" }
regex = { version = "1.10.6" }
regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] }

View file

@ -4,7 +4,6 @@ use base64::write::EncoderWriter;
use std::borrow::Cow;
use std::fmt;
use uv_redacted::DisplaySafeUrl;
use uv_redacted::DisplaySafeUrlRef;
use netrc::Netrc;
use reqwest::Request;
@ -145,7 +144,7 @@ impl Credentials {
/// If a username is provided, it must match the login in the netrc file or [`None`] is returned.
pub(crate) fn from_netrc(
netrc: &Netrc,
url: DisplaySafeUrlRef<'_>,
url: &DisplaySafeUrl,
username: Option<&str>,
) -> Option<Self> {
let host = url.host_str()?;

View file

@ -1,7 +1,7 @@
use std::{io::Write, process::Stdio};
use tokio::process::Command;
use tracing::{instrument, trace, warn};
use uv_redacted::DisplaySafeUrlRef;
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user_once;
use crate::credentials::Credentials;
@ -36,11 +36,7 @@ impl KeyringProvider {
/// Returns [`None`] if no password was found for the username or if any errors
/// are encountered in the keyring backend.
#[instrument(skip_all, fields(url = % url.to_string(), username))]
pub async fn fetch(
&self,
url: DisplaySafeUrlRef<'_>,
username: Option<&str>,
) -> Option<Credentials> {
pub async fn fetch(&self, url: &DisplaySafeUrl, username: Option<&str>) -> Option<Credentials> {
// Validate the request
debug_assert!(
url.host_str().is_some(),
@ -229,7 +225,7 @@ mod tests {
let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(
keyring.fetch(DisplaySafeUrlRef::from(&url), Some("user")),
keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some("user")),
)
.catch_unwind()
.await;
@ -242,7 +238,7 @@ mod tests {
let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(
keyring.fetch(DisplaySafeUrlRef::from(&url), Some(url.username())),
keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some(url.username())),
)
.catch_unwind()
.await;
@ -255,7 +251,7 @@ mod tests {
let keyring = KeyringProvider::empty();
// Panics due to debug assertion; returns `None` in production
let result = std::panic::AssertUnwindSafe(
keyring.fetch(DisplaySafeUrlRef::from(&url), Some(url.username())),
keyring.fetch(DisplaySafeUrl::ref_cast(&url), Some(url.username())),
)
.catch_unwind()
.await;
@ -265,7 +261,7 @@ mod tests {
#[tokio::test]
async fn fetch_url_no_auth() {
let url = Url::parse("https://example.com").unwrap();
let url = DisplaySafeUrlRef::from(&url);
let url = DisplaySafeUrl::ref_cast(&url);
let keyring = KeyringProvider::empty();
let credentials = keyring.fetch(url, Some("user"));
assert!(credentials.await.is_none());
@ -277,7 +273,7 @@ mod tests {
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
assert_eq!(
keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("user"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("user"))
.await,
Some(Credentials::basic(
Some("user".to_string()),
@ -287,7 +283,7 @@ mod tests {
assert_eq!(
keyring
.fetch(
DisplaySafeUrlRef::from(&url.join("test").unwrap()),
DisplaySafeUrl::ref_cast(&url.join("test").unwrap()),
Some("user")
)
.await,
@ -303,7 +299,7 @@ mod tests {
let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([("other.com", "user", "password")]);
let credentials = keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("user"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("user"))
.await;
assert_eq!(credentials, None);
}
@ -318,7 +314,7 @@ mod tests {
assert_eq!(
keyring
.fetch(
DisplaySafeUrlRef::from(&url.join("foo").unwrap()),
DisplaySafeUrl::ref_cast(&url.join("foo").unwrap()),
Some("user")
)
.await,
@ -329,7 +325,7 @@ mod tests {
);
assert_eq!(
keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("user"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("user"))
.await,
Some(Credentials::basic(
Some("user".to_string()),
@ -339,7 +335,7 @@ mod tests {
assert_eq!(
keyring
.fetch(
DisplaySafeUrlRef::from(&url.join("bar").unwrap()),
DisplaySafeUrl::ref_cast(&url.join("bar").unwrap()),
Some("user")
)
.await,
@ -355,7 +351,7 @@ mod tests {
let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
let credentials = keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("user"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("user"))
.await;
assert_eq!(
credentials,
@ -370,7 +366,7 @@ mod tests {
async fn fetch_url_no_username() {
let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]);
let credentials = keyring.fetch(DisplaySafeUrlRef::from(&url), None).await;
let credentials = keyring.fetch(DisplaySafeUrl::ref_cast(&url), None).await;
assert_eq!(
credentials,
Some(Credentials::basic(
@ -385,14 +381,14 @@ mod tests {
let url = Url::parse("https://example.com").unwrap();
let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "foo", "password")]);
let credentials = keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("bar"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("bar"))
.await;
assert_eq!(credentials, None);
// Still fails if we have `foo` in the URL itself
let url = Url::parse("https://foo@example.com").unwrap();
let credentials = keyring
.fetch(DisplaySafeUrlRef::from(&url), Some("bar"))
.fetch(DisplaySafeUrl::ref_cast(&url), Some("bar"))
.await;
assert_eq!(credentials, None);
}

View file

@ -1,7 +1,11 @@
use std::sync::{Arc, LazyLock};
use anyhow::{anyhow, format_err};
use http::{Extensions, StatusCode};
use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlRef};
use netrc::Netrc;
use reqwest::{Request, Response};
use reqwest_middleware::{Error, Middleware, Next};
use tracing::{debug, trace, warn};
use crate::{
CREDENTIALS_CACHE, CredentialsCache, KeyringProvider,
@ -10,11 +14,7 @@ use crate::{
index::{AuthPolicy, Indexes},
realm::Realm,
};
use anyhow::{anyhow, format_err};
use netrc::Netrc;
use reqwest::{Request, Response};
use reqwest_middleware::{Error, Middleware, Next};
use tracing::{debug, trace, warn};
use uv_redacted::DisplaySafeUrl;
/// Strategy for loading netrc files.
enum NetrcMode {
@ -274,7 +274,7 @@ impl Middleware for AuthMiddleware {
trace!("Checking for credentials for {url}");
(request, None)
};
let retry_request_url = DisplaySafeUrlRef::from(retry_request.url());
let retry_request_url = DisplaySafeUrl::ref_cast(retry_request.url());
let username = credentials
.as_ref()
@ -283,13 +283,13 @@ impl Middleware for AuthMiddleware {
let credentials = if let Some(index_url) = maybe_index_url {
self.cache().get_url(index_url, &username).or_else(|| {
self.cache()
.get_realm(Realm::from(&*retry_request_url), username)
.get_realm(Realm::from(&**retry_request_url), username)
})
} else {
// Since there is no known index for this URL, check if there are credentials in
// the realm-level cache.
self.cache()
.get_realm(Realm::from(&*retry_request_url), username)
.get_realm(Realm::from(&**retry_request_url), username)
}
.or(credentials);
@ -433,7 +433,7 @@ impl AuthMiddleware {
} else if let Some(credentials) = self
.fetch_credentials(
Some(&credentials),
DisplaySafeUrlRef::from(request.url()),
DisplaySafeUrl::ref_cast(request.url()),
index_url,
auth_policy,
)
@ -468,7 +468,7 @@ impl AuthMiddleware {
async fn fetch_credentials(
&self,
credentials: Option<&Credentials>,
url: DisplaySafeUrlRef<'_>,
url: &DisplaySafeUrl,
maybe_index_url: Option<&DisplaySafeUrl>,
auth_policy: AuthPolicy,
) -> Option<Arc<Credentials>> {
@ -481,7 +481,7 @@ impl AuthMiddleware {
let key = if let Some(index_url) = maybe_index_url {
(FetchUrl::Index(index_url.clone()), username)
} else {
(FetchUrl::Realm(Realm::from(&*url)), username)
(FetchUrl::Realm(Realm::from(&**url)), username)
};
if !self.cache().fetches.register(key.clone()) {
let credentials = self
@ -529,7 +529,7 @@ impl AuthMiddleware {
if let Some(username) = credentials.and_then(|credentials| credentials.username()) {
if let Some(index_url) = maybe_index_url {
debug!("Checking keyring for credentials for index URL {}@{}", username, index_url);
keyring.fetch(DisplaySafeUrlRef::from(index_url), Some(username)).await
keyring.fetch(DisplaySafeUrl::ref_cast(index_url), Some(username)).await
} else {
debug!("Checking keyring for credentials for full URL {}@{}", username, url);
keyring.fetch(url, Some(username)).await
@ -539,7 +539,7 @@ impl AuthMiddleware {
debug!(
"Checking keyring for credentials for index URL {index_url} without username due to `authenticate = always`"
);
keyring.fetch(DisplaySafeUrlRef::from(index_url), None).await
keyring.fetch(DisplaySafeUrl::ref_cast(index_url), None).await
} else {
None
}

View file

@ -16,6 +16,7 @@ doctest = false
workspace = true
[dependencies]
ref-cast = { workspace = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true }
url = { workspace = true }

View file

@ -1,3 +1,4 @@
use ref_cast::RefCast;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::ops::{Deref, DerefMut};
@ -37,9 +38,10 @@ use url::Url;
/// assert_eq!(url.username(), "");
/// assert_eq!(url.password(), None);
/// ```
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, RefCast)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(transparent))]
#[repr(transparent)]
pub struct DisplaySafeUrl(Url);
impl DisplaySafeUrl {
@ -48,6 +50,12 @@ impl DisplaySafeUrl {
Ok(Self(Url::parse(input)?))
}
/// Cast a `&Url` to a `&DisplaySafeUrl` using ref-cast.
#[inline]
pub fn ref_cast(url: &Url) -> &Self {
RefCast::ref_cast(url)
}
/// Parse a string as an URL, with this URL as the base URL.
#[inline]
pub fn join(&self, input: &str) -> Result<Self, url::ParseError> {
@ -119,7 +127,28 @@ impl Display for DisplaySafeUrl {
impl Debug for DisplaySafeUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
debug_with_redacted_credentials(&self.0, f)
let url = &self.0;
let (username, password) = if url.username() != "" && url.password().is_some() {
(url.username(), Some("****"))
} else if url.username() != "" {
("****", None)
} else if url.password().is_some() {
("", Some("****"))
} else {
("", None)
};
f.debug_struct("DisplaySafeUrl")
.field("scheme", &url.scheme())
.field("cannot_be_a_base", &url.cannot_be_a_base())
.field("username", &username)
.field("password", &password)
.field("host", &url.host())
.field("port", &url.port())
.field("path", &url.path())
.field("query", &url.query())
.field("fragment", &url.fragment())
.finish()
}
}
@ -179,93 +208,6 @@ fn display_with_redacted_credentials(
Ok(())
}
fn debug_with_redacted_credentials(url: &Url, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (username, password) = if url.username() != "" && url.password().is_some() {
(url.username(), Some("****"))
} else if url.username() != "" {
("****", None)
} else if url.password().is_some() {
("", Some("****"))
} else {
("", None)
};
f.debug_struct("DisplaySafeUrl")
.field("scheme", &url.scheme())
.field("cannot_be_a_base", &url.cannot_be_a_base())
.field("username", &username)
.field("password", &password)
.field("host", &url.host())
.field("port", &url.port())
.field("path", &url.path())
.field("query", &url.query())
.field("fragment", &url.fragment())
.finish()
}
/// A wrapper around a [`url::Url`] ref that safely handles credentials for
/// logging purposes.
///
/// Uses the same underlying [`Display`] implementation as [`DisplaySafeUrl`].
///
/// # Examples
///
/// ```
/// use uv_redacted::DisplaySafeUrl;
/// use std::str::FromStr;
///
/// // Create from a `url::Url` ref
/// let url = Url::parse("https://user:password@example.com").unwrap();
/// let log_safe_url = DisplaySafeUrlRef::from(&url);
///
/// // Display will mask secrets
/// assert_eq!(url.to_string(), "https://user:****@example.com/");
///
/// // Since `DisplaySafeUrlRef` provides full access to the underlying `Url` through a
/// // `Deref` implementation, you can still access the username and password
/// assert_eq!(url.username(), "user");
/// assert_eq!(url.password(), Some("password"));
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub struct DisplaySafeUrlRef<'a>(&'a Url);
impl<'a> Deref for DisplaySafeUrlRef<'a> {
type Target = Url;
fn deref(&self) -> &'a Self::Target {
self.0
}
}
impl Display for DisplaySafeUrlRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_with_redacted_credentials(self.0, f)
}
}
impl Debug for DisplaySafeUrlRef<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
debug_with_redacted_credentials(self.0, f)
}
}
impl<'a> From<&'a Url> for DisplaySafeUrlRef<'a> {
fn from(url: &'a Url) -> Self {
DisplaySafeUrlRef(url)
}
}
impl<'a> From<&'a DisplaySafeUrl> for DisplaySafeUrlRef<'a> {
fn from(url: &'a DisplaySafeUrl) -> Self {
DisplaySafeUrlRef(url)
}
}
impl<'a> From<DisplaySafeUrlRef<'a>> for DisplaySafeUrl {
fn from(url: DisplaySafeUrlRef<'a>) -> Self {
DisplaySafeUrl(url.0.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -366,7 +308,7 @@ mod tests {
fn log_safe_url_ref() {
let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple";
let url = Url::parse(url_str).unwrap();
let log_safe_url = DisplaySafeUrlRef::from(&url);
let log_safe_url = DisplaySafeUrl::ref_cast(&url);
assert_eq!(log_safe_url.username(), "user");
assert!(log_safe_url.password().is_some_and(|p| p == "pass"));
assert_eq!(

View file

@ -16,7 +16,7 @@ use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl};
use uv_publish::{
CheckUrlClient, TrustedPublishResult, check_trusted_publishing, files_for_publishing, upload,
};
use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlRef};
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user_once;
use crate::commands::reporters::PublishReporter;
@ -296,7 +296,7 @@ async fn gather_credentials(
if let Some(username) = &username {
debug!("Fetching password from keyring");
if let Some(keyring_password) = keyring_provider
.fetch(DisplaySafeUrlRef::from(&publish_url), Some(username))
.fetch(DisplaySafeUrl::ref_cast(&publish_url), Some(username))
.await
.as_ref()
.and_then(|credentials| credentials.password())