1use std::cmp::Ordering;
8
9fn split_epoch(version: &str) -> (u64, &str) {
20 version
21 .split_once(':')
22 .and_then(|(epoch, rest)| {
23 (!rest.is_empty())
24 .then(|| epoch.parse::<u64>().ok().map(|epoch| (epoch, rest)))
25 .flatten()
26 })
27 .unwrap_or((0, version))
28}
29
30fn split_pkgrel(version: &str) -> (&str, Option<&str>) {
43 version
44 .rsplit_once('-')
45 .and_then(|(pkgver, pkgrel)| {
46 (!pkgver.is_empty()
47 && !pkgrel.is_empty()
48 && pkgrel.chars().all(|character| character.is_ascii_digit()))
49 .then_some((pkgver, Some(pkgrel)))
50 })
51 .unwrap_or((version, None))
52}
53
54fn normalize_version(version: &str) -> String {
66 let (_, without_epoch) = split_epoch(version);
67 split_pkgrel(without_epoch).0.to_string()
68}
69
70fn compare_version_components(left: &str, right: &str) -> Ordering {
83 let left_parts = left.split(['.', '-']).collect::<Vec<_>>();
84 let right_parts = right.split(['.', '-']).collect::<Vec<_>>();
85 for index in 0..left_parts.len().max(right_parts.len()) {
86 let left_segment = left_parts.get(index).copied().unwrap_or("0");
87 let right_segment = right_parts.get(index).copied().unwrap_or("0");
88 let left_end = left_segment
89 .char_indices()
90 .find(|(_, character)| !character.is_ascii_digit())
91 .map_or(left_segment.len(), |(index, _)| index);
92 let right_end = right_segment
93 .char_indices()
94 .find(|(_, character)| !character.is_ascii_digit())
95 .map_or(right_segment.len(), |(index, _)| index);
96 let (left_number, left_suffix) = (&left_segment[..left_end], &left_segment[left_end..]);
97 let (right_number, right_suffix) =
98 (&right_segment[..right_end], &right_segment[right_end..]);
99 let ordering = match (left_number.parse::<u64>(), right_number.parse::<u64>()) {
100 (Ok(left_number), Ok(right_number)) => left_number.cmp(&right_number),
101 (Ok(_), Err(_)) => Ordering::Less,
102 (Err(_), Ok(_)) => Ordering::Greater,
103 (Err(_), Err(_)) => left_segment.cmp(right_segment),
104 };
105 if ordering != Ordering::Equal {
106 return ordering;
107 }
108 match (left_suffix.is_empty(), right_suffix.is_empty()) {
109 (true, false) => return Ordering::Greater,
110 (false, true) => return Ordering::Less,
111 (false, false) if left_suffix != right_suffix => return left_suffix.cmp(right_suffix),
112 _ => {}
113 }
114 }
115 Ordering::Equal
116}
117
118#[must_use]
141pub fn compare_versions(a: &str, b: &str) -> Ordering {
142 let (a_epoch, a_without_epoch) = split_epoch(a);
143 let (b_epoch, b_without_epoch) = split_epoch(b);
144 let epoch_ordering = a_epoch.cmp(&b_epoch);
145 if epoch_ordering != Ordering::Equal {
146 return epoch_ordering;
147 }
148 let (a_pkgver, a_pkgrel) = split_pkgrel(a_without_epoch);
149 let (b_pkgver, b_pkgrel) = split_pkgrel(b_without_epoch);
150 let pkgver_ordering = compare_version_components(a_pkgver, b_pkgver);
151 if pkgver_ordering != Ordering::Equal {
152 return pkgver_ordering;
153 }
154 match (a_pkgrel, b_pkgrel) {
155 (Some(a_pkgrel), Some(b_pkgrel)) => compare_version_components(a_pkgrel, b_pkgrel),
156 _ => Ordering::Equal,
157 }
158}
159
160#[must_use]
192pub fn version_satisfies(version: &str, requirement: &str) -> bool {
193 if requirement.is_empty() {
195 return true;
196 }
197
198 let (op, req_version) = if let Some(rest) = requirement.strip_prefix(">=") {
200 (">=", rest)
201 } else if let Some(rest) = requirement.strip_prefix("<=") {
202 ("<=", rest)
203 } else if let Some(rest) = requirement.strip_prefix("=") {
204 ("=", rest)
205 } else if let Some(rest) = requirement.strip_prefix(">") {
206 (">", rest)
207 } else if let Some(rest) = requirement.strip_prefix("<") {
208 ("<", rest)
209 } else {
210 return true;
212 };
213
214 let comparison = compare_versions(version, req_version);
216
217 match op {
218 ">=" => matches!(comparison, Ordering::Equal | Ordering::Greater),
219 "<=" => matches!(comparison, Ordering::Equal | Ordering::Less),
220 "=" => comparison == Ordering::Equal,
221 ">" => comparison == Ordering::Greater,
222 "<" => comparison == Ordering::Less,
223 _ => true, }
225}
226
227#[must_use]
251pub fn extract_major_component(version: &str) -> Option<u64> {
252 let normalized = normalize_version(version);
253 let token = normalized.split(['.', '-']).next()?;
254 token.parse::<u64>().ok()
255}
256
257#[must_use]
283pub fn is_major_version_bump(old: &str, new: &str) -> bool {
284 match (extract_major_component(old), extract_major_component(new)) {
285 (Some(old_major), Some(new_major)) => new_major > old_major,
286 _ => false,
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_normalize_version() {
296 assert_eq!(normalize_version("1.2.3-1"), "1.2.3");
298 assert_eq!(normalize_version("1.2.3-42"), "1.2.3");
299 assert_eq!(normalize_version("2.0.0-1"), "2.0.0");
300
301 assert_eq!(normalize_version("1.2.3-alpha"), "1.2.3-alpha");
303 assert_eq!(normalize_version("1.2.3-beta1"), "1.2.3-beta1");
304
305 assert_eq!(normalize_version("1.2.3"), "1.2.3");
307 assert_eq!(normalize_version("2.0"), "2.0");
308 }
309
310 #[test]
311 fn test_compare_versions_basic() {
312 assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
314 assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
315 assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
316 assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
317 assert_eq!(compare_versions("1.9.9", "2.0.0"), Ordering::Less);
318 }
319
320 #[test]
321 fn test_compare_versions_missing_segments() {
322 assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Equal);
324 assert_eq!(compare_versions("1.2", "1.2.0"), Ordering::Equal);
325 assert_eq!(compare_versions("1", "1.0.0"), Ordering::Equal);
326 assert_eq!(compare_versions("1.2", "1.2.1"), Ordering::Less);
327 }
328
329 #[test]
340 fn test_compare_versions_pkgrel() {
341 assert_eq!(compare_versions("1.2.3-1", "1.2.3-2"), Ordering::Less);
342 assert_eq!(compare_versions("1.2.3-1", "1.2.3"), Ordering::Equal);
343 assert_eq!(compare_versions("1.2.3", "1.2.3-1"), Ordering::Equal);
344 assert_eq!(compare_versions("1.2.3-10", "1.2.4-1"), Ordering::Less);
345 }
346
347 #[test]
358 fn test_compare_versions_epoch() {
359 assert_eq!(compare_versions("2:1.0-1", "1:99.0-9"), Ordering::Greater);
360 assert_eq!(compare_versions("1:1.0-1", "1.0-99"), Ordering::Greater);
361 assert!(version_satisfies("1:2.0-3", ">=1:2.0-3"));
362 assert!(!version_satisfies("1:2.0-2", ">=1:2.0-3"));
363 }
364
365 #[test]
366 fn test_compare_versions_text_segments() {
367 assert_eq!(compare_versions("1.2.3", "1.2.3alpha"), Ordering::Greater);
369 assert_eq!(compare_versions("1.2.3alpha", "1.2.3"), Ordering::Less);
370 assert_eq!(compare_versions("1.2.3alpha", "1.2.3beta"), Ordering::Less);
371 assert_eq!(
372 compare_versions("1.2.3beta", "1.2.3alpha"),
373 Ordering::Greater
374 );
375 }
376
377 #[test]
378 fn test_compare_versions_mixed() {
379 assert_eq!(compare_versions("1.2.3", "1.2.4"), Ordering::Less);
381 assert_eq!(compare_versions("1.2.3alpha", "1.2.3beta"), Ordering::Less);
382 assert_eq!(compare_versions("1.2.3", "1.2.3alpha"), Ordering::Greater);
383 assert_eq!(compare_versions("1.2.3alpha", "1.2.4"), Ordering::Less);
384 }
385
386 #[test]
387 fn test_compare_versions_edge_cases() {
388 assert_eq!(compare_versions("", ""), Ordering::Equal);
390 assert_eq!(compare_versions("0", "0.0.0"), Ordering::Equal);
391 assert_eq!(compare_versions("10.0.0", "9.9.9"), Ordering::Greater);
392 assert_eq!(compare_versions("1.10.0", "1.9.9"), Ordering::Greater);
393 }
394
395 #[test]
396 fn test_version_satisfies_greater_equal() {
397 assert!(version_satisfies("2.0", ">=1.5"));
398 assert!(version_satisfies("1.5", ">=1.5"));
399 assert!(!version_satisfies("1.0", ">=1.5"));
400 assert!(version_satisfies("1.5.1", ">=1.5"));
401 assert!(version_satisfies("2.0.0", ">=1.5.0"));
402 }
403
404 #[test]
405 fn test_version_satisfies_less_equal() {
406 assert!(version_satisfies("1.0", "<=1.5"));
407 assert!(version_satisfies("1.5", "<=1.5"));
408 assert!(!version_satisfies("2.0", "<=1.5"));
409 assert!(version_satisfies("1.4.9", "<=1.5"));
410 }
411
412 #[test]
413 fn test_version_satisfies_equal() {
414 assert!(version_satisfies("1.5", "=1.5"));
415 assert!(!version_satisfies("1.6", "=1.5"));
416 assert!(!version_satisfies("1.4", "=1.5"));
417 assert!(version_satisfies("1.5.0", "=1.5"));
418 }
419
420 #[test]
421 fn test_version_satisfies_greater() {
422 assert!(version_satisfies("1.6", ">1.5"));
423 assert!(!version_satisfies("1.5", ">1.5"));
424 assert!(!version_satisfies("1.4", ">1.5"));
425 assert!(version_satisfies("2.0", ">1.5"));
426 }
427
428 #[test]
429 fn test_version_satisfies_less() {
430 assert!(version_satisfies("1.4", "<1.5"));
431 assert!(!version_satisfies("1.5", "<1.5"));
432 assert!(!version_satisfies("1.6", "<1.5"));
433 assert!(version_satisfies("1.0", "<1.5"));
434 }
435
436 #[test]
437 fn test_version_satisfies_empty() {
438 assert!(version_satisfies("2.0", ""));
440 assert!(version_satisfies("1.0", ""));
441 assert!(version_satisfies("any-version", ""));
442 }
443
444 #[test]
445 fn test_version_satisfies_no_operator() {
446 assert!(version_satisfies("2.0", "n/a"));
448 assert!(version_satisfies("1.0", "some-text"));
449 }
450
451 #[test]
463 fn test_version_satisfies_pkgrel() {
464 assert!(version_satisfies("1.2.3-1", "=1.2.3"));
465 assert!(version_satisfies("1.2.3", "<=1.2.3-1"));
466 assert!(version_satisfies("1.2.3-10", ">=1.2.3"));
467 assert!(!version_satisfies("1.2.3-5", "=1.2.3-1"));
468 }
469
470 #[test]
471 fn test_extract_major_component() {
472 assert_eq!(extract_major_component("1.2.3"), Some(1));
473 assert_eq!(extract_major_component("2.0.0"), Some(2));
474 assert_eq!(extract_major_component("10.5.2"), Some(10));
475 assert_eq!(extract_major_component("2.0.0-alpha"), Some(2));
476 assert_eq!(extract_major_component("1.2.3-1"), Some(1));
477 assert_eq!(extract_major_component("alpha"), None);
478 assert_eq!(extract_major_component(""), None);
479 }
480
481 #[test]
482 fn test_is_major_version_bump() {
483 assert!(is_major_version_bump("1.2.3", "2.0.0"));
485 assert!(is_major_version_bump("1.0.0", "2.0.0"));
486 assert!(is_major_version_bump("0.9.9", "1.0.0"));
487
488 assert!(!is_major_version_bump("1.2.3", "1.3.0"));
490 assert!(!is_major_version_bump("1.2.3", "1.2.4"));
491 assert!(!is_major_version_bump("1.0.0", "1.9.9"));
492
493 assert!(!is_major_version_bump("2.0.0", "1.9.9"));
495 assert!(!is_major_version_bump("2.0.0", "1.0.0"));
496
497 assert!(!is_major_version_bump("alpha", "1.0.0"));
499 assert!(!is_major_version_bump("1.0.0", "beta"));
500 assert!(!is_major_version_bump("", "1.0.0"));
501 }
502
503 #[test]
504 fn test_is_major_version_bump_pkgrel() {
505 assert!(is_major_version_bump("1.2.3-1", "2.0.0-1"));
507 assert!(!is_major_version_bump("1.2.3-1", "1.3.0-1"));
508 }
509}