1use std::cmp::Ordering;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Version {
18 Semver(semver::Version),
21 Numeric(Vec<u64>),
24 Deb {
31 epoch: u64,
32 upstream: String,
33 revision: String,
34 },
35 Opaque(String),
37}
38
39impl Version {
40 pub fn parse_lenient(s: &str) -> Self {
58 let stripped = s
59 .strip_prefix('v')
60 .or_else(|| s.strip_prefix('V'))
61 .unwrap_or(s);
62
63 if let Ok(v) = semver::Version::parse(stripped) {
64 return Version::Semver(v);
65 }
66
67 let parts: Vec<&str> = stripped.splitn(3, '.').collect();
69 let padded = match parts.len() {
70 1 => Some(format!("{}.0.0", parts[0])),
71 2 => Some(format!("{}.{}.0", parts[0], parts[1])),
72 _ => None,
73 };
74 if let Some(ref padded) = padded {
75 if let Ok(v) = semver::Version::parse(padded) {
76 return Version::Semver(v);
77 }
78 }
79
80 if let Some(segments) = parse_numeric(stripped) {
81 return Version::Numeric(segments);
82 }
83
84 if let Some(deb) = parse_deb(stripped) {
85 return deb;
86 }
87
88 Version::Opaque(s.to_string())
89 }
90
91 pub fn partial_cmp_lenient(&self, other: &Self) -> Option<Ordering> {
121 match (self, other) {
122 (Version::Semver(a), Version::Semver(b)) => Some(a.cmp_precedence(b)),
123 (Version::Numeric(a), Version::Numeric(b)) => Some(numeric_cmp(a, b)),
124 (Version::Semver(a), Version::Numeric(b)) => {
125 Some(numeric_cmp(&[a.major, a.minor, a.patch], b))
126 }
127 (Version::Numeric(a), Version::Semver(b)) => {
128 Some(numeric_cmp(a, &[b.major, b.minor, b.patch]))
129 }
130 (
131 Version::Deb {
132 epoch: ae,
133 upstream: au,
134 revision: arev,
135 },
136 Version::Deb {
137 epoch: be,
138 upstream: bu,
139 revision: brev,
140 },
141 ) => Some(deb_cmp((*ae, au, arev), (*be, bu, brev))),
142 _ => None,
143 }
144 }
145
146 pub fn is_downgrade(&self, new: &Self) -> bool {
166 self.partial_cmp_lenient(new) == Some(Ordering::Greater)
167 }
168}
169
170fn numeric_cmp(a: &[u64], b: &[u64]) -> Ordering {
172 let max_len = a.len().max(b.len());
173 for i in 0..max_len {
174 let x = a.get(i).copied().unwrap_or(0);
175 let y = b.get(i).copied().unwrap_or(0);
176 if x != y {
177 return x.cmp(&y);
178 }
179 }
180 Ordering::Equal
181}
182
183fn parse_numeric(stripped: &str) -> Option<Vec<u64>> {
187 let mut segments = Vec::new();
188 for part in stripped.split('.') {
189 segments.push(part.parse::<u64>().ok()?);
190 }
191 if segments.is_empty() {
192 None
193 } else {
194 Some(segments)
195 }
196}
197
198fn parse_deb(stripped: &str) -> Option<Version> {
206 let (epoch, rest) = split_epoch(stripped);
207
208 if !rest.starts_with(|c: char| c.is_ascii_digit()) {
209 return None;
210 }
211 if !rest.chars().all(is_deb_char) {
212 return None;
213 }
214
215 let (upstream, revision) = match rest.rfind('-') {
218 Some(idx) => (rest[..idx].to_string(), rest[idx + 1..].to_string()),
219 None => (rest.to_string(), String::new()),
220 };
221
222 Some(Version::Deb {
223 epoch,
224 upstream,
225 revision,
226 })
227}
228
229fn split_epoch(s: &str) -> (u64, &str) {
232 if let Some(idx) = s.find([':', '!']) {
233 let (head, tail) = s.split_at(idx);
234 if !head.is_empty() && head.bytes().all(|b| b.is_ascii_digit()) {
235 if let Ok(epoch) = head.parse::<u64>() {
236 return (epoch, &tail[1..]);
237 }
238 }
239 }
240 (0, s)
241}
242
243fn is_deb_char(c: char) -> bool {
245 c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':')
246}
247
248fn deb_cmp(a: (u64, &str, &str), b: (u64, &str, &str)) -> Ordering {
252 a.0.cmp(&b.0)
253 .then_with(|| verrevcmp(a.1, b.1))
254 .then_with(|| verrevcmp(a.2, b.2))
255}
256
257fn verrevcmp(a: &str, b: &str) -> Ordering {
266 let a = a.as_bytes();
267 let b = b.as_bytes();
268 let mut i = 0;
269 let mut j = 0;
270
271 while i < a.len() || j < b.len() {
272 while (i < a.len() && !a[i].is_ascii_digit()) || (j < b.len() && !b[j].is_ascii_digit()) {
273 let ac = a.get(i).map_or(0, |&c| deb_order(c));
274 let bc = b.get(j).map_or(0, |&c| deb_order(c));
275 if ac != bc {
276 return ac.cmp(&bc);
277 }
278 i += 1;
279 j += 1;
280 }
281
282 while i < a.len() && a[i] == b'0' {
283 i += 1;
284 }
285 while j < b.len() && b[j] == b'0' {
286 j += 1;
287 }
288
289 let mut first_diff = 0i32;
290 while i < a.len() && a[i].is_ascii_digit() && j < b.len() && b[j].is_ascii_digit() {
291 if first_diff == 0 {
292 first_diff = i32::from(a[i]) - i32::from(b[j]);
293 }
294 i += 1;
295 j += 1;
296 }
297 if i < a.len() && a[i].is_ascii_digit() {
300 return Ordering::Greater;
301 }
302 if j < b.len() && b[j].is_ascii_digit() {
303 return Ordering::Less;
304 }
305 if first_diff != 0 {
306 return first_diff.cmp(&0);
307 }
308 }
309
310 Ordering::Equal
311}
312
313fn deb_order(c: u8) -> i32 {
319 if c.is_ascii_digit() {
320 0
321 } else if c.is_ascii_alphabetic() {
322 i32::from(c)
323 } else if c == b'~' {
324 -1
325 } else {
326 i32::from(c) + 256
327 }
328}
329
330pub fn is_version_downgrade(old_ver: &str, new_ver: &str) -> bool {
335 Version::parse_lenient(old_ver).is_downgrade(&Version::parse_lenient(new_ver))
336}
337
338pub fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
344 Version::parse_lenient(a).partial_cmp_lenient(&Version::parse_lenient(b))
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn parse_standard_semver() {
353 let v = Version::parse_lenient("1.2.3");
354 assert_eq!(v, Version::Semver(semver::Version::new(1, 2, 3)));
355 }
356
357 #[test]
358 fn parse_v_prefix() {
359 assert_eq!(
360 Version::parse_lenient("v1.2.3"),
361 Version::Semver(semver::Version::new(1, 2, 3))
362 );
363 assert_eq!(
364 Version::parse_lenient("V1.2.3"),
365 Version::Semver(semver::Version::new(1, 2, 3))
366 );
367 }
368
369 #[test]
370 fn parse_two_parts() {
371 assert_eq!(
372 Version::parse_lenient("1.2"),
373 Version::Semver(semver::Version::new(1, 2, 0))
374 );
375 }
376
377 #[test]
378 fn parse_single_part() {
379 assert_eq!(
380 Version::parse_lenient("42"),
381 Version::Semver(semver::Version::new(42, 0, 0))
382 );
383 }
384
385 #[test]
386 fn parse_prerelease() {
387 let v = Version::parse_lenient("1.2.3-beta.1");
388 match v {
389 Version::Semver(sv) => {
390 assert_eq!(sv.major, 1);
391 assert_eq!(sv.minor, 2);
392 assert_eq!(sv.patch, 3);
393 assert!(!sv.pre.is_empty());
394 }
395 other => panic!("expected Semver, got {:?}", other),
396 }
397 }
398
399 #[test]
400 fn parse_build_metadata() {
401 let v = Version::parse_lenient("1.2.3+build.456");
402 match v {
403 Version::Semver(sv) => {
404 assert_eq!((sv.major, sv.minor, sv.patch), (1, 2, 3));
405 assert!(!sv.build.is_empty());
406 }
407 other => panic!("expected Semver, got {:?}", other),
408 }
409 }
410
411 #[test]
412 fn parse_prerelease_and_build() {
413 let v = Version::parse_lenient("1.0.0-alpha.1+build.789");
414 match v {
415 Version::Semver(sv) => {
416 assert_eq!(sv.major, 1);
417 assert!(!sv.pre.is_empty());
418 assert!(!sv.build.is_empty());
419 }
420 other => panic!("expected Semver, got {:?}", other),
421 }
422 }
423
424 #[test]
425 fn parse_v_prefix_two_parts() {
426 assert_eq!(
427 Version::parse_lenient("v1.2"),
428 Version::Semver(semver::Version::new(1, 2, 0))
429 );
430 }
431
432 #[test]
433 fn parse_v_prefix_single_part() {
434 assert_eq!(
435 Version::parse_lenient("v5"),
436 Version::Semver(semver::Version::new(5, 0, 0))
437 );
438 }
439
440 #[test]
441 fn parse_v_prefix_prerelease() {
442 let v = Version::parse_lenient("v2.0.0-rc.1");
443 match v {
444 Version::Semver(sv) => {
445 assert_eq!(sv.major, 2);
446 assert!(!sv.pre.is_empty());
447 }
448 other => panic!("expected Semver, got {:?}", other),
449 }
450 }
451
452 #[test]
453 fn parse_zero_version() {
454 assert_eq!(
455 Version::parse_lenient("0.0.0"),
456 Version::Semver(semver::Version::new(0, 0, 0))
457 );
458 }
459
460 #[test]
461 fn parse_large_numbers() {
462 assert_eq!(
463 Version::parse_lenient("999.888.777"),
464 Version::Semver(semver::Version::new(999, 888, 777))
465 );
466 }
467
468 #[test]
469 fn parse_single_zero() {
470 assert_eq!(
471 Version::parse_lenient("0"),
472 Version::Semver(semver::Version::new(0, 0, 0))
473 );
474 }
475
476 #[test]
477 fn parse_four_part_is_numeric() {
478 assert_eq!(
479 Version::parse_lenient("1.2.3.4"),
480 Version::Numeric(vec![1, 2, 3, 4])
481 );
482 }
483
484 #[test]
485 fn parse_date_based_is_numeric() {
486 assert_eq!(
488 Version::parse_lenient("2024.01.15"),
489 Version::Numeric(vec![2024, 1, 15])
490 );
491 }
492
493 #[test]
494 fn parse_v_prefix_four_part_is_numeric() {
495 assert_eq!(
497 Version::parse_lenient("v1.2.3.4"),
498 Version::Numeric(vec![1, 2, 3, 4])
499 );
500 assert_eq!(
501 Version::parse_lenient("V1.2.3.4"),
502 Version::Numeric(vec![1, 2, 3, 4])
503 );
504 }
505
506 #[test]
507 fn parse_v_prefix_date_based_is_numeric() {
508 assert_eq!(
509 Version::parse_lenient("v2024.01.15"),
510 Version::Numeric(vec![2024, 1, 15])
511 );
512 }
513
514 #[test]
515 fn parse_leading_zeros_is_numeric() {
516 assert_eq!(
517 Version::parse_lenient("01.02.03"),
518 Version::Numeric(vec![1, 2, 3])
519 );
520 }
521
522 #[test]
523 fn parse_non_numeric_is_opaque() {
524 assert_eq!(Version::parse_lenient("abc"), Version::Opaque("abc".into()));
525 assert_eq!(
526 Version::parse_lenient("foo.bar.baz"),
527 Version::Opaque("foo.bar.baz".into())
528 );
529 }
530
531 #[test]
532 fn parse_whitespace_is_opaque() {
533 assert!(matches!(
534 Version::parse_lenient(" 1.2.3"),
535 Version::Opaque(_)
536 ));
537 assert!(matches!(
538 Version::parse_lenient("1.2.3 "),
539 Version::Opaque(_)
540 ));
541 }
542
543 #[test]
544 fn parse_empty_string_is_opaque() {
545 assert!(matches!(Version::parse_lenient(""), Version::Opaque(_)));
546 }
547
548 #[test]
549 fn downgrade_semver() {
550 assert!(is_version_downgrade("2.0.0", "1.5.0"));
551 assert!(is_version_downgrade("1.1.0", "1.0.0"));
552 assert!(is_version_downgrade("1.0.1", "1.0.0"));
553 }
554
555 #[test]
556 fn upgrade_semver_not_flagged() {
557 assert!(!is_version_downgrade("1.0.0", "1.1.0"));
558 assert!(!is_version_downgrade("1.0.0", "2.0.0"));
559 assert!(!is_version_downgrade("1.0.0", "1.0.1"));
560 }
561
562 #[test]
563 fn equal_semver_not_flagged() {
564 assert!(!is_version_downgrade("1.0.0", "1.0.0"));
565 }
566
567 #[test]
568 fn downgrade_v_prefix() {
569 assert!(is_version_downgrade("v2.0.0", "v1.0.0"));
570 assert!(!is_version_downgrade("v1.0.0", "v2.0.0"));
571 }
572
573 #[test]
574 fn downgrade_prerelease() {
575 assert!(is_version_downgrade("1.0.0", "1.0.0-rc1"));
576 assert!(!is_version_downgrade("1.0.0-rc1", "1.0.0"));
577 }
578
579 #[test]
580 fn downgrade_build_metadata() {
581 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.2"));
585 assert!(!is_version_downgrade("1.0.0+build.2", "1.0.0+build.1"));
586 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.1"));
587 assert!(!is_version_downgrade("1.0.0+c144a98", "1.0.0+bc17664"));
590 assert!(!is_version_downgrade("1.0.0+build.10", "1.0.0+build.9"));
591 }
592
593 #[test]
594 fn downgrade_mixed_v_prefix() {
595 assert!(is_version_downgrade("v2.0.0", "1.0.0"));
596 assert!(is_version_downgrade("2.0.0", "v1.0.0"));
597 assert!(!is_version_downgrade("v1.0.0", "2.0.0"));
598 assert!(!is_version_downgrade("1.0.0", "v2.0.0"));
599 }
600
601 #[test]
602 fn downgrade_prerelease_ordering() {
603 assert!(is_version_downgrade("1.0.0-beta.1", "1.0.0-alpha.1"));
604 assert!(is_version_downgrade("1.0.0-rc.1", "1.0.0-beta.1"));
605 assert!(!is_version_downgrade("1.0.0-alpha.1", "1.0.0-beta.1"));
606 assert!(!is_version_downgrade("1.0.0-beta.1", "1.0.0-rc.1"));
607 }
608
609 #[test]
610 fn downgrade_prerelease_numeric_ordering() {
611 assert!(is_version_downgrade("1.0.0-rc.2", "1.0.0-rc.1"));
612 assert!(!is_version_downgrade("1.0.0-rc.1", "1.0.0-rc.2"));
613 }
614
615 #[test]
616 fn downgrade_equal_with_v_prefix() {
617 assert!(!is_version_downgrade("v1.0.0", "v1.0.0"));
618 }
619
620 #[test]
621 fn downgrade_padded_two_part() {
622 assert!(is_version_downgrade("1.2", "1.1"));
623 assert!(!is_version_downgrade("1.1", "1.2"));
624 assert!(!is_version_downgrade("1.2", "1.2"));
625 }
626
627 #[test]
628 fn downgrade_padded_single_part() {
629 assert!(is_version_downgrade("2", "1"));
630 assert!(!is_version_downgrade("1", "2"));
631 assert!(!is_version_downgrade("5", "5"));
632 }
633
634 #[test]
635 fn downgrade_mixed_part_counts_semver() {
636 assert!(is_version_downgrade("2.0", "1.9.9"));
637 assert!(!is_version_downgrade("1.9.9", "2.0"));
638 }
639
640 #[test]
641 fn downgrade_v_prefix_two_part() {
642 assert!(is_version_downgrade("v2.0", "v1.0"));
643 assert!(!is_version_downgrade("v1.0", "v2.0"));
644 }
645
646 #[test]
647 fn downgrade_four_part() {
648 assert!(is_version_downgrade("1.2.3.4", "1.2.3.3"));
649 assert!(!is_version_downgrade("1.2.3.3", "1.2.3.4"));
650 assert!(!is_version_downgrade("1.2.3.4", "1.2.3.4"));
651 }
652
653 #[test]
654 fn downgrade_date_based() {
655 assert!(is_version_downgrade("2024.01.15", "2023.12.01"));
656 assert!(!is_version_downgrade("2023.12.01", "2024.01.15"));
657 }
658
659 #[test]
660 fn downgrade_v_prefix_four_part() {
661 assert!(is_version_downgrade("v1.2.3.4", "v1.2.3.3"));
664 assert!(!is_version_downgrade("v1.2.3.3", "v1.2.3.4"));
665 assert!(!is_version_downgrade("v1.2.3.4", "v1.2.3.4"));
666 }
667
668 #[test]
669 fn downgrade_v_prefix_date_based() {
670 assert!(is_version_downgrade("v2024.01.15", "v2023.12.01"));
671 assert!(!is_version_downgrade("v2023.12.01", "v2024.01.15"));
672 }
673
674 #[test]
675 fn downgrade_non_numeric_not_flagged() {
676 assert!(!is_version_downgrade("abc", "def"));
677 assert!(!is_version_downgrade("foo.bar", "foo.baz"));
678 }
679
680 #[test]
681 fn downgrade_numeric_unequal_length() {
682 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
683 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
684 }
685
686 #[test]
687 fn downgrade_large_major_numeric_equal() {
688 assert!(!is_version_downgrade("2024.1.15", "2024.1.15"));
690 }
691
692 #[test]
693 fn downgrade_semver_vs_four_part() {
694 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
697 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
698 }
699
700 #[test]
701 fn downgrade_v_prefix_vs_four_part() {
702 assert!(!is_version_downgrade("v1.2.3", "1.2.3.4"));
704 assert!(is_version_downgrade("1.2.3.4", "v1.2.3"));
705 }
706
707 #[test]
708 fn downgrade_empty_strings() {
709 assert!(!is_version_downgrade("", "1.0.0"));
710 assert!(!is_version_downgrade("1.0.0", ""));
711 assert!(!is_version_downgrade("", ""));
712 }
713
714 #[test]
717 fn parse_epoch_is_deb() {
718 assert!(matches!(
720 Version::parse_lenient("2:1.0"),
721 Version::Deb { .. }
722 ));
723 assert!(matches!(
724 Version::parse_lenient("1:9.0"),
725 Version::Deb { .. }
726 ));
727 }
728
729 #[test]
730 fn parse_revision_is_deb() {
731 assert!(matches!(
733 Version::parse_lenient("5.1-3"),
734 Version::Deb { .. }
735 ));
736 }
737
738 #[test]
739 fn parse_deb_fields() {
740 match Version::parse_lenient("2:1.2.3-4") {
741 Version::Deb {
742 epoch,
743 upstream,
744 revision,
745 } => {
746 assert_eq!(epoch, 2);
747 assert_eq!(upstream, "1.2.3");
748 assert_eq!(revision, "4");
749 }
750 other => panic!("expected Deb, got {:?}", other),
751 }
752 }
753
754 #[test]
755 fn parse_deb_revision_splits_at_last_hyphen() {
756 match Version::parse_lenient("1.2-2-1") {
759 Version::Deb {
760 epoch,
761 upstream,
762 revision,
763 } => {
764 assert_eq!(epoch, 0);
765 assert_eq!(upstream, "1.2-2");
766 assert_eq!(revision, "1");
767 }
768 other => panic!("expected Deb, got {:?}", other),
769 }
770 }
771
772 #[test]
773 fn parse_pep440_epoch_is_deb() {
774 match Version::parse_lenient("1!2.0") {
775 Version::Deb {
776 epoch,
777 upstream,
778 revision,
779 } => {
780 assert_eq!(epoch, 1);
781 assert_eq!(upstream, "2.0");
782 assert_eq!(revision, "");
783 }
784 other => panic!("expected Deb, got {:?}", other),
785 }
786 }
787
788 #[test]
789 fn parse_tilde_prerelease_is_deb() {
790 assert!(matches!(
792 Version::parse_lenient("1.0.0~rc1"),
793 Version::Deb { .. }
794 ));
795 }
796
797 #[test]
798 fn parse_codename_stays_opaque() {
799 assert!(matches!(
801 Version::parse_lenient("focal-1"),
802 Version::Opaque(_)
803 ));
804 assert!(matches!(
805 Version::parse_lenient("stable"),
806 Version::Opaque(_)
807 ));
808 assert!(matches!(
810 Version::parse_lenient("1:stable"),
811 Version::Opaque(_)
812 ));
813 }
814
815 #[test]
818 fn downgrade_epoch() {
819 assert!(is_version_downgrade("2:1.0", "1:9.0"));
821 assert!(!is_version_downgrade("1:9.0", "2:1.0"));
822 assert!(!is_version_downgrade("1:1.0", "2:0.1"));
824 assert!(is_version_downgrade("2:0.1", "1:1.0"));
825 }
826
827 #[test]
828 fn downgrade_epoch_equal_upstream() {
829 assert!(is_version_downgrade("1:2.0", "1:1.0"));
830 assert!(!is_version_downgrade("1:1.0", "1:2.0"));
831 assert!(!is_version_downgrade("1:1.0", "1:1.0"));
832 }
833
834 #[test]
835 fn downgrade_implicit_epoch_zero() {
836 assert!(!is_version_downgrade("5.1-1", "1:0.1-1"));
839 assert!(is_version_downgrade("1:0.1-1", "0:0.1-1"));
840 }
841
842 #[test]
843 fn downgrade_revision() {
844 assert!(is_version_downgrade("5.1-3", "5.1-2"));
845 assert!(!is_version_downgrade("5.1-2", "5.1-3"));
846 assert!(!is_version_downgrade("5.1-2", "5.1-2"));
847 }
848
849 #[test]
850 fn downgrade_upstream_trumps_revision() {
851 assert!(is_version_downgrade("1:5.2-1", "1:5.1-1"));
853 assert!(!is_version_downgrade("1:5.1-9", "1:5.2-1"));
855 }
856
857 #[test]
858 fn downgrade_absent_revision_equals_zero() {
859 assert!(is_version_downgrade("1:2.0-1", "1:2.0"));
862 assert!(!is_version_downgrade("1:2.0", "1:2.0-1"));
863 }
864
865 #[test]
866 fn downgrade_rpm_release_with_epoch() {
867 assert!(is_version_downgrade("1:1.2.3-2.el8", "1:1.2.3-1.el8"));
870 assert!(!is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-2.el8"));
871 assert!(is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-1.el7"));
873 assert!(!is_version_downgrade("1:1.2.3-1.el7", "1:1.2.3-1.el8"));
874 }
875
876 #[test]
877 fn downgrade_deb_numeric_not_lexical() {
878 assert!(is_version_downgrade("1.10-1", "1.9-1"));
880 assert!(!is_version_downgrade("1.9-1", "1.10-1"));
881 }
882
883 #[test]
884 fn downgrade_deb_tilde_prerelease() {
885 assert!(is_version_downgrade("1.0.0~rc2", "1.0.0~rc1"));
887 assert!(!is_version_downgrade("1.0.0~rc1", "1.0.0~rc2"));
888 assert!(is_version_downgrade("1:1.0~rc1", "1:1.0~beta1"));
889 }
890
891 #[test]
892 fn downgrade_real_world_deb() {
893 assert!(is_version_downgrade(
895 "1:1.1.1f-1ubuntu2.16",
896 "1:1.1.1f-1ubuntu2.15"
897 ));
898 assert!(!is_version_downgrade(
899 "1:1.1.1f-1ubuntu2.15",
900 "1:1.1.1f-1ubuntu2.16"
901 ));
902 }
903
904 #[test]
905 fn downgrade_deb_opaque_not_flagged() {
906 assert!(!is_version_downgrade("focal", "bionic"));
908 assert!(!is_version_downgrade("1:stable", "1:oldstable"));
909 }
910
911 #[test]
912 fn downgrade_deb_vs_semver_not_flagged() {
913 assert!(!is_version_downgrade("2:1.0", "1.0.0"));
915 assert!(!is_version_downgrade("1.0.0", "2:1.0"));
916 }
917
918 #[test]
919 fn deb_canonical_ordering_vectors() {
920 use Ordering::{Equal, Greater, Less};
921
922 let cases = [
931 ("1:1.0~~", "1:1.0~~a", Less),
934 ("1:1.0~~a", "1:1.0~", Less),
935 ("1:1.0~", "1:1.0", Less),
936 ("1:1.0", "1:1.0a", Less),
937 ("1:1.0~rc1", "1:1.0", Less),
940 ("1:1.0~rc1", "1:1.0~rc2", Less),
941 ("1:1.10", "1:1.9", Greater),
943 ("1:1.0", "1:1.00", Equal),
945 ("1:1.01", "1:1.1", Equal),
946 ("1:1.a", "1:1.1", Greater),
948 ("1:1.0a", "1:1.01", Less),
950 ("2:0.1", "1:9.9", Greater),
952 ("1:5.2-1", "1:5.1-9", Greater),
954 ("1:2.0", "1:2.0-0", Equal),
956 ("1:2.0-10", "1:2.0-9", Greater),
958 ];
959
960 for (a, b, expected) in cases {
961 assert!(
964 matches!(Version::parse_lenient(a), Version::Deb { .. }),
965 "{a} no longer parses as Deb"
966 );
967 assert!(
968 matches!(Version::parse_lenient(b), Version::Deb { .. }),
969 "{b} no longer parses as Deb"
970 );
971 match expected {
972 Less => {
974 assert!(is_version_downgrade(b, a), "expected {a} < {b}");
975 assert!(!is_version_downgrade(a, b), "expected {a} < {b}");
976 }
977 Greater => {
979 assert!(is_version_downgrade(a, b), "expected {a} > {b}");
980 assert!(!is_version_downgrade(b, a), "expected {a} > {b}");
981 }
982 Equal => {
984 assert!(!is_version_downgrade(a, b), "expected {a} == {b}");
985 assert!(!is_version_downgrade(b, a), "expected {a} == {b}");
986 }
987 }
988 }
989 }
990
991 #[test]
992 fn compare_orders_comparable_variant_pairs() {
993 use Ordering::{Equal, Greater, Less};
994
995 for (a, b, expected) in [
996 ("2.0.0", "1.5.0", Greater),
997 ("1.0.0", "1.0.0", Equal),
998 ("1.2.3.4", "1.2.3.3", Greater),
999 ("1.2.3", "1.2.3.4", Less),
1000 ("2:1.0-3", "1:9.0-1", Greater),
1001 ("5.1-3", "5.1-3", Equal),
1002 ] {
1003 assert_eq!(compare_versions(a, b), Some(expected), "{a} vs {b}");
1004 assert_eq!(
1005 compare_versions(b, a),
1006 Some(expected.reverse()),
1007 "{b} vs {a}"
1008 );
1009 }
1010 }
1011
1012 #[test]
1013 fn compare_leaves_opaque_and_mixed_variants_unordered() {
1014 for (a, b) in [
1015 ("deadbeef", "1.0.0"),
1016 ("deadbeef", "cafebabe"),
1017 ("deadbeef", "deadbeef"),
1018 ("2:1.0-3", "1.0.0"),
1019 ("5.1-3", "5.1.0.0"),
1020 ] {
1021 assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
1022 assert_eq!(compare_versions(b, a), None, "{b} vs {a}");
1023 }
1024 }
1025
1026 #[test]
1027 fn downgrade_agrees_with_compare() {
1028 use Ordering::Greater;
1029
1030 for (a, b) in [
1031 ("2.0.0", "1.5.0"),
1032 ("1.0.0", "2.0.0"),
1033 ("1.0.0", "1.0.0"),
1034 ("2024.01.15", "2024.01.14"),
1035 ("2:1.0-3", "1:9.0-1"),
1036 ("1.0.0+build.10", "1.0.0+build.9"),
1037 ("deadbeef", "1.0.0"),
1038 ] {
1039 assert_eq!(
1040 is_version_downgrade(a, b),
1041 compare_versions(a, b) == Some(Greater),
1042 "{a} -> {b}"
1043 );
1044 }
1045 }
1046}