diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index 368e1ad33..754237fe2 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -152,9 +152,6 @@ pub enum ErrorKind { #[error(transparent)] InvalidUrl(#[from] uv_distribution_types::ToUrlError), - #[error(transparent)] - JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), - #[error(transparent)] Flat(#[from] FlatIndexError), diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index b53e1ed9a..498124f9b 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -21,7 +21,7 @@ use uv_configuration::KeyringProviderType; use uv_configuration::{IndexStrategy, TrustedHost}; use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ - BuiltDist, File, FileLocation, IndexCapabilities, IndexFormat, IndexLocations, + BuiltDist, File, IndexCapabilities, IndexFormat, IndexLocations, IndexMetadataRef, IndexStatusCodeDecision, IndexStatusCodeStrategy, IndexUrl, IndexUrls, Name, }; use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream}; @@ -682,30 +682,14 @@ impl RegistryClient { let wheel = wheels.best_wheel(); - let location = match &wheel.file.url { - FileLocation::RelativeUrl(base, url) => { - let url = uv_pypi_types::base_url_join_relative(base, url) - .map_err(ErrorKind::JoinRelativeUrl)?; - if url.scheme() == "file" { - let path = url - .to_file_path() - .map_err(|()| ErrorKind::NonFileUrl(url.clone()))?; - WheelLocation::Path(path) - } else { - WheelLocation::Url(url) - } - } - FileLocation::AbsoluteUrl(url) => { - let url = url.to_url().map_err(ErrorKind::InvalidUrl)?; - if url.scheme() == "file" { - let path = url - .to_file_path() - .map_err(|()| ErrorKind::NonFileUrl(url.clone()))?; - WheelLocation::Path(path) - } else { - WheelLocation::Url(url) - } - } + let url = wheel.file.url.to_url().map_err(ErrorKind::InvalidUrl)?; + let location = if url.scheme() == "file" { + let path = url + .to_file_path() + .map_err(|()| ErrorKind::NonFileUrl(url.clone()))?; + WheelLocation::Path(path) + } else { + WheelLocation::Url(url) }; match location { @@ -1233,17 +1217,18 @@ mod tests { use url::Url; use uv_normalize::PackageName; - use uv_pypi_types::{JoinRelativeError, SimpleJson}; + use uv_pypi_types::SimpleJson; use uv_redacted::DisplaySafeUrl; use crate::{SimpleMetadata, SimpleMetadatum, html::SimpleHtml}; + use crate::RegistryClientBuilder; use uv_cache::Cache; + use uv_distribution_types::{FileLocation, ToUrlError}; + use uv_small_str::SmallString; use wiremock::matchers::{basic_auth, method, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; - use crate::RegistryClientBuilder; - type Error = Box; async fn start_test_server(username: &'static str, password: &'static str) -> MockServer { @@ -1507,7 +1492,7 @@ mod tests { /// /// See: #[test] - fn relative_urls_code_artifact() -> Result<(), JoinRelativeError> { + fn relative_urls_code_artifact() -> Result<(), ToUrlError> { let text = r#" @@ -1530,12 +1515,13 @@ mod tests { 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(); + let base = SmallString::from(base.as_str()); // Test parsing of the file urls let urls = files - .iter() - .map(|file| uv_pypi_types::base_url_join_relative(base.as_url().as_str(), &file.url)) - .collect::, JoinRelativeError>>()?; + .into_iter() + .map(|file| FileLocation::new(file.url, &base).to_url()) + .collect::, _>>()?; let urls = urls .iter() .map(DisplaySafeUrl::to_string) diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index a75af3977..165b684c5 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -57,10 +57,7 @@ impl File { .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?, size: file.size, upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond), - url: match split_scheme(&file.url) { - Some(..) => FileLocation::AbsoluteUrl(UrlString::new(file.url)), - None => FileLocation::RelativeUrl(base.clone(), file.url), - }, + url: FileLocation::new(file.url, base), yanked: file.yanked, }) } @@ -77,6 +74,17 @@ pub enum FileLocation { } impl FileLocation { + /// Parse a relative or absolute URL on a page with a base URL. + /// + /// This follows the HTML semantics where a link on a page is resolved relative to the URL of + /// that page. + pub fn new(url: SmallString, base: &SmallString) -> Self { + match split_scheme(&url) { + Some(..) => FileLocation::AbsoluteUrl(UrlString::new(url)), + None => FileLocation::RelativeUrl(base.clone(), url), + } + } + /// Convert this location to a URL. /// /// A relative URL has its base joined to the path. An absolute URL is diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index dcb0a17e3..d72c72757 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -20,7 +20,7 @@ use uv_client::{ }; use uv_distribution_filename::WheelFilename; use uv_distribution_types::{ - BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, InstalledDist, Name, + BuildableSource, BuiltDist, Dist, HashPolicy, Hashed, InstalledDist, Name, SourceDist, }; use uv_extract::hash::Hasher; @@ -179,12 +179,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { match dist { BuiltDist::Registry(wheels) => { let wheel = wheels.best_wheel(); - let url = match &wheel.file.url { - FileLocation::RelativeUrl(base, url) => { - uv_pypi_types::base_url_join_relative(base, url)? - } - FileLocation::AbsoluteUrl(url) => url.to_url()?, - }; + let url = wheel.file.url.to_url()?; // Create a cache entry for the wheel. let wheel_entry = self.build_context.cache().entry( diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 7c2a0f804..7d0a9593b 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -25,8 +25,6 @@ pub enum Error { RelativePath(PathBuf), #[error(transparent)] InvalidUrl(#[from] uv_distribution_types::ToUrlError), - #[error(transparent)] - JoinRelativeUrl(#[from] uv_pypi_types::JoinRelativeError), #[error("Expected a file URL, but received: {0}")] NonFileUrl(DisplaySafeUrl), #[error(transparent)] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 2b73eb4ff..e0c0731d3 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -32,7 +32,7 @@ use uv_client::{ use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ - BuildableSource, DirectorySourceUrl, FileLocation, GitSourceUrl, HashPolicy, Hashed, + BuildableSource, DirectorySourceUrl, GitSourceUrl, HashPolicy, Hashed, PathSourceUrl, SourceDist, SourceUrl, }; use uv_extract::hash::Hasher; @@ -122,12 +122,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .join(dist.version.to_string()), ); - let url = match &dist.file.url { - FileLocation::RelativeUrl(base, url) => { - uv_pypi_types::base_url_join_relative(base, url)? - } - FileLocation::AbsoluteUrl(url) => url.to_url()?, - }; + let url = dist.file.url.to_url()?; // If the URL is a file URL, use the local path directly. if url.scheme() == "file" { @@ -271,12 +266,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .join(dist.version.to_string()), ); - let url = match &dist.file.url { - FileLocation::RelativeUrl(base, url) => { - uv_pypi_types::base_url_join_relative(base, url)? - } - FileLocation::AbsoluteUrl(url) => url.to_url()?, - }; + let url = dist.file.url.to_url()?; // If the URL is a file URL, use the local path directly. if url.scheme() == "file" { diff --git a/crates/uv-pypi-types/src/base_url.rs b/crates/uv-pypi-types/src/base_url.rs index eca7dbdb7..e8dae7968 100644 --- a/crates/uv-pypi-types/src/base_url.rs +++ b/crates/uv-pypi-types/src/base_url.rs @@ -1,37 +1,6 @@ use serde::{Deserialize, Serialize}; 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 = DisplaySafeUrl::parse(base).map_err(|err| JoinRelativeError::ParseError { - original: base.to_string(), - source: err, - })?; - - base_url - .join(relative) - .map_err(|err| JoinRelativeError::ParseError { - original: format!("{base}/{relative}"), - source: err, - }) -} - -/// An error that occurs when `base_url_join_relative` fails. -/// -/// The error message includes the URL (`base` or `maybe_relative`) passed to -/// `base_url_join_relative` that provoked the error. -#[derive(Clone, Debug, thiserror::Error)] -pub enum JoinRelativeError { - #[error("Failed to parse URL: `{original}`")] - ParseError { - original: String, - source: url::ParseError, - }, -} - #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BaseUrl( #[serde(