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> {
117 match self {
118 Self::RelativeUrl(base, path) => {
119 let base_url =
120 DisplaySafeUrl::parse(base).map_err(|err| ToUrlError::InvalidBase {
121 base: base.to_string(),
122 err,
123 })?;
124 let joined = base_url.join(path).map_err(|err| ToUrlError::InvalidJoin {
125 base: base.to_string(),
126 path: path.to_string(),
127 err,
128 })?;
129 Ok(joined)
130 }
131 Self::AbsoluteUrl(absolute) => absolute.to_url(),
132 }
133 }
134}
135
136impl Display for FileLocation {
137 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
138 match self {
139 Self::RelativeUrl(_base, url) => Display::fmt(&url, f),
140 Self::AbsoluteUrl(url) => Display::fmt(&url.0, f),
141 }
142 }
143}
144
145#[derive(
149 Debug,
150 Clone,
151 PartialEq,
152 Eq,
153 PartialOrd,
154 Ord,
155 Hash,
156 Serialize,
157 Deserialize,
158 rkyv::Archive,
159 rkyv::Deserialize,
160 rkyv::Serialize,
161)]
162#[serde(transparent)]
163#[rkyv(derive(Debug))]
164pub struct UrlString(SmallString);
165
166impl UrlString {
167 fn new(url: SmallString) -> Self {
169 Self(url)
170 }
171
172 pub fn to_url(&self) -> Result<DisplaySafeUrl, ToUrlError> {
174 DisplaySafeUrl::from_str(&self.0).map_err(|err| ToUrlError::InvalidAbsolute {
175 absolute: self.0.to_string(),
176 err,
177 })
178 }
179
180 pub fn base_str(&self) -> &str {
182 self.as_ref()
183 .split_once('?')
184 .or_else(|| self.as_ref().split_once('#'))
185 .map(|(path, _)| path)
186 .unwrap_or(self.as_ref())
187 }
188
189 #[must_use]
191 pub fn without_fragment(&self) -> Cow<'_, Self> {
192 self.as_ref()
193 .split_once('#')
194 .map(|(path, _)| Cow::Owned(Self(SmallString::from(path))))
195 .unwrap_or(Cow::Borrowed(self))
196 }
197}
198
199impl AsRef<str> for UrlString {
200 fn as_ref(&self) -> &str {
201 &self.0
202 }
203}
204
205impl From<DisplaySafeUrl> for UrlString {
206 fn from(value: DisplaySafeUrl) -> Self {
207 Self(value.as_str().into())
208 }
209}
210
211impl From<&DisplaySafeUrl> for UrlString {
212 fn from(value: &DisplaySafeUrl) -> Self {
213 Self(value.as_str().into())
214 }
215}
216
217impl Display for UrlString {
218 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
219 fmt::Display::fmt(&self.0, f)
220 }
221}
222
223#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
225pub enum ToUrlError {
226 #[error("Could not parse base URL `{base}` as a valid URL")]
229 InvalidBase {
230 base: String,
232 #[source]
234 err: DisplaySafeUrlError,
235 },
236 #[error("Could not join base URL `{base}` to relative path `{path}`")]
239 InvalidJoin {
240 base: String,
242 path: String,
244 #[source]
246 err: DisplaySafeUrlError,
247 },
248 #[error("Could not parse absolute URL `{absolute}` as a valid URL")]
251 InvalidAbsolute {
252 absolute: String,
254 #[source]
256 err: DisplaySafeUrlError,
257 },
258}
259
260#[cfg(test)]
261mod tests {
262 use std::assert_matches;
263
264 use super::*;
265
266 #[test]
267 fn raw_filename() {
268 let base = SmallString::from("https://example.com/simple/");
269
270 let location = FileLocation::new(
271 SmallString::from("files/example%20pkg.whl?download=1#fragment"),
272 &base,
273 );
274 assert_eq!(location.raw_filename(), "example%20pkg.whl");
275
276 let location = FileLocation::new(
277 SmallString::from("https://files.example.com/example.whl#sha256=digest"),
278 &base,
279 );
280 assert_eq!(location.raw_filename(), "example.whl");
281 }
282
283 #[test]
284 fn base_str() {
285 let url = UrlString("https://example.com/path?query#fragment".into());
286 assert_eq!(url.base_str(), "https://example.com/path");
287
288 let url = UrlString("https://example.com/path#fragment".into());
289 assert_eq!(url.base_str(), "https://example.com/path");
290
291 let url = UrlString("https://example.com/path".into());
292 assert_eq!(url.base_str(), "https://example.com/path");
293 }
294
295 #[test]
296 fn without_fragment() {
297 let url = UrlString("https://example.com/path".into());
299 assert_eq!(&*url.without_fragment(), &url);
300 assert_matches!(url.without_fragment(), Cow::Borrowed(_));
301
302 let url = UrlString("https://example.com/path?query#fragment".into());
304 assert_eq!(
305 &*url.without_fragment(),
306 &UrlString("https://example.com/path?query".into())
307 );
308 assert_matches!(url.without_fragment(), Cow::Owned(_));
309 }
310}