1use std::cmp::Ordering;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum Version {
30 Semver(semver::Version),
33 Numeric(Vec<u64>),
36 Pep440(Pep440),
39 Deb {
44 epoch: u64,
45 upstream: String,
46 revision: String,
47 },
48 Rpm {
56 epoch: u64,
57 version: String,
58 release: String,
59 },
60 Maven(String),
66 Opaque(String),
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct Pep440 {
77 pub epoch: u64,
78 pub release: Vec<u64>,
79 pub pre: Option<(PreRelease, u64)>,
80 pub post: Option<u64>,
81 pub dev: Option<u64>,
82 pub local: Vec<LocalSegment>,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
87pub enum PreRelease {
88 Alpha,
89 Beta,
90 Rc,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
96pub enum LocalSegment {
97 Str(String),
98 Num(u64),
99}
100
101impl Version {
102 pub fn parse_lenient(s: &str) -> Self {
126 let stripped = strip_v_prefix(s);
127
128 if let Ok(v) = semver::Version::parse(stripped) {
129 return Version::Semver(v);
130 }
131
132 let parts: Vec<&str> = stripped.splitn(3, '.').collect();
134 let padded = match parts.len() {
135 1 => Some(format!("{}.0.0", parts[0])),
136 2 => Some(format!("{}.{}.0", parts[0], parts[1])),
137 _ => None,
138 };
139 if let Some(ref padded) = padded {
140 if let Ok(v) = semver::Version::parse(padded) {
141 return Version::Semver(v);
142 }
143 }
144
145 if let Some(segments) = parse_numeric(stripped) {
146 return Version::Numeric(segments);
147 }
148
149 if let Some(pep) = parse_pep440(stripped) {
150 if pep.epoch > 0 || pep.pre.is_some() || pep.post.is_some() || pep.dev.is_some() {
152 return Version::Pep440(pep);
153 }
154 }
155
156 if let Some(deb) = parse_deb(stripped) {
157 return deb;
158 }
159
160 Version::Opaque(s.to_string())
161 }
162
163 pub fn parse_for_ecosystem(ecosystem: Option<&str>, s: &str) -> Self {
204 match Scheme::for_ecosystem(ecosystem) {
205 Scheme::Infer => Version::parse_lenient(s),
206 Scheme::Deb => parse_deb(s)
207 .or_else(|| parse_deb(strip_v_prefix(s)))
208 .unwrap_or_else(|| Version::Opaque(s.to_string())),
209 Scheme::Rpm => parse_rpm(s)
210 .or_else(|| parse_rpm(strip_v_prefix(s)))
211 .unwrap_or_else(|| Version::Opaque(s.to_string())),
212 Scheme::Maven => parse_maven(s)
213 .or_else(|| parse_maven(strip_v_prefix(s)))
214 .unwrap_or_else(|| Version::Opaque(s.to_string())),
215 }
216 }
217
218 pub fn partial_cmp_lenient(&self, other: &Self) -> Option<Ordering> {
262 match (self, other) {
263 (Version::Semver(a), Version::Semver(b)) => Some(a.cmp_precedence(b)),
264 (Version::Numeric(a), Version::Numeric(b)) => Some(numeric_cmp(a, b)),
265 (Version::Semver(a), Version::Numeric(b)) => {
266 Some(numeric_cmp(&[a.major, a.minor, a.patch], b))
267 }
268 (Version::Numeric(a), Version::Semver(b)) => {
269 Some(numeric_cmp(a, &[b.major, b.minor, b.patch]))
270 }
271 (
272 Version::Deb {
273 epoch: ae,
274 upstream: au,
275 revision: arev,
276 },
277 Version::Deb {
278 epoch: be,
279 upstream: bu,
280 revision: brev,
281 },
282 ) => Some(deb_cmp((*ae, au, arev), (*be, bu, brev))),
283 (
284 Version::Rpm {
285 epoch: ae,
286 version: av,
287 release: arel,
288 },
289 Version::Rpm {
290 epoch: be,
291 version: bv,
292 release: brel,
293 },
294 ) => Some(rpm_cmp((*ae, av, arel), (*be, bv, brel))),
295 (Version::Maven(a), Version::Maven(b)) => maven_cmp(a, b),
296 (Version::Pep440(_), _) | (_, Version::Pep440(_)) => {
297 Some(pep440_cmp(&as_pep440(self)?, &as_pep440(other)?))
298 }
299 _ => None,
300 }
301 }
302
303 pub fn is_downgrade(&self, new: &Self) -> bool {
323 self.partial_cmp_lenient(new) == Some(Ordering::Greater)
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
329enum Scheme {
330 Infer,
331 Deb,
332 Rpm,
333 Maven,
334}
335
336impl Scheme {
337 fn for_ecosystem(ecosystem: Option<&str>) -> Self {
338 match ecosystem {
339 Some(e) if e.eq_ignore_ascii_case("deb") => Scheme::Deb,
340 Some(e) if e.eq_ignore_ascii_case("rpm") => Scheme::Rpm,
341 Some(e) if e.eq_ignore_ascii_case("maven") => Scheme::Maven,
342 _ => Scheme::Infer,
343 }
344 }
345}
346
347fn numeric_cmp(a: &[u64], b: &[u64]) -> Ordering {
349 let max_len = a.len().max(b.len());
350 for i in 0..max_len {
351 let x = a.get(i).copied().unwrap_or(0);
352 let y = b.get(i).copied().unwrap_or(0);
353 if x != y {
354 return x.cmp(&y);
355 }
356 }
357 Ordering::Equal
358}
359
360fn parse_numeric(stripped: &str) -> Option<Vec<u64>> {
364 let mut segments = Vec::new();
365 for part in stripped.split('.') {
366 segments.push(part.parse::<u64>().ok()?);
367 }
368 if segments.is_empty() {
369 None
370 } else {
371 Some(segments)
372 }
373}
374
375const PRE_ALIASES: [(&str, PreRelease); 8] = [
378 ("alpha", PreRelease::Alpha),
379 ("beta", PreRelease::Beta),
380 ("preview", PreRelease::Rc),
381 ("pre", PreRelease::Rc),
382 ("rc", PreRelease::Rc),
383 ("a", PreRelease::Alpha),
384 ("b", PreRelease::Beta),
385 ("c", PreRelease::Rc),
386];
387
388const POST_ALIASES: [(&str, ()); 3] = [("post", ()), ("rev", ()), ("r", ())];
390
391const DEV_ALIASES: [(&str, ()); 1] = [("dev", ())];
392
393fn parse_pep440(s: &str) -> Option<Pep440> {
399 let lower = s.to_ascii_lowercase();
400
401 let (head, local) = match lower.split_once('+') {
402 Some((head, tail)) => (head, parse_local(tail)?),
403 None => (lower.as_str(), Vec::new()),
404 };
405 let (epoch, mut rest) = match head.split_once('!') {
406 Some((epoch, tail)) => (epoch.parse::<u64>().ok()?, tail),
407 None => (0, head),
408 };
409
410 let mut release = Vec::new();
411 loop {
412 let end = rest
413 .find(|c: char| !c.is_ascii_digit())
414 .unwrap_or(rest.len());
415 if end == 0 {
416 return None;
417 }
418 release.push(rest[..end].parse::<u64>().ok()?);
419 rest = &rest[end..];
420 match rest.strip_prefix('.') {
421 Some(next) if next.starts_with(|c: char| c.is_ascii_digit()) => rest = next,
423 _ => break,
424 }
425 }
426
427 let (pre, rest) = match take_segment(rest, &PRE_ALIASES) {
428 Some((kind, n, rest)) => (Some((kind, n)), rest),
429 None => (None, rest),
430 };
431 let (post, rest) = match take_segment(rest, &POST_ALIASES) {
432 Some((_, n, rest)) => (Some(n), rest),
433 None => (None, rest),
434 };
435 let (dev, rest) = match take_segment(rest, &DEV_ALIASES) {
436 Some((_, n, rest)) => (Some(n), rest),
437 None => (None, rest),
438 };
439 if !rest.is_empty() {
440 return None;
441 }
442
443 Some(Pep440 {
444 epoch,
445 release,
446 pre,
447 post,
448 dev,
449 local,
450 })
451}
452
453fn take_segment<'a, T: Copy>(s: &'a str, aliases: &[(&str, T)]) -> Option<(T, u64, &'a str)> {
457 let body = s.strip_prefix(['-', '_', '.']).unwrap_or(s);
458 let (name, tag, rest) = aliases
459 .iter()
460 .find_map(|(name, tag)| Some((*name, *tag, body.strip_prefix(*name)?)))?;
461 let digits = rest.strip_prefix(['-', '_', '.']).unwrap_or(rest);
462 let end = digits
463 .find(|c: char| !c.is_ascii_digit())
464 .unwrap_or(digits.len());
465 let n = match end {
466 0 if name.len() == 1 => return None,
467 0 => 0,
468 _ => digits[..end].parse::<u64>().ok()?,
469 };
470 Some((tag, n, &digits[end..]))
471}
472
473fn parse_local(s: &str) -> Option<Vec<LocalSegment>> {
475 let mut segments = Vec::new();
476 for part in s.split(['-', '_', '.']) {
477 if part.is_empty() || !part.chars().all(|c| c.is_ascii_alphanumeric()) {
478 return None;
479 }
480 segments.push(match part.parse::<u64>() {
481 Ok(n) => LocalSegment::Num(n),
482 Err(_) => LocalSegment::Str(part.to_string()),
483 });
484 }
485 Some(segments)
486}
487
488fn as_pep440(v: &Version) -> Option<Pep440> {
492 let plain = |release| Pep440 {
493 epoch: 0,
494 release,
495 pre: None,
496 post: None,
497 dev: None,
498 local: Vec::new(),
499 };
500 match v {
501 Version::Pep440(p) => Some(p.clone()),
502 Version::Numeric(segments) => Some(plain(segments.clone())),
503 Version::Semver(s) if s.pre.is_empty() => Some(plain(vec![s.major, s.minor, s.patch])),
504 Version::Semver(s) => {
505 parse_pep440(&format!("{}.{}.{}-{}", s.major, s.minor, s.patch, s.pre))
506 }
507 Version::Deb { .. } | Version::Rpm { .. } | Version::Maven(_) | Version::Opaque(_) => None,
508 }
509}
510
511fn pep440_cmp(a: &Pep440, b: &Pep440) -> Ordering {
514 a.epoch
515 .cmp(&b.epoch)
516 .then_with(|| numeric_cmp(&a.release, &b.release))
517 .then_with(|| pre_key(a).cmp(&pre_key(b)))
518 .then_with(|| a.post.cmp(&b.post))
519 .then_with(|| dev_key(a).cmp(&dev_key(b)))
520 .then_with(|| a.local.cmp(&b.local))
521}
522
523#[derive(PartialEq, Eq, PartialOrd, Ord)]
526enum PreKey {
527 BeforeAll,
528 Pre(PreRelease, u64),
529 AfterAll,
530}
531
532fn pre_key(v: &Pep440) -> PreKey {
533 match v.pre {
534 Some((kind, n)) => PreKey::Pre(kind, n),
535 None if v.post.is_none() && v.dev.is_some() => PreKey::BeforeAll,
536 None => PreKey::AfterAll,
537 }
538}
539
540fn dev_key(v: &Pep440) -> (bool, u64) {
542 (v.dev.is_none(), v.dev.unwrap_or(0))
543}
544
545fn strip_v_prefix(s: &str) -> &str {
547 s.strip_prefix('v')
548 .or_else(|| s.strip_prefix('V'))
549 .unwrap_or(s)
550}
551
552fn parse_deb(s: &str) -> Option<Version> {
560 let (epoch, rest) = split_epoch(s, &[':', '!']);
561
562 if !rest.starts_with(|c: char| c.is_ascii_digit()) {
563 return None;
564 }
565 if !rest.chars().all(is_deb_char) {
566 return None;
567 }
568
569 let (upstream, revision) = match rest.rfind('-') {
572 Some(idx) => (rest[..idx].to_string(), rest[idx + 1..].to_string()),
573 None => (rest.to_string(), String::new()),
574 };
575
576 Some(Version::Deb {
577 epoch,
578 upstream,
579 revision,
580 })
581}
582
583fn split_epoch<'a>(s: &'a str, seps: &[char]) -> (u64, &'a str) {
587 if let Some(idx) = s.find(seps) {
588 let (head, tail) = s.split_at(idx);
589 if !head.is_empty() && head.bytes().all(|b| b.is_ascii_digit()) {
590 if let Ok(epoch) = head.parse::<u64>() {
591 return (epoch, &tail[1..]);
592 }
593 }
594 }
595 (0, s)
596}
597
598fn is_deb_char(c: char) -> bool {
600 c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':')
601}
602
603fn deb_cmp(a: (u64, &str, &str), b: (u64, &str, &str)) -> Ordering {
607 a.0.cmp(&b.0)
608 .then_with(|| verrevcmp(a.1, b.1))
609 .then_with(|| verrevcmp(a.2, b.2))
610}
611
612fn verrevcmp(a: &str, b: &str) -> Ordering {
621 let a = a.as_bytes();
622 let b = b.as_bytes();
623 let mut i = 0;
624 let mut j = 0;
625
626 while i < a.len() || j < b.len() {
627 while (i < a.len() && !a[i].is_ascii_digit()) || (j < b.len() && !b[j].is_ascii_digit()) {
628 let ac = a.get(i).map_or(0, |&c| deb_order(c));
629 let bc = b.get(j).map_or(0, |&c| deb_order(c));
630 if ac != bc {
631 return ac.cmp(&bc);
632 }
633 i += 1;
634 j += 1;
635 }
636
637 while i < a.len() && a[i] == b'0' {
638 i += 1;
639 }
640 while j < b.len() && b[j] == b'0' {
641 j += 1;
642 }
643
644 let mut first_diff = 0i32;
645 while i < a.len() && a[i].is_ascii_digit() && j < b.len() && b[j].is_ascii_digit() {
646 if first_diff == 0 {
647 first_diff = i32::from(a[i]) - i32::from(b[j]);
648 }
649 i += 1;
650 j += 1;
651 }
652 if i < a.len() && a[i].is_ascii_digit() {
655 return Ordering::Greater;
656 }
657 if j < b.len() && b[j].is_ascii_digit() {
658 return Ordering::Less;
659 }
660 if first_diff != 0 {
661 return first_diff.cmp(&0);
662 }
663 }
664
665 Ordering::Equal
666}
667
668fn deb_order(c: u8) -> i32 {
674 if c.is_ascii_digit() {
675 0
676 } else if c.is_ascii_alphabetic() {
677 i32::from(c)
678 } else if c == b'~' {
679 -1
680 } else {
681 i32::from(c) + 256
682 }
683}
684
685fn parse_rpm(s: &str) -> Option<Version> {
692 let (epoch, rest) = split_epoch(s, &[':']);
693
694 if !rest.starts_with(|c: char| c.is_ascii_digit()) {
695 return None;
696 }
697 if !rest.chars().all(is_rpm_char) {
698 return None;
699 }
700
701 let (version, release) = match rest.rfind('-') {
703 Some(idx) => (rest[..idx].to_string(), rest[idx + 1..].to_string()),
704 None => (rest.to_string(), String::new()),
705 };
706
707 Some(Version::Rpm {
708 epoch,
709 version,
710 release,
711 })
712}
713
714fn is_rpm_char(c: char) -> bool {
718 c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':' | '_' | '^')
719}
720
721fn rpm_cmp(a: (u64, &str, &str), b: (u64, &str, &str)) -> Ordering {
725 a.0.cmp(&b.0)
726 .then_with(|| rpmvercmp(a.1, b.1))
727 .then_with(|| rpmvercmp(a.2, b.2))
728}
729
730fn rpmvercmp(a: &str, b: &str) -> Ordering {
741 let a = a.as_bytes();
742 let b = b.as_bytes();
743 let mut i = 0;
744 let mut j = 0;
745
746 while i < a.len() || j < b.len() {
747 while i < a.len() && is_rpm_separator(a[i]) {
748 i += 1;
749 }
750 while j < b.len() && is_rpm_separator(b[j]) {
751 j += 1;
752 }
753
754 if a.get(i) == Some(&b'~') || b.get(j) == Some(&b'~') {
755 if a.get(i) != Some(&b'~') {
756 return Ordering::Greater;
757 }
758 if b.get(j) != Some(&b'~') {
759 return Ordering::Less;
760 }
761 i += 1;
762 j += 1;
763 continue;
764 }
765
766 if a.get(i) == Some(&b'^') || b.get(j) == Some(&b'^') {
767 if i == a.len() {
768 return Ordering::Less;
769 }
770 if j == b.len() {
771 return Ordering::Greater;
772 }
773 if a.get(i) != Some(&b'^') {
774 return Ordering::Greater;
775 }
776 if b.get(j) != Some(&b'^') {
777 return Ordering::Less;
778 }
779 i += 1;
780 j += 1;
781 continue;
782 }
783
784 if i == a.len() || j == b.len() {
785 break;
786 }
787
788 let numeric = a[i].is_ascii_digit();
789 let a_end = run_end(a, i, numeric);
790 let b_end = run_end(b, j, numeric);
791 if b_end == j {
793 return if numeric {
794 Ordering::Greater
795 } else {
796 Ordering::Less
797 };
798 }
799
800 let mut x = &a[i..a_end];
801 let mut y = &b[j..b_end];
802 if numeric {
803 x = strip_leading_zeros(x);
804 y = strip_leading_zeros(y);
805 if x.len() != y.len() {
806 return x.len().cmp(&y.len());
807 }
808 }
809 match x.cmp(y) {
810 Ordering::Equal => {}
811 ord => return ord,
812 }
813
814 i = a_end;
815 j = b_end;
816 }
817
818 match (i == a.len(), j == b.len()) {
819 (true, true) => Ordering::Equal,
820 (true, false) => Ordering::Less,
821 _ => Ordering::Greater,
822 }
823}
824
825fn is_rpm_separator(c: u8) -> bool {
827 !(c.is_ascii_alphanumeric() || matches!(c, b'~' | b'^'))
828}
829
830fn run_end(s: &[u8], from: usize, numeric: bool) -> usize {
833 let in_run = |c: &&u8| {
834 if numeric {
835 c.is_ascii_digit()
836 } else {
837 c.is_ascii_alphabetic()
838 }
839 };
840 from + s[from..].iter().take_while(in_run).count()
841}
842
843fn strip_leading_zeros(s: &[u8]) -> &[u8] {
845 let zeros = s.iter().take_while(|&&c| c == b'0').count();
846 &s[zeros..]
847}
848
849fn parse_maven(s: &str) -> Option<Version> {
857 if !s.starts_with(|c: char| c.is_ascii_digit()) {
858 return None;
859 }
860 if !s.chars().all(is_maven_char) {
861 return None;
862 }
863 maven_parse(s)?;
864
865 Some(Version::Maven(s.to_string()))
866}
867
868fn is_maven_char(c: char) -> bool {
872 c.is_alphanumeric() || matches!(c, '.' | '-' | '_' | '+')
873}
874
875#[derive(Debug, Clone, PartialEq, Eq)]
879enum MavenItem {
880 Num(String),
881 Qual(String),
882 List(Vec<MavenItem>),
883}
884
885impl MavenItem {
886 fn is_null(&self) -> bool {
887 match self {
888 MavenItem::Num(digits) => digits.is_empty(),
889 MavenItem::Qual(value) => value.is_empty(),
890 MavenItem::List(items) => items.is_empty(),
891 }
892 }
893}
894
895const MAVEN_MAX_DEPTH: usize = 64;
898
899fn maven_cmp(a: &str, b: &str) -> Option<Ordering> {
902 Some(maven_list_cmp(&maven_parse(a)?, &maven_parse(b)?))
903}
904
905fn maven_parse(s: &str) -> Option<Vec<MavenItem>> {
913 let s = s.to_lowercase();
914 let mut stack: Vec<Vec<MavenItem>> = vec![Vec::new()];
915 let mut digits = false;
916 let mut start = 0;
917
918 for (i, c) in s.char_indices() {
919 if matches!(c, '.' | '-' | '_') {
920 let item = if i == start {
921 MavenItem::Num(String::new())
922 } else {
923 maven_item(digits, &s[start..i])
924 };
925 stack.last_mut().expect("stack is never emptied").push(item);
926 start = i + c.len_utf8();
927 if c != '.' {
928 maven_open(&mut stack)?;
929 }
930 continue;
931 }
932
933 let is_digit = c.is_ascii_digit();
934 if i > start && is_digit && !digits {
935 if !stack.last().expect("stack is never emptied").is_empty() {
936 maven_open(&mut stack)?;
937 }
938 let qualifier = MavenItem::Qual(maven_qualifier(&s[start..i], true));
939 stack
940 .last_mut()
941 .expect("stack is never emptied")
942 .push(qualifier);
943 start = i;
944 maven_open(&mut stack)?;
945 } else if i > start && !is_digit && digits {
946 let number = maven_item(true, &s[start..i]);
947 stack
948 .last_mut()
949 .expect("stack is never emptied")
950 .push(number);
951 start = i;
952 maven_open(&mut stack)?;
953 }
954 digits = is_digit;
955 }
956
957 if s.len() > start {
958 if !digits && !stack.last().expect("stack is never emptied").is_empty() {
959 maven_open(&mut stack)?;
960 }
961 let item = maven_item(digits, &s[start..]);
962 stack.last_mut().expect("stack is never emptied").push(item);
963 }
964
965 while stack.len() > 1 {
966 let mut child = stack.pop().expect("length is above one");
967 maven_normalize(&mut child);
968 stack
969 .last_mut()
970 .expect("stack is never emptied")
971 .push(MavenItem::List(child));
972 }
973
974 let mut items = stack.pop().expect("stack is never emptied");
975 maven_normalize(&mut items);
976 Some(items)
977}
978
979fn maven_open(stack: &mut Vec<Vec<MavenItem>>) -> Option<()> {
981 if stack.len() >= MAVEN_MAX_DEPTH {
982 return None;
983 }
984 stack.push(Vec::new());
985 Some(())
986}
987
988fn maven_item(digits: bool, text: &str) -> MavenItem {
991 if digits {
992 MavenItem::Num(text.trim_start_matches('0').to_string())
993 } else {
994 MavenItem::Qual(maven_qualifier(text, false))
995 }
996}
997
998fn maven_qualifier(text: &str, followed_by_digit: bool) -> String {
1002 let lower = text.to_lowercase();
1003
1004 if followed_by_digit {
1005 match lower.as_str() {
1006 "a" => return "alpha".to_string(),
1007 "b" => return "beta".to_string(),
1008 "m" => return "milestone".to_string(),
1009 _ => {}
1010 }
1011 }
1012
1013 match lower.as_str() {
1014 "ga" | "final" | "release" => String::new(),
1015 "cr" => "rc".to_string(),
1016 _ => lower,
1017 }
1018}
1019
1020fn maven_normalize(items: &mut Vec<MavenItem>) {
1023 let mut i = items.len();
1024 while i > 0 {
1025 i -= 1;
1026 if items[i].is_null() {
1027 items.remove(i);
1028 } else if !matches!(items[i], MavenItem::List(_)) {
1029 break;
1030 }
1031 }
1032}
1033
1034fn maven_list_cmp(a: &[MavenItem], b: &[MavenItem]) -> Ordering {
1037 for i in 0..a.len().max(b.len()) {
1038 let ord = match (a.get(i), b.get(i)) {
1039 (Some(x), Some(y)) => maven_item_cmp(x, y),
1040 (Some(x), None) => maven_null_cmp(x),
1041 (None, Some(y)) => maven_null_cmp(y).reverse(),
1042 (None, None) => Ordering::Equal,
1043 };
1044
1045 if ord != Ordering::Equal {
1046 return ord;
1047 }
1048 }
1049
1050 Ordering::Equal
1051}
1052
1053fn maven_item_cmp(a: &MavenItem, b: &MavenItem) -> Ordering {
1055 match (a, b) {
1056 (MavenItem::Num(x), MavenItem::Num(y)) => x.len().cmp(&y.len()).then_with(|| x.cmp(y)),
1057 (MavenItem::Qual(x), MavenItem::Qual(y)) => {
1058 maven_qualifier_rank(x).cmp(&maven_qualifier_rank(y))
1059 }
1060 (MavenItem::List(x), MavenItem::List(y)) => maven_list_cmp(x, y),
1061 (MavenItem::Num(_), _) => Ordering::Greater,
1062 (_, MavenItem::Num(_)) => Ordering::Less,
1063 (MavenItem::List(_), MavenItem::Qual(_)) => Ordering::Greater,
1064 (MavenItem::Qual(_), MavenItem::List(_)) => Ordering::Less,
1065 }
1066}
1067
1068fn maven_null_cmp(item: &MavenItem) -> Ordering {
1071 match item {
1072 MavenItem::Num(digits) => {
1073 if digits.is_empty() {
1074 Ordering::Equal
1075 } else {
1076 Ordering::Greater
1077 }
1078 }
1079 MavenItem::Qual(value) => maven_qualifier_rank(value).cmp(&maven_qualifier_rank("")),
1080 MavenItem::List(items) => items
1081 .iter()
1082 .map(maven_null_cmp)
1083 .find(|ord| *ord != Ordering::Equal)
1084 .unwrap_or(Ordering::Equal),
1085 }
1086}
1087
1088fn maven_qualifier_rank(q: &str) -> (usize, &str) {
1091 const KNOWN: [&str; 7] = ["alpha", "beta", "milestone", "rc", "snapshot", "", "sp"];
1092
1093 match KNOWN.iter().position(|known| *known == q) {
1094 Some(i) => (i, ""),
1095 None => (KNOWN.len(), q),
1096 }
1097}
1098
1099pub fn is_version_downgrade(old_ver: &str, new_ver: &str) -> bool {
1104 Version::parse_lenient(old_ver).is_downgrade(&Version::parse_lenient(new_ver))
1105}
1106
1107pub fn compare_versions(a: &str, b: &str) -> Option<Ordering> {
1113 Version::parse_lenient(a).partial_cmp_lenient(&Version::parse_lenient(b))
1114}
1115
1116pub fn is_version_downgrade_for_ecosystem(
1137 ecosystem: Option<&str>,
1138 old_ver: &str,
1139 new_ver: &str,
1140) -> bool {
1141 Version::parse_for_ecosystem(ecosystem, old_ver)
1142 .is_downgrade(&Version::parse_for_ecosystem(ecosystem, new_ver))
1143}
1144
1145pub fn compare_versions_for_ecosystem(
1170 ecosystem: Option<&str>,
1171 a: &str,
1172 b: &str,
1173) -> Option<Ordering> {
1174 Version::parse_for_ecosystem(ecosystem, a)
1175 .partial_cmp_lenient(&Version::parse_for_ecosystem(ecosystem, b))
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180 use super::*;
1181
1182 fn maven_cmp(a: &str, b: &str) -> Ordering {
1183 super::maven_cmp(a, b).unwrap_or_else(|| panic!("{a} vs {b} exceeds the depth cap"))
1184 }
1185
1186 #[test]
1187 fn parse_standard_semver() {
1188 let v = Version::parse_lenient("1.2.3");
1189 assert_eq!(v, Version::Semver(semver::Version::new(1, 2, 3)));
1190 }
1191
1192 #[test]
1193 fn parse_v_prefix() {
1194 assert_eq!(
1195 Version::parse_lenient("v1.2.3"),
1196 Version::Semver(semver::Version::new(1, 2, 3))
1197 );
1198 assert_eq!(
1199 Version::parse_lenient("V1.2.3"),
1200 Version::Semver(semver::Version::new(1, 2, 3))
1201 );
1202 }
1203
1204 #[test]
1205 fn parse_two_parts() {
1206 assert_eq!(
1207 Version::parse_lenient("1.2"),
1208 Version::Semver(semver::Version::new(1, 2, 0))
1209 );
1210 }
1211
1212 #[test]
1213 fn parse_single_part() {
1214 assert_eq!(
1215 Version::parse_lenient("42"),
1216 Version::Semver(semver::Version::new(42, 0, 0))
1217 );
1218 }
1219
1220 #[test]
1221 fn parse_prerelease() {
1222 let v = Version::parse_lenient("1.2.3-beta.1");
1223 match v {
1224 Version::Semver(sv) => {
1225 assert_eq!(sv.major, 1);
1226 assert_eq!(sv.minor, 2);
1227 assert_eq!(sv.patch, 3);
1228 assert!(!sv.pre.is_empty());
1229 }
1230 other => panic!("expected Semver, got {:?}", other),
1231 }
1232 }
1233
1234 #[test]
1235 fn parse_build_metadata() {
1236 let v = Version::parse_lenient("1.2.3+build.456");
1237 match v {
1238 Version::Semver(sv) => {
1239 assert_eq!((sv.major, sv.minor, sv.patch), (1, 2, 3));
1240 assert!(!sv.build.is_empty());
1241 }
1242 other => panic!("expected Semver, got {:?}", other),
1243 }
1244 }
1245
1246 #[test]
1247 fn parse_prerelease_and_build() {
1248 let v = Version::parse_lenient("1.0.0-alpha.1+build.789");
1249 match v {
1250 Version::Semver(sv) => {
1251 assert_eq!(sv.major, 1);
1252 assert!(!sv.pre.is_empty());
1253 assert!(!sv.build.is_empty());
1254 }
1255 other => panic!("expected Semver, got {:?}", other),
1256 }
1257 }
1258
1259 #[test]
1260 fn parse_v_prefix_two_parts() {
1261 assert_eq!(
1262 Version::parse_lenient("v1.2"),
1263 Version::Semver(semver::Version::new(1, 2, 0))
1264 );
1265 }
1266
1267 #[test]
1268 fn parse_v_prefix_single_part() {
1269 assert_eq!(
1270 Version::parse_lenient("v5"),
1271 Version::Semver(semver::Version::new(5, 0, 0))
1272 );
1273 }
1274
1275 #[test]
1276 fn parse_v_prefix_prerelease() {
1277 let v = Version::parse_lenient("v2.0.0-rc.1");
1278 match v {
1279 Version::Semver(sv) => {
1280 assert_eq!(sv.major, 2);
1281 assert!(!sv.pre.is_empty());
1282 }
1283 other => panic!("expected Semver, got {:?}", other),
1284 }
1285 }
1286
1287 #[test]
1288 fn parse_zero_version() {
1289 assert_eq!(
1290 Version::parse_lenient("0.0.0"),
1291 Version::Semver(semver::Version::new(0, 0, 0))
1292 );
1293 }
1294
1295 #[test]
1296 fn parse_large_numbers() {
1297 assert_eq!(
1298 Version::parse_lenient("999.888.777"),
1299 Version::Semver(semver::Version::new(999, 888, 777))
1300 );
1301 }
1302
1303 #[test]
1304 fn parse_single_zero() {
1305 assert_eq!(
1306 Version::parse_lenient("0"),
1307 Version::Semver(semver::Version::new(0, 0, 0))
1308 );
1309 }
1310
1311 #[test]
1312 fn parse_four_part_is_numeric() {
1313 assert_eq!(
1314 Version::parse_lenient("1.2.3.4"),
1315 Version::Numeric(vec![1, 2, 3, 4])
1316 );
1317 }
1318
1319 #[test]
1320 fn parse_date_based_is_numeric() {
1321 assert_eq!(
1323 Version::parse_lenient("2024.01.15"),
1324 Version::Numeric(vec![2024, 1, 15])
1325 );
1326 }
1327
1328 #[test]
1329 fn parse_v_prefix_four_part_is_numeric() {
1330 assert_eq!(
1332 Version::parse_lenient("v1.2.3.4"),
1333 Version::Numeric(vec![1, 2, 3, 4])
1334 );
1335 assert_eq!(
1336 Version::parse_lenient("V1.2.3.4"),
1337 Version::Numeric(vec![1, 2, 3, 4])
1338 );
1339 }
1340
1341 #[test]
1342 fn parse_v_prefix_date_based_is_numeric() {
1343 assert_eq!(
1344 Version::parse_lenient("v2024.01.15"),
1345 Version::Numeric(vec![2024, 1, 15])
1346 );
1347 }
1348
1349 #[test]
1350 fn parse_leading_zeros_is_numeric() {
1351 assert_eq!(
1352 Version::parse_lenient("01.02.03"),
1353 Version::Numeric(vec![1, 2, 3])
1354 );
1355 }
1356
1357 #[test]
1358 fn parse_non_numeric_is_opaque() {
1359 assert_eq!(Version::parse_lenient("abc"), Version::Opaque("abc".into()));
1360 assert_eq!(
1361 Version::parse_lenient("foo.bar.baz"),
1362 Version::Opaque("foo.bar.baz".into())
1363 );
1364 }
1365
1366 #[test]
1367 fn parse_whitespace_is_opaque() {
1368 assert!(matches!(
1369 Version::parse_lenient(" 1.2.3"),
1370 Version::Opaque(_)
1371 ));
1372 assert!(matches!(
1373 Version::parse_lenient("1.2.3 "),
1374 Version::Opaque(_)
1375 ));
1376 }
1377
1378 #[test]
1379 fn parse_empty_string_is_opaque() {
1380 assert!(matches!(Version::parse_lenient(""), Version::Opaque(_)));
1381 }
1382
1383 #[test]
1384 fn downgrade_semver() {
1385 assert!(is_version_downgrade("2.0.0", "1.5.0"));
1386 assert!(is_version_downgrade("1.1.0", "1.0.0"));
1387 assert!(is_version_downgrade("1.0.1", "1.0.0"));
1388 }
1389
1390 #[test]
1391 fn upgrade_semver_not_flagged() {
1392 assert!(!is_version_downgrade("1.0.0", "1.1.0"));
1393 assert!(!is_version_downgrade("1.0.0", "2.0.0"));
1394 assert!(!is_version_downgrade("1.0.0", "1.0.1"));
1395 }
1396
1397 #[test]
1398 fn equal_semver_not_flagged() {
1399 assert!(!is_version_downgrade("1.0.0", "1.0.0"));
1400 }
1401
1402 #[test]
1403 fn downgrade_v_prefix() {
1404 assert!(is_version_downgrade("v2.0.0", "v1.0.0"));
1405 assert!(!is_version_downgrade("v1.0.0", "v2.0.0"));
1406 }
1407
1408 #[test]
1409 fn downgrade_prerelease() {
1410 assert!(is_version_downgrade("1.0.0", "1.0.0-rc1"));
1411 assert!(!is_version_downgrade("1.0.0-rc1", "1.0.0"));
1412 }
1413
1414 #[test]
1415 fn downgrade_build_metadata() {
1416 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.2"));
1420 assert!(!is_version_downgrade("1.0.0+build.2", "1.0.0+build.1"));
1421 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.1"));
1422 assert!(!is_version_downgrade("1.0.0+c144a98", "1.0.0+bc17664"));
1425 assert!(!is_version_downgrade("1.0.0+build.10", "1.0.0+build.9"));
1426 }
1427
1428 #[test]
1429 fn downgrade_mixed_v_prefix() {
1430 assert!(is_version_downgrade("v2.0.0", "1.0.0"));
1431 assert!(is_version_downgrade("2.0.0", "v1.0.0"));
1432 assert!(!is_version_downgrade("v1.0.0", "2.0.0"));
1433 assert!(!is_version_downgrade("1.0.0", "v2.0.0"));
1434 }
1435
1436 #[test]
1437 fn downgrade_prerelease_ordering() {
1438 assert!(is_version_downgrade("1.0.0-beta.1", "1.0.0-alpha.1"));
1439 assert!(is_version_downgrade("1.0.0-rc.1", "1.0.0-beta.1"));
1440 assert!(!is_version_downgrade("1.0.0-alpha.1", "1.0.0-beta.1"));
1441 assert!(!is_version_downgrade("1.0.0-beta.1", "1.0.0-rc.1"));
1442 }
1443
1444 #[test]
1445 fn downgrade_prerelease_numeric_ordering() {
1446 assert!(is_version_downgrade("1.0.0-rc.2", "1.0.0-rc.1"));
1447 assert!(!is_version_downgrade("1.0.0-rc.1", "1.0.0-rc.2"));
1448 }
1449
1450 #[test]
1451 fn downgrade_equal_with_v_prefix() {
1452 assert!(!is_version_downgrade("v1.0.0", "v1.0.0"));
1453 }
1454
1455 #[test]
1456 fn downgrade_padded_two_part() {
1457 assert!(is_version_downgrade("1.2", "1.1"));
1458 assert!(!is_version_downgrade("1.1", "1.2"));
1459 assert!(!is_version_downgrade("1.2", "1.2"));
1460 }
1461
1462 #[test]
1463 fn downgrade_padded_single_part() {
1464 assert!(is_version_downgrade("2", "1"));
1465 assert!(!is_version_downgrade("1", "2"));
1466 assert!(!is_version_downgrade("5", "5"));
1467 }
1468
1469 #[test]
1470 fn downgrade_mixed_part_counts_semver() {
1471 assert!(is_version_downgrade("2.0", "1.9.9"));
1472 assert!(!is_version_downgrade("1.9.9", "2.0"));
1473 }
1474
1475 #[test]
1476 fn downgrade_v_prefix_two_part() {
1477 assert!(is_version_downgrade("v2.0", "v1.0"));
1478 assert!(!is_version_downgrade("v1.0", "v2.0"));
1479 }
1480
1481 #[test]
1482 fn downgrade_four_part() {
1483 assert!(is_version_downgrade("1.2.3.4", "1.2.3.3"));
1484 assert!(!is_version_downgrade("1.2.3.3", "1.2.3.4"));
1485 assert!(!is_version_downgrade("1.2.3.4", "1.2.3.4"));
1486 }
1487
1488 #[test]
1489 fn downgrade_date_based() {
1490 assert!(is_version_downgrade("2024.01.15", "2023.12.01"));
1491 assert!(!is_version_downgrade("2023.12.01", "2024.01.15"));
1492 }
1493
1494 #[test]
1495 fn downgrade_v_prefix_four_part() {
1496 assert!(is_version_downgrade("v1.2.3.4", "v1.2.3.3"));
1499 assert!(!is_version_downgrade("v1.2.3.3", "v1.2.3.4"));
1500 assert!(!is_version_downgrade("v1.2.3.4", "v1.2.3.4"));
1501 }
1502
1503 #[test]
1504 fn downgrade_v_prefix_date_based() {
1505 assert!(is_version_downgrade("v2024.01.15", "v2023.12.01"));
1506 assert!(!is_version_downgrade("v2023.12.01", "v2024.01.15"));
1507 }
1508
1509 #[test]
1510 fn downgrade_non_numeric_not_flagged() {
1511 assert!(!is_version_downgrade("abc", "def"));
1512 assert!(!is_version_downgrade("foo.bar", "foo.baz"));
1513 }
1514
1515 #[test]
1516 fn downgrade_numeric_unequal_length() {
1517 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
1518 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
1519 }
1520
1521 #[test]
1522 fn downgrade_large_major_numeric_equal() {
1523 assert!(!is_version_downgrade("2024.1.15", "2024.1.15"));
1525 }
1526
1527 #[test]
1528 fn downgrade_semver_vs_four_part() {
1529 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
1532 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
1533 }
1534
1535 #[test]
1536 fn downgrade_v_prefix_vs_four_part() {
1537 assert!(!is_version_downgrade("v1.2.3", "1.2.3.4"));
1539 assert!(is_version_downgrade("1.2.3.4", "v1.2.3"));
1540 }
1541
1542 #[test]
1543 fn downgrade_empty_strings() {
1544 assert!(!is_version_downgrade("", "1.0.0"));
1545 assert!(!is_version_downgrade("1.0.0", ""));
1546 assert!(!is_version_downgrade("", ""));
1547 }
1548
1549 #[test]
1552 fn parse_epoch_is_deb() {
1553 assert!(matches!(
1555 Version::parse_lenient("2:1.0"),
1556 Version::Deb { .. }
1557 ));
1558 assert!(matches!(
1559 Version::parse_lenient("1:9.0"),
1560 Version::Deb { .. }
1561 ));
1562 }
1563
1564 #[test]
1565 fn parse_revision_is_deb() {
1566 assert!(matches!(
1568 Version::parse_lenient("5.1-3"),
1569 Version::Deb { .. }
1570 ));
1571 }
1572
1573 #[test]
1574 fn parse_deb_fields() {
1575 match Version::parse_lenient("2:1.2.3-4") {
1576 Version::Deb {
1577 epoch,
1578 upstream,
1579 revision,
1580 } => {
1581 assert_eq!(epoch, 2);
1582 assert_eq!(upstream, "1.2.3");
1583 assert_eq!(revision, "4");
1584 }
1585 other => panic!("expected Deb, got {:?}", other),
1586 }
1587 }
1588
1589 #[test]
1590 fn parse_deb_revision_splits_at_last_hyphen() {
1591 match Version::parse_lenient("1.2-2-1") {
1594 Version::Deb {
1595 epoch,
1596 upstream,
1597 revision,
1598 } => {
1599 assert_eq!(epoch, 0);
1600 assert_eq!(upstream, "1.2-2");
1601 assert_eq!(revision, "1");
1602 }
1603 other => panic!("expected Deb, got {:?}", other),
1604 }
1605 }
1606
1607 #[test]
1608 fn parse_pep440_epoch_is_pep440() {
1609 match Version::parse_lenient("1!2.0") {
1610 Version::Pep440(p) => {
1611 assert_eq!(p.epoch, 1);
1612 assert_eq!(p.release, vec![2, 0]);
1613 assert_eq!(p.pre, None);
1614 }
1615 other => panic!("expected Pep440, got {:?}", other),
1616 }
1617 }
1618
1619 #[test]
1620 fn parse_deb_keeps_epoch_bang_forms_it_declines() {
1621 match Version::parse_lenient("1!2.0-3") {
1623 Version::Deb {
1624 epoch,
1625 upstream,
1626 revision,
1627 } => {
1628 assert_eq!(epoch, 1);
1629 assert_eq!(upstream, "2.0");
1630 assert_eq!(revision, "3");
1631 }
1632 other => panic!("expected Deb, got {:?}", other),
1633 }
1634 }
1635
1636 #[test]
1637 fn parse_tilde_prerelease_is_deb() {
1638 assert!(matches!(
1640 Version::parse_lenient("1.0.0~rc1"),
1641 Version::Deb { .. }
1642 ));
1643 }
1644
1645 #[test]
1646 fn parse_codename_stays_opaque() {
1647 assert!(matches!(
1649 Version::parse_lenient("focal-1"),
1650 Version::Opaque(_)
1651 ));
1652 assert!(matches!(
1653 Version::parse_lenient("stable"),
1654 Version::Opaque(_)
1655 ));
1656 assert!(matches!(
1658 Version::parse_lenient("1:stable"),
1659 Version::Opaque(_)
1660 ));
1661 }
1662
1663 #[test]
1666 fn downgrade_epoch() {
1667 assert!(is_version_downgrade("2:1.0", "1:9.0"));
1669 assert!(!is_version_downgrade("1:9.0", "2:1.0"));
1670 assert!(!is_version_downgrade("1:1.0", "2:0.1"));
1672 assert!(is_version_downgrade("2:0.1", "1:1.0"));
1673 }
1674
1675 #[test]
1676 fn downgrade_epoch_equal_upstream() {
1677 assert!(is_version_downgrade("1:2.0", "1:1.0"));
1678 assert!(!is_version_downgrade("1:1.0", "1:2.0"));
1679 assert!(!is_version_downgrade("1:1.0", "1:1.0"));
1680 }
1681
1682 #[test]
1683 fn downgrade_implicit_epoch_zero() {
1684 assert!(!is_version_downgrade("5.1-1", "1:0.1-1"));
1687 assert!(is_version_downgrade("1:0.1-1", "0:0.1-1"));
1688 }
1689
1690 #[test]
1691 fn downgrade_revision() {
1692 assert!(is_version_downgrade("5.1-3", "5.1-2"));
1693 assert!(!is_version_downgrade("5.1-2", "5.1-3"));
1694 assert!(!is_version_downgrade("5.1-2", "5.1-2"));
1695 }
1696
1697 #[test]
1698 fn downgrade_upstream_trumps_revision() {
1699 assert!(is_version_downgrade("1:5.2-1", "1:5.1-1"));
1701 assert!(!is_version_downgrade("1:5.1-9", "1:5.2-1"));
1703 }
1704
1705 #[test]
1706 fn downgrade_absent_revision_equals_zero() {
1707 assert!(is_version_downgrade("1:2.0-1", "1:2.0"));
1710 assert!(!is_version_downgrade("1:2.0", "1:2.0-1"));
1711 }
1712
1713 #[test]
1714 fn downgrade_rpm_release_with_epoch() {
1715 assert!(is_version_downgrade("1:1.2.3-2.el8", "1:1.2.3-1.el8"));
1718 assert!(!is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-2.el8"));
1719 assert!(is_version_downgrade("1:1.2.3-1.el8", "1:1.2.3-1.el7"));
1721 assert!(!is_version_downgrade("1:1.2.3-1.el7", "1:1.2.3-1.el8"));
1722 }
1723
1724 #[test]
1725 fn downgrade_deb_numeric_not_lexical() {
1726 assert!(is_version_downgrade("1.10-1", "1.9-1"));
1728 assert!(!is_version_downgrade("1.9-1", "1.10-1"));
1729 }
1730
1731 #[test]
1732 fn downgrade_deb_tilde_prerelease() {
1733 assert!(is_version_downgrade("1.0.0~rc2", "1.0.0~rc1"));
1735 assert!(!is_version_downgrade("1.0.0~rc1", "1.0.0~rc2"));
1736 assert!(is_version_downgrade("1:1.0~rc1", "1:1.0~beta1"));
1737 }
1738
1739 #[test]
1740 fn downgrade_real_world_deb() {
1741 assert!(is_version_downgrade(
1743 "1:1.1.1f-1ubuntu2.16",
1744 "1:1.1.1f-1ubuntu2.15"
1745 ));
1746 assert!(!is_version_downgrade(
1747 "1:1.1.1f-1ubuntu2.15",
1748 "1:1.1.1f-1ubuntu2.16"
1749 ));
1750 }
1751
1752 #[test]
1753 fn downgrade_deb_opaque_not_flagged() {
1754 assert!(!is_version_downgrade("focal", "bionic"));
1756 assert!(!is_version_downgrade("1:stable", "1:oldstable"));
1757 }
1758
1759 #[test]
1760 fn downgrade_deb_vs_semver_not_flagged() {
1761 assert!(!is_version_downgrade("2:1.0", "1.0.0"));
1763 assert!(!is_version_downgrade("1.0.0", "2:1.0"));
1764 }
1765
1766 #[test]
1767 fn deb_canonical_ordering_vectors() {
1768 use Ordering::{Equal, Greater, Less};
1769
1770 let cases = [
1779 ("1:1.0~~", "1:1.0~~a", Less),
1782 ("1:1.0~~a", "1:1.0~", Less),
1783 ("1:1.0~", "1:1.0", Less),
1784 ("1:1.0", "1:1.0a", Less),
1785 ("1:1.0~rc1", "1:1.0", Less),
1788 ("1:1.0~rc1", "1:1.0~rc2", Less),
1789 ("1:1.10", "1:1.9", Greater),
1791 ("1:1.0", "1:1.00", Equal),
1793 ("1:1.01", "1:1.1", Equal),
1794 ("1:1.a", "1:1.1", Greater),
1796 ("1:1.0a", "1:1.01", Less),
1798 ("2:0.1", "1:9.9", Greater),
1800 ("1:5.2-1", "1:5.1-9", Greater),
1802 ("1:2.0", "1:2.0-0", Equal),
1804 ("1:2.0-10", "1:2.0-9", Greater),
1806 ];
1807
1808 for (a, b, expected) in cases {
1809 assert!(
1812 matches!(Version::parse_lenient(a), Version::Deb { .. }),
1813 "{a} no longer parses as Deb"
1814 );
1815 assert!(
1816 matches!(Version::parse_lenient(b), Version::Deb { .. }),
1817 "{b} no longer parses as Deb"
1818 );
1819 match expected {
1820 Less => {
1822 assert!(is_version_downgrade(b, a), "expected {a} < {b}");
1823 assert!(!is_version_downgrade(a, b), "expected {a} < {b}");
1824 }
1825 Greater => {
1827 assert!(is_version_downgrade(a, b), "expected {a} > {b}");
1828 assert!(!is_version_downgrade(b, a), "expected {a} > {b}");
1829 }
1830 Equal => {
1832 assert!(!is_version_downgrade(a, b), "expected {a} == {b}");
1833 assert!(!is_version_downgrade(b, a), "expected {a} == {b}");
1834 }
1835 }
1836 }
1837 }
1838
1839 #[test]
1840 fn compare_orders_comparable_variant_pairs() {
1841 use Ordering::{Equal, Greater, Less};
1842
1843 for (a, b, expected) in [
1844 ("2.0.0", "1.5.0", Greater),
1845 ("1.0.0", "1.0.0", Equal),
1846 ("1.2.3.4", "1.2.3.3", Greater),
1847 ("1.2.3", "1.2.3.4", Less),
1848 ("2:1.0-3", "1:9.0-1", Greater),
1849 ("5.1-3", "5.1-3", Equal),
1850 ] {
1851 assert_eq!(compare_versions(a, b), Some(expected), "{a} vs {b}");
1852 assert_eq!(
1853 compare_versions(b, a),
1854 Some(expected.reverse()),
1855 "{b} vs {a}"
1856 );
1857 }
1858 }
1859
1860 #[test]
1861 fn compare_leaves_opaque_and_mixed_variants_unordered() {
1862 for (a, b) in [
1863 ("deadbeef", "1.0.0"),
1864 ("deadbeef", "cafebabe"),
1865 ("deadbeef", "deadbeef"),
1866 ("2:1.0-3", "1.0.0"),
1867 ("5.1-3", "5.1.0.0"),
1868 ] {
1869 assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
1870 assert_eq!(compare_versions(b, a), None, "{b} vs {a}");
1871 }
1872 }
1873
1874 #[test]
1877 fn parse_pep440_suffixes() {
1878 for s in [
1879 "1.0rc1",
1880 "1.0a1",
1881 "1.0b1",
1882 "1.0.dev1",
1883 "1.0.post1",
1884 "4.2.0rc1",
1885 "1!1.0",
1886 "1.0alpha1",
1887 "1.0-rc-1",
1888 "1.0_beta_2",
1889 "1.0.RC1",
1890 "2.0.post2.dev3",
1891 "1.0rc1+ubuntu.1",
1892 ] {
1893 assert!(
1894 matches!(Version::parse_lenient(s), Version::Pep440(_)),
1895 "{s} should parse as Pep440"
1896 );
1897 }
1898 }
1899
1900 #[test]
1901 fn parse_pep440_fields() {
1902 match Version::parse_lenient("2!4.2.0.post3.dev7") {
1903 Version::Pep440(p) => {
1904 assert_eq!(p.epoch, 2);
1905 assert_eq!(p.release, vec![4, 2, 0]);
1906 assert_eq!(p.pre, None);
1907 assert_eq!(p.post, Some(3));
1908 assert_eq!(p.dev, Some(7));
1909 assert!(p.local.is_empty());
1910 }
1911 other => panic!("expected Pep440, got {:?}", other),
1912 }
1913 }
1914
1915 #[test]
1916 fn parse_pep440_leaves_other_formats_alone() {
1917 for s in [
1918 "1.2.3",
1919 "v1.2",
1920 "42",
1921 "1.2.3-beta.1",
1922 "1.0.0-alpha.1+build.789",
1923 "0.0.0",
1924 ] {
1925 assert!(
1926 matches!(Version::parse_lenient(s), Version::Semver(_)),
1927 "{s} should still be Semver"
1928 );
1929 }
1930 for s in ["1.2.3.4", "2024.01.15", "01.02.03", "v1.2.3.4"] {
1931 assert!(
1932 matches!(Version::parse_lenient(s), Version::Numeric(_)),
1933 "{s} should still be Numeric"
1934 );
1935 }
1936 for s in [
1937 "2:1.0",
1938 "5.1-3",
1939 "2:1.2.3-4",
1940 "1.2-2-1",
1941 "1.0.0~rc1",
1942 "1:1.1.1f-1ubuntu2.16",
1943 "1.0+ubuntu.1",
1944 ] {
1945 assert!(
1946 matches!(Version::parse_lenient(s), Version::Deb { .. }),
1947 "{s} should still be Deb"
1948 );
1949 }
1950 for s in ["abc", "foo.bar.baz", "focal-1", "stable", "1:stable", ""] {
1951 assert!(
1952 matches!(Version::parse_lenient(s), Version::Opaque(_)),
1953 "{s} should still be Opaque"
1954 );
1955 }
1956 }
1957
1958 fn assert_ascending(versions: &[&str]) {
1960 for (i, a) in versions.iter().enumerate() {
1961 for b in &versions[i + 1..] {
1962 assert_eq!(
1963 compare_versions(a, b),
1964 Some(Ordering::Less),
1965 "expected {a} < {b}"
1966 );
1967 assert_eq!(
1968 compare_versions(b, a),
1969 Some(Ordering::Greater),
1970 "expected {b} > {a}"
1971 );
1972 assert!(is_version_downgrade(b, a), "expected {b} -> {a} downgrade");
1973 assert!(!is_version_downgrade(a, b), "expected {a} -> {b} upgrade");
1974 }
1975 }
1976 }
1977
1978 #[test]
1979 fn pep440_release_cycle_ordering() {
1980 assert_ascending(&[
1981 "1.0.dev1",
1982 "1.0a1",
1983 "1.0a2",
1984 "1.0b1",
1985 "1.0rc1",
1986 "1.0",
1987 "1.0.post1",
1988 "1.0.1",
1989 ]);
1990 }
1991
1992 #[test]
1993 fn pep440_dev_ordering_within_segments() {
1994 assert_ascending(&["1.0.dev1", "1.0a1.dev1", "1.0a1", "1.0"]);
1995 assert_ascending(&["1.0", "1.0.post1.dev1", "1.0.post1"]);
1996 }
1997
1998 #[test]
1999 fn pep440_epoch_ordering() {
2000 assert_ascending(&["2.0", "1!1.0", "1!2.0", "2!0.1"]);
2001 }
2002
2003 #[test]
2004 fn pep440_spelling_aliases() {
2005 for (canonical, aliases) in [
2006 ("1.0a1", ["1.0alpha1", "1.0.ALPHA.1", "1.0-a-1"]),
2007 ("1.0b1", ["1.0beta1", "1.0.BETA.1", "1.0_b_1"]),
2008 ("1.0rc1", ["1.0c1", "1.0pre1", "1.0preview1"]),
2009 ("1.0.post1", ["1.0rev1", "1.0r1", "1.0-POST-1"]),
2010 ] {
2011 for alias in aliases {
2012 assert_eq!(
2013 compare_versions(canonical, alias),
2014 Some(Ordering::Equal),
2015 "{alias} should normalize to {canonical}"
2016 );
2017 }
2018 }
2019 assert_eq!(compare_versions("1.0rc", "1.0rc0"), Some(Ordering::Equal));
2021 assert_eq!(compare_versions("1.0rc", "1.0rc1"), Some(Ordering::Less));
2022 }
2023
2024 #[test]
2025 fn pep440_compares_against_semver_and_numeric() {
2026 use Ordering::{Equal, Greater, Less};
2027
2028 for (a, b, expected) in [
2029 ("4.2.0rc1", "4.2.0", Less),
2031 ("1.0rc1", "1.0", Less),
2032 ("1.0.dev1", "1.0", Less),
2033 ("1.0", "1.0.post1", Less),
2034 ("1!1.0", "2.0", Greater),
2035 ("1.0.post1", "1.0.1", Less),
2036 ("1.0.post0", "1.0.0.post0", Equal),
2038 ("1.2.3.4rc1", "1.2.3.4", Less),
2040 ("1.2.3.4.dev1", "1.2.3.3", Greater),
2041 ("1.0.0-rc1", "1.0rc2", Less),
2043 ("1.0.0-alpha.1", "1.0b1", Less),
2044 ] {
2045 assert_eq!(compare_versions(a, b), Some(expected), "{a} vs {b}");
2046 assert_eq!(
2047 compare_versions(b, a),
2048 Some(expected.reverse()),
2049 "{b} vs {a}"
2050 );
2051 }
2052 }
2053
2054 #[test]
2055 fn pep440_local_version_ordering() {
2056 assert_ascending(&["1.0rc1", "1.0rc1+ubuntu", "1.0rc1+ubuntu.1"]);
2058 assert_ascending(&["1.0rc1+abc", "1.0rc1+1"]);
2059 assert_ascending(&["1.0rc1+build.9", "1.0rc1+build.10"]);
2060 assert_eq!(
2061 compare_versions("1.0rc1+UBUNTU-1", "1.0rc1+ubuntu.1"),
2062 Some(Ordering::Equal)
2063 );
2064 }
2065
2066 #[test]
2067 fn pep440_stays_uncomparable_against_deb_and_opaque() {
2068 for (a, b) in [
2069 ("1.0rc1", "2:1.0"),
2070 ("1.0rc1", "1.0.0~rc1"),
2071 ("1.0rc1", "deadbeef"),
2072 ("1.0.0-foo.bar", "1.0rc1"),
2074 ] {
2075 assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
2076 assert_eq!(compare_versions(b, a), None, "{b} vs {a}");
2077 }
2078 }
2079
2080 #[test]
2081 fn downgrade_pep440_gate() {
2082 assert!(!is_version_downgrade("1.0.dev1", "1.0a1"));
2084 assert!(is_version_downgrade("1.0a1", "1.0.dev1"));
2085 assert!(!is_version_downgrade("4.2.0rc1", "4.2.0"));
2087 assert!(is_version_downgrade("4.2.0", "4.2.0rc1"));
2088 assert!(!is_version_downgrade("1.0", "1.0.post1"));
2089 assert!(is_version_downgrade("1.0.post1", "1.0"));
2090 assert!(!is_version_downgrade("1.0rc1", "1.0rc1"));
2091 }
2092
2093 fn letter_suffixed(pattern: &str) -> Vec<String> {
2097 ('a'..='z')
2098 .map(|c| pattern.replace('@', &c.to_string()))
2099 .collect()
2100 }
2101
2102 #[test]
2103 fn bare_single_letter_suffix_parses_as_deb() {
2104 for pattern in ["1.0.2@", "2024@", "1.1.1@-r0", "1.0@"] {
2105 for s in letter_suffixed(pattern) {
2106 assert!(
2107 matches!(Version::parse_lenient(&s), Version::Deb { .. }),
2108 "{s} should parse as Deb"
2109 );
2110 }
2111 }
2112 }
2113
2114 #[test]
2115 fn letter_suffixed_versions_order_across_the_alphabet() {
2116 for pattern in ["1.0.2@", "2024@", "1.1.1@-r0"] {
2117 let versions = letter_suffixed(pattern);
2118 let refs: Vec<&str> = versions.iter().map(String::as_str).collect();
2119 assert_ascending(&refs);
2120 }
2121 assert_ascending(&["2024a", "2024h", "2025a", "2025b"]);
2122 assert_ascending(&["1.1.1a-r0", "1.1.1d-r0", "1.1.1d-r1", "1.1.1w-r0"]);
2123 }
2124
2125 #[test]
2126 fn bare_keyword_longer_than_one_letter_stays_pep440() {
2127 for s in [
2128 "1.0rc",
2129 "1.0.dev",
2130 "1.0.post",
2131 "1.0alpha",
2132 "1.0beta",
2133 "1.0pre",
2134 "1.0preview",
2135 "1.0rev",
2136 "2.0.post2.dev3",
2137 ] {
2138 assert!(
2139 matches!(Version::parse_lenient(s), Version::Pep440(_)),
2140 "{s} should still parse as Pep440"
2141 );
2142 }
2143 }
2144
2145 #[test]
2146 fn single_letter_alias_with_a_number_stays_pep440() {
2147 for s in [
2148 "1.0a1",
2149 "1.0b1",
2150 "1.0c1",
2151 "1.0r1",
2152 "1.0a0",
2153 "1.0-a-1",
2154 "1.0_b_2",
2155 "1.0.c.3",
2156 "1!2.0a1",
2157 "1.0a1+ubuntu.1",
2158 ] {
2159 assert!(
2160 matches!(Version::parse_lenient(s), Version::Pep440(_)),
2161 "{s} should still parse as Pep440"
2162 );
2163 }
2164 }
2165
2166 #[test]
2167 fn downgrade_letter_suffix_gate() {
2168 assert!(is_version_downgrade("1.0.2d", "1.0.2c"));
2169 assert!(!is_version_downgrade("1.0.2c", "1.0.2d"));
2170 assert!(is_version_downgrade("2025a", "2024h"));
2171 assert!(!is_version_downgrade("2024h", "2025a"));
2172 assert!(is_version_downgrade("1.1.1d-r0", "1.1.1a-r0"));
2173 assert!(!is_version_downgrade("1.1.1a-r0", "1.1.1d-r0"));
2174 assert!(!is_version_downgrade("1.0.2a", "1.0.2a"));
2175 assert_eq!(compare_versions("1.0.2", "1.0.2a"), None);
2177 assert!(!is_version_downgrade("1.0.2", "1.0.2a"));
2178 assert!(!is_version_downgrade("1.0.2a", "1.0.2"));
2179 }
2180
2181 #[test]
2182 fn downgrade_agrees_with_compare() {
2183 use Ordering::Greater;
2184
2185 for (a, b) in [
2186 ("2.0.0", "1.5.0"),
2187 ("1.0.0", "2.0.0"),
2188 ("1.0.0", "1.0.0"),
2189 ("2024.01.15", "2024.01.14"),
2190 ("2:1.0-3", "1:9.0-1"),
2191 ("1.0.0+build.10", "1.0.0+build.9"),
2192 ("deadbeef", "1.0.0"),
2193 ] {
2194 assert_eq!(
2195 is_version_downgrade(a, b),
2196 compare_versions(a, b) == Some(Greater),
2197 "{a} -> {b}"
2198 );
2199 }
2200 }
2201
2202 const ECOSYSTEM_CORPUS: &[&str] = &[
2204 "1",
2205 "1.0",
2206 "1.0.0",
2207 "1.2.3",
2208 "v1.2.3",
2209 "2024.01.15",
2210 "1.2.3.4",
2211 "1.0.0-alpha.1",
2212 "1.0.0-alpha.2",
2213 "1.0.0-rc.1",
2214 "1.0.0+build.9",
2215 "1.0.0-foo.bar",
2216 "4.2.0rc1",
2217 "1.0.dev1",
2218 "1.0a1",
2219 "1!1.0",
2220 "1.0.post1",
2221 "1.0.2a",
2222 "1.2.3-1",
2223 "1.2.3-2",
2224 "1.2.3-1ubuntu2",
2225 "1.2.3-1build1",
2226 "1.2.3-1+deb11u1",
2227 "1.0~rc1",
2228 "2:1.0-3",
2229 "4.4.2-2.el7_9",
2230 "deadbeef",
2231 "",
2232 ];
2233
2234 #[test]
2235 fn unknown_ecosystem_parses_exactly_like_parse_lenient() {
2236 for eco in [None, Some("npm"), Some("cargo"), Some("golang")] {
2237 for s in ECOSYSTEM_CORPUS {
2238 assert_eq!(
2239 Version::parse_for_ecosystem(eco, s),
2240 Version::parse_lenient(s),
2241 "{eco:?} / {s}"
2242 );
2243 }
2244 }
2245 }
2246
2247 #[test]
2248 fn unknown_ecosystem_orders_exactly_like_the_string_only_path() {
2249 for eco in [None, Some("npm"), Some("cargo"), Some("golang")] {
2250 for a in ECOSYSTEM_CORPUS {
2251 for b in ECOSYSTEM_CORPUS {
2252 assert_eq!(
2253 compare_versions_for_ecosystem(eco, a, b),
2254 compare_versions(a, b),
2255 "{eco:?} / {a} vs {b}"
2256 );
2257 assert_eq!(
2258 is_version_downgrade_for_ecosystem(eco, a, b),
2259 is_version_downgrade(a, b),
2260 "{eco:?} / {a} -> {b}"
2261 );
2262 }
2263 }
2264 }
2265 }
2266
2267 #[test]
2268 fn semver_prereleases_keep_their_semver_reading() {
2269 for eco in [None, Some("npm"), Some("cargo")] {
2270 assert_eq!(
2271 compare_versions_for_ecosystem(eco, "1.0.0-alpha.1", "1.0.0"),
2272 Some(Ordering::Less),
2273 "{eco:?}"
2274 );
2275 assert!(!is_version_downgrade_for_ecosystem(
2276 eco,
2277 "1.0.0-alpha.1",
2278 "1.0.0"
2279 ));
2280 assert!(is_version_downgrade_for_ecosystem(
2281 eco,
2282 "1.0.0",
2283 "1.0.0-alpha.1"
2284 ));
2285 }
2286 }
2287
2288 #[test]
2289 fn deb_ecosystem_parses_as_deb() {
2290 assert_eq!(
2291 Version::parse_for_ecosystem(Some("deb"), "1.2.3-1ubuntu2"),
2292 Version::Deb {
2293 epoch: 0,
2294 upstream: "1.2.3".into(),
2295 revision: "1ubuntu2".into(),
2296 }
2297 );
2298 assert!(matches!(
2299 Version::parse_for_ecosystem(Some("deb"), "1.2.3"),
2300 Version::Deb { .. }
2301 ));
2302 assert!(matches!(
2303 Version::parse_for_ecosystem(Some("deb"), "1.0.0-alpha.1"),
2304 Version::Deb { .. }
2305 ));
2306 }
2307
2308 #[test]
2309 fn deb_ecosystem_match_ignores_case() {
2310 assert_eq!(
2311 Version::parse_for_ecosystem(Some("DEB"), "1.2.3-1ubuntu2"),
2312 Version::parse_for_ecosystem(Some("deb"), "1.2.3-1ubuntu2")
2313 );
2314 }
2315
2316 #[test]
2317 fn deb_ecosystem_does_not_retry_a_non_deb_string_as_semver() {
2318 assert_eq!(
2319 Version::parse_for_ecosystem(Some("deb"), "4.4.2-2.el7_9"),
2320 Version::Opaque("4.4.2-2.el7_9".into())
2321 );
2322 assert!(matches!(
2323 Version::parse_for_ecosystem(Some("deb"), "v1.2.3-1ubuntu2"),
2324 Version::Deb { .. }
2325 ));
2326 }
2327
2328 #[test]
2330 fn deb_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
2331 use Ordering::{Greater, Less};
2332
2333 for (a, b, expected) in [
2334 ("v1.2.3", "v1.2.4", Less),
2335 ("v1.2.10", "v1.2.9", Greater),
2336 ("V1.2.3", "V1.2.4", Less),
2337 ("v1.2.3-1ubuntu2", "v1.2.3-2", Less),
2338 ] {
2339 assert_eq!(
2340 compare_versions_for_ecosystem(Some("deb"), a, b),
2341 Some(expected),
2342 "{a} vs {b}"
2343 );
2344 assert_eq!(
2345 compare_versions_for_ecosystem(Some("deb"), b, a),
2346 Some(expected.reverse()),
2347 "{b} vs {a}"
2348 );
2349 }
2350
2351 assert!(is_version_downgrade_for_ecosystem(
2352 Some("deb"),
2353 "v1.2.4",
2354 "v1.2.3"
2355 ));
2356 }
2357
2358 #[test]
2360 fn deb_ecosystem_orders_revisions_the_way_dpkg_does() {
2361 use Ordering::{Greater, Less};
2362
2363 for (a, b, expected) in [
2364 ("1.2.3-1ubuntu2", "1.2.3-2", Less),
2365 ("1.2.3-1build1", "1.2.3-2", Less),
2366 ("1.2.3-2ubuntu0.1", "1.2.3-3", Less),
2367 ("1.2.3-1+deb11u1", "1.2.3-2", Less),
2368 ("1.2.3-1+deb11u1", "1.2.3-1+deb11u2", Less),
2369 ("1.2.3-1ubuntu2", "1.2.3-1ubuntu1", Greater),
2370 ("1.2.3-1", "1.2.3-10", Less),
2371 ("1.2.3", "1.2.3-1", Less),
2372 ("1.0-1", "1.0", Greater),
2373 ("1.0~rc1", "1.0", Less),
2374 ("1.0~rc1-1", "1.0-1", Less),
2375 ("1.2.3-1~bpo11+1", "1.2.3-1", Less),
2376 ("2:1.0-1", "10.0-1", Greater),
2377 ("1.1.1n-0+deb11u5", "1.1.1o-1", Less),
2378 ] {
2379 assert_eq!(
2380 compare_versions_for_ecosystem(Some("deb"), a, b),
2381 Some(expected),
2382 "{a} vs {b}"
2383 );
2384 assert_eq!(
2385 compare_versions_for_ecosystem(Some("deb"), b, a),
2386 Some(expected.reverse()),
2387 "{b} vs {a}"
2388 );
2389 }
2390 }
2391
2392 #[test]
2393 fn deb_ecosystem_clears_the_false_downgrade_the_string_only_path_reports() {
2394 assert!(!is_version_downgrade_for_ecosystem(
2395 Some("deb"),
2396 "1.2.3-1ubuntu2",
2397 "1.2.3-2"
2398 ));
2399 assert!(is_version_downgrade("1.2.3-1ubuntu2", "1.2.3-2"));
2400 assert!(is_version_downgrade_for_ecosystem(
2401 Some("deb"),
2402 "1.2.3-2",
2403 "1.2.3-1ubuntu2"
2404 ));
2405 }
2406
2407 #[test]
2408 fn deb_ecosystem_catches_the_downgrades_the_string_only_path_passed() {
2409 for (old, new) in [
2410 ("1.2.3-2", "1.2.3-1ubuntu2"),
2411 ("1.2.3-3", "1.2.3-2ubuntu0.1"),
2412 ("1.2.3-1+deb11u2", "1.2.3-1+deb11u1"),
2413 ] {
2414 assert!(!is_version_downgrade(old, new), "{old} -> {new}");
2415 assert!(
2416 is_version_downgrade_for_ecosystem(Some("deb"), old, new),
2417 "{old} -> {new}"
2418 );
2419 }
2420 }
2421
2422 #[test]
2423 fn deb_ecosystem_orders_plus_revisions_the_string_only_path_read_as_equal() {
2424 assert_eq!(
2425 compare_versions("1.2.3-1+deb11u1", "1.2.3-1+deb11u2"),
2426 Some(Ordering::Equal)
2427 );
2428 assert_eq!(
2429 compare_versions_for_ecosystem(Some("deb"), "1.2.3-1+deb11u1", "1.2.3-1+deb11u2"),
2430 Some(Ordering::Less)
2431 );
2432 }
2433
2434 #[test]
2435 fn deb_ecosystem_makes_previously_uncomparable_pairs_comparable() {
2436 for (a, b) in [("1.0-1", "1.0"), ("1.0~rc1", "1.0"), ("1.0.2a", "1.0.2")] {
2437 assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
2438 assert!(
2439 compare_versions_for_ecosystem(Some("deb"), a, b).is_some(),
2440 "{a} vs {b}"
2441 );
2442 }
2443 }
2444
2445 #[test]
2448 fn rpmvercmp_upstream_vectors() {
2449 use Ordering::{Equal, Greater, Less};
2450
2451 for (a, b, expected) in [
2452 ("1.0", "1.0", Equal),
2453 ("1.0", "2.0", Less),
2454 ("2.0", "1.0", Greater),
2455 ("2.0.1", "2.0.1", Equal),
2456 ("2.0", "2.0.1", Less),
2457 ("2.0.1", "2.0", Greater),
2458 ("2.0.1a", "2.0.1a", Equal),
2459 ("2.0.1a", "2.0.1", Greater),
2460 ("2.0.1", "2.0.1a", Less),
2461 ("5.5p1", "5.5p1", Equal),
2462 ("5.5p1", "5.5p2", Less),
2463 ("5.5p2", "5.5p1", Greater),
2464 ("5.5p10", "5.5p10", Equal),
2465 ("5.5p1", "5.5p10", Less),
2466 ("5.5p10", "5.5p1", Greater),
2467 ("10xyz", "10.1xyz", Less),
2468 ("10.1xyz", "10xyz", Greater),
2469 ("xyz10", "xyz10", Equal),
2470 ("xyz10", "xyz10.1", Less),
2471 ("xyz10.1", "xyz10", Greater),
2472 ("xyz.4", "xyz.4", Equal),
2473 ("xyz.4", "8", Less),
2474 ("8", "xyz.4", Greater),
2475 ("xyz.4", "2", Less),
2476 ("2", "xyz.4", Greater),
2477 ("5.5p2", "5.6p1", Less),
2478 ("5.6p1", "5.5p2", Greater),
2479 ("5.6p1", "6.5p1", Less),
2480 ("6.5p1", "5.6p1", Greater),
2481 ("6.0.rc1", "6.0", Greater),
2482 ("6.0", "6.0.rc1", Less),
2483 ("10b2", "10a1", Greater),
2484 ("10a2", "10b2", Less),
2485 ("1.0aa", "1.0aa", Equal),
2486 ("1.0a", "1.0aa", Less),
2487 ("1.0aa", "1.0a", Greater),
2488 ("10.0001", "10.0001", Equal),
2489 ("10.0001", "10.1", Equal),
2490 ("10.1", "10.0001", Equal),
2491 ("10.0001", "10.0039", Less),
2492 ("10.0039", "10.0001", Greater),
2493 ("4.999.9", "5.0", Less),
2494 ("5.0", "4.999.9", Greater),
2495 ("20101121", "20101121", Equal),
2496 ("20101121", "20101122", Less),
2497 ("20101122", "20101121", Greater),
2498 ("2_0", "2_0", Equal),
2499 ("2.0", "2_0", Equal),
2500 ("2_0", "2.0", Equal),
2501 ("a", "a", Equal),
2502 ("a+", "a+", Equal),
2503 ("a+", "a_", Equal),
2504 ("a_", "a+", Equal),
2505 ("+a", "+a", Equal),
2506 ("+a", "_a", Equal),
2507 ("_a", "+a", Equal),
2508 ("+_", "+_", Equal),
2509 ("_+", "+_", Equal),
2510 ("_+", "_+", Equal),
2511 ("+", "_", Equal),
2512 ("_", "+", Equal),
2513 ("1.0~rc1", "1.0~rc1", Equal),
2514 ("1.0~rc1", "1.0", Less),
2515 ("1.0", "1.0~rc1", Greater),
2516 ("1.0~rc1", "1.0~rc2", Less),
2517 ("1.0~rc2", "1.0~rc1", Greater),
2518 ("1.0~rc1~git123", "1.0~rc1~git123", Equal),
2519 ("1.0~rc1~git123", "1.0~rc1", Less),
2520 ("1.0~rc1", "1.0~rc1~git123", Greater),
2521 ("1.0^", "1.0^", Equal),
2522 ("1.0^", "1.0", Greater),
2523 ("1.0", "1.0^", Less),
2524 ("1.0^git1", "1.0^git1", Equal),
2525 ("1.0^git1", "1.0", Greater),
2526 ("1.0", "1.0^git1", Less),
2527 ("1.0^git1", "1.0^git2", Less),
2528 ("1.0^git2", "1.0^git1", Greater),
2529 ("1.0^git1", "1.01", Less),
2530 ("1.01", "1.0^git1", Greater),
2531 ("1.0^20160101", "1.0^20160101", Equal),
2532 ("1.0^20160101", "1.0.1", Less),
2533 ("1.0.1", "1.0^20160101", Greater),
2534 ("1.0^20160101^git1", "1.0^20160101^git1", Equal),
2535 ("1.0^20160102", "1.0^20160101^git1", Greater),
2536 ("1.0^20160101^git1", "1.0^20160102", Less),
2537 ("1.0~rc1^git1", "1.0~rc1^git1", Equal),
2538 ("1.0~rc1^git1", "1.0~rc1", Greater),
2539 ("1.0~rc1", "1.0~rc1^git1", Less),
2540 ("1.0^git1~pre", "1.0^git1~pre", Equal),
2541 ("1.0^git1", "1.0^git1~pre", Greater),
2542 ("1.0^git1~pre", "1.0^git1", Less),
2543 ("1b.fc17", "1b.fc17", Equal),
2546 ("1b.fc17", "1.fc17", Less),
2547 ("1.fc17", "1b.fc17", Greater),
2548 ("1g.fc17", "1g.fc17", Equal),
2549 ("1g.fc17", "1.fc17", Greater),
2550 ("1.fc17", "1g.fc17", Less),
2551 ("1.1.α", "1.1.α", Equal),
2553 ("1.1.α", "1.1.β", Equal),
2554 ("1.1.β", "1.1.α", Equal),
2555 ("1.1.αα", "1.1.α", Equal),
2556 ("1.1.α", "1.1.ββ", Equal),
2557 ("1.1.ββ", "1.1.αα", Equal),
2558 ] {
2559 assert_eq!(rpmvercmp(a, b), expected, "{a} vs {b}");
2560 }
2561 }
2562
2563 #[test]
2566 fn rpm_ecosystem_orders_epoch_then_version_then_release() {
2567 use Ordering::{Equal, Greater, Less};
2568
2569 for (a, b, expected) in [
2570 ("2:1.0-1", "1:9.9-9", Greater),
2571 ("1.0-1", "0:1.0-1", Equal),
2572 ("1.1-1", "1.0-9", Greater),
2573 ("1.0-2", "1.0-10", Less),
2574 ("1.0-1.el8", "1.0-1.el9", Less),
2575 ("1.0-0", "1.0-1", Less),
2576 ("1.0", "1.0-0", Less),
2578 ] {
2579 assert_eq!(
2580 compare_versions_for_ecosystem(Some("rpm"), a, b),
2581 Some(expected),
2582 "{a} vs {b}"
2583 );
2584 assert_eq!(
2585 compare_versions_for_ecosystem(Some("rpm"), b, a),
2586 Some(expected.reverse()),
2587 "{b} vs {a}"
2588 );
2589 }
2590 }
2591
2592 #[test]
2595 fn rpm_and_deb_disagree_on_ordinary_versions() {
2596 use Ordering::{Equal, Greater, Less};
2597
2598 for (a, b, deb, rpm) in [
2599 ("1.a", "1.1", Some(Greater), Some(Less)),
2602 ("1.fc35", "1.1", Some(Greater), Some(Less)),
2603 ("1.0", "1_0", None, Some(Equal)),
2605 ("1.0^20200101gitabc", "1.0", None, Some(Greater)),
2607 ("4.4.2-2.el7_9", "4.4.2-3.el7_9", None, Some(Less)),
2609 ] {
2610 assert_eq!(
2611 compare_versions_for_ecosystem(Some("deb"), a, b),
2612 deb,
2613 "deb: {a} vs {b}"
2614 );
2615 assert_eq!(
2616 compare_versions_for_ecosystem(Some("rpm"), a, b),
2617 rpm,
2618 "rpm: {a} vs {b}"
2619 );
2620 }
2621 }
2622
2623 #[test]
2624 fn rpm_ecosystem_reverses_a_gate_the_deb_rules_fire_backwards() {
2625 assert!(is_version_downgrade_for_ecosystem(
2626 Some("deb"),
2627 "1.a",
2628 "1.1"
2629 ));
2630 assert!(!is_version_downgrade_for_ecosystem(
2631 Some("rpm"),
2632 "1.a",
2633 "1.1"
2634 ));
2635 assert!(is_version_downgrade_for_ecosystem(
2636 Some("rpm"),
2637 "1.1",
2638 "1.a"
2639 ));
2640 }
2641
2642 #[test]
2643 fn rpm_ecosystem_parses_as_rpm() {
2644 assert_eq!(
2645 Version::parse_for_ecosystem(Some("rpm"), "5.1.8-2.fc35"),
2646 Version::Rpm {
2647 epoch: 0,
2648 version: "5.1.8".into(),
2649 release: "2.fc35".into(),
2650 }
2651 );
2652 assert_eq!(
2653 Version::parse_for_ecosystem(Some("rpm"), "1:2.36.1-2.fc35"),
2654 Version::Rpm {
2655 epoch: 1,
2656 version: "2.36.1".into(),
2657 release: "2.fc35".into(),
2658 }
2659 );
2660 assert_eq!(
2661 Version::parse_for_ecosystem(Some("rpm"), "4.4.2-2.el7_9"),
2662 Version::Rpm {
2663 epoch: 0,
2664 version: "4.4.2".into(),
2665 release: "2.el7_9".into(),
2666 }
2667 );
2668 assert!(matches!(
2669 Version::parse_for_ecosystem(Some("rpm"), "1.2.3"),
2670 Version::Rpm { .. }
2671 ));
2672 assert!(matches!(
2673 Version::parse_for_ecosystem(Some("rpm"), "1.0.0-alpha.1"),
2674 Version::Rpm { .. }
2675 ));
2676 }
2677
2678 #[test]
2679 fn rpm_ecosystem_match_ignores_case() {
2680 assert_eq!(
2681 Version::parse_for_ecosystem(Some("RPM"), "5.1.8-2.fc35"),
2682 Version::parse_for_ecosystem(Some("rpm"), "5.1.8-2.fc35")
2683 );
2684 }
2685
2686 #[test]
2687 fn rpm_ecosystem_keeps_codenames_and_hashes_opaque() {
2688 for s in ["deadbeef", "focal", "", "stable", "1.0 "] {
2689 assert_eq!(
2690 Version::parse_for_ecosystem(Some("rpm"), s),
2691 Version::Opaque(s.to_string()),
2692 "{s}"
2693 );
2694 }
2695 }
2696
2697 #[test]
2698 fn rpm_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
2699 assert!(matches!(
2700 Version::parse_for_ecosystem(Some("rpm"), "v1.2.3-1"),
2701 Version::Rpm { .. }
2702 ));
2703 assert_eq!(
2704 compare_versions_for_ecosystem(Some("rpm"), "v1.2.3", "v1.2.4"),
2705 Some(Ordering::Less)
2706 );
2707 }
2708
2709 #[test]
2710 fn rpm_stays_uncomparable_against_every_other_parse_result() {
2711 let rpm = Version::parse_for_ecosystem(Some("rpm"), "1.2.3-1");
2712 for other in [
2713 Version::parse_for_ecosystem(Some("deb"), "1.2.3-1"),
2714 Version::parse_lenient("1.2.3"),
2715 Version::parse_lenient("2024.01.15"),
2716 Version::parse_lenient("4.2.0rc1"),
2717 Version::parse_lenient("deadbeef"),
2718 ] {
2719 assert_eq!(rpm.partial_cmp_lenient(&other), None, "{other:?}");
2720 assert_eq!(other.partial_cmp_lenient(&rpm), None, "{other:?}");
2721 }
2722 }
2723
2724 #[test]
2725 fn parse_lenient_never_produces_the_rpm_variant() {
2726 for s in
2727 ECOSYSTEM_CORPUS
2728 .iter()
2729 .copied()
2730 .chain(["1.0^20200101", "1_0", "1.a", "5.1.8-2.fc35"])
2731 {
2732 assert!(
2733 !matches!(Version::parse_lenient(s), Version::Rpm { .. }),
2734 "{s}"
2735 );
2736 }
2737 }
2738
2739 #[test]
2742 fn maven_documented_ordering_examples() {
2743 use Ordering::{Equal, Greater, Less};
2744
2745 for (a, b, expected) in [
2746 ("1", "1.1", Less),
2747 ("1-snapshot", "1", Less),
2748 ("1", "1-sp", Less),
2749 ("1-foo2", "1-foo10", Less),
2750 ("1.foo", "1-foo", Equal),
2751 ("1-foo", "1-1", Less),
2752 ("1-1", "1.1", Less),
2753 ("1.ga", "1-ga", Equal),
2754 ("1-ga", "1-0", Equal),
2755 ("1-0", "1_0", Equal),
2756 ("1_0", "1.0", Equal),
2757 ("1.0", "1", Equal),
2758 ("1-sp", "1-ga", Greater),
2759 ("1-sp.1", "1-ga.1", Greater),
2760 ("1-sp-1", "1-ga-1", Less),
2761 ("1-a1", "1-alpha-1", Equal),
2762 ("1.0-alpha1", "1.0-ALPHA1", Equal),
2763 ("1.7", "1.K", Greater),
2764 ("5.zebra", "5.aardvark", Greater),
2765 ("1.α", "1.b", Greater),
2766 ] {
2767 assert_eq!(maven_cmp(a, b), expected, "{a} vs {b}");
2768 assert_eq!(maven_cmp(b, a), expected.reverse(), "{b} vs {a}");
2769 }
2770 }
2771
2772 #[test]
2775 fn maven_documented_splitting_and_trimming_examples() {
2776 for (version, reduced) in [
2777 ("1-1.foo-bar1baz-.1", "1-1.foo-bar-1-baz-0.1"),
2778 ("1.0.0", "1"),
2779 ("1.ga", "1"),
2780 ("1.final", "1"),
2781 ("1.0", "1"),
2782 ("1.", "1"),
2783 ("1-", "1"),
2784 ("1_", "1"),
2785 ("1.0.0-foo.0.0", "1-foo"),
2786 ("1.0.0-0.0.0", "1"),
2787 ] {
2788 assert_eq!(maven_parse(version), maven_parse(reduced), "{version}");
2789 }
2790 }
2791
2792 #[test]
2793 fn maven_ranks_qualifiers_in_the_documented_order() {
2794 let ascending = [
2795 "1-alpha",
2796 "1-beta",
2797 "1-milestone",
2798 "1-rc",
2799 "1-snapshot",
2800 "1",
2801 "1-sp",
2802 ];
2803
2804 for (i, a) in ascending.iter().enumerate() {
2805 for b in &ascending[i + 1..] {
2806 assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
2807 assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
2808 }
2809 assert_eq!(maven_cmp(a, "1-zzz"), Ordering::Less, "{a} vs 1-zzz");
2811 }
2812 assert_eq!(maven_cmp("1-zzz", "1-aaa"), Ordering::Greater);
2813 }
2814
2815 #[test]
2816 fn maven_folds_qualifier_aliases() {
2817 for (a, b) in [
2818 ("1-cr", "1-rc"),
2819 ("1-cr1", "1-rc1"),
2820 ("1-ga", "1"),
2821 ("1-final", "1"),
2822 ("1-release", "1"),
2823 ("1-a1", "1-alpha1"),
2824 ("1-b2", "1-beta2"),
2825 ("1-m3", "1-milestone3"),
2826 ("1-RC1", "1-rc1"),
2827 ] {
2828 assert_eq!(maven_cmp(a, b), Ordering::Equal, "{a} vs {b}");
2829 }
2830
2831 assert_eq!(maven_cmp("1-a", "1-alpha"), Ordering::Greater);
2833 assert_eq!(maven_cmp("1-a.1", "1-alpha.1"), Ordering::Greater);
2834 }
2835
2836 #[test]
2837 fn maven_folds_a_dotted_qualifier_to_the_hyphenated_form() {
2838 for (a, b) in [
2839 ("1.0.0.CR1", "1.0.0-RC1"),
2840 ("1.0.0.Final", "1.0.0"),
2841 ("1.0.0.GA", "1.0.0-ga"),
2842 ("2.0.0.Final", "2.0.0-Final"),
2843 ("1.0.0.Alpha1", "1.0.0-a1"),
2844 ("3.1.0.RELEASE", "3.1.0"),
2845 ] {
2846 assert_eq!(maven_cmp(a, b), Ordering::Equal, "{a} vs {b}");
2847 }
2848
2849 for (a, b) in [
2850 ("1.0.0.CR1", "1.0.0-CR2"),
2851 ("1.0.0.Alpha1", "1.0.0-RC1"),
2852 ("1.0.0.CR1", "1.0.0"),
2853 ("2.0.a", "2-1"),
2854 ("3.1.0.M1", "3.1.0-RC1"),
2855 ("1.0.0.Beta1", "1.0.0.CR1"),
2856 ] {
2857 assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
2858 assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
2859 }
2860 }
2861
2862 #[test]
2865 fn maven_orders_the_published_dotted_ladders() {
2866 for ladder in [
2867 &[
2868 "1.0.0.Alpha1",
2869 "1.0.0-Beta1",
2870 "1.0.0.CR1",
2871 "1.0.0-CR2",
2872 "1.0.0.Final",
2873 ][..],
2874 &[
2875 "3.1.0.M1",
2876 "3.1.0-M2",
2877 "3.1.0.RC1",
2878 "3.1.0-RELEASE",
2879 "3.1.1.RELEASE",
2880 ][..],
2881 &["5.4.2.Final", "5.4.10.Final", "5.5.0.Alpha1", "5.5.0.Final"][..],
2882 &["4.1.9.Final", "4.1.65.Final", "4.1.65.1.Final"][..],
2883 ] {
2884 for (i, a) in ladder.iter().enumerate() {
2885 for b in &ladder[i + 1..] {
2886 assert_eq!(maven_cmp(a, b), Ordering::Less, "{a} vs {b}");
2887 assert_eq!(maven_cmp(b, a), Ordering::Greater, "{b} vs {a}");
2888 }
2889 }
2890 }
2891 }
2892
2893 #[test]
2894 fn maven_nests_a_qualifier_reached_past_an_item() {
2895 assert_eq!(maven_cmp("1-0.alpha", "1-alpha"), Ordering::Greater);
2896 assert_eq!(maven_cmp("1-0.beta", "1-0.alpha"), Ordering::Greater);
2897 assert_eq!(maven_cmp("1-0.alpha", "1-1"), Ordering::Less);
2898 assert_eq!(maven_cmp("1-0.alpha", "1"), Ordering::Less);
2899 }
2900
2901 #[test]
2902 fn maven_declines_a_version_nested_past_the_depth_cap() {
2903 let ok = format!("1{}", "-1".repeat(MAVEN_MAX_DEPTH - 2));
2904 let deep = format!("1{}", "-1".repeat(MAVEN_MAX_DEPTH));
2905
2906 assert!(super::maven_parse(&ok).is_some());
2907 assert!(super::maven_parse(&deep).is_none());
2908 assert_eq!(
2909 Version::parse_for_ecosystem(Some("maven"), &ok),
2910 Version::Maven(ok)
2911 );
2912 assert_eq!(
2913 Version::parse_for_ecosystem(Some("maven"), &deep),
2914 Version::Opaque(deep)
2915 );
2916 }
2917
2918 #[test]
2919 fn maven_declines_a_version_that_would_overflow_the_stack() {
2920 for deep in [format!("1{}", "-1".repeat(200_000)), "1a".repeat(200_000)] {
2921 assert_eq!(
2922 Version::parse_for_ecosystem(Some("maven"), &deep),
2923 Version::Opaque(deep.clone())
2924 );
2925 assert_eq!(
2926 Version::Maven(deep.clone()).partial_cmp_lenient(&Version::Maven(deep)),
2927 None
2928 );
2929 }
2930 }
2931
2932 #[test]
2933 fn maven_ordering_is_antisymmetric_and_transitive_on_the_documented_vectors() {
2934 const CORPUS: &[&str] = &[
2935 "1",
2936 "1.0",
2937 "1.1",
2938 "1-1",
2939 "1.foo",
2940 "1-foo",
2941 "1.bar",
2942 "1-bar",
2943 "1-alpha",
2944 "1-a1",
2945 "1-beta",
2946 "1-milestone",
2947 "1-rc",
2948 "1-cr",
2949 "1-snapshot",
2950 "1-ga",
2951 "1-sp",
2952 "1-sp.1",
2953 "1-sp-1",
2954 "1-ga-1",
2955 "1.0.0-foo.0.0",
2956 "1_0",
2957 "2",
2958 "1.0.1",
2959 "1.0-alpha1",
2960 ];
2961
2962 for a in CORPUS {
2963 for b in CORPUS {
2964 assert_eq!(
2965 maven_cmp(a, b),
2966 maven_cmp(b, a).reverse(),
2967 "asymmetric: {a} vs {b}"
2968 );
2969 for c in CORPUS {
2970 let (ab, bc) = (maven_cmp(a, b), maven_cmp(b, c));
2971 if ab == bc || bc == Ordering::Equal {
2972 assert_eq!(maven_cmp(a, c), ab, "intransitive: {a}, {b}, {c}");
2973 }
2974 }
2975 }
2976 }
2977 }
2978
2979 #[test]
2980 fn maven_compares_numeric_tokens_beyond_u64() {
2981 assert_eq!(
2982 maven_cmp("1.99999999999999999999999", "1.99999999999999999999998"),
2983 Ordering::Greater
2984 );
2985 assert_eq!(maven_cmp("1.0000000000000000000001", "1.2"), Ordering::Less);
2986 }
2987
2988 #[test]
2991 fn maven_ecosystem_orders_a_snapshot_against_its_release() {
2992 assert_eq!(compare_versions("1.0-SNAPSHOT", "1.0"), None);
2993
2994 assert_eq!(
2995 compare_versions_for_ecosystem(Some("maven"), "1.0-SNAPSHOT", "1.0"),
2996 Some(Ordering::Less)
2997 );
2998 assert!(!is_version_downgrade_for_ecosystem(
2999 Some("maven"),
3000 "1.0-SNAPSHOT",
3001 "1.0"
3002 ));
3003 assert!(is_version_downgrade_for_ecosystem(
3004 Some("maven"),
3005 "1.0",
3006 "1.0-SNAPSHOT"
3007 ));
3008 }
3009
3010 #[test]
3011 fn maven_ecosystem_makes_previously_uncomparable_pairs_comparable() {
3012 use Ordering::{Equal, Greater, Less};
3013
3014 for (a, b, expected) in [
3015 ("1.0-SNAPSHOT", "1.0", Less),
3016 ("1.0-M1", "1.0", Less),
3017 ("1.0-sp1", "1.0", Greater),
3018 ("2.0.0.Final", "2.0.0", Equal),
3020 ("1.0-cr1", "1.0-rc1", Equal),
3021 ("1.7.0_80", "1.7.0_79", Greater),
3023 ] {
3024 assert_eq!(compare_versions(a, b), None, "{a} vs {b}");
3025 assert_eq!(
3026 compare_versions_for_ecosystem(Some("maven"), a, b),
3027 Some(expected),
3028 "{a} vs {b}"
3029 );
3030 assert_eq!(
3031 compare_versions_for_ecosystem(Some("maven"), b, a),
3032 Some(expected.reverse()),
3033 "{b} vs {a}"
3034 );
3035 }
3036 }
3037
3038 #[test]
3039 fn maven_ecosystem_reverses_a_gate_the_string_only_path_fires_backwards() {
3040 assert_eq!(
3043 compare_versions("1.0-Final", "1.0-SNAPSHOT"),
3044 Some(Ordering::Less)
3045 );
3046 assert!(is_version_downgrade("1.0-SNAPSHOT", "1.0-Final"));
3047 assert!(!is_version_downgrade("1.0-Final", "1.0-SNAPSHOT"));
3048
3049 assert_eq!(
3050 compare_versions_for_ecosystem(Some("maven"), "1.0-Final", "1.0-SNAPSHOT"),
3051 Some(Ordering::Greater)
3052 );
3053 assert!(!is_version_downgrade_for_ecosystem(
3054 Some("maven"),
3055 "1.0-SNAPSHOT",
3056 "1.0-Final"
3057 ));
3058 assert!(is_version_downgrade_for_ecosystem(
3059 Some("maven"),
3060 "1.0-Final",
3061 "1.0-SNAPSHOT"
3062 ));
3063 }
3064
3065 #[test]
3066 fn maven_ecosystem_parses_as_maven() {
3067 assert_eq!(
3068 Version::parse_for_ecosystem(Some("maven"), "1.0-SNAPSHOT"),
3069 Version::Maven("1.0-SNAPSHOT".into())
3070 );
3071 for s in [
3072 "1.2.3",
3073 "1.0.0-alpha.1",
3074 "2.0.0.Final",
3075 "1.7.0_80",
3076 "1.0+b1",
3077 ] {
3078 assert!(
3079 matches!(
3080 Version::parse_for_ecosystem(Some("maven"), s),
3081 Version::Maven(_)
3082 ),
3083 "{s}"
3084 );
3085 }
3086 }
3087
3088 #[test]
3089 fn maven_ecosystem_match_ignores_case() {
3090 assert_eq!(
3091 Version::parse_for_ecosystem(Some("MAVEN"), "1.0-SNAPSHOT"),
3092 Version::parse_for_ecosystem(Some("maven"), "1.0-SNAPSHOT")
3093 );
3094 }
3095
3096 #[test]
3097 fn maven_ecosystem_keeps_codenames_and_hashes_opaque() {
3098 for s in [
3099 "deadbeef",
3100 "RELEASE",
3101 "LATEST",
3102 "",
3103 "master-SNAPSHOT",
3104 "1.0 ",
3105 ] {
3106 assert_eq!(
3107 Version::parse_for_ecosystem(Some("maven"), s),
3108 Version::Opaque(s.to_string()),
3109 "{s}"
3110 );
3111 }
3112 }
3113
3114 #[test]
3115 fn maven_ecosystem_strips_a_v_prefix_instead_of_skipping_the_pair() {
3116 assert!(matches!(
3117 Version::parse_for_ecosystem(Some("maven"), "v1.2.3"),
3118 Version::Maven(_)
3119 ));
3120 assert_eq!(
3121 compare_versions_for_ecosystem(Some("maven"), "v1.2.3", "v1.2.4"),
3122 Some(Ordering::Less)
3123 );
3124 }
3125
3126 #[test]
3127 fn maven_stays_uncomparable_against_every_other_parse_result() {
3128 let maven = Version::parse_for_ecosystem(Some("maven"), "1.2.3-1");
3129 for other in [
3130 Version::parse_for_ecosystem(Some("deb"), "1.2.3-1"),
3131 Version::parse_for_ecosystem(Some("rpm"), "1.2.3-1"),
3132 Version::parse_lenient("1.2.3"),
3133 Version::parse_lenient("2024.01.15"),
3134 Version::parse_lenient("4.2.0rc1"),
3135 Version::parse_lenient("deadbeef"),
3136 ] {
3137 assert_eq!(maven.partial_cmp_lenient(&other), None, "{other:?}");
3138 assert_eq!(other.partial_cmp_lenient(&maven), None, "{other:?}");
3139 }
3140 }
3141
3142 #[test]
3143 fn parse_lenient_never_produces_the_maven_variant() {
3144 for s in ECOSYSTEM_CORPUS.iter().copied().chain([
3145 "1.0-SNAPSHOT",
3146 "2.0.0.Final",
3147 "1.7.0_80",
3148 "1-sp",
3149 ]) {
3150 assert!(
3151 !matches!(Version::parse_lenient(s), Version::Maven(_)),
3152 "{s}"
3153 );
3154 }
3155 }
3156}