1use crate::schema::FileKind;
2
3pub fn validate_version(version: &str, kind: FileKind) -> Result<(), String> {
5 match kind {
6 FileKind::Any => Ok(()),
7 FileKind::Simple => validate_simple(version),
8 FileKind::Python => validate_python(version),
9 FileKind::Semver => validate_semver(version),
10 }
11}
12
13fn validate_simple(version: &str) -> Result<(), String> {
15 let parts: Vec<&str> = version.split('.').collect();
16 if parts.len() != 3 {
17 return Err(format!(
18 "Invalid simple version: {version}. Expected format: major.minor.patch"
19 ));
20 }
21 for (i, part) in parts.iter().enumerate() {
22 let name = ["major", "minor", "patch"][i];
23 if part.parse::<u32>().is_err() {
24 return Err(format!("Invalid {name} version component: {part}"));
25 }
26 }
27 Ok(())
28}
29
30fn validate_python(version: &str) -> Result<(), String> {
45 if version.is_empty() {
46 return Err("Version string cannot be empty".to_string());
47 }
48
49 let version = version.to_lowercase();
50
51 let version = if let Some(pos) = version.find('!') {
53 let epoch = &version[..pos];
54 if !epoch.chars().all(|c| c.is_ascii_digit()) {
55 return Err(format!("Invalid epoch: {epoch}"));
56 }
57 &version[pos + 1..]
58 } else {
59 version.as_str()
60 };
61
62 let version = if let Some(pos) = version.find('+') {
64 let local = &version[pos + 1..];
65 if !is_valid_local(local) {
66 return Err(format!("Invalid local version: {local}"));
67 }
68 &version[..pos]
69 } else {
70 version
71 };
72
73 parse_main_version(version)
75}
76
77fn is_valid_local(local: &str) -> bool {
78 if local.is_empty() {
79 return false;
80 }
81 local
83 .split('.')
84 .all(|segment| !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric()))
85}
86
87fn parse_main_version(version: &str) -> Result<(), String> {
88 if version.is_empty() {
89 return Err("Version string cannot be empty".to_string());
90 }
91
92 let (release_part, remainder) = split_at_prerelease(version);
94
95 if !is_valid_release(release_part) {
97 return Err(format!("Invalid release version: {release_part}"));
98 }
99
100 if remainder.is_empty() {
101 return Ok(());
102 }
103
104 parse_suffixes(remainder)
106}
107
108fn split_at_prerelease(version: &str) -> (&str, &str) {
109 let markers = ["alpha", "beta", "preview", "rc", "a", "b", "c"];
111
112 let mut earliest_pos = None;
113
114 for marker in markers {
115 if let Some(pos) = version.find(marker) {
116 let before = &version[..pos];
118 if before.is_empty() || before.ends_with('.') || before.chars().last().unwrap().is_ascii_digit() {
119 match earliest_pos {
120 None => earliest_pos = Some(pos),
121 Some(current) if pos < current => earliest_pos = Some(pos),
122 _ => {}
123 }
124 }
125 }
126 }
127
128 if let Some(pos) = version.find(".post") {
130 match earliest_pos {
131 None => earliest_pos = Some(pos),
132 Some(current) if pos < current => earliest_pos = Some(pos),
133 _ => {}
134 }
135 }
136 if let Some(pos) = version.find(".dev") {
137 match earliest_pos {
138 None => earliest_pos = Some(pos),
139 Some(current) if pos < current => earliest_pos = Some(pos),
140 _ => {}
141 }
142 }
143
144 match earliest_pos {
145 Some(pos) => (&version[..pos], &version[pos..]),
146 None => (version, ""),
147 }
148}
149
150fn is_valid_release(release: &str) -> bool {
151 if release.is_empty() {
152 return false;
153 }
154
155 release.split('.').all(|part| {
157 !part.is_empty() && part.chars().all(|c| c.is_ascii_digit())
158 })
159}
160
161fn validate_semver(version: &str) -> Result<(), String> {
170 if version.is_empty() {
171 return Err("Version string cannot be empty".to_string());
172 }
173
174 let version = if let Some(pos) = version.find('+') {
176 let build = &version[pos + 1..];
177 if build.is_empty() || !is_valid_semver_identifier(build) {
178 return Err(format!("Invalid build metadata: {build}"));
179 }
180 &version[..pos]
181 } else {
182 version
183 };
184
185 let (release, prerelease) = if let Some(pos) = version.find('-') {
187 (&version[..pos], Some(&version[pos + 1..]))
188 } else {
189 (version, None)
190 };
191
192 let parts: Vec<&str> = release.split('.').collect();
194 if parts.len() != 3 {
195 return Err(format!(
196 "Invalid semver: {release}. Expected format: major.minor.patch"
197 ));
198 }
199 for (i, part) in parts.iter().enumerate() {
200 let name = ["major", "minor", "patch"][i];
201 if part.parse::<u32>().is_err() {
202 return Err(format!("Invalid {name} version: {part}"));
203 }
204 }
205
206 if let Some(pre) = prerelease
208 && (pre.is_empty() || !is_valid_semver_identifier(pre))
209 {
210 return Err(format!("Invalid prerelease: {pre}"));
211 }
212
213 Ok(())
214}
215
216fn is_valid_semver_identifier(id: &str) -> bool {
217 id.split('.').all(|part| {
219 !part.is_empty() && part.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
220 })
221}
222
223fn parse_suffixes(suffix: &str) -> Result<(), String> {
224 if suffix.is_empty() {
225 return Ok(());
226 }
227
228 let suffix = suffix.to_lowercase();
229 let mut remaining = suffix.as_str();
230
231 let pre_markers = [
233 ("alpha", "a"),
234 ("beta", "b"),
235 ("preview", "rc"),
236 ("rc", "rc"),
237 ("a", "a"),
238 ("b", "b"),
239 ("c", "rc"),
240 ];
241
242 for (marker, _normalized) in pre_markers {
243 if remaining.starts_with(marker) {
244 remaining = &remaining[marker.len()..];
245 let num_end = remaining
247 .chars()
248 .take_while(|c| c.is_ascii_digit())
249 .count();
250 remaining = &remaining[num_end..];
251 break;
252 }
253 }
254
255 if remaining.starts_with(".post") || remaining.starts_with("post") {
257 remaining = remaining.trim_start_matches('.').trim_start_matches("post");
258 let num_end = remaining
259 .chars()
260 .take_while(|c| c.is_ascii_digit())
261 .count();
262 remaining = &remaining[num_end..];
263 }
264
265 if remaining.starts_with(".dev") || remaining.starts_with("dev") {
267 remaining = remaining.trim_start_matches('.').trim_start_matches("dev");
268 let num_end = remaining
269 .chars()
270 .take_while(|c| c.is_ascii_digit())
271 .count();
272 remaining = &remaining[num_end..];
273 }
274
275 if remaining.is_empty() {
276 Ok(())
277 } else {
278 Err(format!("Invalid version suffix: {remaining}"))
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_valid_python_versions() {
288 assert!(validate_python("1").is_ok());
289 assert!(validate_python("1.0").is_ok());
290 assert!(validate_python("1.0.0").is_ok());
291 assert!(validate_python("1.2.3").is_ok());
292 assert!(validate_python("1.2.3.4").is_ok());
293 assert!(validate_python("0.0.1").is_ok());
294 assert!(validate_python("10.20.30").is_ok());
295 }
296
297 #[test]
298 fn test_valid_python_prerelease_versions() {
299 assert!(validate_python("1.0a1").is_ok());
300 assert!(validate_python("1.0b2").is_ok());
301 assert!(validate_python("1.0rc1").is_ok());
302 assert!(validate_python("1.0alpha1").is_ok());
303 assert!(validate_python("1.0beta2").is_ok());
304 assert!(validate_python("1.0.0a1").is_ok());
305 assert!(validate_python("1.0c1").is_ok());
306 assert!(validate_python("1.0preview1").is_ok());
307 }
308
309 #[test]
310 fn test_valid_python_post_versions() {
311 assert!(validate_python("1.0.post1").is_ok());
312 assert!(validate_python("1.0.0.post1").is_ok());
313 assert!(validate_python("1.0a1.post1").is_ok());
314 }
315
316 #[test]
317 fn test_valid_python_dev_versions() {
318 assert!(validate_python("1.0.dev1").is_ok());
319 assert!(validate_python("1.0.0.dev1").is_ok());
320 assert!(validate_python("1.0a1.dev1").is_ok());
321 assert!(validate_python("1.0.post1.dev1").is_ok());
322 }
323
324 #[test]
325 fn test_valid_python_epoch_versions() {
326 assert!(validate_python("1!1.0").is_ok());
327 assert!(validate_python("2!1.0.0").is_ok());
328 }
329
330 #[test]
331 fn test_valid_python_local_versions() {
332 assert!(validate_python("1.0+local").is_ok());
333 assert!(validate_python("1.0+local.version").is_ok());
334 assert!(validate_python("1.0+abc123").is_ok());
335 assert!(validate_python("1.0a1+local").is_ok());
336 }
337
338 #[test]
339 fn test_invalid_python_versions() {
340 assert!(validate_python("").is_err());
341 assert!(validate_python("a.b.c").is_err());
342 assert!(validate_python("1.0+").is_err());
343 assert!(validate_python("1.0.").is_err());
344 assert!(validate_python(".1.0").is_err());
345 assert!(validate_python("1..0").is_err());
346 }
347
348 #[test]
349 fn test_valid_semver_versions() {
350 assert!(validate_semver("1.0.0").is_ok());
351 assert!(validate_semver("1.2.3").is_ok());
352 assert!(validate_semver("0.0.1").is_ok());
353 assert!(validate_semver("10.20.30").is_ok());
354 }
355
356 #[test]
357 fn test_valid_semver_prerelease_versions() {
358 assert!(validate_semver("1.0.0-alpha.1").is_ok());
359 assert!(validate_semver("1.0.0-beta.2").is_ok());
360 assert!(validate_semver("1.0.0-rc.1").is_ok());
361 assert!(validate_semver("1.0.0-0").is_ok());
362 assert!(validate_semver("1.0.0-alpha").is_ok());
363 }
364
365 #[test]
366 fn test_valid_semver_build_versions() {
367 assert!(validate_semver("1.0.0+build").is_ok());
368 assert!(validate_semver("1.0.0+build.123").is_ok());
369 assert!(validate_semver("1.0.0-alpha.1+build").is_ok());
370 }
371
372 #[test]
373 fn test_invalid_semver_versions() {
374 assert!(validate_semver("").is_err());
375 assert!(validate_semver("1.0").is_err()); assert!(validate_semver("1").is_err());
377 assert!(validate_semver("1.0.0-").is_err());
378 assert!(validate_semver("1.0.0+").is_err());
379 }
380}