diff --git a/crates/uv-distribution-types/src/file.rs b/crates/uv-distribution-types/src/file.rs index 948378c0c..81e3e2878 100644 --- a/crates/uv-distribution-types/src/file.rs +++ b/crates/uv-distribution-types/src/file.rs @@ -171,10 +171,21 @@ impl UrlString { } /// Return the [`UrlString`] (as a [`Cow`]) with trailing slash removed. + /// + /// This matches the semantics of [`Url::pop_if_empty`], which will not trim a trailing slash if + /// it's the only path segment, e.g., `https://example.com/` would be unchanged. #[must_use] pub fn without_trailing_slash(&self) -> Cow<'_, Self> { self.as_ref() .strip_suffix('/') + .filter(|path| { + // Only strip the trailing slash if there's _another_ trailing slash that isn't a + // part of the scheme. + path.split_once("://") + .map(|(_scheme, rest)| rest) + .unwrap_or(path) + .contains('/') + }) .map(|path| Cow::Owned(UrlString(SmallString::from(path)))) .unwrap_or(Cow::Borrowed(self)) } @@ -283,5 +294,20 @@ mod tests { url.without_trailing_slash(), Cow::Owned(UrlString("https://example.com/path".into())) ); + + // Does not remove a trailing slash if it's the only path segment + let url = UrlString("https://example.com/".into()); + assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + + // Does not remove a trailing slash if it's the only path segment with a missing scheme + let url = UrlString("example.com/".into()); + assert_eq!(url.without_trailing_slash(), Cow::Borrowed(&url)); + + // Removes the trailing slash when the scheme is missing + let url = UrlString("example.com/path/".into()); + assert_eq!( + url.without_trailing_slash(), + Cow::Owned(UrlString("example.com/path".into())) + ); } }