use std::{
cmp::Ordering,
fmt::{Display, Formatter},
hash::{Hash, Hasher},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::utils::TryFromStrVisitor;
pub use crate::ParseError;
fn compare_non_digits(mut lhs: &str, mut rhs: &str) -> Ordering {
while !lhs.is_empty() || !rhs.is_empty() {
let (lhs_tilde, lhs_found) = lhs.find('~').map_or((lhs.len(), false), |i| (i, true));
let (rhs_tilde, rhs_found) = rhs.find('~').map_or((rhs.len(), false), |i| (i, true));
let c = lhs[..lhs_tilde].cmp(&rhs[..rhs_tilde]);
if c != Ordering::Equal {
return c;
}
if lhs_found && rhs_found {
lhs = &lhs[lhs_tilde + 1..];
rhs = &rhs[rhs_tilde + 1..];
} else if lhs_found {
return Ordering::Less;
} else if rhs_found {
return Ordering::Greater;
} else {
return Ordering::Equal;
}
}
Ordering::Equal
}
fn compare_parts(mut lhs: &str, mut rhs: &str) -> Ordering {
while !lhs.is_empty() || !rhs.is_empty() {
let lhs_digit_start = lhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(lhs.len());
let rhs_digit_start = rhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(rhs.len());
let c = compare_non_digits(&lhs[..lhs_digit_start], &rhs[..rhs_digit_start]);
if c != Ordering::Equal {
return c;
}
lhs = &lhs[lhs_digit_start..];
rhs = &rhs[rhs_digit_start..];
let lhs_digit_end = lhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(lhs.len());
let rhs_digit_end = rhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(rhs.len());
let c = lhs[..lhs_digit_end]
.parse::<u64>()
.unwrap_or(0)
.cmp(&rhs[..rhs_digit_end].parse::<u64>().unwrap_or(0));
if c != Ordering::Equal {
return c;
}
lhs = &lhs[lhs_digit_end..];
rhs = &rhs[rhs_digit_end..];
}
Ordering::Equal
}
#[derive(Clone, Copy, Debug, Error)]
pub enum VersionError {
#[error("invalid epoch")]
InvalidEpoch,
#[error("invalid upstream version")]
InvalidUpstreamVersion,
#[error("invalid Debian revision")]
InvalidDebianRevision,
}
#[derive(Clone, Debug)]
pub struct PackageVersion {
pub(crate) epoch: Option<u32>,
pub(crate) upstream_version: String,
pub(crate) debian_revision: Option<String>,
}
impl PackageVersion {
pub fn new(
epoch: Option<u32>,
upstream_version: &str,
debian_revision: Option<&str>,
) -> Result<Self, VersionError> {
if upstream_version.is_empty()
|| upstream_version.chars().any(|c| {
!(c.is_alphanumeric()
|| ".+~".contains(c)
|| (debian_revision.is_some() && c == '-')
|| (epoch.is_some() && c == ':'))
})
{
return Err(VersionError::InvalidUpstreamVersion);
}
if let Some(rev) = debian_revision {
if rev.is_empty()
|| rev
.chars()
.any(|c| !c.is_alphanumeric() && !".+~".contains(c))
{
return Err(VersionError::InvalidDebianRevision);
}
}
Ok(Self {
epoch,
upstream_version: String::from(upstream_version),
debian_revision: debian_revision.map(String::from),
})
}
pub fn is_native(&self) -> bool {
self.debian_revision.is_none()
}
pub fn has_epoch(&self) -> bool {
self.epoch.is_some()
}
pub fn epoch_or_0(&self) -> u32 {
self.epoch.unwrap_or(0)
}
pub fn has_binnmu_version(&self) -> bool {
self.binnmu_version().is_some()
}
pub fn binnmu_version(&self) -> Option<u32> {
self.debian_revision
.as_ref()
.map_or(&self.upstream_version, |v| v)
.rsplit_once("+b")
.and_then(|(_, binnmu_version)| binnmu_version.parse().ok())
}
pub fn without_binnmu_version(mut self) -> Self {
if let Some(revision) = self.debian_revision.as_mut() {
if let Some(index) = revision.rfind("+b") {
revision.truncate(index);
}
} else if let Some(index) = self.upstream_version.rfind("+b") {
self.upstream_version.truncate(index);
}
self
}
pub fn clone_without_binnmu_version(&self) -> Self {
self.clone().without_binnmu_version()
}
}
impl PartialOrd for PackageVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PackageVersion {
fn cmp(&self, other: &Self) -> Ordering {
match self.epoch_or_0().cmp(&other.epoch_or_0()) {
Ordering::Equal => {}
v => return v,
};
match compare_parts(&self.upstream_version, &other.upstream_version) {
Ordering::Equal => {}
v => return v,
};
match (&self.debian_revision, &other.debian_revision) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(lhs), Some(rhs)) => compare_parts(lhs, rhs),
}
}
}
impl PartialEq for PackageVersion {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for PackageVersion {}
impl TryFrom<&str> for PackageVersion {
type Error = ParseError;
fn try_from(mut value: &str) -> Result<Self, Self::Error> {
let epoch = if let Some((epoch_str, new_value)) = value.split_once(':') {
value = new_value;
Some(
epoch_str
.parse::<u32>()
.map_err(|_| ParseError::InvalidVersion(VersionError::InvalidEpoch))?,
)
} else {
None
};
let debian_revision = if let Some((new_value, debian_revision_str)) = value.rsplit_once('-')
{
value = new_value;
Some(debian_revision_str)
} else {
None
};
Self::new(epoch, value, debian_revision).map_err(ParseError::InvalidVersion)
}
}
impl Display for PackageVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(epoch) = self.epoch {
write!(f, "{epoch}:")?;
}
write!(f, "{}", self.upstream_version)?;
if let Some(debian_revision) = &self.debian_revision {
write!(f, "-{debian_revision}")?;
}
Ok(())
}
}
impl Serialize for PackageVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PackageVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(TryFromStrVisitor::<Self>::new("a package version"))
}
}
impl Hash for PackageVersion {
fn hash<H: Hasher>(&self, state: &mut H) {
self.epoch_or_0().hash(state);
self.upstream_version.hash(state);
self.debian_revision.hash(state);
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn conversion() {
let version = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
assert_eq!(version.epoch, Some(2));
assert_eq!(version.upstream_version, "1.0+dfsg");
assert_eq!(version.debian_revision, Some("1".into()));
}
#[test]
fn invalid_epoch() {
assert!(PackageVersion::try_from("-1:1.0-1").is_err());
assert!(PackageVersion::try_from(":1.0-1").is_err());
assert!(PackageVersion::try_from("a1:1.0-1").is_err());
}
#[test]
fn invalid_upstream_version() {
assert!(PackageVersion::try_from("-1").is_err());
assert!(PackageVersion::try_from("0:-1").is_err());
assert!(PackageVersion::new(None, "1:2", None).is_err());
assert!(PackageVersion::new(None, "1-2", None).is_err());
}
#[test]
fn multi_dash() {
let version = PackageVersion::try_from("1.0-2-1").unwrap();
assert_eq!(version.epoch, None);
assert_eq!(version.upstream_version, "1.0-2");
assert_eq!(version.debian_revision.unwrap(), "1");
}
#[test]
fn multi_colon() {
let version = PackageVersion::try_from("1:1.0:2-1").unwrap();
assert_eq!(version.epoch.unwrap(), 1);
assert_eq!(version.upstream_version, "1.0:2");
assert_eq!(version.debian_revision.unwrap(), "1");
}
#[test]
fn binnum() {
let version = PackageVersion::try_from("1.0-1").unwrap();
assert!(!version.has_binnmu_version());
assert_eq!(version.binnmu_version(), None);
let version = PackageVersion::try_from("1.0-1+b1").unwrap();
assert!(version.has_binnmu_version());
assert_eq!(version.binnmu_version(), Some(1u32));
}
#[test]
fn strip_binnum() {
let version = PackageVersion::try_from("1.0-1+b1").unwrap();
let version = version.without_binnmu_version();
assert_eq!(version, PackageVersion::try_from("1.0-1").unwrap());
assert!(!version.has_binnmu_version());
assert_eq!(version.binnmu_version(), None);
}
#[test]
fn compare_non_digits_invariants() {
assert_eq!(compare_non_digits("~~", "~~a"), Ordering::Less);
assert_eq!(compare_non_digits("~~a", "~"), Ordering::Less);
assert_eq!(compare_non_digits("~", ""), Ordering::Less);
assert_eq!(compare_non_digits("", "a"), Ordering::Less);
}
#[test]
fn epoch_compare() {
let version1 = PackageVersion::try_from("2.0-1").unwrap();
let version2 = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
assert!(version2.has_epoch());
assert!(!version1.has_epoch());
assert!(version1 < version2);
}
#[test]
fn zero_epoch_compare() {
let version1 = PackageVersion::try_from("2.0-1").unwrap();
let version2 = PackageVersion::try_from("0:2.0-1").unwrap();
assert_eq!(version1, version2);
}
#[test]
fn equal_compare() {
let version1 = PackageVersion::try_from("2.0-1").unwrap();
assert_eq!(version1, version1);
let version1 = PackageVersion::try_from("2a.0-1").unwrap();
assert_eq!(version1, version1);
let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
assert_eq!(version1, version1);
}
#[test]
fn tilde_plus_compare() {
let version1 = PackageVersion::try_from("2.0~dfsg-1").unwrap();
let version2 = PackageVersion::try_from("2.0-1").unwrap();
assert!(version1 < version2);
let version2 = PackageVersion::try_from("2.0+dfsg-1").unwrap();
assert!(version1 < version2);
let version1 = PackageVersion::try_from("2.0-1").unwrap();
assert!(version1 < version2);
let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
let version2 = PackageVersion::try_from("2+dfsg2-1").unwrap();
assert!(version1 < version2);
let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
let version2 = PackageVersion::try_from("2.1-1").unwrap();
assert!(version1 < version2);
}
#[test]
fn letters_compare() {
let version1 = PackageVersion::try_from("2dfsg-1").unwrap();
let version2 = PackageVersion::try_from("2-1").unwrap();
assert!(version1 > version2);
}
#[test]
fn less_compare() {
let version1 = PackageVersion::try_from("2-1").unwrap();
let version2 = PackageVersion::try_from("2.0-1").unwrap();
assert!(version1 < version2);
}
#[test]
fn native_version_binnmu() {
let version1 = PackageVersion::try_from("2+b1").unwrap();
let version2 = PackageVersion::try_from("2").unwrap();
assert!(version1.has_binnmu_version());
assert_eq!(version1.binnmu_version(), Some(1));
assert!(!version2.has_binnmu_version());
assert_eq!(version1.without_binnmu_version(), version2);
}
}