diff --git a/Cargo.lock b/Cargo.lock index 1f3b1f1db..1852cef58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index 81a3a50f0..e6bc5a7ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index b376857d0..68d98b2e3 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -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 { let host = url.host_str()?; diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index fbfc2e962..41b92114a 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -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 { + pub async fn fetch(&self, url: &DisplaySafeUrl, username: Option<&str>) -> Option { // 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); } diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 15d20fd8a..1842effb3 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -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> { @@ -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 } diff --git a/crates/uv-redacted/Cargo.toml b/crates/uv-redacted/Cargo.toml index bc37321ab..52fca3f89 100644 --- a/crates/uv-redacted/Cargo.toml +++ b/crates/uv-redacted/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] +ref-cast = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true } url = { workspace = true } diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs index 61b82a7f6..2f040c4c4 100644 --- a/crates/uv-redacted/src/lib.rs +++ b/crates/uv-redacted/src/lib.rs @@ -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 { @@ -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> 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!( diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 843fc3f84..63a0f2756 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -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())