1use std::borrow::Cow;
2use std::fmt::{self, Display, Formatter};
3use std::str::FromStr;
4use std::sync::Arc;
5
6use jiff::Timestamp;
7use serde::{Deserialize, Serialize};
8
9use uv_pep440::{VersionSpecifiers, VersionSpecifiersParseError};
10use uv_pep508::split_scheme;
11use uv_pypi_types::{CoreMetadata, HashDigests, Yanked};
12use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
13use uv_small_str::SmallString;
14
15#[derive(Debug, thiserror::Error)]
17pub enum FileConversionError {
18 #[error("Failed to parse `requires-python`: `{0}`")]
19 RequiresPython(String, #[source] VersionSpecifiersParseError),
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
24#[rkyv(derive(Debug))]
25pub struct File {
26 pub dist_info_metadata: bool,
27 pub filename: SmallString,
28 pub hashes: HashDigests,
29 pub requires_python: Option<Arc<VersionSpecifiers>>,
30 pub size: Option<u64>,
31 pub upload_time_utc_ms: Option<i64>,
36 pub url: FileLocation,
37 pub yanked: Option<Box<Yanked>>,
38 pub zstd: Option<Box<Zstd>>,
42}
43
44impl File {
45 pub fn try_from_pypi(
47 file: uv_pypi_types::PypiFile,
48 base: &SmallString,
49 ) -> Result<Self, FileConversionError> {
50 Ok(Self {
51 dist_info_metadata: file
52 .core_metadata
53 .as_ref()
54 .is_some_and(CoreMetadata::is_available),
55 filename: file.filename,
56 hashes: HashDigests::from(file.hashes),
57 requires_python: file
58 .requires_python
59 .transpose()
60 .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?,
61 size: file.size,
62 upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond),
63 url: FileLocation::new(file.url, base),
64 yanked: file.yanked,
65 zstd: None,
66 })
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
72#[rkyv(derive(Debug))]
73pub enum FileLocation {
74 RelativeUrl(SmallString, SmallString),
76 AbsoluteUrl(UrlString),
78}
79
80impl FileLocation {
81 pub fn new(url: SmallString, base: &SmallString) -> Self {
86 match split_scheme(&url) {
87 Some(..) => Self::AbsoluteUrl(UrlString::new(url)),
88 None => Self::RelativeUrl(base.clone(), url),
89 }
90 }
91
92 pub fn raw_filename(&self) -> &str {
96 let path = match self {
97 Self::RelativeUrl(_, path) => path.as_ref(),
98 Self::AbsoluteUrl(url) => url.as_ref(),
99 };
100 let path = path.split_once(['?', '#']).map_or(path, |(path, _)| path);
101 path.rsplit_once('/').map_or(path, |(_, filename)| filename)
102 }
103
104 pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
116 match self {
117 Self::RelativeUrl(base, path) => {
118 let base_url =
119 DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase {
120 base: base.to_string(),
121 err,
122 })?;
123 let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin {
124 base: base.to_string(),
125 path: path.to_string(),
126 err,
127 })?;
128 Ok(joined)
129 }
130 Self::AbsoluteUrl(absolute) => absolute.to_url(),
131 }
132 }
133}
134
135impl Display for FileLocation {
136 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
137 match self {
138 Self::RelativeUrl(_base, url) => Display::fmt(&url, f),
139 Self::AbsoluteUrl(url) => Display::fmt(&url.0, f),
140 }
141 }
142}
143
144#[derive(
148 Debug,
149 Clone,
150 PartialEq,
151 Eq,
152 PartialOrd,
153 Ord,
154 Hash,
155 Serialize,
156 Deserialize,
157 rkyv::Archive,
158 rkyv::Deserialize,
159 rkyv::Serialize,
160)]
161#[serde(transparent)]
162#[rkyv(derive(Debug))]
163pub struct UrlString(SmallString);
164
165impl UrlString {
166 fn new(url: SmallString) -> Self {
168 Self(url)
169 }
170
171 pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
173 DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute {
174 absolute: self.0.to_string(),
175 err,
176 })
177 }
178
179 pub fn base_str(&self) -> &str {
181 self.as_ref()
182 .split_once('?')
183 .or_else(|| self.as_ref().split_once('#'))
184 .map(|(path, _)| path)
185 .unwrap_or(self.as_ref())
186 }
187
188 #[must_use]
190 pub fn without_fragment(&self) -> Cow<'_, Self> {
191 self.as_ref()
192 .split_once('#')
193 .map(|(path, _)| Cow::Owned(Self(SmallString::from(path))))
194 .unwrap_or(Cow::Borrowed(self))
195 }
196}
197
198impl AsRef<str> for UrlString {
199 fn as_ref(&self) -> &str {
200 &self.0
201 }
202}
203
204impl From<DisplaySafeUrl> for UrlString {
205 fn from(value: DisplaySafeUrl) -> Self {
206 Self(value.as_str().into())
207 }
208}
209
210impl From<&DisplaySafeUrl> for UrlString {
211 fn from(value: &DisplaySafeUrl) -> Self {
212 Self(value.as_str().into())
213 }
214}
215
216impl Display for UrlString {
217 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218 fmt::Display::fmt(&self.0, f)
219 }
220}
221
222#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
224pub enum ToUrlError {
225 #[error("Could not parse base URL `{base}` as a valid URL")]
228 InvalidBase {
229 base: String,
231 #[source]
233 err: DisplaySafeUrlError,
234 },
235 #[error("Could not join base URL `{base}` to relative path `{path}`")]
238 InvalidJoin {
239 base: String,
241 path: String,
243 #[source]
245 err: DisplaySafeUrlError,
246 },
247 #[error("Could not parse absolute URL `{absolute}` as a valid URL")]
250 InvalidAbsolute {
251 absolute: String,
253 #[source]
255 err: DisplaySafeUrlError,
256 },
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
263pub struct Zstd {
264 pub hashes: HashDigests,
265 pub size: Option<u64>,
266}
267
268#[cfg(test)]
269mod tests {
270 use std::assert_matches;
271
272 use super::*;
273
274 #[test]
275 fn raw_filename() {
276 let base = SmallString::from("https://example.com/simple/");
277
278 let location = FileLocation::new(
279 SmallString::from("files/example%20pkg.whl?download=1#fragment"),
280 &base,
281 );
282 assert_eq!(location.raw_filename(), "example%20pkg.whl");
283
284 let location = FileLocation::new(
285 SmallString::from("https://files.example.com/example.whl#sha256=digest"),
286 &base,
287 );
288 assert_eq!(location.raw_filename(), "example.whl");
289 }
290
291 #[test]
292 fn base_str() {
293 let url = UrlString("https://example.com/path?query#fragment".into());
294 assert_eq!(url.base_str(), "https://example.com/path");
295
296 let url = UrlString("https://example.com/path#fragment".into());
297 assert_eq!(url.base_str(), "https://example.com/path");
298
299 let url = UrlString("https://example.com/path".into());
300 assert_eq!(url.base_str(), "https://example.com/path");
301 }
302
303 #[test]
304 fn without_fragment() {
305 let url = UrlString("https://example.com/path".into());
307 assert_eq!(&*url.without_fragment(), &url);
308 assert_matches!(url.without_fragment(), Cow::Borrowed(_));
309
310 let url = UrlString("https://example.com/path?query#fragment".into());
312 assert_eq!(
313 &*url.without_fragment(),
314 &UrlString("https://example.com/path?query".into())
315 );
316 assert_matches!(url.without_fragment(), Cow::Owned(_));
317 }
318}