knott 0.1.3

Fast Rust package manager helper for Arch Linux repos and the AUR
Documentation
use std::cmp::Ordering;

pub fn vercmp(left: &str, right: &str) -> Ordering {
    let (left_epoch, left_rest) = split_epoch(left);
    let (right_epoch, right_rest) = split_epoch(right);

    match left_epoch.cmp(&right_epoch) {
        Ordering::Equal => {}
        other => return other,
    }

    let (left_ver, left_rel) = split_release(left_rest);
    let (right_ver, right_rel) = split_release(right_rest);

    match compare_version_part(left_ver, right_ver) {
        Ordering::Equal => compare_version_part(left_rel.unwrap_or(""), right_rel.unwrap_or("")),
        other => other,
    }
}

fn split_epoch(value: &str) -> (u64, &str) {
    if let Some((epoch, rest)) = value.split_once(':') {
        (epoch.parse().unwrap_or(0), rest)
    } else {
        (0, value)
    }
}

fn split_release(value: &str) -> (&str, Option<&str>) {
    if let Some(index) = value.rfind('-') {
        (&value[..index], Some(&value[index + 1..]))
    } else {
        (value, None)
    }
}

fn compare_version_part(left: &str, right: &str) -> Ordering {
    let left = left.as_bytes();
    let right = right.as_bytes();
    let mut li = 0;
    let mut ri = 0;

    loop {
        while li < left.len() && !left[li].is_ascii_alphanumeric() {
            li += 1;
        }
        while ri < right.len() && !right[ri].is_ascii_alphanumeric() {
            ri += 1;
        }

        if li == left.len() || ri == right.len() {
            return left[li..].len().cmp(&right[ri..].len());
        }

        let left_digit = left[li].is_ascii_digit();
        let right_digit = right[ri].is_ascii_digit();

        if left_digit && right_digit {
            let l_start = li;
            let r_start = ri;
            while li < left.len() && left[li].is_ascii_digit() {
                li += 1;
            }
            while ri < right.len() && right[ri].is_ascii_digit() {
                ri += 1;
            }

            let l_trim = trim_zeroes(&left[l_start..li]);
            let r_trim = trim_zeroes(&right[r_start..ri]);
            match l_trim.len().cmp(&r_trim.len()) {
                Ordering::Equal => match l_trim.cmp(r_trim) {
                    Ordering::Equal => {}
                    other => return other,
                },
                other => return other,
            }
        } else if !left_digit && !right_digit {
            let l_start = li;
            let r_start = ri;
            while li < left.len() && left[li].is_ascii_alphabetic() {
                li += 1;
            }
            while ri < right.len() && right[ri].is_ascii_alphabetic() {
                ri += 1;
            }

            match left[l_start..li].cmp(&right[r_start..ri]) {
                Ordering::Equal => {}
                other => return other,
            }
        } else {
            return if left_digit {
                Ordering::Greater
            } else {
                Ordering::Less
            };
        }
    }
}

fn trim_zeroes(mut value: &[u8]) -> &[u8] {
    while value.first() == Some(&b'0') {
        value = &value[1..];
    }
    value
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn compares_numeric_segments() {
        assert_eq!(vercmp("1.10", "1.9"), Ordering::Greater);
        assert_eq!(vercmp("1.02", "1.2"), Ordering::Equal);
    }

    #[test]
    fn compares_epoch_before_version() {
        assert_eq!(vercmp("1:1.0", "2.0"), Ordering::Greater);
    }

    #[test]
    fn compares_pkgrel_after_pkgver() {
        assert_eq!(vercmp("1.0-2", "1.0-1"), Ordering::Greater);
    }
}