diff --git a/crates/pep508-rs/src/unnamed.rs b/crates/pep508-rs/src/unnamed.rs index 6390cf8dffa3..778b0237f3ac 100644 --- a/crates/pep508-rs/src/unnamed.rs +++ b/crates/pep508-rs/src/unnamed.rs @@ -236,6 +236,12 @@ fn preprocess_unnamed_url( #[cfg(feature = "non-pep508-extensions")] if let Some(working_dir) = working_dir { let url = VerbatimUrl::parse_path(path.as_ref(), working_dir) + .map_err(|err| Pep508Error { + message: Pep508ErrorSource::::UrlError(err), + start, + len, + input: cursor.to_string(), + })? .with_given(url.to_string()); return Ok((url, extras)); } @@ -270,6 +276,12 @@ fn preprocess_unnamed_url( _ => { if let Some(working_dir) = working_dir { let url = VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + .map_err(|err| Pep508Error { + message: Pep508ErrorSource::::UrlError(err), + start, + len, + input: cursor.to_string(), + })? .with_given(url.to_string()); return Ok((url, extras)); } @@ -288,8 +300,14 @@ fn preprocess_unnamed_url( } else { // Ex) `../editable/` if let Some(working_dir) = working_dir { - let url = - VerbatimUrl::parse_path(expanded.as_ref(), working_dir).with_given(url.to_string()); + let url = VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + .map_err(|err| Pep508Error { + message: Pep508ErrorSource::::UrlError(err), + start, + len, + input: cursor.to_string(), + })? + .with_given(url.to_string()); return Ok((url, extras)); } diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index 6c61e0ff3f8b..f1351392f22e 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -38,25 +38,26 @@ impl VerbatimUrl { /// Create a [`VerbatimUrl`] from a file path. /// /// Assumes that the path is absolute. - pub fn from_path(path: impl AsRef) -> Self { + pub fn from_path(path: impl AsRef) -> Result { let path = path.as_ref(); // Normalize the path. - let path = normalize_path(path).expect("path is absolute"); + let path = normalize_path(path) + .map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?; // Extract the fragment, if it exists. 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())); + .map_err(|_| VerbatimUrlError::UrlConversion(path.to_path_buf()))?; // Set the fragment, if it exists. if let Some(fragment) = fragment { url.set_fragment(Some(fragment)); } - Self { url, given: None } + Ok(Self { url, given: None }) } /// Parse a URL from a string, expanding any environment variables. @@ -67,7 +68,10 @@ impl VerbatimUrl { /// Parse a URL from an absolute or relative path. #[cfg(feature = "non-pep508-extensions")] // PEP 508 arguably only allows absolute file URLs. - pub fn parse_path(path: impl AsRef, working_dir: impl AsRef) -> Self { + pub fn parse_path( + path: impl AsRef, + working_dir: impl AsRef, + ) -> Result { let path = path.as_ref(); // Convert the path to an absolute path, if necessary. @@ -78,26 +82,22 @@ impl VerbatimUrl { }; // Normalize the path. - let path = normalize_path(&path).expect("path is absolute"); + let path = normalize_path(&path) + .map_err(|err| VerbatimUrlError::Normalization(path.to_path_buf(), err))?; // Extract the fragment, if it exists. 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(), - working_dir.as_ref().display() - ) - }); + let mut url = 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 { url.set_fragment(Some(fragment)); } - Self { url, given: None } + Ok(Self { url, given: None }) } /// Parse a URL from an absolute path. @@ -108,12 +108,12 @@ impl VerbatimUrl { let path = if path.is_absolute() { path.to_path_buf() } else { - return Err(VerbatimUrlError::RelativePath(path.to_path_buf())); + return Err(VerbatimUrlError::WorkingDirectory(path.to_path_buf())); }; // Normalize the path. let Ok(path) = normalize_path(&path) else { - return Err(VerbatimUrlError::RelativePath(path)); + return Err(VerbatimUrlError::WorkingDirectory(path)); }; // Extract the fragment, if it exists. @@ -226,7 +226,7 @@ impl Pep508Url for VerbatimUrl { #[cfg(feature = "non-pep508-extensions")] if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::parse_path(path.as_ref(), working_dir) + return Ok(VerbatimUrl::parse_path(path.as_ref(), working_dir)? .with_given(url.to_string())); } @@ -245,7 +245,7 @@ impl Pep508Url for VerbatimUrl { _ => { #[cfg(feature = "non-pep508-extensions")] if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)? .with_given(url.to_string())); } @@ -257,7 +257,7 @@ impl Pep508Url for VerbatimUrl { // Ex) `../editable/` #[cfg(feature = "non-pep508-extensions")] if let Some(working_dir) = working_dir { - return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir) + return Ok(VerbatimUrl::parse_path(expanded.as_ref(), working_dir)? .with_given(url.to_string())); } @@ -275,7 +275,15 @@ pub enum VerbatimUrlError { /// Received a relative path, but no working directory was provided. #[error("relative path without a working directory: {0}")] - RelativePath(PathBuf), + WorkingDirectory(PathBuf), + + /// Received a path that could not be converted to a URL. + #[error("path could not be converted to a URL: {0}")] + UrlConversion(PathBuf), + + /// Received a path that could not be normalized. + #[error("path could not be normalized: {0}")] + Normalization(PathBuf, #[source] std::io::Error), } /// Expand all available environment variables. diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 441a6bf9b71e..243c4a0e0477 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -297,10 +297,15 @@ impl EditableRequirement { VerbatimUrl::parse_path(expanded.as_ref(), working_dir.as_ref()) }; + let url = url.map_err(|err| RequirementsTxtParserError::VerbatimUrl { + source: err, + url: expanded.to_string(), + })?; + // Create a `PathBuf`. let path = url .to_file_path() - .map_err(|()| RequirementsTxtParserError::InvalidEditablePath(expanded.to_string()))?; + .map_err(|()| RequirementsTxtParserError::UrlConversion(expanded.to_string()))?; // Add the verbatim representation of the URL to the `VerbatimUrl`. let url = url.with_given(requirement.to_string()); @@ -1034,7 +1039,11 @@ pub enum RequirementsTxtParserError { start: usize, end: usize, }, - InvalidEditablePath(String), + VerbatimUrl { + source: pep508_rs::VerbatimUrlError, + url: String, + }, + UrlConversion(String), UnsupportedUrl(String), MissingRequirementPrefix(String), NoBinary { @@ -1091,7 +1100,7 @@ impl RequirementsTxtParserError { fn with_offset(self, offset: usize) -> Self { match self { Self::IO(err) => Self::IO(err), - Self::InvalidEditablePath(given) => Self::InvalidEditablePath(given), + Self::UrlConversion(given) => Self::UrlConversion(given), Self::Url { source, url, @@ -1103,6 +1112,7 @@ impl RequirementsTxtParserError { start: start + offset, end: end + offset, }, + Self::VerbatimUrl { source, url } => Self::VerbatimUrl { source, url }, Self::UnsupportedUrl(url) => Self::UnsupportedUrl(url), Self::MissingRequirementPrefix(given) => Self::MissingRequirementPrefix(given), Self::NoBinary { @@ -1171,12 +1181,15 @@ impl Display for RequirementsTxtParserError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::IO(err) => err.fmt(f), - Self::InvalidEditablePath(given) => { - write!(f, "Invalid editable path: {given}") - } Self::Url { url, start, .. } => { write!(f, "Invalid URL at position {start}: `{url}`") } + Self::VerbatimUrl { source, url } => { + write!(f, "Invalid URL: `{url}`: {source}") + } + Self::UrlConversion(given) => { + write!(f, "Unable to convert URL to path: {given}") + } Self::UnsupportedUrl(url) => { write!(f, "Unsupported URL (expected a `file://` scheme): `{url}`") } @@ -1231,7 +1244,8 @@ impl std::error::Error for RequirementsTxtParserError { match &self { Self::IO(err) => err.source(), Self::Url { source, .. } => Some(source), - Self::InvalidEditablePath(_) => None, + Self::VerbatimUrl { source, .. } => Some(source), + Self::UrlConversion(_) => None, Self::UnsupportedUrl(_) => None, Self::MissingRequirementPrefix(_) => None, Self::NoBinary { source, .. } => Some(source), @@ -1260,10 +1274,13 @@ impl Display for RequirementsTxtFileError { self.file.user_display(), ) } - RequirementsTxtParserError::InvalidEditablePath(given) => { + RequirementsTxtParserError::VerbatimUrl { url, .. } => { + write!(f, "Invalid URL in `{}`: `{url}`", self.file.user_display()) + } + RequirementsTxtParserError::UrlConversion(given) => { write!( f, - "Invalid editable path in `{}`: {given}", + "Unable to convert URL to path `{}`: {given}", self.file.user_display() ) } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 1973396b9bdb..72ddadd3cb1c 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -17,12 +17,20 @@ use crate::{Connectivity, Error, ErrorKind, RegistryClient}; #[derive(Debug, thiserror::Error)] pub enum FlatIndexError { #[error("Failed to read `--find-links` directory: {0}")] - FindLinksDirectory(PathBuf, #[source] std::io::Error), + FindLinksDirectory(PathBuf, #[source] FindLinksDirectoryError), #[error("Failed to read `--find-links` URL: {0}")] FindLinksUrl(Url, #[source] Error), } +#[derive(Debug, thiserror::Error)] +pub enum FindLinksDirectoryError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + VerbatimUrl(#[from] pep508_rs::VerbatimUrlError), +} + #[derive(Debug, Default, Clone)] pub struct FlatIndexEntries { /// The list of `--find-links` entries. @@ -202,10 +210,10 @@ impl<'a> FlatIndexClient<'a> { } /// Read a flat remote index from a `--find-links` directory. - fn read_from_directory(path: &PathBuf) -> Result { + fn read_from_directory(path: &PathBuf) -> Result { // Absolute paths are required for the URL conversion. let path = fs_err::canonicalize(path)?; - let index_url = IndexUrl::Path(VerbatimUrl::from_path(&path)); + let index_url = IndexUrl::Path(VerbatimUrl::from_path(&path)?); let mut dists = Vec::new(); for entry in fs_err::read_dir(path)? { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 3a91029765f0..b318f6232ad3 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -21,6 +21,8 @@ pub enum Error { // Network error #[error("Failed to parse URL: {0}")] Url(String, #[source] url::ParseError), + #[error("Expected an absolute path, but received: {}", _0.user_display())] + RelativePath(PathBuf), #[error(transparent)] JoinRelativeUrl(#[from] pypi_types::JoinRelativeError), #[error("Git operation failed")] diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 0149a0cfb0d1..74eadf6e70e0 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -94,10 +94,10 @@ impl<'a> RegistryWheelIndex<'a> { .filter_map(|flat_index| match flat_index { FlatIndexLocation::Path(path) => { let path = fs_err::canonicalize(path).ok()?; - Some(IndexUrl::Path(VerbatimUrl::from_path(path))) + Some(IndexUrl::Path(VerbatimUrl::from_path(path).ok()?)) } FlatIndexLocation::Url(url) => { - Some(IndexUrl::Url(VerbatimUrl::unknown(url.clone()))) + Some(IndexUrl::Url(VerbatimUrl::from_url(url.clone()))) } }) .collect(); diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index f8cb105c965e..324cdba61899 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -105,7 +105,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Url::parse(url).map_err(|err| Error::Url(url.clone(), err))? } FileLocation::Path(path) => { - let url = Url::from_file_path(path).expect("path is absolute"); + let url = Url::from_file_path(path) + .map_err(|()| Error::RelativePath(path.clone()))?; return self .archive( source, @@ -262,7 +263,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Url::parse(url).map_err(|err| Error::Url(url.clone(), err))? } FileLocation::Path(path) => { - let url = Url::from_file_path(path).expect("path is absolute"); + let url = Url::from_file_path(path) + .map_err(|()| Error::RelativePath(path.clone()))?; return self .archive_metadata( source, diff --git a/crates/uv-requirements/src/pyproject.rs b/crates/uv-requirements/src/pyproject.rs index 484d573a6be5..de91b0922a1b 100644 --- a/crates/uv-requirements/src/pyproject.rs +++ b/crates/uv-requirements/src/pyproject.rs @@ -59,6 +59,8 @@ pub enum LoweringError { InvalidEntry, #[error(transparent)] InvalidUrl(#[from] url::ParseError), + #[error(transparent)] + InvalidVerbatimUrl(#[from] pep508_rs::VerbatimUrlError), #[error("Can't combine URLs from both `project.dependencies` and `tool.uv.sources`")] ConflictingUrls, #[error("Could not normalize path: `{0}`")] @@ -551,7 +553,7 @@ fn path_source( project_dir: &Path, editable: bool, ) -> Result { - let url = VerbatimUrl::parse_path(&path, project_dir).with_given(path.clone()); + let url = VerbatimUrl::parse_path(&path, project_dir)?.with_given(path.clone()); let path_buf = PathBuf::from(&path); let path_buf = path_buf .absolutize_from(project_dir) diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 9b5291781aca..0a7284d3f1fd 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -132,7 +132,7 @@ impl RequirementsSpecification { project: None, requirements: vec![UnresolvedRequirementSpecification { requirement: UnresolvedRequirement::Unnamed(UnnamedRequirement { - url: VerbatimUrl::from_path(path), + url: VerbatimUrl::from_path(path)?, extras: vec![], marker: None, origin: None,