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: Option<HashDigests>,
28 pub filename: SmallString,
29 pub hashes: HashDigests,
30 pub requires_python: Option<Arc<VersionSpecifiers>>,
31 pub size: Option<u64>,
32 pub upload_time_utc_ms: Option<i64>,
37 pub url: FileLocation,
38 pub yanked: Option<Box<Yanked>>,
39}
40
41impl File {
42 pub fn try_from_pypi(
44 file: uv_pypi_types::PypiFile,
45 base: &SmallString,
46 ) -> Result<Self, FileConversionError> {
47 Ok(Self {
48 dist_info_metadata: Self::dist_info_metadata(file.core_metadata),
49 filename: file.filename,
50 hashes: HashDigests::from(file.hashes),
51 requires_python: file
52 .requires_python
53 .transpose()
54 .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?,
55 size: file.size,
56 upload_time_utc_ms: file.upload_time.map(Timestamp::as_millisecond),
57 url: FileLocation::new(file.url, base),
58 yanked: file.yanked,
59 })
60 }
61
62 fn dist_info_metadata(metadata: Option<CoreMetadata>) -> Option<HashDigests> {
63 match metadata? {
64 CoreMetadata::Bool(false) => None,
65 CoreMetadata::Bool(true) => Some(HashDigests::empty()),
66 CoreMetadata::Hashes(hashes) => Some(HashDigests::from(hashes)),
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
73#[rkyv(derive(Debug))]
74pub enum FileLocation {
75 RelativeUrl(SmallString, SmallString),
77 AbsoluteUrl(UrlString),
79}
80
81impl FileLocation {
82 pub fn new(url: SmallString, base: &SmallString) -> Self {
87 match split_scheme(&url) {
88 Some(..) => Self::AbsoluteUrl(UrlString::new(url)),
89 None => Self::RelativeUrl(base.clone(), url),
90 }
91 }
92
93 pub fn raw_filename(&self) -> &str {
97 let path = match self {
98 Self::RelativeUrl(_, path) => path.as_ref(),
99 Self::AbsoluteUrl(url) => url.as_ref(),
100 };
101 let path = path.split_once(['?', '#']).map_or(path, |(path, _)| path);
102 path.rsplit_once('/').map_or(path, |(_, filename)| filename)
103 }
104
105 pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
114 match self {
115 Self::RelativeUrl(base, path) => {
116 let base_url =
117 DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase {
118 base: base.to_string(),
119 err,
120 })?;
121 let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin {
122 base: base.to_string(),
123 path: path.to_string(),
124 err,
125 })?;
126 Ok(joined)
127 }
128 Self::AbsoluteUrl(absolute) => absolute.to_url(),
129 }
130 }
131}
132
133impl Display for FileLocation {
134 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::RelativeUrl(_base, url) => Display::fmt(&url, f),
137 Self::AbsoluteUrl(url) => Display::fmt(&url.0, f),
138 }
139 }
140}
141
142#[derive(
146 Debug,
147 Clone,
148 PartialEq,
149 Eq,
150 PartialOrd,
151 Ord,
152 Hash,
153 Serialize,
154 Deserialize,
155 rkyv::Archive,
156 rkyv::Deserialize,
157 rkyv::Serialize,
158)]
159#[serde(transparent)]
160#[rkyv(derive(Debug))]
161pub struct UrlString(SmallString);
162
163impl UrlString {
164 fn new(url: SmallString) -> Self {
166 Self(url)
167 }
168
169 pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
171 DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute {
172 absolute: self.0.to_string(),
173 err,
174 })
175 }
176
177 pub fn base_str(&self) -> &str {
179 self.as_ref()
180 .split_once(['?', '#'])
181 .map(|(path, _)| path)
182 .unwrap_or(self.as_ref())
183 }
184
185 #[must_use]
187 pub fn without_fragment(&self) -> Cow<'_, Self> {
188 self.as_ref()
189 .split_once('#')
190 .map(|(path, _)| Cow::Owned(Self(SmallString::from(path))))
191 .unwrap_or(Cow::Borrowed(self))
192 }
193}
194
195impl AsRef<str> for UrlString {
196 fn as_ref(&self) -> &str {
197 &self.0
198 }
199}
200
201impl From<DisplaySafeUrl> for UrlString {
202 fn from(value: DisplaySafeUrl) -> Self {
203 Self(value.as_str().into())
204 }
205}
206
207impl From<&DisplaySafeUrl> for UrlString {
208 fn from(value: &DisplaySafeUrl) -> Self {
209 Self(value.as_str().into())
210 }
211}
212
213impl Display for UrlString {
214 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
215 fmt::Display::fmt(&self.0, f)
216 }
217}
218
219#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
221pub enum ToUrlError {
222 #[error("Could not parse base URL `{base}` as a valid URL")]
225 InvalidBase {
226 base: String,
228 #[source]
230 err: DisplaySafeUrlError,
231 },
232 #[error("Could not join base URL `{base}` to relative path `{path}`")]
235 InvalidJoin {
236 base: String,
238 path: String,
240 #[source]
242 err: DisplaySafeUrlError,
243 },
244 #[error("Could not parse absolute URL `{absolute}` as a valid URL")]
247 InvalidAbsolute {
248 absolute: String,
250 #[source]
252 err: DisplaySafeUrlError,
253 },
254}
255
256#[cfg(test)]
257mod tests {
258 use std::assert_matches;
259
260 use super::*;
261
262 #[test]
263 fn raw_filename() {
264 let base = SmallString::from("https://example.com/simple/");
265
266 let location = FileLocation::new(
267 SmallString::from("files/example%20pkg.whl?download=1#fragment"),
268 &base,
269 );
270 assert_eq!(location.raw_filename(), "example%20pkg.whl");
271
272 let location = FileLocation::new(
273 SmallString::from("https://files.example.com/example.whl#sha256=digest"),
274 &base,
275 );
276 assert_eq!(location.raw_filename(), "example.whl");
277 }
278
279 #[test]
280 fn base_str() {
281 let url = UrlString("https://example.com/path?query#fragment".into());
282 assert_eq!(url.base_str(), "https://example.com/path");
283
284 let url = UrlString("https://example.com/path#fragment".into());
285 assert_eq!(url.base_str(), "https://example.com/path");
286
287 let url = UrlString("https://example.com/path#fragment?query".into());
288 assert_eq!(url.base_str(), "https://example.com/path");
289
290 let url = UrlString("https://example.com/path#fragment/part?query".into());
291 assert_eq!(url.base_str(), "https://example.com/path");
292
293 let url = UrlString("https://example.com/path".into());
294 assert_eq!(url.base_str(), "https://example.com/path");
295 }
296
297 #[test]
298 fn without_fragment() {
299 let url = UrlString("https://example.com/path".into());
301 assert_eq!(&*url.without_fragment(), &url);
302 assert_matches!(url.without_fragment(), Cow::Borrowed(_));
303
304 let url = UrlString("https://example.com/path?query#fragment".into());
306 assert_eq!(
307 &*url.without_fragment(),
308 &UrlString("https://example.com/path?query".into())
309 );
310 assert_matches!(url.without_fragment(), Cow::Owned(_));
311 }
312}