use {
crate::error::{DebianError, Result},
std::{
cmp::Ordering,
fmt::{Display, Formatter},
str::FromStr,
},
};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct PackageVersion {
epoch: Option<u32>,
upstream_version: String,
debian_revision: Option<String>,
}
impl PackageVersion {
pub fn parse(s: &str) -> Result<Self> {
let (epoch, remainder) = if let Some(pos) = s.find(':') {
(Some(&s[0..pos]), &s[pos + 1..])
} else {
(None, s)
};
let (upstream, debian) = if let Some(pos) = remainder.rfind('-') {
(&remainder[0..pos], Some(&remainder[pos + 1..]))
} else {
(remainder, None)
};
let epoch = if let Some(epoch) = epoch {
if !epoch.chars().all(|c| c.is_ascii_digit()) {
return Err(DebianError::EpochNonNumeric(s.to_string()));
}
Some(u32::from_str(epoch)?)
} else {
None
};
if !upstream.chars().all(|c| match c {
c if c.is_ascii_alphanumeric() => true,
'.' | '+' | '~' => true,
'-' => debian.is_some(),
_ => false,
}) {
return Err(DebianError::UpstreamVersionIllegalChar(s.to_string()));
}
let upstream_version = upstream.to_string();
let debian_revision = if let Some(debian) = debian {
if !debian.chars().all(|c| match c {
c if c.is_ascii_alphanumeric() => true,
'+' | '.' | '~' => true,
_ => false,
}) {
return Err(DebianError::DebianRevisionIllegalChar(s.to_string()));
}
Some(debian.to_string())
} else {
None
};
Ok(Self {
epoch,
upstream_version,
debian_revision,
})
}
pub fn epoch(&self) -> Option<u32> {
self.epoch
}
pub fn epoch_assumed(&self) -> u32 {
if let Some(epoch) = &self.epoch {
*epoch
} else {
0
}
}
pub fn upstream_version(&self) -> &str {
&self.upstream_version
}
pub fn debian_revision(&self) -> Option<&str> {
self.debian_revision.as_deref()
}
}
impl Display for PackageVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}{}{}{}{}",
if let Some(epoch) = self.epoch {
format!("{}", epoch)
} else {
"".to_string()
},
if self.epoch.is_some() { ":" } else { "" },
self.upstream_version,
if self.debian_revision.is_some() {
"-"
} else {
""
},
if let Some(v) = &self.debian_revision {
v
} else {
""
}
)
}
}
fn split_first_digit(s: &str) -> (&str, &str) {
let first_nondigit_index = s.chars().position(|c| c.is_ascii_digit());
match first_nondigit_index {
Some(0) => ("", s),
Some(pos) => (&s[0..pos], &s[pos..]),
None => (s, ""),
}
}
fn split_first_nondigit(s: &str) -> (&str, &str) {
let pos = s.chars().position(|c| !c.is_ascii_digit());
match pos {
Some(0) => ("", s),
Some(pos) => (&s[0..pos], &s[pos..]),
None => (s, ""),
}
}
fn split_first_digit_number(s: &str) -> (u64, &str) {
let (digits, remaining) = split_first_nondigit(s);
let numeric = if digits.is_empty() {
0
} else {
u64::from_str(digits).expect("digits should deserialize to string")
};
(numeric, remaining)
}
fn lexical_compare(a: &str, b: &str) -> Ordering {
let mut a_chars = a.chars();
let mut b_chars = b.chars();
loop {
let ord = match (a_chars.next(), b_chars.next()) {
(Some('~'), Some('~')) => Ordering::Equal,
(Some('~'), _) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(None, Some('~')) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(Some(a), Some(b)) if a.is_ascii_alphabetic() && !b.is_ascii_alphabetic() => {
Ordering::Less
}
(Some(a), Some(b)) if !a.is_ascii_alphabetic() && b.is_ascii_alphabetic() => {
Ordering::Greater
}
(Some(a), Some(b)) => a.cmp(&b),
(None, None) => break,
};
if ord != Ordering::Equal {
return ord;
}
}
Ordering::Equal
}
fn compare_component(a: &str, b: &str) -> Ordering {
let mut a_remaining = a;
let mut b_remaining = b;
loop {
let a_res = split_first_digit(a_remaining);
let a_leading_nondigit = a_res.0;
a_remaining = a_res.1;
let b_res = split_first_digit(b_remaining);
let b_leading_nondigit = b_res.0;
b_remaining = b_res.1;
match lexical_compare(a_leading_nondigit, b_leading_nondigit) {
Ordering::Equal => {}
res => {
return res;
}
}
let a_res = split_first_digit_number(a_remaining);
let a_numeric = a_res.0;
a_remaining = a_res.1;
let b_res = split_first_digit_number(b_remaining);
let b_numeric = b_res.0;
b_remaining = b_res.1;
match a_numeric.cmp(&b_numeric) {
Ordering::Equal => {}
res => {
return res;
}
}
if a_remaining.is_empty() && b_remaining.is_empty() {
return Ordering::Equal;
}
}
}
impl PartialOrd<Self> 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_assumed().cmp(&other.epoch_assumed()) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => {
match compare_component(&self.upstream_version, &other.upstream_version) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => {
let a = self.debian_revision.as_deref().unwrap_or("0");
let b = other.debian_revision.as_deref().unwrap_or("0");
compare_component(a, b)
}
}
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse() -> Result<()> {
assert_eq!(
PackageVersion::parse("1:4.7.0+dfsg1-2")?,
PackageVersion {
epoch: Some(1),
upstream_version: "4.7.0+dfsg1".into(),
debian_revision: Some("2".into()),
}
);
assert_eq!(
PackageVersion::parse("3.3.2.final~github")?,
PackageVersion {
epoch: None,
upstream_version: "3.3.2.final~github".into(),
debian_revision: None,
}
);
assert_eq!(
PackageVersion::parse("3.3.2.final~github-2")?,
PackageVersion {
epoch: None,
upstream_version: "3.3.2.final~github".into(),
debian_revision: Some("2".into()),
}
);
assert_eq!(
PackageVersion::parse("0.18.0+dfsg-2+b1")?,
PackageVersion {
epoch: None,
upstream_version: "0.18.0+dfsg".into(),
debian_revision: Some("2+b1".into())
}
);
Ok(())
}
#[test]
fn format() -> Result<()> {
for s in ["1:4.7.0+dfsg1-2", "3.3.2.final~github", "0.18.0+dfsg-2+b1"] {
let v = PackageVersion::parse(s)?;
assert_eq!(format!("{}", v), s);
}
Ok(())
}
#[test]
fn test_lexical_compare() {
assert_eq!(lexical_compare("~~", "~~a"), Ordering::Less);
assert_eq!(lexical_compare("~~a", "~~"), Ordering::Greater);
assert_eq!(lexical_compare("~~a", "~"), Ordering::Less);
assert_eq!(lexical_compare("~", "~~a"), Ordering::Greater);
assert_eq!(lexical_compare("~", ""), Ordering::Less);
assert_eq!(lexical_compare("", "~"), Ordering::Greater);
assert_eq!(lexical_compare("", "a"), Ordering::Less);
assert_eq!(lexical_compare("a", ""), Ordering::Greater);
assert_eq!(lexical_compare("a", "b"), Ordering::Less);
assert_eq!(lexical_compare("b", "a"), Ordering::Greater);
assert_eq!(lexical_compare("c", "db"), Ordering::Less);
assert_eq!(lexical_compare("b", "+a"), Ordering::Less);
}
#[test]
fn test_compare_component() {
assert_eq!(
compare_component("1.0~beta1~svn1245", "1.0~beta1"),
Ordering::Less
);
assert_eq!(compare_component("1.0~beta1", "1.0"), Ordering::Less);
}
#[test]
fn compare_version() {
assert_eq!(
PackageVersion {
epoch: Some(1),
upstream_version: "ignored".into(),
debian_revision: None,
}
.cmp(&PackageVersion {
epoch: Some(0),
upstream_version: "ignored".into(),
debian_revision: None
}),
Ordering::Greater
);
}
}