deb-version7 0.1.1

Parsing and comparing of Debian package versions
Documentation
// Rust port of lib/dpkg/version.c
//
// Copyright © 1995 Ian Jackson <ijackson@chiark.greenend.org.uk>
// Copyright © 2008-2014 Guillem Jover <guillem@debian.org>
// Copyright © 2024 Kunal Mehta <legoktm@debian.org>
//
// This is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use crate::Error;
use std::{cmp::Ordering, fmt::Display, str::FromStr};

#[derive(Debug, Clone)]
pub struct DebVersion {
    pub(crate) epoch: u32,
    // note: dpkg has this field as optional, but we require it
    pub(crate) version: String,
    pub(crate) revision: Option<String>,
}

impl DebVersion {
    /// Epoch of the version, if not specified it is 0.
    pub fn epoch(&self) -> u32 {
        self.epoch
    }

    /// The upstream version
    pub fn version(&self) -> &str {
        &self.version
    }

    /// The Debian revision, if it is a non-native package
    pub fn revision(&self) -> Option<&str> {
        self.revision.as_deref()
    }
}

impl PartialEq for DebVersion {
    fn eq(&self, other: &Self) -> bool {
        dpkg_version_compare(self, other) == Ordering::Equal
    }
}

impl Eq for DebVersion {}

impl PartialOrd for DebVersion {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for DebVersion {
    fn cmp(&self, other: &Self) -> Ordering {
        dpkg_version_compare(self, other)
    }
}

impl FromStr for DebVersion {
    type Err = Error;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        crate::parsehelp::parseversion(input)
    }
}

impl Display for DebVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if self.epoch > 0 {
            write!(f, "{}:", self.epoch)?;
        }
        write!(f, "{}", self.version)?;
        if let Some(revision) = &self.revision {
            write!(f, "-{revision}")?;
        }
        Ok(())
    }
}

/// Give a weight to the character to order in the version comparison.
fn order(char: char) -> i32 {
    match char {
        '0'..='9' => 0,
        'A'..='Z' | 'a'..='z' => char as i32,
        '~' => -1,
        val => val as i32 + 256,
    }
}

fn verrevcmp(a: &str, b: &str) -> i32 {
    let mut index_a = 0;
    let mut index_b = 0;
    let chars_a = a.chars().collect::<Vec<_>>();
    let chars_b = b.chars().collect::<Vec<_>>();

    while index_a < a.len() || index_b < b.len() {
        let mut first_diff = 0;

        while (index_a < a.len() && !chars_a[index_a].is_ascii_digit())
            || (index_b < b.len() && !chars_b[index_b].is_ascii_digit())
        {
            let ac = if index_a < a.len() {
                order(chars_a[index_a])
            } else {
                0
            };
            let bc = if index_b < b.len() {
                order(chars_b[index_b])
            } else {
                0
            };

            if ac != bc {
                return ac - bc;
            }

            index_a += 1;
            index_b += 1;
        }

        while index_a < a.len() && chars_a[index_a] == '0' {
            index_a += 1;
        }
        while index_b < b.len() && chars_b[index_b] == '0' {
            index_b += 1;
        }

        while index_a < a.len()
            && chars_a[index_a].is_ascii_digit()
            && index_b < b.len()
            && chars_b[index_b].is_ascii_digit()
        {
            if first_diff == 0 {
                first_diff = chars_a[index_a] as i32 - chars_b[index_b] as i32;
            }
            index_a += 1;
            index_b += 1;
        }

        if index_a < a.len() && chars_a[index_a].is_ascii_digit() {
            return 1;
        }
        if index_b < b.len() && chars_b[index_b].is_ascii_digit() {
            return -1;
        }
        if first_diff != 0 {
            return first_diff;
        }
    }

    0
}

/// Compares two Debian versions.
///
/// This function follows the convention of the comparator functions used by
/// qsort().
///
/// See `deb-version(5)`
pub(crate) fn dpkg_version_compare(a: &DebVersion, b: &DebVersion) -> Ordering {
    if a.epoch > b.epoch {
        return Ordering::Greater;
    }
    if a.epoch < b.epoch {
        return Ordering::Less;
    }

    let rc = verrevcmp(&a.version, &b.version);
    if rc != 0 {
        return rc.cmp(&0);
    }

    match (&a.revision, &b.revision) {
        (Some(a_revision), Some(b_revision)) => verrevcmp(a_revision, b_revision).cmp(&0),
        (None, None) => Ordering::Equal,
        (None, Some(_)) => Ordering::Less,
        (Some(_), None) => Ordering::Greater,
    }
}