use std::cmp::Ordering;
pub fn rpmvercmp(a: &str, b: &str) -> Ordering {
let a_segs = segments(a);
let b_segs = segments(b);
for (sa, sb) in a_segs.iter().zip(b_segs.iter()) {
let is_a_num = sa.chars().next().is_some_and(|c| c.is_ascii_digit());
let is_b_num = sb.chars().next().is_some_and(|c| c.is_ascii_digit());
match (is_a_num, is_b_num) {
(true, false) => return Ordering::Greater,
(false, true) => return Ordering::Less,
(true, true) => {
let a_trimmed = sa.trim_start_matches('0');
let b_trimmed = sb.trim_start_matches('0');
match a_trimmed.len().cmp(&b_trimmed.len()) {
Ordering::Equal => match a_trimmed.cmp(b_trimmed) {
Ordering::Equal => continue,
ord => return ord,
},
ord => return ord,
}
}
(false, false) => match sa.cmp(sb) {
Ordering::Equal => continue,
ord => return ord,
},
}
}
a_segs.len().cmp(&b_segs.len())
}
fn segments(s: &str) -> Vec<&str> {
let mut segs = Vec::new();
let mut chars = s.as_bytes();
while !chars.is_empty() {
let skip = chars
.iter()
.position(|c| c.is_ascii_alphanumeric())
.unwrap_or(chars.len());
chars = &chars[skip..];
if chars.is_empty() {
break;
}
let is_digit = chars[0].is_ascii_digit();
let len = chars
.iter()
.position(|c| {
if is_digit {
!c.is_ascii_digit()
} else {
!c.is_ascii_alphabetic()
}
})
.unwrap_or(chars.len());
segs.push(std::str::from_utf8(&chars[..len]).unwrap());
chars = &chars[len..];
}
segs
}
pub fn compare_evr(a: &str, b: &str) -> Ordering {
let (a_epoch, a_ver, a_rel) = parse_evr(a);
let (b_epoch, b_ver, b_rel) = parse_evr(b);
match a_epoch.cmp(&b_epoch) {
Ordering::Equal => {}
ord => return ord,
}
match rpmvercmp(a_ver, b_ver) {
Ordering::Equal => {}
ord => return ord,
}
match (a_rel, b_rel) {
(Some(ar), Some(br)) => rpmvercmp(ar, br),
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
}
}
fn parse_evr(evr: &str) -> (u64, &str, Option<&str>) {
let (epoch, rest) = match evr.split_once(':') {
Some((e, r)) => (e.parse::<u64>().unwrap_or(0), r),
None => (0, evr),
};
let (version, release) = match rest.rsplit_once('-') {
Some((v, r)) => (v, Some(r)),
None => (rest, None),
};
(epoch, version, release)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rpmvercmp_equal() {
assert_eq!(rpmvercmp("1.0", "1.0"), Ordering::Equal);
}
#[test]
fn test_rpmvercmp_numeric() {
assert_eq!(rpmvercmp("1.1", "1.2"), Ordering::Less);
assert_eq!(rpmvercmp("1.2", "1.1"), Ordering::Greater);
}
#[test]
fn test_rpmvercmp_longer_numeric() {
assert_eq!(rpmvercmp("1.10", "1.9"), Ordering::Greater);
assert_eq!(rpmvercmp("1.9", "1.10"), Ordering::Less);
}
#[test]
fn test_rpmvercmp_alpha_vs_numeric() {
assert_eq!(rpmvercmp("1.0a", "1.01"), Ordering::Less);
assert_eq!(rpmvercmp("1.01", "1.0a"), Ordering::Greater);
}
#[test]
fn test_rpmvercmp_alpha() {
assert_eq!(rpmvercmp("1.0a", "1.0b"), Ordering::Less);
assert_eq!(rpmvercmp("1.0b", "1.0a"), Ordering::Greater);
}
#[test]
fn test_rpmvercmp_more_segments() {
assert_eq!(rpmvercmp("1.0.0", "1.0"), Ordering::Greater);
assert_eq!(rpmvercmp("1.0", "1.0.0"), Ordering::Less);
}
#[test]
fn test_rpmvercmp_leading_zeros() {
assert_eq!(rpmvercmp("1.01", "1.1"), Ordering::Equal);
}
#[test]
fn test_rpmvercmp_tilde_separators() {
assert_eq!(rpmvercmp("1.0", "1.0"), Ordering::Equal);
}
#[test]
fn test_compare_evr_simple() {
assert_eq!(compare_evr("1.0-1", "2.0-1"), Ordering::Less);
assert_eq!(compare_evr("2.0-1", "1.0-1"), Ordering::Greater);
}
#[test]
fn test_compare_evr_epoch() {
assert_eq!(compare_evr("2:1.0-1", "1:2.0-1"), Ordering::Greater);
assert_eq!(compare_evr("1:2.0-1", "2:1.0-1"), Ordering::Less);
}
#[test]
fn test_compare_evr_release() {
assert_eq!(compare_evr("1.0-1.fc41", "1.0-2.fc41"), Ordering::Less);
assert_eq!(compare_evr("1.0-2.fc41", "1.0-1.fc41"), Ordering::Greater);
}
#[test]
fn test_compare_evr_no_release() {
assert_eq!(compare_evr("5.14.0", "5.16.0"), Ordering::Less);
assert_eq!(compare_evr("5.16.0", "5.14.0"), Ordering::Greater);
}
#[test]
fn test_compare_evr_no_epoch() {
assert_eq!(compare_evr("1.5.0-3.el9", "1.6.3-1.fc44"), Ordering::Less);
}
#[test]
fn test_parse_evr_full() {
assert_eq!(parse_evr("2:1.5.0-3.el9"), (2, "1.5.0", Some("3.el9")));
}
#[test]
fn test_parse_evr_no_epoch() {
assert_eq!(parse_evr("1.5.0-3.el9"), (0, "1.5.0", Some("3.el9")));
}
#[test]
fn test_parse_evr_no_release() {
assert_eq!(parse_evr("5.16.0"), (0, "5.16.0", None));
}
#[test]
fn test_compare_evr_glibc_symbols() {
assert_eq!(compare_evr("GLIBC_2.28", "GLIBC_2.38"), Ordering::Less);
assert_eq!(compare_evr("GLIBC_2.38", "GLIBC_2.28"), Ordering::Greater);
}
}