diff --git a/Cargo.lock b/Cargo.lock index f2eeb7015..1f3b1f1db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4671,9 +4671,9 @@ dependencies = [ "test-log", "tokio", "tracing", - "tracing-test", "url", "uv-once-map", + "uv-redacted", "uv-small-str", "uv-static", "uv-warnings", @@ -4801,7 +4801,6 @@ dependencies = [ "serde", "tempfile", "tracing", - "url", "uv-cache-info", "uv-cache-key", "uv-dirs", @@ -4809,6 +4808,7 @@ dependencies = [ "uv-fs", "uv-normalize", "uv-pypi-types", + "uv-redacted", "uv-static", "walkdir", ] @@ -4836,6 +4836,7 @@ dependencies = [ "percent-encoding", "seahash", "url", + "uv-redacted", ] [[package]] @@ -4858,6 +4859,7 @@ dependencies = [ "uv-pep508", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-resolver", "uv-settings", "uv-static", @@ -5087,6 +5089,7 @@ dependencies = [ "uv-pep508", "uv-platform-tags", "uv-pypi-types", + "uv-redacted", "uv-types", "uv-workspace", "walkdir", @@ -5144,6 +5147,7 @@ dependencies = [ "uv-pep508", "uv-platform-tags", "uv-pypi-types", + "uv-redacted", "uv-small-str", "version-ranges", ] @@ -5318,6 +5322,7 @@ dependencies = [ "uv-platform-tags", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-static", "uv-types", "uv-warnings", @@ -5413,6 +5418,7 @@ dependencies = [ "uv-fs", "uv-normalize", "uv-pep440", + "uv-redacted", "version-ranges", ] @@ -5470,6 +5476,7 @@ dependencies = [ "uv-fs", "uv-metadata", "uv-pypi-types", + "uv-redacted", "uv-static", "uv-warnings", ] @@ -5501,6 +5508,7 @@ dependencies = [ "uv-normalize", "uv-pep440", "uv-pep508", + "uv-redacted", "uv-small-str", ] @@ -5554,6 +5562,7 @@ dependencies = [ "uv-pep508", "uv-platform-tags", "uv-pypi-types", + "uv-redacted", "uv-state", "uv-static", "uv-trampoline-builder", @@ -5568,6 +5577,8 @@ dependencies = [ name = "uv-redacted" version = "0.0.1" dependencies = [ + "schemars", + "serde", "url", ] @@ -5598,6 +5609,7 @@ dependencies = [ "uv-normalize", "uv-pep508", "uv-pypi-types", + "uv-redacted", "uv-requirements-txt", "uv-resolver", "uv-types", @@ -5633,6 +5645,7 @@ dependencies = [ "uv-normalize", "uv-pep508", "uv-pypi-types", + "uv-redacted", "uv-warnings", ] @@ -5684,6 +5697,7 @@ dependencies = [ "uv-platform-tags", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-requirements-txt", "uv-small-str", "uv-static", @@ -5707,6 +5721,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-pypi-types", + "uv-redacted", "uv-settings", "uv-workspace", ] @@ -5736,6 +5751,7 @@ dependencies = [ "uv-pep508", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-resolver", "uv-static", "uv-torch", @@ -5851,7 +5867,6 @@ dependencies = [ "anyhow", "rustc-hash", "thiserror 2.0.12", - "url", "uv-cache", "uv-configuration", "uv-distribution-filename", @@ -5863,6 +5878,7 @@ dependencies = [ "uv-pep508", "uv-pypi-types", "uv-python", + "uv-redacted", "uv-workspace", ] @@ -5917,7 +5933,6 @@ dependencies = [ "toml", "toml_edit", "tracing", - "url", "uv-build-backend", "uv-cache-key", "uv-distribution-types", @@ -5929,6 +5944,7 @@ dependencies = [ "uv-pep440", "uv-pep508", "uv-pypi-types", + "uv-redacted", "uv-static", "uv-warnings", ] diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index 2717254d9..e63fb1a50 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -11,6 +11,7 @@ workspace = true [dependencies] uv-once-map = { workspace = true } +uv-redacted = { workspace = true } uv-small-str = { workspace = true } uv-static = { workspace = true } uv-warnings = { workspace = true } @@ -36,5 +37,4 @@ insta = { version = "1.40.0" } tempfile = { workspace = true } test-log = { version = "0.2.16", features = ["trace"], default-features = false } tokio = { workspace = true } -tracing-test = { workspace = true } wiremock = { workspace = true } diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index 5c57c8c18..274efab60 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -9,6 +9,7 @@ use tracing::trace; use url::Url; use uv_once_map::OnceMap; +use uv_redacted::DisplaySafeUrl; use crate::Realm; use crate::credentials::{Credentials, Username}; @@ -18,7 +19,7 @@ type FxOnceMap = OnceMap>; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) enum FetchUrl { /// A full index URL - Index(Url), + Index(DisplaySafeUrl), /// A realm URL Realm(Realm), } diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index ed27334f1..dfc05c5f2 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -3,6 +3,8 @@ use base64::read::DecoderReader; 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; @@ -141,7 +143,11 @@ impl Credentials { /// Return [`Credentials`] for a [`Url`] from a [`Netrc`] file, if any. /// /// 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: &Url, username: Option<&str>) -> Option { + pub(crate) fn from_netrc( + netrc: &Netrc, + url: &DisplaySafeUrlRef<'_>, + username: Option<&str>, + ) -> Option { let host = url.host_str()?; let entry = netrc .hosts @@ -299,7 +305,7 @@ impl Credentials { /// /// Any existing credentials will be overridden. #[must_use] - pub fn apply(&self, mut url: Url) -> Url { + pub fn apply(&self, mut url: DisplaySafeUrl) -> DisplaySafeUrl { if let Some(username) = self.username() { let _ = url.set_username(username); } diff --git a/crates/uv-auth/src/index.rs b/crates/uv-auth/src/index.rs index 9419a9a22..e17bbd8fe 100644 --- a/crates/uv-auth/src/index.rs +++ b/crates/uv-auth/src/index.rs @@ -2,6 +2,7 @@ use std::fmt::{self, Display, Formatter}; use rustc_hash::FxHashSet; use url::Url; +use uv_redacted::DisplaySafeUrl; /// When to use authentication. #[derive( @@ -53,10 +54,10 @@ impl Display for AuthPolicy { // could potentially make sense for a future refactor. #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct Index { - pub url: Url, + pub url: DisplaySafeUrl, /// The root endpoint where authentication is applied. /// For PEP 503 endpoints, this excludes `/simple`. - pub root_url: Url, + pub root_url: DisplaySafeUrl, pub auth_policy: AuthPolicy, } @@ -95,7 +96,7 @@ impl Indexes { } /// Get the index URL prefix for a URL if one exists. - pub fn index_url_for(&self, url: &Url) -> Option<&Url> { + pub fn index_url_for(&self, url: &Url) -> Option<&DisplaySafeUrl> { self.find_prefix_index(url).map(|index| &index.url) } diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index 842f2853a..925a6756f 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 url::Url; +use uv_redacted::DisplaySafeUrlRef; use uv_warnings::warn_user_once; use crate::credentials::Credentials; @@ -36,7 +36,11 @@ 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: &Url, username: Option<&str>) -> Option { + pub async fn fetch( + &self, + url: &DisplaySafeUrlRef<'_>, + username: Option<&str>, + ) -> Option { // Validate the request debug_assert!( url.host_str().is_some(), @@ -217,15 +221,18 @@ impl KeyringProvider { mod tests { use super::*; use futures::FutureExt; + use url::Url; #[tokio::test] async fn fetch_url_no_host() { let url = Url::parse("file:/etc/bin/").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some("user"))) - .catch_unwind() - .await; + let result = std::panic::AssertUnwindSafe( + keyring.fetch(&DisplaySafeUrlRef::from(&url), Some("user")), + ) + .catch_unwind() + .await; assert!(result.is_err()); } @@ -234,9 +241,11 @@ mod tests { let url = Url::parse("https://user:password@example.com").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some(url.username()))) - .catch_unwind() - .await; + let result = std::panic::AssertUnwindSafe( + keyring.fetch(&DisplaySafeUrlRef::from(&url), Some(url.username())), + ) + .catch_unwind() + .await; assert!(result.is_err()); } @@ -245,15 +254,18 @@ mod tests { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::empty(); // Panics due to debug assertion; returns `None` in production - let result = std::panic::AssertUnwindSafe(keyring.fetch(&url, Some(url.username()))) - .catch_unwind() - .await; + let result = std::panic::AssertUnwindSafe( + keyring.fetch(&DisplaySafeUrlRef::from(&url), Some(url.username())), + ) + .catch_unwind() + .await; assert!(result.is_err()); } #[tokio::test] async fn fetch_url_no_auth() { let url = Url::parse("https://example.com").unwrap(); + let url = DisplaySafeUrlRef::from(&url); let keyring = KeyringProvider::empty(); let credentials = keyring.fetch(&url, Some("user")); assert!(credentials.await.is_none()); @@ -264,7 +276,9 @@ mod tests { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); assert_eq!( - keyring.fetch(&url, Some("user")).await, + keyring + .fetch(&DisplaySafeUrlRef::from(&url), Some("user")) + .await, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) @@ -272,7 +286,10 @@ mod tests { ); assert_eq!( keyring - .fetch(&url.join("test").unwrap(), Some("user")) + .fetch( + &DisplaySafeUrlRef::from(&url.join("test").unwrap()), + Some("user") + ) .await, Some(Credentials::basic( Some("user".to_string()), @@ -285,7 +302,9 @@ mod tests { async fn fetch_url_no_match() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([("other.com", "user", "password")]); - let credentials = keyring.fetch(&url, Some("user")).await; + let credentials = keyring + .fetch(&DisplaySafeUrlRef::from(&url), Some("user")) + .await; assert_eq!(credentials, None); } @@ -297,21 +316,33 @@ mod tests { (url.host_str().unwrap(), "user", "other-password"), ]); assert_eq!( - keyring.fetch(&url.join("foo").unwrap(), Some("user")).await, + keyring + .fetch( + &DisplaySafeUrlRef::from(&url.join("foo").unwrap()), + Some("user") + ) + .await, Some(Credentials::basic( Some("user".to_string()), Some("password".to_string()) )) ); assert_eq!( - keyring.fetch(&url, Some("user")).await, + keyring + .fetch(&DisplaySafeUrlRef::from(&url), Some("user")) + .await, Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) )) ); assert_eq!( - keyring.fetch(&url.join("bar").unwrap(), Some("user")).await, + keyring + .fetch( + &DisplaySafeUrlRef::from(&url.join("bar").unwrap()), + Some("user") + ) + .await, Some(Credentials::basic( Some("user".to_string()), Some("other-password".to_string()) @@ -323,7 +354,9 @@ mod tests { async fn fetch_url_username() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "user", "password")]); - let credentials = keyring.fetch(&url, Some("user")).await; + let credentials = keyring + .fetch(&DisplaySafeUrlRef::from(&url), Some("user")) + .await; assert_eq!( credentials, Some(Credentials::basic( @@ -337,7 +370,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(&url, None).await; + let credentials = keyring.fetch(&DisplaySafeUrlRef::from(&url), None).await; assert_eq!( credentials, Some(Credentials::basic( @@ -351,12 +384,16 @@ mod tests { async fn fetch_url_username_no_match() { let url = Url::parse("https://example.com").unwrap(); let keyring = KeyringProvider::dummy([(url.host_str().unwrap(), "foo", "password")]); - let credentials = keyring.fetch(&url, Some("bar")).await; + let credentials = keyring + .fetch(&DisplaySafeUrlRef::from(&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(&url, Some("bar")).await; + let credentials = keyring + .fetch(&DisplaySafeUrlRef::from(&url), Some("bar")) + .await; assert_eq!(credentials, None); } } diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 6aa96a245..90a957630 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -1,7 +1,6 @@ use std::sync::{Arc, LazyLock}; use tracing::trace; -use url::Url; use cache::CredentialsCache; pub use credentials::Credentials; @@ -9,6 +8,7 @@ pub use index::{AuthPolicy, Index, Indexes}; pub use keyring::KeyringProvider; pub use middleware::AuthMiddleware; use realm::Realm; +use uv_redacted::DisplaySafeUrl; mod cache; mod credentials; @@ -28,7 +28,7 @@ pub(crate) static CREDENTIALS_CACHE: LazyLock = /// Populate the global authentication store with credentials on a URL, if there are any. /// /// Returns `true` if the store was updated. -pub fn store_credentials_from_url(url: &Url) -> bool { +pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool { if let Some(credentials) = Credentials::from_url(url) { trace!("Caching credentials for {url}"); CREDENTIALS_CACHE.insert(url, Arc::new(credentials)); @@ -41,7 +41,7 @@ pub fn store_credentials_from_url(url: &Url) -> bool { /// Populate the global authentication store with credentials on a URL, if there are any. /// /// Returns `true` if the store was updated. -pub fn store_credentials(url: &Url, credentials: Arc) { +pub fn store_credentials(url: &DisplaySafeUrl, credentials: Arc) { trace!("Caching credentials for {url}"); CREDENTIALS_CACHE.insert(url, credentials); } diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index f31a01540..c219a2cf6 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -1,7 +1,7 @@ use std::sync::{Arc, LazyLock}; use http::{Extensions, StatusCode}; -use url::Url; +use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlRef}; use crate::{ CREDENTIALS_CACHE, CredentialsCache, KeyringProvider, @@ -274,6 +274,7 @@ impl Middleware for AuthMiddleware { trace!("Checking for credentials for {url}"); (request, None) }; + let retry_request_url = DisplaySafeUrlRef::from(retry_request.url()); let username = credentials .as_ref() @@ -282,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); @@ -307,7 +308,7 @@ impl Middleware for AuthMiddleware { if let Some(credentials) = self .fetch_credentials( credentials.as_deref(), - retry_request.url(), + retry_request_url, maybe_index_url, auth_policy, ) @@ -362,7 +363,7 @@ impl AuthMiddleware { // Nothing to insert into the cache if we don't have credentials return next.run(request, extensions).await; }; - let url = request.url().clone(); + let url = DisplaySafeUrl::from(request.url().clone()); if matches!(auth_policy, AuthPolicy::Always) && credentials.password().is_none() { return Err(Error::Middleware(format_err!("Missing password for {url}"))); } @@ -387,8 +388,8 @@ impl AuthMiddleware { mut request: Request, extensions: &mut Extensions, next: Next<'_>, - url: &str, - index_url: Option<&Url>, + url: &DisplaySafeUrl, + index_url: Option<&DisplaySafeUrl>, auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let credentials = Arc::new(credentials); @@ -430,7 +431,12 @@ impl AuthMiddleware { // Do not insert already-cached credentials None } else if let Some(credentials) = self - .fetch_credentials(Some(&credentials), request.url(), index_url, auth_policy) + .fetch_credentials( + Some(&credentials), + DisplaySafeUrlRef::from(request.url()), + index_url, + auth_policy, + ) .await { request = credentials.authenticate(request); @@ -462,8 +468,8 @@ impl AuthMiddleware { async fn fetch_credentials( &self, credentials: Option<&Credentials>, - url: &Url, - maybe_index_url: Option<&Url>, + url: DisplaySafeUrlRef<'_>, + maybe_index_url: Option<&DisplaySafeUrl>, auth_policy: AuthPolicy, ) -> Option> { let username = Username::from( @@ -475,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 @@ -502,7 +508,7 @@ impl AuthMiddleware { debug!("Checking netrc for credentials for {url}"); Credentials::from_netrc( netrc, - url, + &url, credentials .as_ref() .and_then(|credentials| credentials.username()), @@ -523,17 +529,17 @@ 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(index_url, Some(username)).await + keyring.fetch(&DisplaySafeUrlRef::from(index_url), Some(username)).await } else { debug!("Checking keyring for credentials for full URL {}@{}", username, url); - keyring.fetch(url, Some(username)).await + keyring.fetch(&url, Some(username)).await } } else if matches!(auth_policy, AuthPolicy::Always) { if let Some(index_url) = maybe_index_url { debug!( "Checking keyring for credentials for index URL {index_url} without username due to `authenticate = always`" ); - keyring.fetch(index_url, None).await + keyring.fetch(&DisplaySafeUrlRef::from(index_url), None).await } else { None } @@ -558,24 +564,17 @@ impl AuthMiddleware { } } -fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> String { - if !tracing::enabled!(tracing::Level::DEBUG) { - return request.url().to_string(); - } - - let mut url = request.url().clone(); +fn tracing_url(request: &Request, credentials: Option<&Credentials>) -> DisplaySafeUrl { + let mut url = DisplaySafeUrl::from(request.url().clone()); if let Some(creds) = credentials { - if creds.password().is_some() { - if let Some(username) = creds.username() { - let _ = url.set_username(username); - } - let _ = url.set_password(Some("****")); - // A username on its own might be a secret token. - } else if creds.username().is_some() { - let _ = url.set_username("****"); + if let Some(username) = creds.username() { + let _ = url.set_username(username); + } + if let Some(password) = creds.password() { + let _ = url.set_password(Some(password)); } } - url.to_string() + url } #[cfg(test)] @@ -1749,13 +1748,13 @@ mod tests { let base_url_2 = base_url.join("prefix_2")?; let indexes = Indexes::from_indexes(vec![ Index { - url: base_url_1.clone(), - root_url: base_url_1.clone(), + url: DisplaySafeUrl::from(base_url_1.clone()), + root_url: DisplaySafeUrl::from(base_url_1.clone()), auth_policy: AuthPolicy::Auto, }, Index { - url: base_url_2.clone(), - root_url: base_url_2.clone(), + url: DisplaySafeUrl::from(base_url_2.clone()), + root_url: DisplaySafeUrl::from(base_url_2.clone()), auth_policy: AuthPolicy::Auto, }, ]); @@ -1857,8 +1856,8 @@ mod tests { let base_url = Url::parse(&server.uri())?; let index_url = base_url.join("prefix_1")?; let indexes = Indexes::from_indexes(vec![Index { - url: index_url.clone(), - root_url: index_url.clone(), + url: DisplaySafeUrl::from(index_url.clone()), + root_url: DisplaySafeUrl::from(index_url.clone()), auth_policy: AuthPolicy::Auto, }]); @@ -1912,7 +1911,7 @@ mod tests { } fn indexes_for(url: &Url, policy: AuthPolicy) -> Indexes { - let mut url = url.clone(); + let mut url = DisplaySafeUrl::from(url.clone()); url.set_password(None).ok(); url.set_username("").ok(); Indexes::from_indexes(vec![Index { @@ -2104,16 +2103,14 @@ mod tests { } #[test] - #[tracing_test::traced_test(level = "debug")] fn test_tracing_url() { // No credentials let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, None), - "https://pypi-proxy.fly.dev/basic-auth/simple" + DisplaySafeUrl::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap() ); - // Mask username if there is a username but no password let creds = Credentials::Basic { username: Username::new(Some(String::from("user"))), password: None, @@ -2121,10 +2118,9 @@ mod tests { let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, Some(&creds)), - "https://****@pypi-proxy.fly.dev/basic-auth/simple" + DisplaySafeUrl::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap() ); - // Log username but mask password if a password is present let creds = Credentials::Basic { username: Username::new(Some(String::from("user"))), password: Some(Password::new(String::from("password"))), @@ -2132,7 +2128,8 @@ mod tests { let req = create_request("https://pypi-proxy.fly.dev/basic-auth/simple"); assert_eq!( tracing_url(&req, Some(&creds)), - "https://user:****@pypi-proxy.fly.dev/basic-auth/simple" + DisplaySafeUrl::parse("https://user:password@pypi-proxy.fly.dev/basic-auth/simple") + .unwrap() ); } diff --git a/crates/uv-cache-key/Cargo.toml b/crates/uv-cache-key/Cargo.toml index a50f3ca65..931a24db3 100644 --- a/crates/uv-cache-key/Cargo.toml +++ b/crates/uv-cache-key/Cargo.toml @@ -17,6 +17,8 @@ doctest = false workspace = true [dependencies] +uv-redacted = { workspace = true } + hex = { workspace = true } memchr = { workspace = true } percent-encoding = { workspace = true } diff --git a/crates/uv-cache-key/src/canonical_url.rs b/crates/uv-cache-key/src/canonical_url.rs index 50300f666..19f5a3d7c 100644 --- a/crates/uv-cache-key/src/canonical_url.rs +++ b/crates/uv-cache-key/src/canonical_url.rs @@ -4,6 +4,7 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use url::Url; +use uv_redacted::DisplaySafeUrl; use crate::cache_key::{CacheKey, CacheKeyHasher}; @@ -16,10 +17,10 @@ use crate::cache_key::{CacheKey, CacheKeyHasher}; /// string value of the `Url` it contains. This is intentional, because all fetching should still /// happen within the context of the original URL. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub struct CanonicalUrl(Url); +pub struct CanonicalUrl(DisplaySafeUrl); impl CanonicalUrl { - pub fn new(url: &Url) -> Self { + pub fn new(url: &DisplaySafeUrl) -> Self { let mut url = url.clone(); // If the URL cannot be a base, then it's not a valid URL anyway. @@ -42,8 +43,8 @@ impl CanonicalUrl { // almost certainly not using the same case conversion rules that GitHub // does. (See issue #84) if url.host_str() == Some("github.com") { - url.set_scheme(url.scheme().to_lowercase().as_str()) - .unwrap(); + let scheme = url.scheme().to_lowercase(); + url.set_scheme(&scheme).unwrap(); let path = url.path().to_lowercase(); url.set_path(&path); } @@ -56,7 +57,8 @@ impl CanonicalUrl { .is_some_and(|ext| ext.eq_ignore_ascii_case("git")); if needs_chopping { let prefix = &prefix[..prefix.len() - 4]; - url.set_path(&format!("{prefix}@{suffix}")); + let path = format!("{prefix}@{suffix}"); + url.set_path(&path); } } else { // Ex) `git+https://github.com/pypa/sample-namespace-packages.git` @@ -97,7 +99,7 @@ impl CanonicalUrl { } pub fn parse(url: &str) -> Result { - Ok(Self::new(&Url::parse(url)?)) + Ok(Self::new(&DisplaySafeUrl::parse(url)?)) } } @@ -117,7 +119,7 @@ impl Hash for CanonicalUrl { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: CanonicalUrl) -> Self { value.0 } @@ -138,10 +140,10 @@ impl std::fmt::Display for CanonicalUrl { /// [`CanonicalUrl`] values, but the same [`RepositoryUrl`], since they map to the same /// resource. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub struct RepositoryUrl(Url); +pub struct RepositoryUrl(DisplaySafeUrl); impl RepositoryUrl { - pub fn new(url: &Url) -> Self { + pub fn new(url: &DisplaySafeUrl) -> Self { let mut url = CanonicalUrl::new(url).0; // If a Git URL ends in a reference (like a branch, tag, or commit), remove it. @@ -163,7 +165,7 @@ impl RepositoryUrl { } pub fn parse(url: &str) -> Result { - Ok(Self::new(&Url::parse(url)?)) + Ok(Self::new(&DisplaySafeUrl::parse(url)?)) } } diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index a3a9ab76b..779309f0f 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -24,6 +24,7 @@ uv-distribution-types = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-normalize = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } @@ -35,5 +36,4 @@ same-file = { workspace = true } serde = { workspace = true, features = ["derive"] } tempfile = { workspace = true } tracing = { workspace = true } -url = { workspace = true } walkdir = { workspace = true } diff --git a/crates/uv-cache/src/wheel.rs b/crates/uv-cache/src/wheel.rs index d00a2895c..76103f0ff 100644 --- a/crates/uv-cache/src/wheel.rs +++ b/crates/uv-cache/src/wheel.rs @@ -1,9 +1,8 @@ use std::path::{Path, PathBuf}; -use url::Url; - use uv_cache_key::{CanonicalUrl, cache_digest}; use uv_distribution_types::IndexUrl; +use uv_redacted::DisplaySafeUrl; /// Cache wheels and their metadata, both from remote wheels and built from source distributions. #[derive(Debug, Clone)] @@ -11,16 +10,16 @@ pub enum WheelCache<'a> { /// Either PyPI or an alternative index, which we key by index URL. Index(&'a IndexUrl), /// A direct URL dependency, which we key by URL. - Url(&'a Url), + Url(&'a DisplaySafeUrl), /// A path dependency, which we key by URL. - Path(&'a Url), + Path(&'a DisplaySafeUrl), /// An editable dependency, which we key by URL. - Editable(&'a Url), + Editable(&'a DisplaySafeUrl), /// A Git dependency, which we key by URL and SHA. /// /// Note that this variant only exists for source distributions; wheels can't be delivered /// through Git. - Git(&'a Url, &'a str), + Git(&'a DisplaySafeUrl, &'a str), } impl WheelCache<'_> { @@ -30,7 +29,7 @@ impl WheelCache<'_> { WheelCache::Index(IndexUrl::Pypi(_)) => WheelCacheKind::Pypi.root(), WheelCache::Index(url) => WheelCacheKind::Index .root() - .join(cache_digest(&CanonicalUrl::new(url))), + .join(cache_digest(&CanonicalUrl::new(url.url()))), WheelCache::Url(url) => WheelCacheKind::Url .root() .join(cache_digest(&CanonicalUrl::new(url))), diff --git a/crates/uv-cli/Cargo.toml b/crates/uv-cli/Cargo.toml index debc21a74..5713af93f 100644 --- a/crates/uv-cli/Cargo.toml +++ b/crates/uv-cli/Cargo.toml @@ -25,6 +25,7 @@ uv-normalize = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["clap", "schemars"]} +uv-redacted = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } uv-settings = { workspace = true, features = ["schemars"] } uv-static = { workspace = true } diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 77c866f25..0dac96000 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -8,7 +8,6 @@ use clap::builder::Styles; use clap::builder::styling::{AnsiColor, Effects, Style}; use clap::{Args, Parser, Subcommand}; -use url::Url; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, @@ -19,6 +18,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName, PipGroupName}; use uv_pep508::{MarkerTree, Requirement}; use uv_pypi_types::VerbatimParsedUrl; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; +use uv_redacted::DisplaySafeUrl; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_static::EnvVars; use uv_torch::TorchMode; @@ -5897,7 +5897,7 @@ pub struct PublishArgs { /// /// Defaults to PyPI's publish URL (). #[arg(long, env = EnvVars::UV_PUBLISH_URL)] - pub publish_url: Option, + pub publish_url: Option, /// Check an index URL for existing files to skip duplicate uploads. /// diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 2db0a920e..f5fda246d 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -21,6 +21,7 @@ use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; use uv_pep508::MarkerEnvironment; use uv_platform_tags::Platform; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_version::version; use uv_warnings::warn_user_once; @@ -407,7 +408,7 @@ enum Security { impl BaseClient { /// Selects the appropriate client based on the host's trustworthiness. - pub fn for_host(&self, url: &Url) -> &ClientWithMiddleware { + pub fn for_host(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware { if self.disable_ssl(url) { &self.dangerous_client } else { @@ -416,7 +417,7 @@ impl BaseClient { } /// Returns `true` if the host is trusted to use the insecure client. - pub fn disable_ssl(&self, url: &Url) -> bool { + pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool { self.allow_insecure_host .iter() .any(|allow_insecure_host| allow_insecure_host.matches(url)) diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index f5ade702b..5821fe9f3 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -12,6 +12,7 @@ use tracing::{Instrument, debug, info_span, instrument, trace, warn}; use uv_cache::{CacheEntry, Freshness}; use uv_fs::write_atomic; +use uv_redacted::DisplaySafeUrl; use crate::BaseClient; use crate::base_client::is_extended_transient_error; @@ -481,11 +482,11 @@ impl CachedClient { cached: DataWithCachePolicy, new_cache_policy_builder: CachePolicyBuilder, ) -> Result { - let url = req.url().clone(); + let url = DisplaySafeUrl::from(req.url().clone()); debug!("Sending revalidation request for: {url}"); let response = self .0 - .for_host(req.url()) + .for_host(&url) .execute(req) .instrument(info_span!("revalidation_request", url = url.as_str())) .await @@ -521,7 +522,7 @@ impl CachedClient { &self, req: Request, ) -> Result<(Response, Option>), Error> { - let url = req.url().clone(); + let url = DisplaySafeUrl::from(req.url().clone()); trace!("Sending fresh {} request for {}", req.method(), url); let cache_policy_builder = CachePolicyBuilder::new(&req); let response = self diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index ad1e06823..6629171e9 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -3,11 +3,10 @@ use std::ops::Deref; use async_http_range_reader::AsyncHttpRangeReaderError; use async_zip::error::ZipError; -use url::Url; use uv_distribution_filename::{WheelFilename, WheelFilenameError}; use uv_normalize::PackageName; -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; use crate::middleware::OfflineError; use crate::{FlatIndexError, html}; @@ -30,12 +29,12 @@ impl Error { } /// Create a new error from a JSON parsing error. - pub(crate) fn from_json_err(err: serde_json::Error, url: Url) -> Self { + pub(crate) fn from_json_err(err: serde_json::Error, url: DisplaySafeUrl) -> Self { ErrorKind::BadJson { source: err, url }.into() } /// Create a new error from an HTML parsing error. - pub(crate) fn from_html_err(err: html::Error, url: Url) -> Self { + pub(crate) fn from_html_err(err: html::Error, url: DisplaySafeUrl) -> Self { ErrorKind::BadHtml { source: err, url }.into() } @@ -160,10 +159,10 @@ pub enum ErrorKind { Flat(#[from] FlatIndexError), #[error("Expected a file URL, but received: {0}")] - NonFileUrl(Url), + NonFileUrl(DisplaySafeUrl), #[error("Expected an index URL, but received non-base URL: {0}")] - CannotBeABase(Url), + CannotBeABase(DisplaySafeUrl), #[error("Failed to read metadata: `{0}`")] Metadata(String, #[source] uv_metadata::Error), @@ -196,16 +195,22 @@ pub enum ErrorKind { /// An error that happened while making a request or in a reqwest middleware. #[error("Failed to fetch: `{0}`")] - WrappedReqwestError(Url, #[source] WrappedReqwestError), + WrappedReqwestError(DisplaySafeUrl, #[source] WrappedReqwestError), - #[error("Received some unexpected JSON from {}", redacted_url(url))] - BadJson { source: serde_json::Error, url: Url }, + #[error("Received some unexpected JSON from {}", url)] + BadJson { + source: serde_json::Error, + url: DisplaySafeUrl, + }, - #[error("Received some unexpected HTML from {}", redacted_url(url))] - BadHtml { source: html::Error, url: Url }, + #[error("Received some unexpected HTML from {}", url)] + BadHtml { + source: html::Error, + url: DisplaySafeUrl, + }, #[error("Failed to read zip with range requests: `{0}`")] - AsyncHttpRangeReader(Url, #[source] AsyncHttpRangeReaderError), + AsyncHttpRangeReader(DisplaySafeUrl, #[source] AsyncHttpRangeReaderError), #[error("{0} is not a valid wheel filename")] WheelFilename(#[source] WheelFilenameError), @@ -232,13 +237,13 @@ pub enum ErrorKind { Encode(#[source] rmp_serde::encode::Error), #[error("Missing `Content-Type` header for {0}")] - MissingContentType(Url), + MissingContentType(DisplaySafeUrl), #[error("Invalid `Content-Type` header for {0}")] - InvalidContentTypeHeader(Url, #[source] http::header::ToStrError), + InvalidContentTypeHeader(DisplaySafeUrl, #[source] http::header::ToStrError), #[error("Unsupported `Content-Type` \"{1}\" for {0}. Expected JSON or HTML.")] - UnsupportedMediaType(Url, String), + UnsupportedMediaType(DisplaySafeUrl, String), #[error("Reading from cache archive failed: {0}")] ArchiveRead(String), @@ -253,11 +258,14 @@ pub enum ErrorKind { } impl ErrorKind { - pub(crate) fn from_reqwest(url: Url, error: reqwest::Error) -> Self { + pub(crate) fn from_reqwest(url: DisplaySafeUrl, error: reqwest::Error) -> Self { Self::WrappedReqwestError(url, WrappedReqwestError::from(error)) } - pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { + pub(crate) fn from_reqwest_middleware( + url: DisplaySafeUrl, + err: reqwest_middleware::Error, + ) -> Self { if let reqwest_middleware::Error::Middleware(ref underlying) = err { if let Some(err) = underlying.downcast_ref::() { return Self::Offline(err.url().to_string()); diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index ca2166e18..0670fbe36 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -10,7 +10,7 @@ use uv_cache_key::cache_digest; use uv_distribution_filename::DistFilename; use uv_distribution_types::{File, FileLocation, IndexUrl, UrlString}; use uv_pypi_types::HashDigests; -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use crate::cached_client::{CacheControl, CachedClientError}; @@ -20,13 +20,13 @@ use crate::{CachedClient, Connectivity, Error, ErrorKind, OwnedArchive}; #[derive(Debug, thiserror::Error)] pub enum FlatIndexError { #[error("Expected a file URL, but received: {0}")] - NonFileUrl(Url), + NonFileUrl(DisplaySafeUrl), #[error("Failed to read `--find-links` directory: {0}")] FindLinksDirectory(PathBuf, #[source] FindLinksDirectoryError), #[error("Failed to read `--find-links` URL: {0}")] - FindLinksUrl(Url, #[source] Error), + FindLinksUrl(DisplaySafeUrl, #[source] Error), } #[derive(Debug, thiserror::Error)] @@ -159,7 +159,7 @@ impl<'a> FlatIndexClient<'a> { /// Read a flat remote index from a `--find-links` URL. async fn read_from_url( &self, - url: &Url, + url: &DisplaySafeUrl, flat_index: &IndexUrl, ) -> Result { let cache_entry = self.cache.entry( @@ -180,7 +180,7 @@ impl<'a> FlatIndexClient<'a> { .client .uncached() .for_host(url) - .get(url.clone()) + .get(Url::from(url.clone())) .header("Accept-Encoding", "gzip") .header("Accept", "text/html") .build() @@ -189,7 +189,7 @@ impl<'a> FlatIndexClient<'a> { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. - let url = response.url().clone(); + let url = DisplaySafeUrl::from(response.url().clone()); let text = response .text() @@ -208,7 +208,7 @@ impl<'a> FlatIndexClient<'a> { Ok(file) => Some(file), Err(err) => { // Ignore files with unparsable version specifiers. - warn!("Skipping file in {}: {err}", redacted_url(&url)); + warn!("Skipping file in {}: {err}", &url); None } } @@ -294,7 +294,7 @@ impl<'a> FlatIndexClient<'a> { }; // SAFETY: The index path is itself constructed from a URL. - let url = Url::from_file_path(entry.path()).unwrap(); + let url = DisplaySafeUrl::from_file_path(entry.path()).unwrap(); let file = File { dist_info_metadata: false, @@ -303,7 +303,7 @@ impl<'a> FlatIndexClient<'a> { requires_python: None, size: None, upload_time_utc_ms: None, - url: FileLocation::AbsoluteUrl(UrlString::from(&url)), + url: FileLocation::AbsoluteUrl(UrlString::from(url)), yanked: None, }; diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 106bd35a3..e62da22d7 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -8,6 +8,7 @@ use url::Url; use uv_pep440::VersionSpecifiers; use uv_pypi_types::{BaseUrl, CoreMetadata, File, Hashes, Yanked}; use uv_pypi_types::{HashError, LenientVersionSpecifiers}; +use uv_redacted::DisplaySafeUrl; /// A parsed structure from PyPI "HTML" index format for a single package. #[derive(Debug, Clone)] @@ -27,7 +28,7 @@ impl SimpleHtml { // Parse the first `` tag, if any, to determine the base URL to which all // relative URLs should be resolved. The HTML spec requires that the `` tag // appear before other tags with attribute values of URLs. - let base = BaseUrl::from( + let base = BaseUrl::from(DisplaySafeUrl::from( dom.nodes() .iter() .filter_map(|node| node.as_tag()) @@ -37,7 +38,7 @@ impl SimpleHtml { .transpose()? .flatten() .unwrap_or_else(|| url.clone()), - ); + )); // Parse each `` tag, to extract the filename, hash, and URL. let mut files: Vec = dom @@ -278,21 +279,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -335,21 +322,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -395,21 +368,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "index.python.org", - ), - ), - port: None, - path: "/", - query: None, - fragment: None, - }, + https://index.python.org/, ), files: [ File { @@ -452,21 +411,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -509,21 +454,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -566,21 +497,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -621,21 +538,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -673,28 +576,14 @@ mod tests { "; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [], } - "###); + "); } #[test] @@ -711,28 +600,14 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [], } - "###); + "); } #[test] @@ -752,21 +627,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -807,21 +668,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -863,21 +710,7 @@ mod tests { Ok( SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -920,21 +753,7 @@ mod tests { Ok( SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -994,21 +813,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "storage.googleapis.com", - ), - ), - port: None, - path: "/jax-releases/jax_cuda_releases.html", - query: None, - fragment: None, - }, + https://storage.googleapis.com/jax-releases/jax_cuda_releases.html, ), files: [ File { @@ -1076,21 +881,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "account.d.codeartifact.us-west-2.amazonaws.com", - ), - ), - port: None, - path: "/pypi/shared-packages-pypi/simple/flask/", - query: None, - fragment: None, - }, + https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/, ), files: [ File { @@ -1179,21 +970,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/jinja2/", - query: None, - fragment: None, - }, + https://download.pytorch.org/whl/jinja2/, ), files: [ File { @@ -1252,21 +1029,7 @@ mod tests { insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( - Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "account.d.codeartifact.us-west-2.amazonaws.com", - ), - ), - port: None, - path: "/pypi/shared-packages-pypi/simple/flask/", - query: None, - fragment: None, - }, + https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/, ), files: [ File { diff --git a/crates/uv-client/src/middleware.rs b/crates/uv-client/src/middleware.rs index 13bd94904..f3a899b73 100644 --- a/crates/uv-client/src/middleware.rs +++ b/crates/uv-client/src/middleware.rs @@ -1,19 +1,19 @@ use http::Extensions; use std::fmt::Debug; +use uv_redacted::DisplaySafeUrl; use reqwest::{Request, Response}; use reqwest_middleware::{Middleware, Next}; -use url::Url; /// A custom error type for the offline middleware. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct OfflineError { - url: Url, + url: DisplaySafeUrl, } impl OfflineError { /// Returns the URL that caused the error. - pub(crate) fn url(&self) -> &Url { + pub(crate) fn url(&self) -> &DisplaySafeUrl { &self.url } } @@ -43,7 +43,7 @@ impl Middleware for OfflineMiddleware { ) -> reqwest_middleware::Result { Err(reqwest_middleware::Error::Middleware( OfflineError { - url: req.url().clone(), + url: DisplaySafeUrl::from(req.url().clone()), } .into(), )) diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 9831fb81d..9e8ea23b7 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -31,7 +31,7 @@ use uv_pep440::Version; use uv_pep508::MarkerEnvironment; use uv_platform_tags::Platform; use uv_pypi_types::{ResolutionMetadata, SimpleJson}; -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use uv_torch::TorchStrategy; @@ -251,12 +251,12 @@ impl RegistryClient { } /// Return the [`BaseClient`] used by this client. - pub fn uncached_client(&self, url: &Url) -> &ClientWithMiddleware { + pub fn uncached_client(&self, url: &DisplaySafeUrl) -> &ClientWithMiddleware { self.client.uncached().for_host(url) } /// Returns `true` if SSL verification is disabled for the given URL. - pub fn disable_ssl(&self, url: &Url) -> bool { + pub fn disable_ssl(&self, url: &DisplaySafeUrl) -> bool { self.client.uncached().disable_ssl(url) } @@ -485,10 +485,7 @@ impl RegistryClient { // ref https://github.com/servo/rust-url/issues/333 .push(""); - trace!( - "Fetching metadata for {package_name} from {}", - redacted_url(&url) - ); + trace!("Fetching metadata for {package_name} from {url}"); let cache_entry = self.cache.entry( CacheBucket::Simple, @@ -554,13 +551,13 @@ impl RegistryClient { async fn fetch_remote_index( &self, package_name: &PackageName, - url: &Url, + url: &DisplaySafeUrl, cache_entry: &CacheEntry, cache_control: CacheControl, ) -> Result, Error> { let simple_request = self .uncached_client(url) - .get(url.clone()) + .get(Url::from(url.clone())) .header("Accept-Encoding", "gzip") .header("Accept", MediaType::accepts()) .build() @@ -569,7 +566,7 @@ impl RegistryClient { async { // Use the response URL, rather than the request URL, as the base for relative URLs. // This ensures that we handle redirects and other URL transformations correctly. - let url = response.url().clone(); + let url = DisplaySafeUrl::from(response.url().clone()); let content_type = response .headers() @@ -629,7 +626,7 @@ impl RegistryClient { async fn fetch_local_index( &self, package_name: &PackageName, - url: &Url, + url: &DisplaySafeUrl, ) -> Result, Error> { let path = url .to_file_path() @@ -669,7 +666,7 @@ impl RegistryClient { /// A local file path. Path(PathBuf), /// A remote URL. - Url(Url), + Url(DisplaySafeUrl), } let wheel = wheels.best_wheel(); @@ -770,14 +767,15 @@ impl RegistryClient { &self, index: &IndexUrl, file: &File, - url: &Url, + url: &DisplaySafeUrl, capabilities: &IndexCapabilities, ) -> Result { // If the metadata file is available at its own url (PEP 658), download it from there. let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?; if file.dist_info_metadata { let mut url = url.clone(); - url.set_path(&format!("{}.metadata", url.path())); + let path = format!("{}.metadata", url.path()); + url.set_path(&path); let cache_entry = self.cache.entry( CacheBucket::Wheels, @@ -818,7 +816,7 @@ impl RegistryClient { }; let req = self .uncached_client(&url) - .get(url.clone()) + .get(Url::from(url.clone())) .build() .map_err(|err| ErrorKind::from_reqwest(url.clone(), err))?; Ok(self @@ -844,7 +842,7 @@ impl RegistryClient { async fn wheel_metadata_no_pep658<'data>( &self, filename: &'data WheelFilename, - url: &'data Url, + url: &'data DisplaySafeUrl, index: Option<&'data IndexUrl>, cache_shard: WheelCache<'data>, capabilities: &'data IndexCapabilities, @@ -874,7 +872,7 @@ impl RegistryClient { if index.is_none_or(|index| capabilities.supports_range_requests(index)) { let req = self .uncached_client(url) - .head(url.clone()) + .head(Url::from(url.clone())) .header( "accept-encoding", http::HeaderValue::from_static("identity"), @@ -895,7 +893,7 @@ impl RegistryClient { let mut reader = AsyncHttpRangeReader::from_head_response( self.uncached_client(url).clone(), response, - url.clone(), + Url::from(url.clone()), headers.clone(), ) .await @@ -949,7 +947,7 @@ impl RegistryClient { // Create a request to stream the file. let req = self .uncached_client(url) - .get(url.clone()) + .get(Url::from(url.clone())) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading @@ -1141,7 +1139,11 @@ impl SimpleMetadata { } /// Read the [`SimpleMetadata`] from an HTML index. - fn from_html(text: &str, package_name: &PackageName, url: &Url) -> Result { + fn from_html( + text: &str, + package_name: &PackageName, + url: &DisplaySafeUrl, + ) -> Result { let SimpleHtml { base, files } = SimpleHtml::parse(text, url).map_err(|err| Error::from_html_err(err, url.clone()))?; @@ -1220,10 +1222,9 @@ impl Connectivity { mod tests { use std::str::FromStr; - use url::Url; - use uv_normalize::PackageName; use uv_pypi_types::{JoinRelativeError, SimpleJson}; + use uv_redacted::DisplaySafeUrl; use crate::{SimpleMetadata, SimpleMetadatum, html::SimpleHtml}; @@ -1263,7 +1264,7 @@ mod tests { } "#; let data: SimpleJson = serde_json::from_str(response).unwrap(); - let base = Url::parse("https://pypi.org/simple/pyflyby/").unwrap(); + let base = DisplaySafeUrl::parse("https://pypi.org/simple/pyflyby/").unwrap(); let simple_metadata = SimpleMetadata::from_files( data.files, &PackageName::from_str("pyflyby").unwrap(), @@ -1300,7 +1301,7 @@ mod tests { "#; // Note the lack of a trailing `/` here is important for coverage of url-join behavior - let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask") + let base = DisplaySafeUrl::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask") .unwrap(); let SimpleHtml { base, files } = SimpleHtml::parse(text, &base).unwrap(); @@ -1309,7 +1310,10 @@ mod tests { .iter() .map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url)) .collect::, JoinRelativeError>>()?; - let urls = urls.iter().map(Url::as_str).collect::>(); + let urls = urls + .iter() + .map(DisplaySafeUrl::to_string) + .collect::>(); insta::assert_debug_snapshot!(urls, @r#" [ "https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/0.1/Flask-0.1.tar.gz", diff --git a/crates/uv-client/tests/it/remote_metadata.rs b/crates/uv-client/tests/it/remote_metadata.rs index 72bc73222..1dbdf1bad 100644 --- a/crates/uv-client/tests/it/remote_metadata.rs +++ b/crates/uv-client/tests/it/remote_metadata.rs @@ -1,13 +1,13 @@ use std::str::FromStr; use anyhow::Result; -use url::Url; use uv_cache::Cache; use uv_client::RegistryClientBuilder; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{BuiltDist, DirectUrlBuiltDist, IndexCapabilities}; use uv_pep508::VerbatimUrl; +use uv_redacted::DisplaySafeUrl; #[tokio::test] async fn remote_metadata_with_and_without_cache() -> Result<()> { @@ -21,7 +21,7 @@ async fn remote_metadata_with_and_without_cache() -> Result<()> { let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?; let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist { filename, - location: Box::new(Url::parse(url).unwrap()), + location: Box::new(DisplaySafeUrl::parse(url).unwrap()), url: VerbatimUrl::from_str(url).unwrap(), }); let capabilities = IndexCapabilities::default(); diff --git a/crates/uv-client/tests/it/user_agent_version.rs b/crates/uv-client/tests/it/user_agent_version.rs index 42eebf3be..b10249154 100644 --- a/crates/uv-client/tests/it/user_agent_version.rs +++ b/crates/uv-client/tests/it/user_agent_version.rs @@ -16,6 +16,7 @@ use uv_client::LineHaul; use uv_client::RegistryClientBuilder; use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder}; use uv_platform_tags::{Arch, Os, Platform}; +use uv_redacted::DisplaySafeUrl; use uv_version::version; #[tokio::test] @@ -54,12 +55,12 @@ async fn test_user_agent_has_version() -> Result<()> { let client = RegistryClientBuilder::new(cache).build(); // Send request to our dummy server - let url = Url::from_str(&format!("http://{addr}"))?; + let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; let res = client .cached_client() .uncached() .for_host(&url) - .get(url) + .get(Url::from(url)) .send() .await?; @@ -151,12 +152,12 @@ async fn test_user_agent_has_linehaul() -> Result<()> { let client = builder.build(); // Send request to our dummy server - let url = Url::from_str(&format!("http://{addr}"))?; + let url = DisplaySafeUrl::from_str(&format!("http://{addr}"))?; let res = client .cached_client() .uncached() .for_host(&url) - .get(url) + .get(Url::from(url)) .send() .await?; diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 91943937e..dc5a70166 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -27,6 +27,7 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-small-str = { workspace = true } arcstr = { workspace = true } @@ -50,3 +51,6 @@ version-ranges = { workspace = true } [dev-dependencies] toml = { workspace = true } + +[features] +schemars = ["dep:schemars", "uv-redacted/schemars"] diff --git a/crates/uv-distribution-types/src/buildable.rs b/crates/uv-distribution-types/src/buildable.rs index 3fe6fe8db..c97bb362f 100644 --- a/crates/uv-distribution-types/src/buildable.rs +++ b/crates/uv-distribution-types/src/buildable.rs @@ -1,13 +1,13 @@ use std::borrow::Cow; use std::path::Path; -use url::Url; use uv_distribution_filename::SourceDistExtension; use uv_git_types::GitUrl; use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::VerbatimUrl; use uv_normalize::PackageName; +use uv_redacted::DisplaySafeUrl; use crate::{DirectorySourceDist, GitSourceDist, Name, PathSourceDist, SourceDist}; @@ -102,8 +102,8 @@ pub enum SourceUrl<'a> { } impl SourceUrl<'_> { - /// Return the [`Url`] of the source. - pub fn url(&self) -> &Url { + /// Return the [`DisplaySafeUrl`] of the source. + pub fn url(&self) -> &DisplaySafeUrl { match self { Self::Direct(dist) => dist.url, Self::Git(dist) => dist.url, @@ -147,7 +147,7 @@ impl std::fmt::Display for SourceUrl<'_> { #[derive(Debug, Clone)] pub struct DirectSourceUrl<'a> { - pub url: &'a Url, + pub url: &'a DisplaySafeUrl, pub subdirectory: Option<&'a Path>, pub ext: SourceDistExtension, } @@ -185,7 +185,7 @@ impl<'a> From<&'a GitSourceDist> for GitSourceUrl<'a> { #[derive(Debug, Clone)] pub struct PathSourceUrl<'a> { - pub url: &'a Url, + pub url: &'a DisplaySafeUrl, pub path: Cow<'a, Path>, pub ext: SourceDistExtension, } @@ -208,7 +208,7 @@ impl<'a> From<&'a PathSourceDist> for PathSourceUrl<'a> { #[derive(Debug, Clone)] pub struct DirectorySourceUrl<'a> { - pub url: &'a Url, + pub url: &'a DisplaySafeUrl, pub install_path: Cow<'a, Path>, pub editable: bool, } diff --git a/crates/uv-distribution-types/src/error.rs b/crates/uv-distribution-types/src/error.rs index fc1c4f588..34dc29e78 100644 --- a/crates/uv-distribution-types/src/error.rs +++ b/crates/uv-distribution-types/src/error.rs @@ -1,6 +1,5 @@ -use url::Url; - use uv_normalize::PackageName; +use uv_redacted::DisplaySafeUrl; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -17,7 +16,7 @@ pub enum Error { MissingPathSegments(String), #[error("Distribution not found at: {0}")] - NotFound(Url), + NotFound(DisplaySafeUrl), #[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")] PackageNameMismatch(PackageName, PackageName, String), diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 02b981768..e17901f80 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -3,11 +3,11 @@ use std::str::FromStr; use jiff::Timestamp; use serde::{Deserialize, Serialize}; -use url::Url; use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError}; use uv_pep508::split_scheme; use uv_pypi_types::{CoreMetadata, HashDigests, Yanked}; +use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; /// Error converting [`uv_pypi_types::File`] to [`distribution_type::File`]. @@ -87,13 +87,14 @@ impl FileLocation { /// This returns an error if any of the URL parsing fails, or if, for /// example, the location is a path and the path isn't valid UTF-8. /// (Because URLs must be valid UTF-8.) - pub fn to_url(&self) -> Result { + pub fn to_url(&self) -> Result { match *self { FileLocation::RelativeUrl(ref base, ref path) => { - let base_url = Url::parse(base).map_err(|err| ToUrlError::InvalidBase { - base: base.to_string(), - err, - })?; + let base_url = + DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase { + base: base.to_string(), + err, + })?; let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin { base: base.to_string(), path: path.to_string(), @@ -142,9 +143,9 @@ impl UrlString { Self(url) } - /// Converts a [`UrlString`] to a [`Url`]. - pub fn to_url(&self) -> Result { - Url::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute { + /// Converts a [`UrlString`] to a [`DisplaySafeUrl`]. + pub fn to_url(&self) -> Result { + DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute { absolute: self.0.to_string(), err, }) @@ -178,14 +179,14 @@ impl AsRef for UrlString { } } -impl From for UrlString { - fn from(value: Url) -> Self { +impl From for UrlString { + fn from(value: DisplaySafeUrl) -> Self { Self(value.as_str().into()) } } -impl From<&Url> for UrlString { - fn from(value: &Url) -> Self { +impl From<&DisplaySafeUrl> for UrlString { + fn from(value: &DisplaySafeUrl) -> Self { Self(value.as_str().into()) } } diff --git a/crates/uv-distribution-types/src/id.rs b/crates/uv-distribution-types/src/id.rs index 43fb355e4..b68b4f24c 100644 --- a/crates/uv-distribution-types/src/id.rs +++ b/crates/uv-distribution-types/src/id.rs @@ -1,12 +1,12 @@ use std::fmt::{Display, Formatter}; use std::path::PathBuf; -use url::Url; use uv_cache_key::{CanonicalUrl, RepositoryUrl}; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::HashDigest; +use uv_redacted::DisplaySafeUrl; /// A unique identifier for a package. A package can either be identified by a name (e.g., `black`) /// or a URL (e.g., `git+https://github.com/psf/black`). @@ -25,7 +25,7 @@ impl PackageId { } /// Create a new [`PackageId`] from a URL. - pub fn from_url(url: &Url) -> Self { + pub fn from_url(url: &DisplaySafeUrl) -> Self { Self::Url(CanonicalUrl::new(url)) } } @@ -55,7 +55,7 @@ impl VersionId { } /// Create a new [`VersionId`] from a URL. - pub fn from_url(url: &Url) -> Self { + pub fn from_url(url: &DisplaySafeUrl) -> Self { Self::Url(CanonicalUrl::new(url)) } } diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index d3f974ad3..8ac7c3cd4 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -3,9 +3,9 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use thiserror::Error; -use url::Url; use uv_auth::{AuthPolicy, Credentials}; +use uv_redacted::DisplaySafeUrl; use crate::index_name::{IndexName, IndexNameError}; use crate::origin::Origin; @@ -82,7 +82,7 @@ pub struct Index { /// url = "https://pypi.org/simple" /// publish-url = "https://upload.pypi.org/legacy/" /// ``` - pub publish_url: Option, + pub publish_url: Option, /// When uv should use authentication for requests to the index. /// /// ```toml @@ -193,7 +193,7 @@ impl Index { } /// Return the raw [`Url`] of the index. - pub fn raw_url(&self) -> &Url { + pub fn raw_url(&self) -> &DisplaySafeUrl { self.url.url() } @@ -201,7 +201,7 @@ impl Index { /// /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// removed. This is useful, e.g., for credential propagation to other endpoints on the index. - pub fn root_url(&self) -> Option { + pub fn root_url(&self) -> Option { self.url.root() } diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index f67063a41..f569a6a80 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -11,10 +11,12 @@ use thiserror::Error; use url::{ParseError, Url}; use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme}; +use uv_redacted::DisplaySafeUrl; use crate::{Index, IndexStatusCodeStrategy, Verbatim}; -static PYPI_URL: LazyLock = LazyLock::new(|| Url::parse("https://pypi.org/simple").unwrap()); +static PYPI_URL: LazyLock = + LazyLock::new(|| DisplaySafeUrl::parse("https://pypi.org/simple").unwrap()); static DEFAULT_INDEX: LazyLock = LazyLock::new(|| { Index::from_index_url(IndexUrl::Pypi(Arc::new(VerbatimUrl::from_url( @@ -69,7 +71,7 @@ impl IndexUrl { /// /// For indexes with a `/simple` endpoint, this is simply the URL with the final segment /// removed. This is useful, e.g., for credential propagation to other endpoints on the index. - pub fn root(&self) -> Option { + pub fn root(&self) -> Option { let mut segments = self.url().path_segments()?; let last = match segments.next_back()? { // If the last segment is empty due to a trailing `/`, skip it (as in `pop_if_empty`) @@ -108,7 +110,7 @@ impl schemars::JsonSchema for IndexUrl { impl IndexUrl { /// Return the raw URL for the index. - pub fn url(&self) -> &Url { + pub fn url(&self) -> &DisplaySafeUrl { match self { Self::Pypi(url) => url.raw(), Self::Url(url) => url.raw(), @@ -116,8 +118,8 @@ impl IndexUrl { } } - /// Convert the index URL into a [`Url`]. - pub fn into_url(self) -> Url { + /// Convert the index URL into a [`DisplaySafeUrl`]. + pub fn into_url(self) -> DisplaySafeUrl { match self { Self::Pypi(url) => url.to_url(), Self::Url(url) => url.to_url(), @@ -126,7 +128,7 @@ impl IndexUrl { } /// Return the redacted URL for the index, omitting any sensitive credentials. - pub fn redacted(&self) -> Cow<'_, Url> { + pub fn without_credentials(&self) -> Cow<'_, DisplaySafeUrl> { let url = self.url(); if url.username().is_empty() && url.password().is_none() { Cow::Borrowed(url) @@ -222,7 +224,7 @@ impl From for IndexUrl { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(index: IndexUrl) -> Self { match index { IndexUrl::Pypi(url) => url.to_url(), diff --git a/crates/uv-distribution-types/src/installed.rs b/crates/uv-distribution-types/src/installed.rs index c96002729..d5813f4c7 100644 --- a/crates/uv-distribution-types/src/installed.rs +++ b/crates/uv-distribution-types/src/installed.rs @@ -14,6 +14,7 @@ use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::{DirectUrl, MetadataError}; +use uv_redacted::DisplaySafeUrl; use crate::{DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VersionOrUrlRef}; @@ -86,7 +87,7 @@ pub struct InstalledDirectUrlDist { pub name: PackageName, pub version: Version, pub direct_url: Box, - pub url: Url, + pub url: DisplaySafeUrl, pub editable: bool, pub path: Box, pub cache_info: Option, @@ -112,7 +113,7 @@ pub struct InstalledLegacyEditable { pub version: Version, pub egg_link: Box, pub target: Box, - pub target_url: Url, + pub target_url: DisplaySafeUrl, pub egg_info: Box, } @@ -144,7 +145,7 @@ impl InstalledDist { version, editable: matches!(&direct_url, DirectUrl::LocalDirectory { dir_info, .. } if dir_info.editable == Some(true)), direct_url: Box::new(direct_url), - url, + url: DisplaySafeUrl::from(url), path: path.to_path_buf().into_boxed_path(), cache_info, }))), @@ -272,7 +273,7 @@ impl InstalledDist { version: Version::from_str(&egg_metadata.version)?, egg_link: path.to_path_buf().into_boxed_path(), target: target.into_boxed_path(), - target_url: url, + target_url: DisplaySafeUrl::from(url), egg_info: egg_info.into_boxed_path(), }))); } diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index c062e8b01..44030ffee 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -50,6 +50,7 @@ use uv_pep508::{Pep508Url, VerbatimUrl}; use uv_pypi_types::{ ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl, ParsedUrl, VerbatimParsedUrl, }; +use uv_redacted::DisplaySafeUrl; pub use crate::annotation::*; pub use crate::any::*; @@ -147,12 +148,12 @@ pub enum InstalledVersion<'a> { Version(&'a Version), /// A URL, used to identify a distribution at an arbitrary location, along with the version /// specifier to which it resolved. - Url(&'a Url, &'a Version), + Url(&'a DisplaySafeUrl, &'a Version), } impl InstalledVersion<'_> { /// If it is a URL, return its value. - pub fn url(&self) -> Option<&Url> { + pub fn url(&self) -> Option<&DisplaySafeUrl> { match self { InstalledVersion::Version(_) => None, InstalledVersion::Url(url, _) => Some(url), @@ -258,7 +259,7 @@ pub struct DirectUrlBuiltDist { /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl` pub filename: WheelFilename, /// The URL without the subdirectory fragment. - pub location: Box, + pub location: Box, /// The URL as it was provided by the user. pub url: VerbatimUrl, } @@ -299,7 +300,7 @@ pub struct DirectUrlSourceDist { /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip` pub name: PackageName, /// The URL without the subdirectory fragment. - pub location: Box, + pub location: Box, /// The subdirectory within the archive in which the source distribution is located. pub subdirectory: Option>, /// The file extension, e.g. `tar.gz`, `zip`, etc. @@ -353,7 +354,7 @@ impl Dist { pub fn from_http_url( name: PackageName, url: VerbatimUrl, - location: Url, + location: DisplaySafeUrl, subdirectory: Option>, ext: DistExtension, ) -> Result { @@ -1168,7 +1169,7 @@ impl RemoteSource for Dist { } } -impl Identifier for Url { +impl Identifier for DisplaySafeUrl { fn distribution_id(&self) -> DistributionId { DistributionId::Url(uv_cache_key::CanonicalUrl::new(self)) } @@ -1461,7 +1462,7 @@ impl Identifier for BuildableSource<'_> { #[cfg(test)] mod test { use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString}; - use url::Url; + use uv_redacted::DisplaySafeUrl; /// Ensure that we don't accidentally grow the `Dist` sizes. #[test] @@ -1485,7 +1486,7 @@ mod test { "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment", "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3", ] { - let url = Url::parse(url).unwrap(); + let url = DisplaySafeUrl::parse(url).unwrap(); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); let url = UrlString::from(url.clone()); assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}"); diff --git a/crates/uv-distribution-types/src/requirement.rs b/crates/uv-distribution-types/src/requirement.rs index 9a2686273..432cc4e12 100644 --- a/crates/uv-distribution-types/src/requirement.rs +++ b/crates/uv-distribution-types/src/requirement.rs @@ -4,7 +4,6 @@ use std::path::Path; use std::str::FromStr; use thiserror::Error; -use url::Url; use uv_distribution_filename::DistExtension; use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to}; @@ -14,6 +13,7 @@ use uv_pep440::VersionSpecifiers; use uv_pep508::{ MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker, }; +use uv_redacted::DisplaySafeUrl; use crate::{IndexMetadata, IndexUrl}; @@ -391,7 +391,7 @@ pub enum RequirementSource { /// e.g.`foo @ https://example.org/foo-1.0.zip`. Url { /// The remote location of the archive file, without subdirectory fragment. - location: Url, + location: DisplaySafeUrl, /// For source distributions, the path to the distribution if it is not in the archive /// root. subdirectory: Option>, @@ -682,7 +682,7 @@ enum RequirementSourceWire { Git { git: String }, /// Ex) `source = { url = "" }` Direct { - url: Url, + url: DisplaySafeUrl, subdirectory: Option, }, /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }` @@ -697,7 +697,7 @@ enum RequirementSourceWire { Registry { #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)] specifier: VersionSpecifiers, - index: Option, + index: Option, conflict: Option, }, } @@ -711,7 +711,7 @@ impl From for RequirementSourceWire { conflict, } => { let index = index.map(|index| index.url.into_url()).map(|mut index| { - redact_credentials(&mut index); + index.remove_credentials(); index }); Self::Registry { @@ -736,8 +736,8 @@ impl From for RequirementSourceWire { } => { let mut url = git.repository().clone(); - // Redact the credentials. - redact_credentials(&mut url); + // Remove the credentials. + url.remove_credentials(); // Clear out any existing state. url.set_fragment(None); @@ -826,7 +826,7 @@ impl TryFrom for RequirementSource { conflict, }), RequirementSourceWire::Git { git } => { - let mut repository = Url::parse(&git)?; + let mut repository = DisplaySafeUrl::parse(&git)?; let mut reference = GitReference::DefaultBranch; let mut subdirectory: Option = None; @@ -848,13 +848,14 @@ impl TryFrom for RequirementSource { repository.set_fragment(None); repository.set_query(None); - // Redact the credentials. - redact_credentials(&mut repository); + // Remove the credentials. + repository.remove_credentials(); // Create a PEP 508-compatible URL. - let mut url = Url::parse(&format!("git+{repository}"))?; + let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?; if let Some(rev) = reference.as_str() { - url.set_path(&format!("{}@{}", url.path(), rev)); + let path = format!("{}@{}", url.path(), rev); + url.set_path(&path); } if let Some(subdirectory) = subdirectory.as_ref() { url.set_fragment(Some(&format!("subdirectory={subdirectory}"))); @@ -940,18 +941,6 @@ impl TryFrom for RequirementSource { } } -/// Remove the credentials from a URL, allowing the generic `git` username (without a password) -/// in SSH URLs, as in, `ssh://git@github.com/...`. -pub fn redact_credentials(url: &mut Url) { - // For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the - // username. - if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() { - return; - } - let _ = url.set_password(None); - let _ = url.set_username(""); -} - #[cfg(test)] mod tests { use std::path::PathBuf; diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 55975fcc4..2c490590b 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -33,6 +33,7 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-types = { workspace = true } uv-workspace = { workspace = true } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 1e3d8d9ba..2c05b4d2c 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -27,6 +27,7 @@ use uv_extract::hash::Hasher; use uv_fs::write_atomic; use uv_platform_tags::Tags; use uv_pypi_types::{HashDigest, HashDigests}; +use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildStack}; use crate::archive::Archive; @@ -529,7 +530,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { /// Stream a wheel from a URL, unzipping it into the cache as it's downloaded. async fn stream_wheel( &self, - url: Url, + url: DisplaySafeUrl, filename: &WheelFilename, size: Option, wheel_entry: &CacheEntry, @@ -666,7 +667,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { /// Download a wheel from a URL, then unzip it into the cache. async fn download_wheel( &self, - url: Url, + url: DisplaySafeUrl, filename: &WheelFilename, size: Option, wheel_entry: &CacheEntry, @@ -980,11 +981,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } /// Returns a GET [`reqwest::Request`] for the given URL. - fn request(&self, url: Url) -> Result { + fn request(&self, url: DisplaySafeUrl) -> Result { self.client .unmanaged .uncached_client(&url) - .get(url) + .get(Url::from(url)) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 2e9dccae6..c19867e75 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use owo_colors::OwoColorize; use tokio::task::JoinError; -use url::Url; use zip::result::ZipError; use crate::metadata::MetadataError; @@ -13,6 +12,7 @@ use uv_fs::Simplified; use uv_normalize::PackageName; use uv_pep440::{Version, VersionSpecifiers}; use uv_pypi_types::{HashAlgorithm, HashDigest}; +use uv_redacted::DisplaySafeUrl; use uv_types::AnyErrorBuild; #[derive(Debug, thiserror::Error)] @@ -28,7 +28,7 @@ pub enum Error { #[error(transparent)] JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), #[error("Expected a file URL, but received: {0}")] - NonFileUrl(Url), + NonFileUrl(DisplaySafeUrl), #[error(transparent)] Git(#[from] uv_git::GitResolverError), #[error(transparent)] @@ -89,7 +89,7 @@ pub enum Error { #[error("The source distribution is missing a `PKG-INFO` file")] MissingPkgInfo, #[error("The source distribution `{}` has no subdirectory `{}`", _0, _1.display())] - MissingSubdirectory(Url, PathBuf), + MissingSubdirectory(DisplaySafeUrl, PathBuf), #[error("Failed to extract static metadata from `PKG-INFO`")] PkgInfo(#[source] uv_pypi_types::MetadataError), #[error("Failed to extract metadata from `requires.txt`")] @@ -103,7 +103,7 @@ pub enum Error { #[error(transparent)] MetadataLowering(#[from] MetadataError), #[error("Distribution not found at: {0}")] - NotFound(Url), + NotFound(DisplaySafeUrl), #[error("Attempted to re-extract the source distribution for `{}`, but the {} hash didn't match. Run `{}` to clear the cache.", _0, _1, "uv cache clean".green())] CacheHeal(String, HashAlgorithm), #[error("The source distribution requires Python {0}, but {1} is installed")] diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 197b9de06..dd0974a99 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -4,7 +4,6 @@ use std::path::{Path, PathBuf}; use either::Either; use thiserror::Error; -use url::Url; use uv_distribution_filename::DistExtension; use uv_distribution_types::{ @@ -15,6 +14,7 @@ use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep440::VersionSpecifiers; use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository}; use uv_pypi_types::{ConflictItem, ParsedUrlError, VerbatimParsedUrl}; +use uv_redacted::DisplaySafeUrl; use uv_workspace::Workspace; use uv_workspace::pyproject::{PyProjectToml, Source, Sources}; @@ -528,11 +528,11 @@ pub enum LoweringError { #[error(transparent)] InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError), #[error("Fragments are not allowed in URLs: `{0}`")] - ForbiddenFragment(Url), + ForbiddenFragment(DisplaySafeUrl), #[error( "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)" )] - MissingGitSource(PackageName, Url), + MissingGitSource(PackageName, DisplaySafeUrl), #[error("`workspace = false` is not yet supported")] WorkspaceFalse, #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")] @@ -572,7 +572,7 @@ impl std::fmt::Display for SourceKind { /// Convert a Git source into a [`RequirementSource`]. fn git_source( - git: &Url, + git: &DisplaySafeUrl, subdirectory: Option>, rev: Option, tag: Option, @@ -587,9 +587,10 @@ fn git_source( }; // Create a PEP 508-compatible URL. - let mut url = Url::parse(&format!("git+{git}"))?; + let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?; if let Some(rev) = reference.as_str() { - url.set_path(&format!("{}@{}", url.path(), rev)); + let path = format!("{}@{}", url.path(), rev); + url.set_path(&path); } if let Some(subdirectory) = subdirectory.as_ref() { let subdirectory = subdirectory @@ -611,7 +612,7 @@ fn git_source( /// Convert a URL source into a [`RequirementSource`]. fn url_source( requirement: &uv_pep508::Requirement, - url: Url, + url: DisplaySafeUrl, subdirectory: Option>, ) -> Result { let mut verbatim_url = url.clone(); diff --git a/crates/uv-distribution/src/reporter.rs b/crates/uv-distribution/src/reporter.rs index 9be3a5fa4..befc21a18 100644 --- a/crates/uv-distribution/src/reporter.rs +++ b/crates/uv-distribution/src/reporter.rs @@ -1,9 +1,8 @@ use std::sync::Arc; -use url::Url; - use uv_distribution_types::BuildableSource; use uv_pep508::PackageName; +use uv_redacted::DisplaySafeUrl; pub trait Reporter: Send + Sync { /// Callback to invoke when a source distribution build is kicked off. @@ -13,10 +12,10 @@ pub trait Reporter: Send + Sync { fn on_build_complete(&self, source: &BuildableSource, id: usize); /// Callback to invoke when a repository checkout begins. - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize); /// Callback to invoke when a download is kicked off. fn on_download_start(&self, name: &PackageName, size: Option) -> usize; @@ -44,11 +43,11 @@ struct Facade { } impl uv_git::Reporter for Facade { - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 9173106dc..18bac0010 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -20,6 +20,7 @@ use reqwest::{Response, StatusCode}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{Instrument, debug, info_span, instrument, warn}; use url::Url; +use uv_redacted::DisplaySafeUrl; use zip::ZipArchive; use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; @@ -386,7 +387,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { async fn url<'data>( &self, source: &BuildableSource<'data>, - url: &'data Url, + url: &'data DisplaySafeUrl, cache_shard: &CacheShard, subdirectory: Option<&'data Path>, ext: SourceDistExtension, @@ -582,7 +583,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if let Some(subdirectory) = subdirectory { if !source_dist_entry.path().join(subdirectory).is_dir() { return Err(Error::MissingSubdirectory( - url.clone(), + DisplaySafeUrl::from(url.clone()), subdirectory.to_path_buf(), )); } @@ -715,7 +716,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed_local() .instrument(info_span!("download", source_dist = %source)) }; - let req = Self::request(url.clone(), client.unmanaged)?; + let req = Self::request(DisplaySafeUrl::from(url.clone()), client.unmanaged)?; let revision = client .managed(|client| { client.cached_client().get_serde_with_retry( @@ -740,7 +741,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { client .cached_client() .skip_cache_with_retry( - Self::request(url.clone(), client)?, + Self::request(DisplaySafeUrl::from(url.clone()), client)?, &cache_entry, download, ) @@ -2077,7 +2078,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { client .cached_client() .skip_cache_with_retry( - Self::request(url.clone(), client)?, + Self::request(DisplaySafeUrl::from(url.clone()), client)?, &cache_entry, download, ) @@ -2402,10 +2403,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } /// Returns a GET [`reqwest::Request`] for the given URL. - fn request(url: Url, client: &RegistryClient) -> Result { + fn request( + url: DisplaySafeUrl, + client: &RegistryClient, + ) -> Result { client .uncached_client(&url) - .get(url) + .get(Url::from(url)) .header( // `reqwest` defaults to accepting compressed responses. // Specify identity encoding to get consistent .whl downloading diff --git a/crates/uv-git-types/Cargo.toml b/crates/uv-git-types/Cargo.toml index 7158879de..a374d7cbe 100644 --- a/crates/uv-git-types/Cargo.toml +++ b/crates/uv-git-types/Cargo.toml @@ -16,9 +16,9 @@ doctest = false workspace = true [dependencies] +uv-redacted = { workspace = true } + serde = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } - -uv-redacted = { workspace = true } diff --git a/crates/uv-git-types/src/lib.rs b/crates/uv-git-types/src/lib.rs index 7e2328db0..dbfa02ea3 100644 --- a/crates/uv-git-types/src/lib.rs +++ b/crates/uv-git-types/src/lib.rs @@ -3,9 +3,7 @@ pub use crate::oid::{GitOid, OidParseError}; pub use crate::reference::GitReference; use thiserror::Error; -use url::Url; - -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; mod github; mod oid; @@ -16,7 +14,7 @@ pub enum GitUrlParseError { #[error( "Unsupported Git URL scheme `{0}:` in `{1}` (expected one of `https:`, `ssh:`, or `file:`)" )] - UnsupportedGitScheme(String, Url), + UnsupportedGitScheme(String, DisplaySafeUrl), } /// A URL reference to a Git repository. @@ -24,7 +22,7 @@ pub enum GitUrlParseError { pub struct GitUrl { /// The URL of the Git repository, with any query parameters, fragments, and leading `git+` /// removed. - repository: Url, + repository: DisplaySafeUrl, /// The reference to the commit to use, which could be a branch, tag or revision. reference: GitReference, /// The precise commit to use, if known. @@ -34,7 +32,7 @@ pub struct GitUrl { impl GitUrl { /// Create a new [`GitUrl`] from a repository URL and a reference. pub fn from_reference( - repository: Url, + repository: DisplaySafeUrl, reference: GitReference, ) -> Result { Self::from_fields(repository, reference, None) @@ -42,7 +40,7 @@ impl GitUrl { /// Create a new [`GitUrl`] from a repository URL and a precise commit. pub fn from_commit( - repository: Url, + repository: DisplaySafeUrl, reference: GitReference, precise: GitOid, ) -> Result { @@ -51,7 +49,7 @@ impl GitUrl { /// Create a new [`GitUrl`] from a repository URL and a precise commit, if known. pub fn from_fields( - repository: Url, + repository: DisplaySafeUrl, reference: GitReference, precise: Option, ) -> Result { @@ -86,7 +84,7 @@ impl GitUrl { } /// Return the [`Url`] of the Git repository. - pub fn repository(&self) -> &Url { + pub fn repository(&self) -> &DisplaySafeUrl { &self.repository } @@ -101,11 +99,11 @@ impl GitUrl { } } -impl TryFrom for GitUrl { +impl TryFrom for GitUrl { type Error = GitUrlParseError; /// Initialize a [`GitUrl`] source from a URL. - fn try_from(mut url: Url) -> Result { + fn try_from(mut url: DisplaySafeUrl) -> Result { // Remove any query parameters and fragments. url.set_fragment(None); url.set_query(None); @@ -126,13 +124,14 @@ impl TryFrom for GitUrl { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(git: GitUrl) -> Self { let mut url = git.repository; // If we have a precise commit, add `@` and the commit hash to the URL. if let Some(precise) = git.precise { - url.set_path(&format!("{}@{}", url.path(), precise)); + let path = format!("{}@{}", url.path(), precise); + url.set_path(&path); } else { // Otherwise, add the branch or tag name. match git.reference { @@ -141,7 +140,8 @@ impl From for Url { | GitReference::BranchOrTag(rev) | GitReference::NamedRef(rev) | GitReference::BranchOrTagOrCommit(rev) => { - url.set_path(&format!("{}@{}", url.path(), rev)); + let path = format!("{}@{}", url.path(), rev); + url.set_path(&path); } GitReference::DefaultBranch => {} } @@ -153,6 +153,6 @@ impl From for Url { impl std::fmt::Display for GitUrl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", redacted_url(&self.repository)) + write!(f, "{}", &self.repository) } } diff --git a/crates/uv-git/src/credentials.rs b/crates/uv-git/src/credentials.rs index d8a67a250..051560980 100644 --- a/crates/uv-git/src/credentials.rs +++ b/crates/uv-git/src/credentials.rs @@ -1,10 +1,9 @@ use std::collections::HashMap; use std::sync::{Arc, LazyLock, RwLock}; use tracing::trace; -use url::Url; use uv_auth::Credentials; use uv_cache_key::RepositoryUrl; -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; /// Global authentication cache for a uv invocation. /// @@ -30,9 +29,9 @@ impl GitStore { /// Populate the global authentication store with credentials on a Git URL, if there are any. /// /// Returns `true` if the store was updated. -pub fn store_credentials_from_url(url: &Url) -> bool { +pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool { if let Some(credentials) = Credentials::from_url(url) { - trace!("Caching credentials for {}", redacted_url(url)); + trace!("Caching credentials for {url}"); GIT_STORE.insert(RepositoryUrl::new(url), credentials); true } else { diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index d31444081..298c205ba 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -16,6 +16,7 @@ use url::Url; use uv_fs::Simplified; use uv_git_types::{GitHubRepository, GitOid, GitReference}; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_version::version; @@ -132,7 +133,7 @@ impl Display for ReferenceOrOid<'_> { #[derive(PartialEq, Clone, Debug)] pub(crate) struct GitRemote { /// URL to a remote repository. - url: Url, + url: DisplaySafeUrl, } /// A local clone of a remote repository's database. Multiple [`GitCheckout`]s @@ -205,12 +206,12 @@ impl GitRepository { impl GitRemote { /// Creates an instance for a remote repository URL. - pub(crate) fn new(url: &Url) -> Self { + pub(crate) fn new(url: &DisplaySafeUrl) -> Self { Self { url: url.clone() } } /// Gets the remote repository URL. - pub(crate) fn url(&self) -> &Url { + pub(crate) fn url(&self) -> &DisplaySafeUrl { &self.url } diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index 6c6b38072..2031c49e5 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -9,11 +9,10 @@ use std::sync::Arc; use anyhow::Result; use reqwest_middleware::ClientWithMiddleware; use tracing::{debug, instrument}; -use url::Url; use uv_cache_key::{RepositoryUrl, cache_digest}; use uv_git_types::GitUrl; -use uv_redacted::redacted_url; +use uv_redacted::DisplaySafeUrl; use crate::GIT_STORE; use crate::git::GitRemote; @@ -101,10 +100,7 @@ impl GitSource { // situation that we have a locked revision but the database // doesn't have it. (locked_rev, db) => { - debug!( - "Updating Git source `{}`", - redacted_url(self.git.repository()) - ); + debug!("Updating Git source `{}`", self.git.repository()); // Report the checkout operation to the reporter. let task = self.reporter.as_ref().map(|reporter| { @@ -181,8 +177,8 @@ impl Fetch { pub trait Reporter: Send + Sync { /// Callback to invoke when a repository checkout begins. - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize); } diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index 02ab10b43..a78dec23b 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -31,6 +31,7 @@ uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } uv-types = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv-installer/src/preparer.rs b/crates/uv-installer/src/preparer.rs index eaf3b5b6d..7181fa454 100644 --- a/crates/uv-installer/src/preparer.rs +++ b/crates/uv-installer/src/preparer.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use futures::{FutureExt, Stream, TryFutureExt, TryStreamExt, stream::FuturesUnordered}; use tracing::{debug, instrument}; -use url::Url; use uv_cache::Cache; use uv_configuration::BuildOptions; @@ -14,6 +13,7 @@ use uv_distribution_types::{ }; use uv_pep508::PackageName; use uv_platform_tags::Tags; +use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, HashStrategy, InFlight}; /// Prepare distributions for installation. @@ -268,10 +268,10 @@ pub trait Reporter: Send + Sync { fn on_build_complete(&self, source: &BuildableSource, id: usize); /// Callback to invoke when a repository checkout begins. - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize); + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize); } impl dyn Reporter { @@ -299,11 +299,11 @@ impl uv_distribution::Reporter for Facade { self.reporter.on_build_complete(source, id); } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, index: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, index: usize) { self.reporter.on_checkout_complete(url, rev, index); } diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 5351e04a8..d0ed782ff 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -6,7 +6,6 @@ use std::path::PathBuf; use anyhow::{Context, Result}; use fs_err as fs; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; -use url::Url; use uv_distribution_types::{ Diagnostic, InstalledDist, Name, NameRequirementSpecification, Requirement, @@ -18,6 +17,7 @@ use uv_pep440::{Version, VersionSpecifiers}; use uv_pep508::VersionOrUrl; use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonEnvironment}; +use uv_redacted::DisplaySafeUrl; use uv_types::InstalledPackagesProvider; use uv_warnings::warn_user; @@ -38,7 +38,7 @@ pub struct SitePackages { /// virtual environment, which we handle gracefully. by_name: FxHashMap>, /// The installed editable distributions, keyed by URL. - by_url: FxHashMap>, + by_url: FxHashMap>, } impl SitePackages { @@ -174,7 +174,7 @@ impl SitePackages { } /// Returns the distributions installed from the given URL, if any. - pub fn get_urls(&self, url: &Url) -> Vec<&InstalledDist> { + pub fn get_urls(&self, url: &DisplaySafeUrl) -> Vec<&InstalledDist> { let Some(indexes) = self.by_url.get(url) else { return Vec::new(); }; diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 1fff96287..7494a722d 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -22,6 +22,7 @@ workspace = true uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } +uv-redacted = { workspace = true } arcstr = { workspace = true} boxcar = { workspace = true } diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index a78678d92..e313db86d 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -144,23 +144,50 @@ impl Requirement { self.version_or_url = None; } } + + /// Returns a [`Display`] implementation that doesn't mask credentials. + pub fn displayable_with_credentials(&self) -> impl Display { + RequirementDisplay { + requirement: self, + display_credentials: true, + } + } } impl Display for Requirement { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name)?; - if !self.extras.is_empty() { + RequirementDisplay { + requirement: self, + display_credentials: false, + } + .fmt(f) + } +} + +struct RequirementDisplay<'a, T> +where + T: Pep508Url + Display, +{ + requirement: &'a Requirement, + display_credentials: bool, +} + +impl Display for RequirementDisplay<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.requirement.name)?; + if !self.requirement.extras.is_empty() { write!( f, "[{}]", - self.extras + self.requirement + .extras .iter() .map(ToString::to_string) .collect::>() .join(",") )?; } - if let Some(version_or_url) = &self.version_or_url { + if let Some(version_or_url) = &self.requirement.version_or_url { match version_or_url { VersionOrUrl::VersionSpecifier(version_specifier) => { let version_specifier: Vec = @@ -168,12 +195,17 @@ impl Display for Requirement { write!(f, "{}", version_specifier.join(","))?; } VersionOrUrl::Url(url) => { + let url_string = if self.display_credentials { + url.displayable_with_credentials().to_string() + } else { + url.to_string() + }; // We add the space for markers later if necessary - write!(f, " @ {url}")?; + write!(f, " @ {url_string}")?; } } } - if let Some(marker) = self.marker.contents() { + if let Some(marker) = self.requirement.marker.contents() { write!(f, " ; {marker}")?; } Ok(()) @@ -255,6 +287,9 @@ pub trait Pep508Url: Display + Debug + Sized { /// Parse a url from `name @ `. Defaults to [`Url::parse_url`]. fn parse_url(url: &str, working_dir: Option<&Path>) -> Result; + + /// Returns a [`Display`] implementation that doesn't mask credentials. + fn displayable_with_credentials(&self) -> impl Display; } impl Pep508Url for Url { @@ -263,6 +298,10 @@ impl Pep508Url for Url { fn parse_url(url: &str, _working_dir: Option<&Path>) -> Result { Url::parse(url) } + + fn displayable_with_credentials(&self) -> impl Display { + self + } } /// A reporter for warnings that occur during marker parsing or evaluation. diff --git a/crates/uv-pep508/src/unnamed.rs b/crates/uv-pep508/src/unnamed.rs index cbde5fb06..d5c1820bb 100644 --- a/crates/uv-pep508/src/unnamed.rs +++ b/crates/uv-pep508/src/unnamed.rs @@ -66,9 +66,9 @@ impl UnnamedRequirementUrl for VerbatimUrl { /// dependencies. This isn't compliant with PEP 508, but is common in `requirements.txt`, which /// is implementation-defined. #[derive(Hash, Debug, Clone, Eq, PartialEq)] -pub struct UnnamedRequirement { +pub struct UnnamedRequirement { /// The direct URL that defines the version specifier. - pub url: Url, + pub url: ReqUrl, /// The list of extras such as `security`, `tests` in /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. pub extras: Box<[ExtraName]>, diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index 67580ec62..5ef81f188 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::cmp::Ordering; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::hash::Hash; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -13,6 +13,7 @@ use url::{ParseError, Url}; #[cfg_attr(not(feature = "non-pep508-extensions"), allow(unused_imports))] use uv_fs::{normalize_absolute_path, normalize_url_path}; +use uv_redacted::DisplaySafeUrl; use crate::Pep508Url; @@ -20,7 +21,7 @@ use crate::Pep508Url; #[derive(Debug, Clone, Eq)] pub struct VerbatimUrl { /// The parsed URL. - url: Url, + url: DisplaySafeUrl, /// The URL as it was provided by the user. given: Option, } @@ -39,14 +40,17 @@ impl PartialEq for VerbatimUrl { impl VerbatimUrl { /// Create a [`VerbatimUrl`] from a [`Url`]. - pub fn from_url(url: Url) -> Self { + pub fn from_url(url: DisplaySafeUrl) -> Self { Self { url, given: None } } /// Parse a URL from a string. pub fn parse_url(given: impl AsRef) -> Result { let url = Url::parse(given.as_ref())?; - Ok(Self { url, given: None }) + Ok(Self { + url: DisplaySafeUrl::from(url), + given: None, + }) } /// Parse a URL from an absolute or relative path. @@ -72,8 +76,10 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(&path); // Convert to a URL. - let mut url = Url::from_file_path(path.clone()) - .map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?; + let mut url = DisplaySafeUrl::from( + Url::from_file_path(path.clone()) + .map_err(|()| VerbatimUrlError::UrlConversion(path.to_path_buf()))?, + ); // Set the fragment, if it exists. if let Some(fragment) = fragment { @@ -102,8 +108,10 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(&path); // Convert to a URL. - let mut url = Url::from_file_path(path.clone()) - .unwrap_or_else(|()| panic!("path is absolute: {}", path.display())); + let mut url = DisplaySafeUrl::from( + Url::from_file_path(path.clone()) + .unwrap_or_else(|()| panic!("path is absolute: {}", path.display())), + ); // Set the fragment, if it exists. if let Some(fragment) = fragment { @@ -130,8 +138,10 @@ impl VerbatimUrl { let (path, fragment) = split_fragment(path); // Convert to a URL. - let mut url = Url::from_file_path(path.clone()) - .unwrap_or_else(|()| panic!("path is absolute: {}", path.display())); + let mut url = DisplaySafeUrl::from( + Url::from_file_path(path.clone()) + .unwrap_or_else(|()| panic!("path is absolute: {}", path.display())), + ); // Set the fragment, if it exists. if let Some(fragment) = fragment { @@ -155,18 +165,18 @@ impl VerbatimUrl { self.given.as_deref() } - /// Return the underlying [`Url`]. - pub fn raw(&self) -> &Url { + /// Return the underlying [`DisplaySafeUrl`]. + pub fn raw(&self) -> &DisplaySafeUrl { &self.url } - /// Convert a [`VerbatimUrl`] into a [`Url`]. - pub fn to_url(&self) -> Url { + /// Convert a [`VerbatimUrl`] into a [`DisplaySafeUrl`]. + pub fn to_url(&self) -> DisplaySafeUrl { self.url.clone() } - /// Convert a [`VerbatimUrl`] into a [`Url`]. - pub fn into_url(self) -> Url { + /// Convert a [`VerbatimUrl`] into a [`DisplaySafeUrl`]. + pub fn into_url(self) -> DisplaySafeUrl { self.url } @@ -206,7 +216,7 @@ impl std::fmt::Display for VerbatimUrl { } impl Deref for VerbatimUrl { - type Target = Url; + type Target = DisplaySafeUrl; fn deref(&self) -> &Self::Target { &self.url @@ -215,10 +225,22 @@ impl Deref for VerbatimUrl { impl From for VerbatimUrl { fn from(url: Url) -> Self { + VerbatimUrl::from_url(DisplaySafeUrl::from(url)) + } +} + +impl From for VerbatimUrl { + fn from(url: DisplaySafeUrl) -> Self { VerbatimUrl::from_url(url) } } +impl From for Url { + fn from(url: VerbatimUrl) -> Self { + Url::from(url.url) + } +} + #[cfg(feature = "serde")] impl serde::Serialize for VerbatimUrl { fn serialize(&self, serializer: S) -> Result @@ -235,7 +257,7 @@ impl<'de> serde::Deserialize<'de> for VerbatimUrl { where D: serde::Deserializer<'de>, { - let url = Url::deserialize(deserializer)?; + let url = DisplaySafeUrl::deserialize(deserializer)?; Ok(VerbatimUrl::from_url(url)) } } @@ -314,6 +336,10 @@ impl Pep508Url for VerbatimUrl { Err(Self::Err::NotAUrl(expanded.to_string())) } } + + fn displayable_with_credentials(&self) -> impl Display { + self.url.to_string_with_credentials() + } } /// An error that can occur when parsing a [`VerbatimUrl`]. diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index 254d83d03..c3dfeef39 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -23,6 +23,7 @@ uv-extract = { workspace = true } uv-fs = { workspace = true } uv-metadata = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 72e4d102a..dd8358439 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -38,6 +38,7 @@ use uv_extract::hash::{HashReader, Hasher}; use uv_fs::{ProgressReader, Simplified}; use uv_metadata::read_metadata_async_seek; use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError}; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_warnings::{warn_user, warn_user_once}; @@ -59,7 +60,7 @@ pub enum PublishError { #[error("Failed to publish: `{}`", _0.user_display())] PublishPrepare(PathBuf, #[source] Box), #[error("Failed to publish `{}` to {}", _0.user_display(), _1)] - PublishSend(PathBuf, Url, #[source] PublishSendError), + PublishSend(PathBuf, DisplaySafeUrl, #[source] PublishSendError), #[error("Failed to obtain token for trusted publishing")] TrustedPublishing(#[from] TrustedPublishingError), #[error("{0} are not allowed when using trusted publishing")] @@ -308,7 +309,7 @@ pub async fn check_trusted_publishing( password: Option<&str>, keyring_provider: KeyringProviderType, trusted_publishing: TrustedPublishing, - registry: &Url, + registry: &DisplaySafeUrl, client: &BaseClient, ) -> Result { match trusted_publishing { @@ -379,7 +380,7 @@ pub async fn upload( file: &Path, raw_filename: &str, filename: &DistFilename, - registry: &Url, + registry: &DisplaySafeUrl, client: &BaseClient, credentials: &Credentials, check_url_client: Option<&CheckUrlClient<'_>>, @@ -751,7 +752,7 @@ async fn build_request( file: &Path, raw_filename: &str, filename: &DistFilename, - registry: &Url, + registry: &DisplaySafeUrl, client: &BaseClient, credentials: &Credentials, form_metadata: &[(&'static str, String)], @@ -790,7 +791,7 @@ async fn build_request( let mut request = client .for_host(&url) - .post(url) + .post(Url::from(url)) .multipart(form) // Ask PyPI for a structured error messages instead of HTML-markup error messages. // For other registries, we ask them to return plain text over HTML. See @@ -889,10 +890,10 @@ mod tests { use itertools::Itertools; use std::path::PathBuf; use std::sync::Arc; - use url::Url; use uv_auth::Credentials; use uv_client::BaseClientBuilder; use uv_distribution_filename::DistFilename; + use uv_redacted::DisplaySafeUrl; struct DummyReporter; @@ -972,7 +973,7 @@ mod tests { &file, raw_filename, &filename, - &Url::parse("https://example.org/upload").unwrap(), + &DisplaySafeUrl::parse("https://example.org/upload").unwrap(), &BaseClientBuilder::new().build(), &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &form_metadata, @@ -1121,7 +1122,7 @@ mod tests { &file, raw_filename, &filename, - &Url::parse("https://example.org/upload").unwrap(), + &DisplaySafeUrl::parse("https://example.org/upload").unwrap(), &BaseClientBuilder::new().build(), &Credentials::basic(Some("ferris".to_string()), Some("F3RR!S".to_string())), &form_metadata, diff --git a/crates/uv-publish/src/trusted_publishing.rs b/crates/uv-publish/src/trusted_publishing.rs index bdbc8077d..4e45f924a 100644 --- a/crates/uv-publish/src/trusted_publishing.rs +++ b/crates/uv-publish/src/trusted_publishing.rs @@ -12,6 +12,7 @@ use std::fmt::Display; use thiserror::Error; use tracing::{debug, trace}; use url::Url; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; #[derive(Debug, Error)] @@ -23,9 +24,9 @@ pub enum TrustedPublishingError { #[error(transparent)] Url(#[from] url::ParseError), #[error("Failed to fetch: `{0}`")] - Reqwest(Url, #[source] reqwest::Error), + Reqwest(DisplaySafeUrl, #[source] reqwest::Error), #[error("Failed to fetch: `{0}`")] - ReqwestMiddleware(Url, #[source] reqwest_middleware::Error), + ReqwestMiddleware(DisplaySafeUrl, #[source] reqwest_middleware::Error), #[error(transparent)] SerdeJson(#[from] serde_json::error::Error), #[error( @@ -94,7 +95,7 @@ pub struct OidcTokenClaims { /// Returns the short-lived token to use for uploading. pub(crate) async fn get_token( - registry: &Url, + registry: &DisplaySafeUrl, client: &ClientWithMiddleware, ) -> Result { // If this fails, we can skip the audience request. @@ -124,15 +125,16 @@ pub(crate) async fn get_token( } async fn get_audience( - registry: &Url, + registry: &DisplaySafeUrl, client: &ClientWithMiddleware, ) -> Result { // `pypa/gh-action-pypi-publish` uses `netloc` (RFC 1808), which is deprecated for authority // (RFC 3986). - let audience_url = Url::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?; + let audience_url = + DisplaySafeUrl::parse(&format!("https://{}/_/oidc/audience", registry.authority()))?; debug!("Querying the trusted publishing audience from {audience_url}"); let response = client - .get(audience_url.clone()) + .get(Url::from(audience_url.clone())) .send() .await .map_err(|err| TrustedPublishingError::ReqwestMiddleware(audience_url.clone(), err))?; @@ -154,14 +156,14 @@ async fn get_oidc_token( let oidc_token_url = env::var(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL).map_err(|err| { TrustedPublishingError::from_var_err(EnvVars::ACTIONS_ID_TOKEN_REQUEST_URL, err) })?; - let mut oidc_token_url = Url::parse(&oidc_token_url)?; + let mut oidc_token_url = DisplaySafeUrl::parse(&oidc_token_url)?; oidc_token_url .query_pairs_mut() .append_pair("audience", audience); debug!("Querying the trusted publishing OIDC token from {oidc_token_url}"); let authorization = format!("bearer {oidc_token_request_token}"); let response = client - .get(oidc_token_url.clone()) + .get(Url::from(oidc_token_url.clone())) .header(header::AUTHORIZATION, authorization) .send() .await @@ -188,11 +190,11 @@ fn decode_oidc_token(oidc_token: &str) -> Option { } async fn get_publish_token( - registry: &Url, + registry: &DisplaySafeUrl, oidc_token: &str, client: &ClientWithMiddleware, ) -> Result { - let mint_token_url = Url::parse(&format!( + let mint_token_url = DisplaySafeUrl::parse(&format!( "https://{}/_/oidc/mint-token", registry.authority() ))?; @@ -201,7 +203,7 @@ async fn get_publish_token( token: oidc_token.to_string(), }; let response = client - .post(mint_token_url.clone()) + .post(Url::from(mint_token_url.clone())) .body(serde_json::to_vec(&mint_token_payload)?) .send() .await diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index 2393240b9..0a94cc9ad 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -21,6 +21,7 @@ uv-git-types = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } +uv-redacted = { workspace = true } uv-small-str = { workspace = true } hashbrown = { workspace = true } diff --git a/crates/uv-pypi-types/src/base_url.rs b/crates/uv-pypi-types/src/base_url.rs index 535407cf6..eca7dbdb7 100644 --- a/crates/uv-pypi-types/src/base_url.rs +++ b/crates/uv-pypi-types/src/base_url.rs @@ -1,9 +1,12 @@ use serde::{Deserialize, Serialize}; -use url::Url; +use uv_redacted::DisplaySafeUrl; /// Join a relative URL to a base URL. -pub fn base_url_join_relative(base: &str, relative: &str) -> Result { - let base_url = Url::parse(base).map_err(|err| JoinRelativeError::ParseError { +pub fn base_url_join_relative( + base: &str, + relative: &str, +) -> Result { + let base_url = DisplaySafeUrl::parse(base).map_err(|err| JoinRelativeError::ParseError { original: base.to_string(), source: err, })?; @@ -32,26 +35,26 @@ pub enum JoinRelativeError { #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BaseUrl( #[serde( - serialize_with = "Url::serialize_internal", - deserialize_with = "Url::deserialize_internal" + serialize_with = "DisplaySafeUrl::serialize_internal", + deserialize_with = "DisplaySafeUrl::deserialize_internal" )] - Url, + DisplaySafeUrl, ); impl BaseUrl { - /// Return the underlying [`Url`]. - pub fn as_url(&self) -> &Url { + /// Return the underlying [`DisplaySafeUrl`]. + pub fn as_url(&self) -> &DisplaySafeUrl { &self.0 } - /// Return the underlying [`Url`] as a serialized string. + /// Return the underlying [`DisplaySafeUrl`] as a serialized string. pub fn as_str(&self) -> &str { self.0.as_str() } } -impl From for BaseUrl { - fn from(url: Url) -> Self { +impl From for BaseUrl { + fn from(url: DisplaySafeUrl) -> Self { Self(url) } } diff --git a/crates/uv-pypi-types/src/parsed_url.rs b/crates/uv-pypi-types/src/parsed_url.rs index c918c1cf1..9517dfdc6 100644 --- a/crates/uv-pypi-types/src/parsed_url.rs +++ b/crates/uv-pypi-types/src/parsed_url.rs @@ -9,6 +9,7 @@ use uv_git_types::{GitUrl, GitUrlParseError}; use uv_pep508::{ Pep508Url, UnnamedRequirementUrl, VerbatimUrl, VerbatimUrlError, looks_like_git_repository, }; +use uv_redacted::DisplaySafeUrl; use crate::{ArchiveInfo, DirInfo, DirectUrl, VcsInfo, VcsKind}; @@ -61,6 +62,10 @@ impl Pep508Url for VerbatimParsedUrl { verbatim, }) } + + fn displayable_with_credentials(&self) -> impl Display { + self.verbatim.displayable_with_credentials() + } } impl UnnamedRequirementUrl for VerbatimParsedUrl { @@ -194,7 +199,7 @@ impl ParsedUrl { /// * `file:///home/ferris/my_project/my_project-0.1.0-py3-none-any.whl` #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] pub struct ParsedPathUrl { - pub url: Url, + pub url: DisplaySafeUrl, /// The absolute path to the distribution which we use for installing. pub install_path: Box, /// The file extension, e.g. `tar.gz`, `zip`, etc. @@ -203,7 +208,7 @@ pub struct ParsedPathUrl { impl ParsedPathUrl { /// Construct a [`ParsedPathUrl`] from a path requirement source. - pub fn from_source(install_path: Box, ext: DistExtension, url: Url) -> Self { + pub fn from_source(install_path: Box, ext: DistExtension, url: DisplaySafeUrl) -> Self { Self { url, install_path, @@ -218,7 +223,7 @@ impl ParsedPathUrl { /// * `file:///home/ferris/my_project` #[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Ord)] pub struct ParsedDirectoryUrl { - pub url: Url, + pub url: DisplaySafeUrl, /// The absolute path to the distribution which we use for installing. pub install_path: Box, pub editable: bool, @@ -227,7 +232,12 @@ pub struct ParsedDirectoryUrl { impl ParsedDirectoryUrl { /// Construct a [`ParsedDirectoryUrl`] from a path requirement source. - pub fn from_source(install_path: Box, editable: bool, r#virtual: bool, url: Url) -> Self { + pub fn from_source( + install_path: Box, + editable: bool, + r#virtual: bool, + url: DisplaySafeUrl, + ) -> Self { Self { url, install_path, @@ -255,21 +265,22 @@ impl ParsedGitUrl { } } -impl TryFrom for ParsedGitUrl { +impl TryFrom for ParsedGitUrl { type Error = ParsedUrlError; /// Supports URLs with and without the `git+` prefix. /// /// When the URL includes a prefix, it's presumed to come from a PEP 508 requirement; when it's /// excluded, it's presumed to come from `tool.uv.sources`. - fn try_from(url_in: Url) -> Result { + fn try_from(url_in: DisplaySafeUrl) -> Result { let subdirectory = get_subdirectory(&url_in).map(PathBuf::into_boxed_path); let url = url_in .as_str() .strip_prefix("git+") .unwrap_or(url_in.as_str()); - let url = Url::parse(url).map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?; + let url = DisplaySafeUrl::parse(url) + .map_err(|err| ParsedUrlError::UrlParse(url.to_string(), err))?; let url = GitUrl::try_from(url)?; Ok(Self { url, subdirectory }) } @@ -283,14 +294,18 @@ impl TryFrom for ParsedGitUrl { /// * A source dist with a recognizable extension but invalid name: `https://github.com/foo-labs/foo/archive/master.zip#egg=pkg&subdirectory=packages/bar` #[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct ParsedArchiveUrl { - pub url: Url, + pub url: DisplaySafeUrl, pub subdirectory: Option>, pub ext: DistExtension, } impl ParsedArchiveUrl { /// Construct a [`ParsedArchiveUrl`] from a URL requirement source. - pub fn from_source(location: Url, subdirectory: Option>, ext: DistExtension) -> Self { + pub fn from_source( + location: DisplaySafeUrl, + subdirectory: Option>, + ext: DistExtension, + ) -> Self { Self { url: location, subdirectory, @@ -299,10 +314,10 @@ impl ParsedArchiveUrl { } } -impl TryFrom for ParsedArchiveUrl { +impl TryFrom for ParsedArchiveUrl { type Error = ParsedUrlError; - fn try_from(mut url: Url) -> Result { + fn try_from(mut url: DisplaySafeUrl) -> Result { // Extract the `#subdirectory` fragment, if present. let subdirectory = get_subdirectory(&url).map(PathBuf::into_boxed_path); url.set_fragment(None); @@ -338,10 +353,10 @@ fn get_subdirectory(url: &Url) -> Option { Some(PathBuf::from(subdirectory)) } -impl TryFrom for ParsedUrl { +impl TryFrom for ParsedUrl { type Error = ParsedUrlError; - fn try_from(url: Url) -> Result { + fn try_from(url: DisplaySafeUrl) -> Result { if let Some((prefix, ..)) = url.scheme().split_once('+') { match prefix { "git" => Ok(Self::Git(ParsedGitUrl::try_from(url)?)), @@ -464,7 +479,7 @@ impl From<&ParsedGitUrl> for DirectUrl { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: ParsedUrl) -> Self { match value { ParsedUrl::Path(value) => value.into(), @@ -475,19 +490,19 @@ impl From for Url { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: ParsedPathUrl) -> Self { value.url } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: ParsedDirectoryUrl) -> Self { value.url } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: ParsedArchiveUrl) -> Self { let mut url = value.url; if let Some(subdirectory) = value.subdirectory { @@ -497,7 +512,7 @@ impl From for Url { } } -impl From for Url { +impl From for DisplaySafeUrl { fn from(value: ParsedGitUrl) -> Self { let mut url = Self::parse(&format!("{}{}", "git+", Self::from(value.url).as_str())) .expect("Git URL is invalid"); @@ -511,33 +526,36 @@ impl From for Url { #[cfg(test)] mod tests { use anyhow::Result; - use url::Url; use crate::parsed_url::ParsedUrl; + use uv_redacted::DisplaySafeUrl; #[test] fn direct_url_from_url() -> Result<()> { - let expected = Url::parse("git+https://github.com/pallets/flask.git")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - let expected = Url::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); - assert_eq!(expected, actual); - - let expected = Url::parse("git+https://github.com/pallets/flask.git@2.0.0")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + let expected = DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git")?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); assert_eq!(expected, actual); let expected = - Url::parse("git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git#subdirectory=pkg_dir")?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + let expected = DisplaySafeUrl::parse("git+https://github.com/pallets/flask.git@2.0.0")?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); + assert_eq!(expected, actual); + + let expected = DisplaySafeUrl::parse( + "git+https://github.com/pallets/flask.git@2.0.0#subdirectory=pkg_dir", + )?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); assert_eq!(expected, actual); // TODO(charlie): Preserve other fragments. - let expected = - Url::parse("git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + let expected = DisplaySafeUrl::parse( + "git+https://github.com/pallets/flask.git#egg=flask&subdirectory=pkg_dir", + )?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); assert_ne!(expected, actual); Ok(()) @@ -546,8 +564,8 @@ mod tests { #[test] #[cfg(unix)] fn direct_url_from_url_absolute() -> Result<()> { - let expected = Url::parse("file:///path/to/directory")?; - let actual = Url::from(ParsedUrl::try_from(expected.clone())?); + let expected = DisplaySafeUrl::parse("file:///path/to/directory")?; + let actual = DisplaySafeUrl::from(ParsedUrl::try_from(expected.clone())?); assert_eq!(expected, actual); Ok(()) } diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 25317017b..59a9829e0 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -29,6 +29,7 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-state = { workspace = true } uv-static = { workspace = true } uv-trampoline-builder = { workspace = true } diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index c4a342856..4989208be 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -26,6 +26,7 @@ use uv_distribution_filename::{ExtensionError, SourceDistExtension}; use uv_extract::hash::Hasher; use uv_fs::{Simplified, rename_with_retry}; use uv_pypi_types::{HashAlgorithm, HashDigest}; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use crate::PythonVariant; @@ -51,9 +52,9 @@ pub enum Error { #[error("Invalid request key (too many parts): {0}")] TooManyParts(String), #[error("Failed to download {0}")] - NetworkError(Url, #[source] WrappedReqwestError), + NetworkError(DisplaySafeUrl, #[source] WrappedReqwestError), #[error("Failed to download {0}")] - NetworkMiddlewareError(Url, #[source] anyhow::Error), + NetworkMiddlewareError(DisplaySafeUrl, #[source] anyhow::Error), #[error("Failed to extract archive: {0}")] ExtractError(String, #[source] uv_extract::Error), #[error("Failed to hash installation")] @@ -1060,11 +1061,14 @@ fn parse_json_downloads( } impl Error { - pub(crate) fn from_reqwest(url: Url, err: reqwest::Error) -> Self { + pub(crate) fn from_reqwest(url: DisplaySafeUrl, err: reqwest::Error) -> Self { Self::NetworkError(url, WrappedReqwestError::from(err)) } - pub(crate) fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { + pub(crate) fn from_reqwest_middleware( + url: DisplaySafeUrl, + err: reqwest_middleware::Error, + ) -> Self { match err { reqwest_middleware::Error::Middleware(error) => { Self::NetworkMiddlewareError(url, error) @@ -1155,6 +1159,7 @@ async fn read_url( url: &Url, client: &BaseClient, ) -> Result<(impl AsyncRead + Unpin, Option), Error> { + let url = DisplaySafeUrl::from(url.clone()); if url.scheme() == "file" { // Loads downloaded distribution from the given `file://` URL. let path = url @@ -1167,8 +1172,8 @@ async fn read_url( Ok((Either::Left(reader), Some(size))) } else { let response = client - .for_host(url) - .get(url.clone()) + .for_host(&url) + .get(Url::from(url.clone())) .send() .await .map_err(|err| Error::from_reqwest_middleware(url.clone(), err))?; @@ -1176,7 +1181,7 @@ async fn read_url( // Ensure the request was successful. response .error_for_status_ref() - .map_err(|err| Error::from_reqwest(url.clone(), err))?; + .map_err(|err| Error::from_reqwest(url, err))?; let size = response.content_length(); let stream = response diff --git a/crates/uv-redacted/Cargo.toml b/crates/uv-redacted/Cargo.toml index bf337ad23..bc37321ab 100644 --- a/crates/uv-redacted/Cargo.toml +++ b/crates/uv-redacted/Cargo.toml @@ -16,4 +16,9 @@ doctest = false workspace = true [dependencies] +schemars = { workspace = true, optional = true } +serde = { workspace = true } url = { workspace = true } + +[features] +schemars = ["dep:schemars"] diff --git a/crates/uv-redacted/src/lib.rs b/crates/uv-redacted/src/lib.rs index 36ef5a46f..e0d72db4c 100644 --- a/crates/uv-redacted/src/lib.rs +++ b/crates/uv-redacted/src/lib.rs @@ -1,21 +1,241 @@ -use std::borrow::Cow; - +use serde::{Deserialize, Serialize}; +use std::fmt::Write; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; use url::Url; -/// Return a version of the URL with redacted credentials, allowing the generic `git` username (without a password) -/// in SSH URLs, as in, `ssh://git@github.com/...`. -pub fn redacted_url(url: &Url) -> Cow<'_, Url> { - if url.username().is_empty() && url.password().is_none() { - return Cow::Borrowed(url); - } - if url.scheme() == "ssh" && url.username() == "git" && url.password().is_none() { - return Cow::Borrowed(url); +/// A [`Url`] wrapper that redacts credentials when displaying the URL. +/// +/// `DisplaySafeUrl` wraps the standard [`url::Url`] type, providing functionality to mask +/// secrets by default when the URL is displayed or logged. This helps prevent accidental +/// exposure of sensitive information in logs and debug output. +/// +/// # Examples +/// +/// ``` +/// use uv_redacted::DisplaySafeUrl; +/// use std::str::FromStr; +/// +/// // Create a `DisplaySafeUrl` from a `&str` +/// let mut url = DisplaySafeUrl::parse("https://user:password@example.com").unwrap(); +/// +/// // Display will mask secrets +/// assert_eq!(url.to_string(), "https://user:****@example.com/"); +/// +/// // You can still access the username and password +/// assert_eq!(url.username(), "user"); +/// assert_eq!(url.password(), Some("password")); +/// +/// // And you can still update the username and password +/// let _ = url.set_username("new_user"); +/// let _ = url.set_password(Some("new_password")); +/// assert_eq!(url.username(), "new_user"); +/// assert_eq!(url.password(), Some("new_password")); +/// +/// // It is also possible to remove the credentials entirely +/// url.remove_credentials(); +/// assert_eq!(url.username(), ""); +/// assert_eq!(url.password(), None); +/// ``` +#[derive(Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(transparent))] +pub struct DisplaySafeUrl(Url); + +impl DisplaySafeUrl { + #[inline] + pub fn parse(input: &str) -> Result { + Ok(Self(Url::parse(input)?)) } - let mut url = url.clone(); - let _ = url.set_username(""); - let _ = url.set_password(None); - Cow::Owned(url) + /// Parse a string as an URL, with this URL as the base URL. + #[inline] + pub fn join(&self, input: &str) -> Result { + self.0.join(input).map(DisplaySafeUrl::from) + } + + /// Serialize with Serde using the internal representation of the `Url` struct. + #[inline] + pub fn serialize_internal(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize_internal(serializer) + } + + /// Serialize with Serde using the internal representation of the `Url` struct. + #[inline] + pub fn deserialize_internal<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Url::deserialize_internal(deserializer).map(DisplaySafeUrl::from) + } + + #[allow(clippy::result_unit_err)] + pub fn from_file_path>(path: P) -> Result { + Url::from_file_path(path).map(DisplaySafeUrl::from) + } + + /// Remove the credentials from a URL, allowing the generic `git` username (without a password) + /// in SSH URLs, as in, `ssh://git@github.com/...`. + #[inline] + pub fn remove_credentials(&mut self) { + // For URLs that use the `git` convention (i.e., `ssh://git@github.com/...`), avoid dropping the + // username. + if self.0.scheme() == "ssh" && self.0.username() == "git" && self.0.password().is_none() { + return; + } + let _ = self.0.set_username(""); + let _ = self.0.set_password(None); + } + + /// Returns string representation without masking credentials. + #[inline] + pub fn to_string_with_credentials(&self) -> String { + self.0.to_string() + } +} + +impl Deref for DisplaySafeUrl { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for DisplaySafeUrl { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::fmt::Display for DisplaySafeUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_with_obfuscated_credentials(&self.0, f) + } +} + +impl std::fmt::Debug for DisplaySafeUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +impl From for DisplaySafeUrl { + fn from(url: Url) -> Self { + DisplaySafeUrl(url) + } +} + +impl From for Url { + fn from(url: DisplaySafeUrl) -> Self { + url.0 + } +} + +impl FromStr for DisplaySafeUrl { + type Err = url::ParseError; + + fn from_str(input: &str) -> Result { + Ok(Self(Url::from_str(input)?)) + } +} + +fn fmt_with_obfuscated_credentials(url: &Url, mut f: W) -> std::fmt::Result { + if url.password().is_none() && url.username() == "" { + return write!(f, "{url}"); + } + + write!(f, "{}://", url.scheme())?; + + if url.username() != "" && url.password().is_some() { + write!(f, "{}", url.username())?; + write!(f, ":****@")?; + } else if url.username() != "" { + write!(f, "****@")?; + } else if url.password().is_some() { + write!(f, ":****@")?; + } + + write!(f, "{}", url.host_str().unwrap_or(""))?; + + if let Some(port) = url.port() { + write!(f, ":{port}")?; + } + + write!(f, "{}", url.path())?; + if let Some(query) = url.query() { + write!(f, "?{query}")?; + } + if let Some(fragment) = url.fragment() { + write!(f, "#{fragment}")?; + } + + Ok(()) +} + +/// 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")); +pub struct DisplaySafeUrlRef<'a>(&'a Url); + +impl<'a> Deref for DisplaySafeUrlRef<'a> { + type Target = Url; + + fn deref(&self) -> &'a Self::Target { + self.0 + } +} + +impl std::fmt::Display for DisplaySafeUrlRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fmt_with_obfuscated_credentials(self.0, f) + } +} + +impl std::fmt::Debug for DisplaySafeUrlRef<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self}") + } +} + +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)] @@ -24,49 +244,103 @@ mod tests { #[test] fn from_url_no_credentials() { - let url = Url::parse("https://pypi-proxy.fly.dev/basic-auth/simple").unwrap(); - let redacted = redacted_url(&url); - assert_eq!(redacted.username(), ""); - assert!(redacted.password().is_none()); - assert_eq!( - format!("{redacted}"), - "https://pypi-proxy.fly.dev/basic-auth/simple" - ); + let url_str = "https://pypi-proxy.fly.dev/basic-auth/simple"; + let url = Url::parse(url_str).unwrap(); + let log_safe_url = DisplaySafeUrl::from(url); + assert_eq!(log_safe_url.username(), ""); + assert!(log_safe_url.password().is_none()); + assert_eq!(format!("{log_safe_url}"), url_str); } #[test] fn from_url_username_and_password() { - let url = Url::parse("https://user:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); - let redacted = redacted_url(&url); - assert_eq!(redacted.username(), ""); - assert!(redacted.password().is_none()); + let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; + let url = Url::parse(url_str).unwrap(); + let log_safe_url = DisplaySafeUrl::from(url); + assert_eq!(log_safe_url.username(), "user"); + assert!(log_safe_url.password().is_some_and(|p| p == "pass")); assert_eq!( - format!("{redacted}"), - "https://pypi-proxy.fly.dev/basic-auth/simple" + format!("{log_safe_url}"), + "https://user:****@pypi-proxy.fly.dev/basic-auth/simple" ); } #[test] fn from_url_just_password() { - let url = Url::parse("https://:pass@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); - let redacted = redacted_url(&url); - assert_eq!(redacted.username(), ""); - assert!(redacted.password().is_none()); + let url_str = "https://:pass@pypi-proxy.fly.dev/basic-auth/simple"; + let url = Url::parse(url_str).unwrap(); + let log_safe_url = DisplaySafeUrl::from(url); + assert_eq!(log_safe_url.username(), ""); + assert!(log_safe_url.password().is_some_and(|p| p == "pass")); assert_eq!( - format!("{redacted}"), - "https://pypi-proxy.fly.dev/basic-auth/simple" + format!("{log_safe_url}"), + "https://:****@pypi-proxy.fly.dev/basic-auth/simple" ); } #[test] fn from_url_just_username() { - let url = Url::parse("https://user@pypi-proxy.fly.dev/basic-auth/simple").unwrap(); - let redacted = redacted_url(&url); - assert_eq!(redacted.username(), ""); - assert!(redacted.password().is_none()); + let url_str = "https://user@pypi-proxy.fly.dev/basic-auth/simple"; + let url = Url::parse(url_str).unwrap(); + let log_safe_url = DisplaySafeUrl::from(url); + assert_eq!(log_safe_url.username(), "user"); + assert!(log_safe_url.password().is_none()); assert_eq!( - format!("{redacted}"), + format!("{log_safe_url}"), + "https://****@pypi-proxy.fly.dev/basic-auth/simple" + ); + } + + #[test] + fn parse_url_string() { + let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; + let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap(); + assert_eq!(log_safe_url.username(), "user"); + assert!(log_safe_url.password().is_some_and(|p| p == "pass")); + assert_eq!( + format!("{log_safe_url}"), + "https://user:****@pypi-proxy.fly.dev/basic-auth/simple" + ); + } + + #[test] + fn remove_credentials() { + let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; + let mut log_safe_url = DisplaySafeUrl::parse(url_str).unwrap(); + log_safe_url.remove_credentials(); + assert_eq!(log_safe_url.username(), ""); + assert!(log_safe_url.password().is_none()); + assert_eq!( + format!("{log_safe_url}"), "https://pypi-proxy.fly.dev/basic-auth/simple" ); } + + #[test] + fn to_string_with_credentials() { + let url_str = "https://user:pass@pypi-proxy.fly.dev/basic-auth/simple"; + let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap(); + assert_eq!(&log_safe_url.to_string_with_credentials(), url_str); + } + + #[test] + fn url_join() { + let url_str = "https://token@example.com/abc/"; + let log_safe_url = DisplaySafeUrl::parse(url_str).unwrap(); + let foo_url = log_safe_url.join("foo").unwrap(); + assert_eq!(format!("{foo_url}"), "https://****@example.com/abc/foo"); + } + + #[test] + 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); + assert_eq!(log_safe_url.username(), "user"); + assert!(log_safe_url.password().is_some_and(|p| p == "pass")); + assert_eq!( + format!("{log_safe_url}"), + "https://user:****@pypi-proxy.fly.dev/basic-auth/simple" + ); + } } diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 617a76123..f82aa2c1c 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -23,6 +23,7 @@ uv-fs = { workspace = true } uv-normalize = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-warnings = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index 439e6f547..0d7f144bd 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -54,6 +54,8 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_pep508::{Pep508Error, RequirementOrigin, VerbatimUrl, expand_env_vars}; use uv_pypi_types::VerbatimParsedUrl; +#[cfg(feature = "http")] +use uv_redacted::DisplaySafeUrl; use crate::requirement::EditableError; pub use crate::requirement::RequirementsTxtRequirement; @@ -949,11 +951,11 @@ async fn read_url_to_string( url: path.as_ref().to_owned(), })?; - let url = Url::from_str(path_utf8) + let url = DisplaySafeUrl::from_str(path_utf8) .map_err(|err| RequirementsTxtParserError::InvalidUrl(path_utf8.to_string(), err))?; let response = client .for_host(&url) - .get(url.clone()) + .get(Url::from(url.clone())) .send() .await .map_err(|err| RequirementsTxtParserError::from_reqwest_middleware(url.clone(), err))?; @@ -1047,7 +1049,7 @@ pub enum RequirementsTxtParserError { url: PathBuf, }, #[cfg(feature = "http")] - Reqwest(Url, reqwest_middleware::Error), + Reqwest(DisplaySafeUrl, reqwest_middleware::Error), #[cfg(feature = "http")] InvalidUrl(String, url::ParseError), } @@ -1301,11 +1303,11 @@ impl From for RequirementsTxtParserError { #[cfg(feature = "http")] impl RequirementsTxtParserError { - fn from_reqwest(url: Url, err: reqwest::Error) -> Self { + fn from_reqwest(url: DisplaySafeUrl, err: reqwest::Error) -> Self { Self::Reqwest(url, reqwest_middleware::Error::Reqwest(err)) } - fn from_reqwest_middleware(url: Url, err: reqwest_middleware::Error) -> Self { + fn from_reqwest_middleware(url: DisplaySafeUrl, err: reqwest_middleware::Error) -> Self { Self::Reqwest(url, err) } } @@ -2039,7 +2041,7 @@ mod test { insta::with_settings!({ filters => path_filters(&path_filter(temp_dir.path())), }, { - insta::assert_debug_snapshot!(requirements, @r###" + insta::assert_debug_snapshot!(requirements, @r#" RequirementsTxt { requirements: [], constraints: [], @@ -2050,34 +2052,14 @@ mod test { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/foo/bar", - query: None, - fragment: None, - }, + url: file:///foo/bar, install_path: "/foo/bar", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/foo/bar", - query: None, - fragment: None, - }, + url: file:///foo/bar, given: Some( "/foo/bar", ), @@ -2102,7 +2084,7 @@ mod test { no_binary: None, only_binary: None, } - "###); + "#); }); Ok(()) @@ -2187,7 +2169,7 @@ mod test { insta::with_settings!({ filters => path_filters(&path_filter(temp_dir.path())), }, { - insta::assert_debug_snapshot!(requirements, @r###" + insta::assert_debug_snapshot!(requirements, @r#" RequirementsTxt { requirements: [ RequirementEntry { @@ -2333,21 +2315,7 @@ mod test { editables: [], index_url: Some( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple/", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple/, given: Some( "https://test.pypi.org/simple/", ), @@ -2359,7 +2327,7 @@ mod test { no_binary: All, only_binary: None, } - "###); + "#); }); Ok(()) @@ -2402,7 +2370,7 @@ mod test { insta::with_settings!({ filters => path_filters(&path_filter(temp_dir.path())), }, { - insta::assert_debug_snapshot!(requirements, @r###" + insta::assert_debug_snapshot!(requirements, @r#" RequirementsTxt { requirements: [ RequirementEntry { @@ -2411,33 +2379,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.3.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.3.0-py3-none-any.whl, install_path: "/importlib_metadata-8.3.0-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.3.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.3.0-py3-none-any.whl, given: Some( "importlib_metadata-8.3.0-py3-none-any.whl", ), @@ -2460,33 +2408,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0-py3-none-any.whl, install_path: "/importlib_metadata-8.2.0-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0-py3-none-any.whl, given: Some( "importlib_metadata-8.2.0-py3-none-any.whl", ), @@ -2509,33 +2437,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0-py3-none-any.whl, install_path: "/importlib_metadata-8.2.0-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0-py3-none-any.whl, given: Some( "importlib_metadata-8.2.0-py3-none-any.whl", ), @@ -2562,33 +2470,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, install_path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, given: Some( "importlib_metadata-8.2.0+local-py3-none-any.whl", ), @@ -2611,33 +2499,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, install_path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, given: Some( "importlib_metadata-8.2.0+local-py3-none-any.whl", ), @@ -2660,33 +2528,13 @@ mod test { url: VerbatimParsedUrl { parsed_url: Path( ParsedPathUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, install_path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", ext: Wheel, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/importlib_metadata-8.2.0+local-py3-none-any.whl", - query: None, - fragment: None, - }, + url: file:///importlib_metadata-8.2.0+local-py3-none-any.whl, given: Some( "importlib_metadata-8.2.0+local-py3-none-any.whl", ), @@ -2717,7 +2565,7 @@ mod test { no_binary: None, only_binary: None, } - "###); + "#); }); Ok(()) diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap index b59788026..80d7d6147 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap @@ -39,21 +39,7 @@ RequirementsTxt { parsed_url: Git( ParsedGitUrl { url: GitUrl { - repository: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas.git", - query: None, - fragment: None, - }, + repository: https://github.com/pandas-dev/pandas.git, reference: DefaultBranch, precise: None, }, @@ -61,21 +47,7 @@ RequirementsTxt { }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "git+https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas.git", - query: None, - fragment: None, - }, + url: git+https://github.com/pandas-dev/pandas.git, given: Some( "git+https://github.com/pandas-dev/pandas.git", ), diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap index b0ebd7157..9886e6d8b 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap @@ -10,34 +10,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, given: Some( "./scripts/packages/black_editable", ), @@ -60,34 +40,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, given: Some( "./scripts/packages/black_editable", ), @@ -114,34 +74,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black_editable, given: Some( "file:///scripts/packages/black_editable", ), @@ -164,34 +104,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), @@ -214,34 +134,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), @@ -264,34 +164,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:///scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap index 86f25edf6..c211da005 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap @@ -12,34 +12,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -69,34 +49,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -126,34 +86,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -183,34 +123,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -240,34 +160,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -290,34 +190,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable[d", - query: None, - fragment: None, - }, + url: file:///editable[d, install_path: "/editable[d", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable[d", - query: None, - fragment: None, - }, + url: file:///editable[d, given: Some( "./editable[d", ), @@ -340,34 +220,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), @@ -390,34 +250,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "/editable", - query: None, - fragment: None, - }, + url: file:///editable, given: Some( "./editable", ), diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap index b59788026..80d7d6147 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap @@ -39,21 +39,7 @@ RequirementsTxt { parsed_url: Git( ParsedGitUrl { url: GitUrl { - repository: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas.git", - query: None, - fragment: None, - }, + repository: https://github.com/pandas-dev/pandas.git, reference: DefaultBranch, precise: None, }, @@ -61,21 +47,7 @@ RequirementsTxt { }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "git+https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "github.com", - ), - ), - port: None, - path: "/pandas-dev/pandas.git", - query: None, - fragment: None, - }, + url: git+https://github.com/pandas-dev/pandas.git, given: Some( "git+https://github.com/pandas-dev/pandas.git", ), diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap index 6f3410ddd..9c801be04 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap @@ -1,5 +1,5 @@ --- -source: crates/requirements-txt/src/lib.rs +source: crates/uv-requirements-txt/src/lib.rs expression: actual --- RequirementsTxt { @@ -10,34 +10,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, given: Some( "./scripts/packages/black_editable", ), @@ -60,34 +40,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, given: Some( "./scripts/packages/black_editable", ), @@ -114,34 +74,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, install_path: "/scripts/packages/black_editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black_editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black_editable, given: Some( "file:///scripts/packages/black_editable", ), @@ -164,34 +104,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), @@ -214,34 +134,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), @@ -264,34 +164,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, install_path: "/scripts/packages/black editable", editable: false, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//scripts/packages/black%20editable", - query: None, - fragment: None, - }, + url: file:////scripts/packages/black%20editable, given: Some( "./scripts/packages/black editable", ), diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap index 12909f7e8..3f651d4ee 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap @@ -12,34 +12,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -69,34 +49,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -126,34 +86,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -183,34 +123,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -240,34 +160,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -290,34 +190,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable[d", - query: None, - fragment: None, - }, + url: file:////editable[d, install_path: "/editable[d", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable[d", - query: None, - fragment: None, - }, + url: file:////editable[d, given: Some( "./editable[d", ), @@ -340,34 +220,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), @@ -390,34 +250,14 @@ RequirementsTxt { url: VerbatimParsedUrl { parsed_url: Directory( ParsedDirectoryUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, install_path: "/editable", editable: true, virtual: false, }, ), verbatim: VerbatimUrl { - url: Url { - scheme: "file", - cannot_be_a_base: false, - username: "", - password: None, - host: None, - port: None, - path: "//editable", - query: None, - fragment: None, - }, + url: file:////editable, given: Some( "./editable", ), diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index d3fbf929b..25da28b7a 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -28,6 +28,7 @@ uv-git = { workspace = true } uv-normalize = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-requirements-txt = { workspace = true, features = ["http"] } uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true } diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 4e7ab2b47..a540e4642 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -16,6 +16,7 @@ use uv_distribution_types::{ use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; use uv_pep508::RequirementOrigin; +use uv_redacted::DisplaySafeUrl; use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_types::{BuildContext, HashStrategy}; @@ -180,7 +181,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { return Ok(metadata); } - let Ok(url) = Url::from_directory_path(source_tree) else { + let Ok(url) = Url::from_directory_path(source_tree).map(DisplaySafeUrl::from) else { return Err(anyhow::anyhow!("Failed to convert path to URL")); }; let source = SourceUrl::Directory(DirectorySourceUrl { diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 79645906a..715dacab8 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -33,6 +33,7 @@ uv-pep508 = { workspace = true } uv-platform-tags = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } +uv-redacted = { workspace = true } uv-requirements-txt = { workspace = true } uv-small-str = { workspace = true } uv-static = { workspace = true } diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index 1ab2ce152..767d42e0b 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -33,6 +33,7 @@ use uv_pep440::Version; use uv_pep508::{MarkerEnvironment, MarkerTree, VerbatimUrl}; use uv_platform_tags::{TagCompatibility, TagPriority, Tags}; use uv_pypi_types::{HashDigests, Hashes, ParsedGitUrl, VcsKind}; +use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use crate::lock::export::ExportableRequirements; @@ -93,7 +94,7 @@ pub enum PylockTomlErrorKind { #[error("`packages.vcs` entry for `{0}` must have a `url` or `path`")] VcsMissingPathUrl(PackageName), #[error("URL must end in a valid wheel filename: `{0}`")] - UrlMissingFilename(Url), + UrlMissingFilename(DisplaySafeUrl), #[error("Path must end in a valid wheel filename: `{0}`")] PathMissingFilename(Box), #[error("Failed to convert path to URL")] @@ -204,7 +205,7 @@ pub struct PylockTomlPackage { #[serde(skip_serializing_if = "Option::is_none")] pub version: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub index: Option, + pub index: Option, #[serde( skip_serializing_if = "uv_pep508::marker::ser::is_empty", serialize_with = "uv_pep508::marker::ser::serialize", @@ -247,7 +248,7 @@ struct PylockTomlDirectory { struct PylockTomlVcs { r#type: VcsKind, #[serde(skip_serializing_if = "Option::is_none")] - url: Option, + url: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -261,7 +262,7 @@ struct PylockTomlVcs { #[serde(rename_all = "kebab-case")] struct PylockTomlArchive { #[serde(skip_serializing_if = "Option::is_none")] - url: Option, + url: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -284,7 +285,7 @@ struct PylockTomlSdist { #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] - url: Option, + url: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[serde( @@ -305,7 +306,7 @@ struct PylockTomlWheel { #[serde(skip_serializing_if = "Option::is_none")] name: Option, #[serde(skip_serializing_if = "Option::is_none")] - url: Option, + url: Option, #[serde(skip_serializing_if = "Option::is_none")] path: Option, #[serde( @@ -1324,7 +1325,7 @@ impl PylockTomlWheel { &self, install_path: &Path, name: &PackageName, - index: Option<&Url>, + index: Option<&DisplaySafeUrl>, ) -> Result { let filename = self.filename(name)?.into_owned(); @@ -1332,7 +1333,8 @@ impl PylockTomlWheel { UrlString::from(url) } else if let Some(path) = self.path.as_ref() { let path = install_path.join(path); - let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?; + let url = DisplaySafeUrl::from_file_path(path) + .map_err(|()| PylockTomlErrorKind::PathToUrl)?; UrlString::from(url) } else { return Err(PylockTomlErrorKind::WheelMissingPathUrl(name.clone())); @@ -1408,8 +1410,10 @@ impl PylockTomlVcs { let mut url = if let Some(url) = self.url.as_ref() { url.clone() } else if let Some(path) = self.path.as_ref() { - Url::from_directory_path(install_path.join(path)) - .map_err(|()| PylockTomlErrorKind::PathToUrl)? + DisplaySafeUrl::from( + Url::from_directory_path(install_path.join(path)) + .map_err(|()| PylockTomlErrorKind::PathToUrl)?, + ) } else { return Err(PylockTomlErrorKind::VcsMissingPathUrl(name.clone())); }; @@ -1427,7 +1431,7 @@ impl PylockTomlVcs { }; // Reconstruct the PEP 508-compatible URL from the `GitSource`. - let url = Url::from(ParsedGitUrl { + let url = DisplaySafeUrl::from(ParsedGitUrl { url: git_url.clone(), subdirectory: subdirectory.clone(), }); @@ -1469,7 +1473,7 @@ impl PylockTomlSdist { install_path: &Path, name: &PackageName, version: Option<&Version>, - index: Option<&Url>, + index: Option<&DisplaySafeUrl>, ) -> Result { let filename = self.filename(name)?.into_owned(); let ext = SourceDistExtension::from_path(filename.as_ref())?; @@ -1485,7 +1489,8 @@ impl PylockTomlSdist { UrlString::from(url) } else if let Some(path) = self.path.as_ref() { let path = install_path.join(path); - let url = Url::from_file_path(path).map_err(|()| PylockTomlErrorKind::PathToUrl)?; + let url = DisplaySafeUrl::from_file_path(path) + .map_err(|()| PylockTomlErrorKind::PathToUrl)?; UrlString::from(url) } else { return Err(PylockTomlErrorKind::SdistMissingPathUrl(name.clone())); diff --git a/crates/uv-resolver/src/lock/export/requirements_txt.rs b/crates/uv-resolver/src/lock/export/requirements_txt.rs index fe1e17046..7766790ff 100644 --- a/crates/uv-resolver/src/lock/export/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/export/requirements_txt.rs @@ -13,6 +13,7 @@ use uv_fs::Simplified; use uv_git_types::GitReference; use uv_normalize::PackageName; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; +use uv_redacted::DisplaySafeUrl; use crate::lock::export::{ExportableRequirement, ExportableRequirements}; use crate::lock::{Package, PackageId, Source}; @@ -94,7 +95,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { .expect("Internal Git URLs must have supported schemes"); // Reconstruct the PEP 508-compatible URL from the `GitSource`. - let url = Url::from(ParsedGitUrl { + let url = DisplaySafeUrl::from(ParsedGitUrl { url: git_url.clone(), subdirectory: git.subdirectory.clone(), }); @@ -102,7 +103,7 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { write!(f, "{} @ {}", package.id.name, url)?; } Source::Direct(url, direct) => { - let url = Url::from(ParsedArchiveUrl { + let url = DisplaySafeUrl::from(ParsedArchiveUrl { url: url.to_url().map_err(|_| std::fmt::Error)?, subdirectory: direct.subdirectory.clone(), ext: DistExtension::Source(SourceDistExtension::TarGz), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 76be1494b..fc79af00c 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -30,7 +30,7 @@ use uv_distribution_types::{ Dist, DistributionMetadata, FileLocation, GitSourceDist, IndexLocations, IndexMetadata, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Requirement, RequirementSource, ResolvedDist, StaticMetadata, - ToUrlError, UrlString, redact_credentials, + ToUrlError, UrlString, }; use uv_fs::{PortablePath, PortablePathBuf, relative_to}; use uv_git::{RepositoryReference, ResolvedRepositoryReference}; @@ -45,6 +45,7 @@ use uv_pypi_types::{ ConflictPackage, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl, ParsedGitUrl, }; +use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; use uv_types::{BuildContext, HashStrategy}; use uv_workspace::WorkspaceMember; @@ -1404,7 +1405,7 @@ impl Lock { .into_iter() .filter_map(|index| match index.url() { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { - Some(UrlString::from(index.url().redacted().as_ref())) + Some(UrlString::from(index.url().without_credentials().as_ref())) } IndexUrl::Path(_) => None, }) @@ -2238,7 +2239,7 @@ impl Package { Source::Direct(url, direct) => { let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); - let url = Url::from(ParsedArchiveUrl { + let url = DisplaySafeUrl::from(ParsedArchiveUrl { url: url.to_url().map_err(LockErrorKind::InvalidUrl)?, subdirectory: direct.subdirectory.clone(), ext: DistExtension::Wheel, @@ -2400,7 +2401,7 @@ impl Package { GitUrl::from_commit(url, GitReference::from(git.kind.clone()), git.precise)?; // Reconstruct the PEP 508-compatible URL from the `GitSource`. - let url = Url::from(ParsedGitUrl { + let url = DisplaySafeUrl::from(ParsedGitUrl { url: git_url.clone(), subdirectory: git.subdirectory.clone(), }); @@ -2419,7 +2420,7 @@ impl Package { return Ok(None); }; let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; - let url = Url::from(ParsedArchiveUrl { + let url = DisplaySafeUrl::from(ParsedArchiveUrl { url: location.clone(), subdirectory: direct.subdirectory.clone(), ext: DistExtension::Source(ext), @@ -2498,8 +2499,9 @@ impl Package { name: name.clone(), version: version.clone(), })?; - let file_url = Url::from_file_path(workspace_root.join(path).join(file_path)) - .map_err(|()| LockErrorKind::PathToUrl)?; + let file_url = + DisplaySafeUrl::from_file_path(workspace_root.join(path).join(file_path)) + .map_err(|()| LockErrorKind::PathToUrl)?; let filename = sdist .filename() .ok_or_else(|| LockErrorKind::MissingFilename { @@ -3192,7 +3194,7 @@ impl Source { match index_url { IndexUrl::Pypi(_) | IndexUrl::Url(_) => { // Remove any sensitive credentials from the index URL. - let redacted = index_url.redacted(); + let redacted = index_url.without_credentials(); let source = RegistrySource::Url(UrlString::from(redacted.as_ref())); Ok(Source::Registry(source)) } @@ -3405,7 +3407,7 @@ impl TryFrom for Source { match wire { Registry { registry } => Ok(Source::Registry(registry.into())), Git { git } => { - let url = Url::parse(&git) + let url = DisplaySafeUrl::parse(&git) .map_err(|err| SourceParseError::InvalidUrl { given: git.to_string(), err, @@ -3913,12 +3915,12 @@ impl From for GitReference { } } -/// Construct the lockfile-compatible [`URL`] for a [`GitSourceDist`]. -fn locked_git_url(git_dist: &GitSourceDist) -> Url { +/// Construct the lockfile-compatible [`DisplaySafeUrl`] for a [`GitSourceDist`]. +fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl { let mut url = git_dist.git.repository().clone(); - // Redact the credentials. - redact_credentials(&mut url); + // Remove the credentials. + url.remove_credentials(); // Clear out any existing state. url.set_fragment(None); @@ -4183,8 +4185,9 @@ impl Wheel { .into()); } }; - let file_url = Url::from_file_path(root.join(index_path).join(file_path)) - .map_err(|()| LockErrorKind::PathToUrl)?; + let file_url = + DisplaySafeUrl::from_file_path(root.join(index_path).join(file_path)) + .map_err(|()| LockErrorKind::PathToUrl)?; let file = Box::new(uv_distribution_types::File { dist_info_metadata: false, filename: SmallString::from(filename.to_string()), @@ -4571,8 +4574,8 @@ fn normalize_file_location(location: &FileLocation) -> Result UrlString { +/// Convert a [`DisplaySafeUrl`] into a normalized [`UrlString`] by removing the fragment. +fn normalize_url(mut url: DisplaySafeUrl) -> UrlString { url.set_fragment(None); UrlString::from(url) } @@ -4606,8 +4609,8 @@ fn normalize_requirement( let git = { let mut repository = git.repository().clone(); - // Redact the credentials. - redact_credentials(&mut repository); + // Remove the credentials. + repository.remove_credentials(); // Remove the fragment and query from the URL; they're already present in the source. repository.set_fragment(None); @@ -4617,7 +4620,7 @@ fn normalize_requirement( }; // Reconstruct the PEP 508 URL from the underlying data. - let url = Url::from(ParsedGitUrl { + let url = DisplaySafeUrl::from(ParsedGitUrl { url: git.clone(), subdirectory: subdirectory.clone(), }); @@ -4692,7 +4695,7 @@ fn normalize_requirement( let index = index .map(|index| index.url.into_url()) .map(|mut index| { - redact_credentials(&mut index); + index.remove_credentials(); index }) .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))); @@ -4715,14 +4718,14 @@ fn normalize_requirement( ext, url: _, } => { - // Redact the credentials. - redact_credentials(&mut location); + // Remove the credentials. + location.remove_credentials(); // Remove the fragment from the URL; it's already present in the source. location.set_fragment(None); // Reconstruct the PEP 508 URL from the underlying data. - let url = Url::from(ParsedArchiveUrl { + let url = DisplaySafeUrl::from(ParsedArchiveUrl { url: location.clone(), subdirectory: subdirectory.clone(), ext, diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index 06d11aad6..b7b83a19b 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -1518,7 +1518,7 @@ impl std::fmt::Display for PubGrubHint { "hint".bold().cyan(), ":".bold(), name.cyan(), - found_index.redacted().cyan(), + found_index.without_credentials().cyan(), PackageRange::compatibility(&PubGrubPackage::base(name), range, None).cyan(), next_index.cyan(), "--index-strategy unsafe-best-match".green(), @@ -1530,7 +1530,7 @@ impl std::fmt::Display for PubGrubHint { "{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).", "hint".bold().cyan(), ":".bold(), - index.redacted().cyan(), + index.without_credentials().cyan(), "401 Unauthorized".red(), ) } @@ -1540,7 +1540,7 @@ impl std::fmt::Display for PubGrubHint { "{}{} An index URL ({}) could not be queried due to a lack of valid authentication credentials ({}).", "hint".bold().cyan(), ":".bold(), - index.redacted().cyan(), + index.without_credentials().cyan(), "403 Forbidden".red(), ) } diff --git a/crates/uv-resolver/src/redirect.rs b/crates/uv-resolver/src/redirect.rs index 830962d9a..7d2539aba 100644 --- a/crates/uv-resolver/src/redirect.rs +++ b/crates/uv-resolver/src/redirect.rs @@ -1,8 +1,7 @@ -use url::Url; - use uv_git::GitResolver; use uv_pep508::VerbatimUrl; use uv_pypi_types::{ParsedGitUrl, ParsedUrl, VerbatimParsedUrl}; +use uv_redacted::DisplaySafeUrl; /// Map a URL to a precise URL, if possible. pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> VerbatimParsedUrl { @@ -26,7 +25,7 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba url: new_git_url, subdirectory: subdirectory.clone(), }; - let new_url = Url::from(new_parsed_url.clone()); + let new_url = DisplaySafeUrl::from(new_parsed_url.clone()); let new_verbatim_url = apply_redirect(&url.verbatim, new_url); VerbatimParsedUrl { parsed_url: ParsedUrl::Git(new_parsed_url), @@ -36,7 +35,7 @@ pub(crate) fn url_to_precise(url: VerbatimParsedUrl, git: &GitResolver) -> Verba /// Given a [`VerbatimUrl`] and a redirect, apply the redirect to the URL while preserving as much /// of the verbatim representation as possible. -fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl { +fn apply_redirect(url: &VerbatimUrl, redirect: DisplaySafeUrl) -> VerbatimUrl { let redirect = VerbatimUrl::from_url(redirect); // The redirect should be the "same" URL, but with a specific commit hash added after the `@`. @@ -85,9 +84,8 @@ fn apply_redirect(url: &VerbatimUrl, redirect: Url) -> VerbatimUrl { #[cfg(test)] mod tests { - use url::Url; - use uv_pep508::VerbatimUrl; + use uv_redacted::DisplaySafeUrl; use crate::redirect::apply_redirect; @@ -97,8 +95,9 @@ mod tests { // to the given representation. let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git")? .with_given("git+https://github.com/flask.git"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + let redirect = DisplaySafeUrl::parse( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )?; let expected = VerbatimUrl::parse_url( "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", @@ -111,8 +110,9 @@ mod tests { // representation. let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? .with_given("git+https://${DOMAIN}.com/flask.git@main"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + let redirect = DisplaySafeUrl::parse( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )?; let expected = VerbatimUrl::parse_url( "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", @@ -123,8 +123,9 @@ mod tests { // If there's a conflict after the `@`, discard the original representation. let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git@main")? .with_given("git+https://github.com/flask.git@${TAG}"); - let redirect = - Url::parse("https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe")?; + let redirect = DisplaySafeUrl::parse( + "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", + )?; let expected = VerbatimUrl::parse_url( "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe", @@ -134,7 +135,7 @@ mod tests { // We should preserve subdirectory fragments. let verbatim = VerbatimUrl::parse_url("https://github.com/flask.git#subdirectory=src")? .with_given("git+https://github.com/flask.git#subdirectory=src"); - let redirect = Url::parse( + let redirect = DisplaySafeUrl::parse( "https://github.com/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe#subdirectory=src", )?; diff --git a/crates/uv-resolver/src/resolution/display.rs b/crates/uv-resolver/src/resolution/display.rs index 2968074b7..2f70f00f6 100644 --- a/crates/uv-resolver/src/resolution/display.rs +++ b/crates/uv-resolver/src/resolution/display.rs @@ -290,7 +290,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> { // `# from https://pypi.org/simple`). if self.include_index_annotation { if let Some(index) = node.dist.index() { - let url = index.redacted(); + let url = index.without_credentials(); writeln!(f, "{}", format!(" # from {url}").green())?; } } diff --git a/crates/uv-resolver/src/resolver/reporter.rs b/crates/uv-resolver/src/resolver/reporter.rs index f2bf0f006..f2eeea3fa 100644 --- a/crates/uv-resolver/src/resolver/reporter.rs +++ b/crates/uv-resolver/src/resolver/reporter.rs @@ -1,9 +1,8 @@ use std::sync::Arc; -use url::Url; - use uv_distribution_types::{BuildableSource, VersionOrUrlRef}; use uv_normalize::PackageName; +use uv_redacted::DisplaySafeUrl; pub type BuildId = usize; @@ -31,10 +30,10 @@ pub trait Reporter: Send + Sync { fn on_download_complete(&self, name: &PackageName, id: usize); /// Callback to invoke when a repository checkout begins. - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize; + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize; /// Callback to invoke when a repository checkout completes. - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize); + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize); } impl dyn Reporter { @@ -62,11 +61,11 @@ impl uv_distribution::Reporter for Facade { self.reporter.on_build_complete(source, id); } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } diff --git a/crates/uv-resolver/src/resolver/system.rs b/crates/uv-resolver/src/resolver/system.rs index 49c23952b..806b1c01c 100644 --- a/crates/uv-resolver/src/resolver/system.rs +++ b/crates/uv-resolver/src/resolver/system.rs @@ -1,10 +1,10 @@ use std::str::FromStr; use pubgrub::Ranges; -use url::Url; use uv_normalize::PackageName; use uv_pep440::Version; +use uv_redacted::DisplaySafeUrl; use uv_torch::TorchBackend; use crate::pubgrub::{PubGrubDependency, PubGrubPackage, PubGrubPackageInner}; @@ -21,7 +21,7 @@ impl SystemDependency { /// Extract a [`SystemDependency`] from an index URL. /// /// For example, given `https://download.pytorch.org/whl/cu124`, returns CUDA 12.4. - pub(super) fn from_index(index: &Url) -> Option { + pub(super) fn from_index(index: &DisplaySafeUrl) -> Option { let backend = TorchBackend::from_index(index)?; let cuda_version = backend.cuda_version()?; Some(Self { @@ -51,22 +51,21 @@ impl From for PubGrubDependency { mod tests { use std::str::FromStr; - use url::Url; - use uv_normalize::PackageName; use uv_pep440::Version; + use uv_redacted::DisplaySafeUrl; use crate::resolver::system::SystemDependency; #[test] fn pypi() { - let url = Url::parse("https://pypi.org/simple").unwrap(); + let url = DisplaySafeUrl::parse("https://pypi.org/simple").unwrap(); assert_eq!(SystemDependency::from_index(&url), None); } #[test] fn pytorch_cuda_12_4() { - let url = Url::parse("https://download.pytorch.org/whl/cu124").unwrap(); + let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/cu124").unwrap(); assert_eq!( SystemDependency::from_index(&url), Some(SystemDependency { @@ -78,7 +77,7 @@ mod tests { #[test] fn pytorch_cpu() { - let url = Url::parse("https://download.pytorch.org/whl/cpu").unwrap(); + let url = DisplaySafeUrl::parse("https://download.pytorch.org/whl/cpu").unwrap(); assert_eq!(SystemDependency::from_index(&url), None); } } diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index 32a6b68b9..993633918 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -14,6 +14,7 @@ workspace = true uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-settings = { workspace = true } uv-workspace = { workspace = true } diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 5f169f126..1023b4141 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -12,6 +12,7 @@ use url::Url; use uv_pep440::VersionSpecifiers; use uv_pep508::PackageName; use uv_pypi_types::VerbatimParsedUrl; +use uv_redacted::DisplaySafeUrl; use uv_settings::{GlobalOptions, ResolverInstallerOptions}; use uv_workspace::pyproject::Sources; @@ -25,7 +26,7 @@ pub enum Pep723Item { /// A PEP 723 script provided via `stdin`. Stdin(Pep723Metadata), /// A PEP 723 script provided via a remote URL. - Remote(Pep723Metadata, Url), + Remote(Pep723Metadata, DisplaySafeUrl), } impl Pep723Item { diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index fd4c3c739..7e9f4b526 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -28,6 +28,7 @@ uv-options-metadata = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true, features = ["schemars", "clap"] } +uv-redacted = { workspace = true } uv-resolver = { workspace = true, features = ["schemars", "clap"] } uv-static = { workspace = true } uv-torch = { workspace = true, features = ["schemars", "clap"] } diff --git a/crates/uv-settings/src/combine.rs b/crates/uv-settings/src/combine.rs index 3469cedce..900b56d42 100644 --- a/crates/uv-settings/src/combine.rs +++ b/crates/uv-settings/src/combine.rs @@ -11,6 +11,7 @@ use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipInd use uv_install_wheel::LinkMode; use uv_pypi_types::{SchemaConflicts, SupportedEnvironments}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; +use uv_redacted::DisplaySafeUrl; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_torch::TorchMode; @@ -82,6 +83,7 @@ impl_combine_or!(IndexStrategy); impl_combine_or!(IndexUrl); impl_combine_or!(KeyringProviderType); impl_combine_or!(LinkMode); +impl_combine_or!(DisplaySafeUrl); impl_combine_or!(NonZeroUsize); impl_combine_or!(PathBuf); impl_combine_or!(PipExtraIndex); diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 58632511b..effe93199 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,7 +1,6 @@ use std::{fmt::Debug, num::NonZeroUsize, path::Path, path::PathBuf}; use serde::{Deserialize, Serialize}; -use url::Url; use uv_cache_info::CacheKey; use uv_configuration::{ @@ -17,6 +16,7 @@ use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep508::Requirement; use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; use uv_python::{PythonDownloads, PythonPreference, PythonVersion}; +use uv_redacted::DisplaySafeUrl; use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode}; use uv_static::EnvVars; use uv_torch::TorchMode; @@ -1837,7 +1837,7 @@ pub struct OptionsWire { // #[serde(flatten)] // publish: PublishOptions - publish_url: Option, + publish_url: Option, trusted_publishing: Option, check_url: Option, @@ -2019,7 +2019,7 @@ pub struct PublishOptions { publish-url = "https://test.pypi.org/legacy/" "# )] - pub publish_url: Option, + pub publish_url: Option, /// Configure trusted publishing via GitHub Actions. /// diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 08a00e006..0973a5218 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -27,12 +27,12 @@ uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } uv-python = { workspace = true } +uv-redacted = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } rustc-hash = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } [features] default = [] diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index f48ce87ce..fe472b349 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use std::sync::Arc; use rustc_hash::FxHashMap; -use url::Url; use uv_configuration::HashCheckingMode; use uv_distribution_types::{ @@ -12,6 +11,7 @@ use uv_distribution_types::{ use uv_normalize::PackageName; use uv_pep440::Version; use uv_pypi_types::{HashDigest, HashDigests, HashError, ResolverMarkerEnvironment}; +use uv_redacted::DisplaySafeUrl; #[derive(Debug, Default, Clone)] pub enum HashStrategy { @@ -76,7 +76,7 @@ impl HashStrategy { } /// Return the [`HashPolicy`] for the given direct URL package. - pub fn get_url(&self, url: &Url) -> HashPolicy { + pub fn get_url(&self, url: &DisplaySafeUrl) -> HashPolicy { match self { Self::None => HashPolicy::None, Self::Generate(mode) => HashPolicy::Generate(*mode), @@ -109,7 +109,7 @@ impl HashStrategy { } /// Returns `true` if the given direct URL package is allowed. - pub fn allows_url(&self, url: &Url) -> bool { + pub fn allows_url(&self, url: &DisplaySafeUrl) -> bool { match self { Self::None => true, Self::Generate(_) => true, diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index 59bc02f29..e69a114a8 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -27,6 +27,7 @@ uv-options-metadata = { workspace = true } uv-pep440 = { workspace = true } uv-pep508 = { workspace = true } uv-pypi-types = { workspace = true } +uv-redacted = { workspace = true } uv-static = { workspace = true } uv-warnings = { workspace = true } @@ -42,7 +43,6 @@ tokio = { workspace = true } toml = { workspace = true } toml_edit = { workspace = true } tracing = { workspace = true } -url = { workspace = true } [dev-dependencies] anyhow = { workspace = true } @@ -52,7 +52,7 @@ regex = { workspace = true } tempfile = { workspace = true } [features] -schemars = ["dep:schemars", "uv-pypi-types/schemars"] +schemars = ["dep:schemars", "uv-pypi-types/schemars", "uv-redacted/schemars"] [package.metadata.cargo-shear] ignored = ["uv-options-metadata"] diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 4efd322cc..2b0e44c16 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -17,7 +17,6 @@ use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashSet}; use serde::{Deserialize, Deserializer, Serialize, de::IntoDeserializer, de::SeqAccess}; use thiserror::Error; -use url::Url; use uv_build_backend::BuildBackendSettings; use uv_distribution_types::{Index, IndexName, RequirementSource}; use uv_fs::{PortablePathBuf, relative_to}; @@ -29,6 +28,7 @@ use uv_pep508::MarkerTree; use uv_pypi_types::{ Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl, }; +use uv_redacted::DisplaySafeUrl; #[derive(Error, Debug)] pub enum PyprojectTomlError { @@ -891,7 +891,7 @@ pub enum Source { /// ``` Git { /// The repository URL (without the `git+` prefix). - git: Url, + git: DisplaySafeUrl, /// The path to the directory with the `pyproject.toml`, if it's not in the archive root. subdirectory: Option, // Only one of the three may be used; we'll validate this later and emit a custom error. @@ -915,7 +915,7 @@ pub enum Source { /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" } /// ``` Url { - url: Url, + url: DisplaySafeUrl, /// For source distributions, the path to the directory with the `pyproject.toml`, if it's /// not in the archive root. subdirectory: Option, @@ -989,12 +989,12 @@ impl<'de> Deserialize<'de> for Source { #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] struct CatchAll { - git: Option, + git: Option, subdirectory: Option, rev: Option, tag: Option, branch: Option, - url: Option, + url: Option, path: Option, editable: Option, package: Option, @@ -1083,7 +1083,7 @@ impl<'de> Deserialize<'de> for Source { // If the user prefixed the URL with `git+`, strip it. let git = if let Some(git) = git.as_str().strip_prefix("git+") { - Url::parse(git).map_err(serde::de::Error::custom)? + DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)? } else { git }; diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 36f6b3083..150fbbdd8 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -7,7 +7,6 @@ use thiserror::Error; use toml_edit::{ Array, ArrayOfTables, DocumentMut, Formatted, Item, RawString, Table, TomlError, Value, }; -use url::Url; use uv_cache_key::CanonicalUrl; use uv_distribution_types::Index; @@ -15,6 +14,7 @@ use uv_fs::PortablePath; use uv_normalize::GroupName; use uv_pep440::{Version, VersionParseError, VersionSpecifier, VersionSpecifiers}; use uv_pep508::{ExtraName, MarkerTree, PackageName, Requirement, VersionOrUrl}; +use uv_redacted::DisplaySafeUrl; use crate::pyproject::{DependencyType, Source}; @@ -171,6 +171,7 @@ impl PyProjectTomlMut { &mut self, req: &Requirement, source: Option<&Source>, + raw: bool, ) -> Result { // Get or create `project.dependencies`. let dependencies = self @@ -180,7 +181,7 @@ impl PyProjectTomlMut { .as_array_mut() .ok_or(Error::MalformedDependencies)?; - let edit = add_dependency(req, dependencies, source.is_some())?; + let edit = add_dependency(req, dependencies, source.is_some(), raw)?; if let Some(source) = source { self.add_source(&req.name, source)?; @@ -196,6 +197,7 @@ impl PyProjectTomlMut { &mut self, req: &Requirement, source: Option<&Source>, + raw: bool, ) -> Result { // Get or create `tool.uv.dev-dependencies`. let dev_dependencies = self @@ -213,7 +215,7 @@ impl PyProjectTomlMut { .as_array_mut() .ok_or(Error::MalformedDependencies)?; - let edit = add_dependency(req, dev_dependencies, source.is_some())?; + let edit = add_dependency(req, dev_dependencies, source.is_some(), raw)?; if let Some(source) = source { self.add_source(&req.name, source)?; @@ -267,7 +269,7 @@ impl PyProjectTomlMut { if table .get("url") .and_then(|item| item.as_str()) - .and_then(|url| Url::parse(url).ok()) + .and_then(|url| DisplaySafeUrl::parse(url).ok()) .is_some_and(|url| { CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url()) }) @@ -304,10 +306,10 @@ impl PyProjectTomlMut { if table .get("url") .and_then(|item| item.as_str()) - .and_then(|url| Url::parse(url).ok()) + .and_then(|url| DisplaySafeUrl::parse(url).ok()) .is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url())) { - let mut formatted = Formatted::new(index.url.redacted().to_string()); + let mut formatted = Formatted::new(index.url.without_credentials().to_string()); if let Some(value) = table.get("url").and_then(Item::as_value) { if let Some(prefix) = value.decor().prefix() { formatted.decor_mut().set_prefix(prefix.clone()); @@ -365,7 +367,7 @@ impl PyProjectTomlMut { if table .get("url") .and_then(|item| item.as_str()) - .and_then(|url| Url::parse(url).ok()) + .and_then(|url| DisplaySafeUrl::parse(url).ok()) .is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())) { return false; @@ -400,6 +402,7 @@ impl PyProjectTomlMut { group: &ExtraName, req: &Requirement, source: Option<&Source>, + raw: bool, ) -> Result { // Get or create `project.optional-dependencies`. let optional_dependencies = self @@ -428,7 +431,7 @@ impl PyProjectTomlMut { .as_array_mut() .ok_or(Error::MalformedDependencies)?; - let added = add_dependency(req, group, source.is_some())?; + let added = add_dependency(req, group, source.is_some(), raw)?; // If `project.optional-dependencies` is an inline table, reformat it. // @@ -457,6 +460,7 @@ impl PyProjectTomlMut { group: &GroupName, req: &Requirement, source: Option<&Source>, + raw: bool, ) -> Result { // Get or create `dependency-groups`. let dependency_groups = self @@ -492,7 +496,7 @@ impl PyProjectTomlMut { .as_array_mut() .ok_or(Error::MalformedDependencies)?; - let added = add_dependency(req, group, source.is_some())?; + let added = add_dependency(req, group, source.is_some(), raw)?; // To avoid churn in pyproject.toml, we only sort new group keys if the // existing keys were sorted. @@ -999,6 +1003,7 @@ pub fn add_dependency( req: &Requirement, deps: &mut Array, has_source: bool, + raw: bool, ) -> Result { let mut to_replace = find_dependencies(&req.name, Some(&req.marker), deps); @@ -1057,7 +1062,11 @@ pub fn add_dependency( Sort::Unsorted }; - let req_string = req.to_string(); + let req_string = if raw { + req.displayable_with_credentials().to_string() + } else { + req.to_string() + }; let index = match sort { Sort::CaseInsensitive => deps.iter().position(|dep| { dep.as_str().is_some_and(|dep| { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index aaaeb8f3c..2a2cbb5e6 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -24,7 +24,7 @@ use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; use uv_distribution_types::{ Index, IndexName, IndexUrls, NameRequirementSpecification, Requirement, RequirementSource, - UnresolvedRequirement, VersionId, redact_credentials, + UnresolvedRequirement, VersionId, }; use uv_fs::Simplified; use uv_git::GIT_STORE; @@ -33,6 +33,7 @@ use uv_normalize::{DEV_DEPENDENCIES, DefaultExtras, PackageName}; use uv_pep508::{ExtraName, MarkerTree, UnnamedRequirement, VersionOrUrl}; use uv_pypi_types::{ParsedUrl, VerbatimParsedUrl}; use uv_python::{Interpreter, PythonDownloads, PythonEnvironment, PythonPreference, PythonRequest}; +use uv_redacted::DisplaySafeUrl; use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification}; use uv_resolver::FlatIndex; use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; @@ -627,7 +628,7 @@ fn edits( } }; - // Redact any credentials. By default, we avoid writing sensitive credentials to files that + // Remove any credentials. By default, we avoid writing sensitive credentials to files that // will be checked into version control (e.g., `pyproject.toml` and `uv.lock`). Instead, // we store the credentials in a global store, and reuse them during resolution. The // expectation is that subsequent resolutions steps will succeed by reading from (e.g.) the @@ -649,7 +650,7 @@ fn edits( GIT_STORE.insert(RepositoryUrl::new(&git), credentials); // Redact the credentials. - redact_credentials(&mut git); + git.remove_credentials(); } Some(Source::Git { git, @@ -705,13 +706,15 @@ fn edits( // Update the `pyproject.toml`. let edit = match &dependency_type { - DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?, - DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?, + DependencyType::Production => { + toml.add_dependency(&requirement, source.as_ref(), raw)? + } + DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref(), raw)?, DependencyType::Optional(extra) => { - toml.add_optional_dependency(extra, &requirement, source.as_ref())? + toml.add_optional_dependency(extra, &requirement, source.as_ref(), raw)? } DependencyType::Group(group) => { - toml.add_dependency_group_requirement(group, &requirement, source.as_ref())? + toml.add_dependency_group_requirement(group, &requirement, source.as_ref(), raw)? } }; @@ -863,6 +866,7 @@ async fn lock_and_sync( // Invalidate the project metadata. if let AddTarget::Project(VirtualProject::Project(ref project), _) = target { let url = Url::from_file_path(project.project_root()) + .map(DisplaySafeUrl::from) .expect("project root is a valid URL"); let version_id = VersionId::from_url(&url); let existing = lock_state.index().distributions().remove(&version_id); diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 038891779..8e813173e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -30,6 +30,7 @@ use uv_python::{ PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile, VersionFileDiscoveryOptions, }; +use uv_redacted::DisplaySafeUrl; use uv_requirements::{RequirementsSource, RequirementsSpecification}; use uv_resolver::{Installable, Lock, Preference}; use uv_scripts::Pep723Item; @@ -1198,7 +1199,7 @@ pub(crate) enum RunCommand { /// Execute a `pythonw` script provided via `stdin`. PythonGuiStdin(Vec, Vec), /// Execute a Python script provided via a remote URL. - PythonRemote(Url, tempfile::NamedTempFile, Vec), + PythonRemote(DisplaySafeUrl, tempfile::NamedTempFile, Vec), /// Execute an external command. External(OsString, Vec), /// Execute an empty command (in practice, `python` with no arguments). @@ -1464,7 +1465,7 @@ impl RunCommand { // We don't do this check on Windows since the file path would // be invalid anyway, and thus couldn't refer to a local file. if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) { - let url = Url::parse(&target.to_string_lossy())?; + let url = DisplaySafeUrl::parse(&target.to_string_lossy())?; let file_stem = url .path_segments() @@ -1481,7 +1482,11 @@ impl RunCommand { .native_tls(network_settings.native_tls) .allow_insecure_host(network_settings.allow_insecure_host.clone()) .build(); - let response = client.for_host(&url).get(url.clone()).send().await?; + let response = client + .for_host(&url) + .get(Url::from(url.clone())) + .send() + .await?; // Stream the response to the file. let mut writer = file.as_file(); diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 98d7a0699..70cf053e7 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -8,7 +8,6 @@ use console::Term; use owo_colors::OwoColorize; use tokio::sync::Semaphore; use tracing::{debug, info}; -use url::Url; use uv_auth::Credentials; use uv_cache::Cache; use uv_client::{AuthIntegration, BaseClient, BaseClientBuilder, RegistryClientBuilder}; @@ -17,6 +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_warnings::warn_user_once; use crate::commands::reporters::PublishReporter; @@ -26,7 +26,7 @@ use crate::settings::NetworkSettings; pub(crate) async fn publish( paths: Vec, - publish_url: Url, + publish_url: DisplaySafeUrl, trusted_publishing: TrustedPublishing, keyring_provider: KeyringProviderType, network_settings: &NetworkSettings, @@ -196,7 +196,7 @@ enum Prompt { /// /// Returns the publish URL, the username and the password. async fn gather_credentials( - mut publish_url: Url, + mut publish_url: DisplaySafeUrl, mut username: Option, mut password: Option, trusted_publishing: TrustedPublishing, @@ -205,7 +205,7 @@ async fn gather_credentials( check_url: Option<&IndexUrl>, prompt: Prompt, printer: Printer, -) -> Result<(Url, Credentials)> { +) -> Result<(DisplaySafeUrl, Credentials)> { // Support reading username and password from the URL, for symmetry with the index API. if let Some(url_password) = publish_url.password() { if password.is_some_and(|password| password != url_password) { @@ -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(&publish_url, Some(username)) + .fetch(&DisplaySafeUrlRef::from(&publish_url), Some(username)) .await .as_ref() .and_then(|credentials| credentials.password()) @@ -342,13 +342,14 @@ mod tests { use std::str::FromStr; use insta::assert_snapshot; - use url::Url; + + use uv_redacted::DisplaySafeUrl; async fn get_credentials( - url: Url, + url: DisplaySafeUrl, username: Option, password: Option, - ) -> Result<(Url, Credentials)> { + ) -> Result<(DisplaySafeUrl, Credentials)> { let client = BaseClientBuilder::new().build(); gather_credentials( url, @@ -366,10 +367,10 @@ mod tests { #[tokio::test] async fn username_password_sources() { - let example_url = Url::from_str("https://example.com").unwrap(); - let example_url_username = Url::from_str("https://ferris@example.com").unwrap(); + let example_url = DisplaySafeUrl::from_str("https://example.com").unwrap(); + let example_url_username = DisplaySafeUrl::from_str("https://ferris@example.com").unwrap(); let example_url_username_password = - Url::from_str("https://ferris:f3rr1s@example.com").unwrap(); + DisplaySafeUrl::from_str("https://ferris:f3rr1s@example.com").unwrap(); let (publish_url, credentials) = get_credentials(example_url.clone(), None, None) .await diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index ed0f11bc6..3943c941e 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -8,8 +8,6 @@ use std::time::Duration; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; use rustc_hash::FxHashMap; -use url::Url; -use uv_redacted::redacted_url; use crate::commands::human_readable_bytes; use crate::printer::Printer; @@ -20,6 +18,7 @@ use uv_distribution_types::{ use uv_normalize::PackageName; use uv_pep440::Version; use uv_python::PythonInstallationKey; +use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; /// Since downloads, fetches and builds run in parallel, their message output order is @@ -359,8 +358,7 @@ impl ProgressReporter { self.on_request_start(Direction::Upload, name, size) } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { - let url = redacted_url(url); + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { let ProgressMode::Multi { multi_progress, state, @@ -390,8 +388,7 @@ impl ProgressReporter { id } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { - let url = redacted_url(url); + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { let ProgressMode::Multi { state, multi_progress, @@ -481,11 +478,11 @@ impl uv_installer::PrepareReporter for PrepareReporter { self.reporter.on_download_complete(id); } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } } @@ -545,11 +542,11 @@ impl uv_resolver::ResolverReporter for ResolverReporter { self.reporter.on_build_complete(source, id); } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } @@ -587,11 +584,11 @@ impl uv_distribution::Reporter for ResolverReporter { self.reporter.on_download_complete(id); } - fn on_checkout_start(&self, url: &Url, rev: &str) -> usize { + fn on_checkout_start(&self, url: &DisplaySafeUrl, rev: &str) -> usize { self.reporter.on_checkout_start(url, rev) } - fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { + fn on_checkout_complete(&self, url: &DisplaySafeUrl, rev: &str, id: usize) { self.reporter.on_checkout_complete(url, rev, id); } } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index dce7c58f0..007fdc872 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -4,8 +4,6 @@ use std::path::PathBuf; use std::process; use std::str::FromStr; -use url::Url; - use uv_cache::{CacheArgs, Refresh}; use uv_cli::comma::CommaSeparatedRequirements; use uv_cli::{ @@ -35,6 +33,7 @@ use uv_normalize::{PackageName, PipGroupName}; use uv_pep508::{ExtraName, MarkerTree, RequirementOrigin}; use uv_pypi_types::SupportedEnvironments; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; +use uv_redacted::DisplaySafeUrl; use uv_resolver::{ AnnotationStyle, DependencyMode, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode, }; @@ -3162,7 +3161,7 @@ pub(crate) struct PublishSettings { pub(crate) index: Option, // Both CLI and configuration. - pub(crate) publish_url: Url, + pub(crate) publish_url: DisplaySafeUrl, pub(crate) trusted_publishing: TrustedPublishing, pub(crate) keyring_provider: KeyringProviderType, pub(crate) check_url: Option, @@ -3207,7 +3206,7 @@ impl PublishSettings { publish_url: args .publish_url .combine(publish_url) - .unwrap_or_else(|| Url::parse(PYPI_PUBLISH_URL).unwrap()), + .unwrap_or_else(|| DisplaySafeUrl::parse(PYPI_PUBLISH_URL).unwrap()), trusted_publishing: trusted_publishing .combine(args.trusted_publishing) .unwrap_or_default(), diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 4aa9f1ac7..37ea0bb3a 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1622,9 +1622,13 @@ pub async fn download_to_disk(url: &str, path: &Path) { let client = uv_client::BaseClientBuilder::new() .allow_insecure_host(trusted_hosts) .build(); - let url: reqwest::Url = url.parse().unwrap(); + let url = url.parse().unwrap(); let client = client.for_host(&url); - let response = client.request(http::Method::GET, url).send().await.unwrap(); + let response = client + .request(http::Method::GET, reqwest::Url::from(url)) + .send() + .await + .unwrap(); let mut file = tokio::fs::File::create(path).await.unwrap(); let mut stream = response.bytes_stream(); diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index dda76b0b9..9ff41e445 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -405,6 +405,8 @@ fn add_git_private_source() -> Result<()> { fn add_git_private_raw() -> Result<()> { let context = TestContext::new("3.12"); let token = decode_token(READ_ONLY_GITHUB_TOKEN); + let mut filters = context.filters(); + filters.push((&token, "***")); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(indoc! {r#" @@ -415,7 +417,7 @@ fn add_git_private_raw() -> Result<()> { dependencies = [] "#})?; - uv_snapshot!(context.filters(), context.add().arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")).arg("--raw-sources"), @r" + uv_snapshot!(filters, context.add().arg(format!("uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage")).arg("--raw-sources"), @r" success: true exit_code: 0 ----- stdout ----- @@ -429,16 +431,11 @@ fn add_git_private_raw() -> Result<()> { let pyproject_toml = context.read("pyproject.toml"); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - insta::with_settings!({ - filters => filters + filters => filters.clone() }, { assert_snapshot!( - pyproject_toml, @r###" + pyproject_toml, @r#" [project] name = "project" version = "0.1.0" @@ -446,14 +443,14 @@ fn add_git_private_raw() -> Result<()> { dependencies = [ "uv-private-pypackage @ git+https://***@github.com/astral-test/uv-private-pypackage", ] - "### + "# ); }); let lock = context.read("uv.lock"); insta::with_settings!({ - filters => context.filters(), + filters => filters.clone(), }, { assert_snapshot!( lock, @r#" @@ -484,7 +481,7 @@ fn add_git_private_raw() -> Result<()> { }); // Install from the lockfile. - uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r" + uv_snapshot!(filters, context.sync().arg("--frozen"), @r" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 1f4fb22fc..8a2c8769b 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7983,11 +7983,6 @@ fn lock_redact_git_pep508() -> Result<()> { let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" @@ -8000,7 +7995,7 @@ fn lock_redact_git_pep508() -> Result<()> { token = token, })?; - uv_snapshot!(&filters, context.lock(), @r###" + uv_snapshot!(&context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8012,7 +8007,7 @@ fn lock_redact_git_pep508() -> Result<()> { let lock = context.read("uv.lock"); insta::with_settings!({ - filters => filters.clone(), + filters => context.filters(), }, { assert_snapshot!( lock, @r#" @@ -8043,7 +8038,7 @@ fn lock_redact_git_pep508() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8053,7 +8048,7 @@ fn lock_redact_git_pep508() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8073,11 +8068,6 @@ fn lock_redact_git_sources() -> Result<()> { let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" @@ -8093,7 +8083,7 @@ fn lock_redact_git_sources() -> Result<()> { token = token, })?; - uv_snapshot!(&filters, context.lock(), @r###" + uv_snapshot!(&context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8105,7 +8095,7 @@ fn lock_redact_git_sources() -> Result<()> { let lock = context.read("uv.lock"); insta::with_settings!({ - filters => filters.clone(), + filters => context.filters(), }, { assert_snapshot!( lock, @r#" @@ -8136,7 +8126,7 @@ fn lock_redact_git_sources() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8146,7 +8136,7 @@ fn lock_redact_git_sources() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8166,11 +8156,6 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { let context = TestContext::new("3.12").with_filtered_link_mode_warning(); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { r#" @@ -8183,7 +8168,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { token = token, })?; - uv_snapshot!(&filters, context.lock(), @r###" + uv_snapshot!(&context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8196,7 +8181,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { let lock = context.read("uv.lock"); insta::with_settings!({ - filters => filters.clone(), + filters => context.filters(), }, { assert_snapshot!( lock, @r#" @@ -8221,7 +8206,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8232,7 +8217,7 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8249,12 +8234,6 @@ fn lock_redact_git_pep508_non_project() -> Result<()> { #[test] fn lock_redact_index_sources() -> Result<()> { let context = TestContext::new("3.12").with_filtered_link_mode_warning(); - let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -8274,7 +8253,7 @@ fn lock_redact_index_sources() -> Result<()> { "#, )?; - uv_snapshot!(&filters, context.lock(), @r###" + uv_snapshot!(&context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8286,7 +8265,7 @@ fn lock_redact_index_sources() -> Result<()> { let lock = context.read("uv.lock"); insta::with_settings!({ - filters => filters.clone(), + filters => context.filters(), }, { assert_snapshot!( lock, @r#" @@ -8321,7 +8300,7 @@ fn lock_redact_index_sources() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8331,7 +8310,7 @@ fn lock_redact_index_sources() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8348,12 +8327,6 @@ fn lock_redact_index_sources() -> Result<()> { #[test] fn lock_redact_url_sources() -> Result<()> { let context = TestContext::new("3.12").with_filtered_link_mode_warning(); - let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(r#" @@ -8367,7 +8340,7 @@ fn lock_redact_url_sources() -> Result<()> { iniconfig = { url = "https://public:heron@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl" } "#)?; - uv_snapshot!(&filters, context.lock(), @r###" + uv_snapshot!(&context.filters(), context.lock(), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8379,7 +8352,7 @@ fn lock_redact_url_sources() -> Result<()> { let lock = context.read("uv.lock"); insta::with_settings!({ - filters => filters.clone(), + filters => context.filters(), }, { assert_snapshot!( lock, @r#" @@ -8413,7 +8386,7 @@ fn lock_redact_url_sources() -> Result<()> { }); // Re-run with `--locked`. - uv_snapshot!(&filters, context.lock().arg("--locked"), @r###" + uv_snapshot!(&context.filters(), context.lock().arg("--locked"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -8423,7 +8396,7 @@ fn lock_redact_url_sources() -> Result<()> { "###); // Install from the lockfile. - uv_snapshot!(&filters, context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r###" + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r" success: true exit_code: 0 ----- stdout ----- @@ -8431,8 +8404,8 @@ fn lock_redact_url_sources() -> Result<()> { ----- stderr ----- Prepared 1 package in [TIME] Installed 1 package in [TIME] - + iniconfig==2.0.0 (from https://public:heron@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) - "###); + + iniconfig==2.0.0 (from https://public:****@pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl) + "); Ok(()) } @@ -15837,7 +15810,7 @@ fn lock_explicit_default_index() -> Result<()> { DEBUG No workspace root found, using project root DEBUG Ignoring existing lockfile due to mismatched requirements for: `project==0.1.0` Requested: {Requirement { name: PackageName("anyio"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([]), index: None, conflict: None }, origin: None }} - Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("test.pypi.org")), port: None, path: "/simple", query: None, fragment: None }, given: None }), format: Simple }), conflict: None }, origin: None }} + Existing: {Requirement { name: PackageName("iniconfig"), extras: [], groups: [], marker: true, source: Registry { specifier: VersionSpecifiers([VersionSpecifier { operator: Equal, version: "2.0.0" }]), index: Some(IndexMetadata { url: Url(VerbatimUrl { url: https://test.pypi.org/simple, given: None }), format: Simple }), conflict: None }, origin: None }} DEBUG Solving with installed Python version: 3.12.[X] DEBUG Solving with target Python version: >=3.12 DEBUG Adding direct dependency: project* diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 7a205f891..7b1234c35 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -2119,18 +2119,12 @@ fn install_git_private_https_pat() { let context = TestContext::new("3.8"); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let package = format!( "uv-private-pypackage@ git+https://{token}@github.com/astral-test/uv-private-pypackage" ); - uv_snapshot!(filters, context.pip_install().arg(package) - , @r###" + uv_snapshot!(context.filters(), context.pip_install().arg(package) + , @r" success: true exit_code: 0 ----- stdout ----- @@ -2139,8 +2133,8 @@ fn install_git_private_https_pat() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) - "###); + + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); context.assert_installed("uv_private_pypackage", "0.1.0"); } @@ -2153,16 +2147,11 @@ fn install_git_private_https_pat_mixed_with_public() { let context = TestContext::new("3.8"); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let package = format!( "uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage" ); - uv_snapshot!(filters, context.pip_install().arg(package).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), + uv_snapshot!(context.filters(), context.pip_install().arg(package).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###" success: true exit_code: 0 @@ -2172,7 +2161,7 @@ fn install_git_private_https_pat_mixed_with_public() { Resolved 2 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] - + uv-private-pypackage==0.1.0 (from git+https://***@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) "###); @@ -2187,11 +2176,6 @@ fn install_git_private_https_multiple_pat() { let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); - let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")] - .into_iter() - .chain(context.filters()) - .collect(); - let package_1 = format!( "uv-private-pypackage @ git+https://{token_1}@github.com/astral-test/uv-private-pypackage" ); @@ -2199,7 +2183,7 @@ fn install_git_private_https_multiple_pat() { "uv-private-pypackage-2 @ git+https://{token_2}@github.com/astral-test/uv-private-pypackage-2" ); - uv_snapshot!(filters, context.pip_install().arg(package_1).arg(package_2) + uv_snapshot!(context.filters(), context.pip_install().arg(package_1).arg(package_2) , @r###" success: true exit_code: 0 @@ -2209,8 +2193,8 @@ fn install_git_private_https_multiple_pat() { Resolved 2 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] - + uv-private-pypackage==0.1.0 (from git+https://***_1@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) - + uv-private-pypackage-2==0.1.0 (from git+https://***_2@github.com/astral-test/uv-private-pypackage-2@45c0bec7365710f09b1f4dbca61c86dde9537e4e) + + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + + uv-private-pypackage-2==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage-2@45c0bec7365710f09b1f4dbca61c86dde9537e4e) "###); context.assert_installed("uv_private_pypackage", "0.1.0"); @@ -2223,11 +2207,7 @@ fn install_git_private_https_pat_at_ref() { let context = TestContext::new("3.8"); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let mut filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - + let mut filters = context.filters(); filters.push((r"git\+https://", "")); // A user is _required_ on Windows @@ -2241,8 +2221,8 @@ fn install_git_private_https_pat_at_ref() { let package = format!( "uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac" ); - uv_snapshot!(filters, context.pip_install() - .arg(package), @r###" + uv_snapshot!(context.filters(), context.pip_install() + .arg(package), @r" success: true exit_code: 0 ----- stdout ----- @@ -2251,8 +2231,8 @@ fn install_git_private_https_pat_at_ref() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + uv-private-pypackage==0.1.0 (from ***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) - "###); + + uv-private-pypackage==0.1.0 (from git+https://****@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) + "); context.assert_installed("uv_private_pypackage", "0.1.0"); } @@ -2270,12 +2250,7 @@ fn install_git_private_https_pat_and_username() { let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); let user = "astral-test-bot"; - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - - uv_snapshot!(filters, context.pip_install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")) + uv_snapshot!(context.filters(), context.pip_install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")) , @r###" success: true exit_code: 0 @@ -2285,7 +2260,7 @@ fn install_git_private_https_pat_and_username() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:***@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) + + uv-private-pypackage==0.1.0 (from git+https://astral-test-bot:****@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac) "###); context.assert_installed("uv_private_pypackage", "0.1.0"); @@ -2301,7 +2276,10 @@ fn install_git_private_https_pat_not_authorized() { let token = "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"; let mut filters = context.filters(); - filters.insert(0, (token, "***")); + // TODO(john): We need this filter because we are displaying the token when + // an underlying process error message is being displayed. We should actually + // mask it. + filters.push((token, "***")); filters.push(("`.*/git fetch (.*)`", "`git fetch $1`")); // We provide a username otherwise (since the token is invalid), the git cli will prompt for a password @@ -2314,7 +2292,7 @@ fn install_git_private_https_pat_not_authorized() { ----- stdout ----- ----- stderr ----- - × Failed to download and build `uv-private-pypackage @ git+https://git:***@github.com/astral-test/uv-private-pypackage` + × Failed to download and build `uv-private-pypackage @ git+https://git:****@github.com/astral-test/uv-private-pypackage` ├─▶ Git operation failed ├─▶ failed to clone into: [CACHE_DIR]/git-v0/db/8401f5508e3e612d ╰─▶ process didn't exit successfully: `git fetch --force --update-head-ok 'https://git:***@github.com/astral-test/uv-private-pypackage' '+HEAD:refs/remotes/origin/HEAD'` (exit status: 128) @@ -2334,17 +2312,12 @@ fn install_github_artifact_private_https_pat_mixed_with_public() { let context = TestContext::new("3.8"); let token = decode_token(common::READ_ONLY_GITHUB_TOKEN); - let filters: Vec<_> = [(token.as_str(), "***")] - .into_iter() - .chain(context.filters()) - .collect(); - let private_package = format!( "uv-private-pypackage @ https://{token}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl" ); let public_package = "uv-public-pypackage @ https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl"; - uv_snapshot!(filters, context.pip_install().arg(private_package).arg(public_package), + uv_snapshot!(context.filters(), context.pip_install().arg(private_package).arg(public_package), @r###" success: true exit_code: 0 @@ -2354,7 +2327,7 @@ fn install_github_artifact_private_https_pat_mixed_with_public() { Resolved 2 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] - + uv-private-pypackage==0.1.0 (from https://***@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) + + uv-private-pypackage==0.1.0 (from https://****@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) + uv-public-pypackage==0.1.0 (from https://raw.githubusercontent.com/astral-test/uv-public-pypackage/main/dist/uv_public_pypackage-0.1.0-py3-none-any.whl) "###); @@ -2370,11 +2343,6 @@ fn install_github_artifact_private_https_multiple_pat() { let token_1 = decode_token(common::READ_ONLY_GITHUB_TOKEN); let token_2 = decode_token(common::READ_ONLY_GITHUB_TOKEN_2); - let filters: Vec<_> = [(token_1.as_str(), "***_1"), (token_2.as_str(), "***_2")] - .into_iter() - .chain(context.filters()) - .collect(); - let package_1 = format!( "uv-private-pypackage @ https://astral-test-bot:{token_1}@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl" ); @@ -2382,7 +2350,7 @@ fn install_github_artifact_private_https_multiple_pat() { "uv-private-pypackage-2 @ https://astral-test-bot:{token_2}@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl" ); - uv_snapshot!(filters, context.pip_install().arg(package_1).arg(package_2) + uv_snapshot!(context.filters(), context.pip_install().arg(package_1).arg(package_2) , @r###" success: true exit_code: 0 @@ -2392,8 +2360,8 @@ fn install_github_artifact_private_https_multiple_pat() { Resolved 2 packages in [TIME] Prepared 2 packages in [TIME] Installed 2 packages in [TIME] - + uv-private-pypackage==0.1.0 (from https://astral-test-bot:***_1@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) - + uv-private-pypackage-2==0.1.0 (from https://astral-test-bot:***_2@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl) + + uv-private-pypackage==0.1.0 (from https://astral-test-bot:****@raw.githubusercontent.com/astral-test/uv-private-pypackage/main/dist/uv_private_pypackage-0.1.0-py3-none-any.whl) + + uv-private-pypackage-2==0.1.0 (from https://astral-test-bot:****@raw.githubusercontent.com/astral-test/uv-private-pypackage-2/main/dist/uv_private_pypackage_2-0.1.0-py3-none-any.whl) "###); context.assert_installed("uv_private_pypackage", "0.1.0"); diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index dd61389b5..fd62ad7be 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -112,21 +112,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -293,21 +279,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -475,21 +447,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -689,21 +647,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -1032,21 +976,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -1240,21 +1170,7 @@ fn resolve_index_url() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -1272,21 +1188,7 @@ fn resolve_index_url() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple, given: Some( "https://test.pypi.org/simple", ), @@ -1455,21 +1357,7 @@ fn resolve_index_url() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple, given: Some( "https://test.pypi.org/simple", ), @@ -1489,21 +1377,7 @@ fn resolve_index_url() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -1521,21 +1395,7 @@ fn resolve_index_url() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple, given: Some( "https://test.pypi.org/simple", ), @@ -1728,21 +1588,7 @@ fn resolve_find_links() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl/torch_stable.html", - query: None, - fragment: None, - }, + url: https://download.pytorch.org/whl/torch_stable.html, given: Some( "https://download.pytorch.org/whl/torch_stable.html", ), @@ -2097,21 +1943,7 @@ fn resolve_top_level() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl", - query: None, - fragment: None, - }, + url: https://download.pytorch.org/whl, given: Some( "https://download.pytorch.org/whl", ), @@ -2129,21 +1961,7 @@ fn resolve_top_level() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple, given: Some( "https://test.pypi.org/simple", ), @@ -2310,21 +2128,7 @@ fn resolve_top_level() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", - ), - ), - port: None, - path: "/whl", - query: None, - fragment: None, - }, + url: https://download.pytorch.org/whl, given: Some( "https://download.pytorch.org/whl", ), @@ -2342,21 +2146,7 @@ fn resolve_top_level() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://test.pypi.org/simple, given: Some( "https://test.pypi.org/simple", ), @@ -3537,21 +3327,7 @@ fn resolve_both() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -3843,21 +3619,7 @@ fn resolve_config_file() -> anyhow::Result<()> { name: None, url: Pypi( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://pypi.org/simple, given: Some( "https://pypi.org/simple", ), @@ -4629,21 +4391,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -4663,21 +4411,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ), @@ -4844,21 +4578,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -4878,21 +4598,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ), @@ -5065,21 +4771,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -5099,21 +4791,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ), @@ -5281,21 +4959,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -5315,21 +4979,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ), @@ -5504,21 +5154,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -5538,21 +5174,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ), @@ -5720,21 +5342,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "cli.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://cli.pypi.org/simple, given: Some( "https://cli.pypi.org/simple", ), @@ -5754,21 +5362,7 @@ fn index_priority() -> anyhow::Result<()> { name: None, url: Url( VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "file.pypi.org", - ), - ), - port: None, - path: "/simple", - query: None, - fragment: None, - }, + url: https://file.pypi.org/simple, given: Some( "https://file.pypi.org/simple", ),