diff --git a/crates/distribution-types/src/file.rs b/crates/distribution-types/src/file.rs index 71ee37614852..920d144fe863 100644 --- a/crates/distribution-types/src/file.rs +++ b/crates/distribution-types/src/file.rs @@ -7,7 +7,7 @@ use thiserror::Error; use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError}; use pypi_types::{DistInfoMetadata, Hashes, Yanked}; use url::Url; -use uv_auth::safe_copy_url_auth; +use uv_auth::safe_copy_url_auth_to_str; /// Error converting [`pypi_types::File`] to [`distribution_type::File`]. #[derive(Debug, Error)] @@ -53,12 +53,12 @@ impl File { size: file.size, upload_time_utc_ms: file.upload_time.map(|dt| dt.timestamp_millis()), url: if file.url.contains("://") { - let url = safe_copy_url_auth( - base, - Url::parse(&file.url) - .map_err(|err| FileConversionError::Url(file.url.clone(), err))?, - ); - FileLocation::AbsoluteUrl(url.to_string()) + let url = safe_copy_url_auth_to_str(base, &file.url) + .map_err(|err| FileConversionError::Url(file.url.clone(), err))? + .map(|url| url.to_string()) + .unwrap_or(file.url); + + FileLocation::AbsoluteUrl(url) } else { FileLocation::RelativeUrl(base.to_string(), file.url) }, diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 92f1d8d0121c..0f9635d51f8b 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -2,24 +2,38 @@ use tracing::warn; use url::Url; +/// Optimized version of [`safe_copy_url_auth`] which avoids parsing a string +/// into a URL unless the given URL has authentication to copy. Useful for patterns +/// where the returned URL would immediately be cast into a string. +/// +/// Returns [`Err`] if there is authentication to copy and `new_url` is not a valid URL. +/// Returns [`None`] if there is no authentication to copy. +pub fn safe_copy_url_auth_to_str( + trusted_url: &Url, + new_url: &str, +) -> Result, url::ParseError> { + if trusted_url.username().is_empty() && trusted_url.password().is_none() { + return Ok(None); + } + + let new_url = Url::parse(new_url)?; + Ok(Some(safe_copy_url_auth(trusted_url, new_url))) +} + /// Copy authentication from one URL to another URL if applicable. /// /// See [`should_retain_auth`] for details on when authentication is retained. #[must_use] -pub fn safe_copy_url_auth(request_url: &Url, mut response_url: Url) -> Url { - if should_retain_auth(request_url, &response_url) { - response_url - .set_username(request_url.username()) - .unwrap_or_else(|_| { - warn!("Failed to transfer username to response URL: {response_url}") - }); - response_url - .set_password(request_url.password()) - .unwrap_or_else(|_| { - warn!("Failed to transfer password to response URL: {response_url}") - }); +pub fn safe_copy_url_auth(trusted_url: &Url, mut new_url: Url) -> Url { + if should_retain_auth(trusted_url, &new_url) { + new_url + .set_username(trusted_url.username()) + .unwrap_or_else(|_| warn!("Failed to transfer username to response URL: {new_url}")); + new_url + .set_password(trusted_url.password()) + .unwrap_or_else(|_| warn!("Failed to transfer password to response URL: {new_url}")); } - response_url + new_url } /// Determine if authentication information should be retained on a new URL. @@ -27,7 +41,7 @@ pub fn safe_copy_url_auth(request_url: &Url, mut response_url: Url) -> Url { /// /// /// -fn should_retain_auth(request_url: &Url, response_url: &Url) -> bool { +fn should_retain_auth(trusted_url: &Url, new_url: &Url) -> bool { // The "scheme" and "authority" components must match to retain authentication // The "authority", is composed of the host and port. @@ -36,12 +50,12 @@ fn should_retain_auth(request_url: &Url, response_url: &Url) -> bool { // Note some clients such as Python's `requests` library allow an upgrade // from `http` to `https` but this is not spec-compliant. // - if request_url.scheme() != response_url.scheme() { + if trusted_url.scheme() != new_url.scheme() { return false; } // The host must always be an exact match. - if request_url.host() != response_url.host() { + if trusted_url.host() != new_url.host() { return false; } @@ -49,7 +63,7 @@ fn should_retain_auth(request_url: &Url, response_url: &Url) -> bool { // The port is only allowed to differ if it it matches the "default port" for the scheme. // However, `reqwest` sets the `port` to `None` if it matches the default port so we do // not need any special handling here. - if request_url.port() != response_url.port() { + if trusted_url.port() != new_url.port() { return false; }