uv_distribution_filename/
source_dist.rs1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3
4use crate::SourceDistExtension;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7use uv_normalize::{InvalidNameError, PackageName};
8use uv_pep440::{Version, VersionParseError};
9
10#[derive(
13 Clone,
14 Debug,
15 PartialEq,
16 Eq,
17 PartialOrd,
18 Ord,
19 Serialize,
20 Deserialize,
21 rkyv::Archive,
22 rkyv::Deserialize,
23 rkyv::Serialize,
24)]
25#[rkyv(derive(Debug))]
26pub struct SourceDistFilename {
27 pub name: PackageName,
28 pub version: Version,
29 pub extension: SourceDistExtension,
30}
31
32impl SourceDistFilename {
33 pub fn parse(
36 filename: &str,
37 extension: SourceDistExtension,
38 package_name: &PackageName,
39 ) -> Result<Self, SourceDistFilenameError> {
40 if filename.len() <= extension.name().len() + 1 {
42 return Err(SourceDistFilenameError {
43 filename: filename.to_string(),
44 kind: SourceDistFilenameErrorKind::Extension,
45 });
46 }
47
48 let stem = &filename[..(filename.len() - (extension.name().len() + 1))];
49
50 if stem.len() <= package_name.as_ref().len() + "-".len() {
51 return Err(SourceDistFilenameError {
52 filename: filename.to_string(),
53 kind: SourceDistFilenameErrorKind::Filename(package_name.clone()),
54 });
55 }
56 let actual_package_name = PackageName::from_str(&stem[..package_name.as_ref().len()])
57 .map_err(|err| SourceDistFilenameError {
58 filename: filename.to_string(),
59 kind: SourceDistFilenameErrorKind::PackageName(err),
60 })?;
61 if actual_package_name != *package_name {
62 return Err(SourceDistFilenameError {
63 filename: filename.to_string(),
64 kind: SourceDistFilenameErrorKind::Filename(package_name.clone()),
65 });
66 }
67
68 let version =
70 Version::from_str(&stem[package_name.as_ref().len() + "-".len()..]).map_err(|err| {
71 SourceDistFilenameError {
72 filename: filename.to_string(),
73 kind: SourceDistFilenameErrorKind::Version(err),
74 }
75 })?;
76
77 Ok(Self {
78 name: package_name.clone(),
79 version,
80 extension,
81 })
82 }
83
84 pub fn parsed_normalized_filename(filename: &str) -> Result<Self, SourceDistFilenameError> {
89 let Ok(extension) = SourceDistExtension::from_path(filename) else {
90 return Err(SourceDistFilenameError {
91 filename: filename.to_string(),
92 kind: SourceDistFilenameErrorKind::Extension,
93 });
94 };
95
96 if filename.len() <= extension.name().len() + 1 {
98 return Err(SourceDistFilenameError {
99 filename: filename.to_string(),
100 kind: SourceDistFilenameErrorKind::Extension,
101 });
102 }
103
104 let stem = &filename[..(filename.len() - (extension.name().len() + 1))];
105
106 let Some((package_name, version)) = stem.rsplit_once('-') else {
107 return Err(SourceDistFilenameError {
108 filename: filename.to_string(),
109 kind: SourceDistFilenameErrorKind::Minus,
110 });
111 };
112 let package_name =
113 PackageName::from_str(package_name).map_err(|err| SourceDistFilenameError {
114 filename: filename.to_string(),
115 kind: SourceDistFilenameErrorKind::PackageName(err),
116 })?;
117
118 let version = Version::from_str(version).map_err(|err| SourceDistFilenameError {
120 filename: filename.to_string(),
121 kind: SourceDistFilenameErrorKind::Version(err),
122 })?;
123
124 Ok(Self {
125 name: package_name,
126 version,
127 extension,
128 })
129 }
130}
131
132impl Display for SourceDistFilename {
133 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134 write!(
135 f,
136 "{}-{}.{}",
137 self.name.as_dist_info_name(),
138 self.version,
139 self.extension
140 )
141 }
142}
143
144#[derive(Error, Debug, Clone)]
145pub struct SourceDistFilenameError {
146 filename: String,
147 kind: SourceDistFilenameErrorKind,
148}
149
150impl Display for SourceDistFilenameError {
151 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
152 write!(
153 f,
154 "Failed to parse source distribution filename {}: {}",
155 self.filename, self.kind
156 )
157 }
158}
159
160#[derive(Error, Debug, Clone)]
161enum SourceDistFilenameErrorKind {
162 #[error("Name doesn't start with package name {0}")]
163 Filename(PackageName),
164 #[error("File extension is invalid")]
165 Extension,
166 #[error("Version section is invalid")]
167 Version(#[from] VersionParseError),
168 #[error(transparent)]
169 PackageName(#[from] InvalidNameError),
170 #[error("Missing name-version separator")]
171 Minus,
172}
173
174#[cfg(test)]
175mod tests {
176 use std::str::FromStr;
177
178 use uv_normalize::PackageName;
179
180 use crate::{SourceDistExtension, SourceDistFilename};
181
182 #[test]
187 fn roundtrip() {
188 for normalized in [
189 "foo_lib-1.2.3.zip",
190 "foo_lib-1.2.3a3.zip",
191 "foo_lib-1.2.3.tar.gz",
192 "foo_lib-1.2.3.tar.bz2",
193 "foo_lib-1.2.3.tar.zst",
194 "foo_lib-1.2.3.tar.xz",
195 "foo_lib-1.2.3.tar.lz",
196 "foo_lib-1.2.3.tar.lzma",
197 "foo_lib-1.2.3.tgz",
198 "foo_lib-1.2.3.tbz",
199 "foo_lib-1.2.3.tlz",
200 "foo_lib-1.2.3.txz",
201 ] {
202 let ext = SourceDistExtension::from_path(normalized).unwrap();
203 assert_eq!(
204 SourceDistFilename::parse(
205 normalized,
206 ext,
207 &PackageName::from_str("foo_lib").unwrap()
208 )
209 .unwrap()
210 .to_string(),
211 normalized
212 );
213 }
214 }
215
216 #[test]
217 fn errors() {
218 for invalid in ["b-1.2.3.zip", "a-1.2.3-gamma.3.zip"] {
219 let ext = SourceDistExtension::from_path(invalid).unwrap();
220 assert!(
221 SourceDistFilename::parse(invalid, ext, &PackageName::from_str("a").unwrap())
222 .is_err()
223 );
224 }
225 }
226
227 #[test]
228 fn name_too_long() {
229 assert!(
230 SourceDistFilename::parse(
231 "foo.zip",
232 SourceDistExtension::Zip,
233 &PackageName::from_str("foo-lib").unwrap()
234 )
235 .is_err()
236 );
237 }
238}