use {
crate::error::{DebianError, Result},
std::{
cmp::Ordering,
fmt::{Display, Formatter},
str::FromStr,
},
};
#[derive(Clone, Debug, Eq, PartialEq)]
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 compare_char = |a: &char, b: &char| -> Ordering {
match (a, b) {
('~', '~') => Ordering::Equal,
('~', _) => Ordering::Less,
(_, '~') => Ordering::Greater,
(a, b) if a.is_ascii_alphabetic() && !b.is_ascii_alphabetic() => Ordering::Less,
(a, b) if !a.is_ascii_alphabetic() && b.is_ascii_alphabetic() => Ordering::Greater,
(_, _) => Ordering::Equal,
}
};
let mut a_chars = a.chars().collect::<Vec<_>>();
let mut b_chars = b.chars().collect::<Vec<_>>();
a_chars.sort_by(compare_char);
b_chars.sort_by(compare_char);
for pos in 0..std::cmp::max(a_chars.len(), b_chars.len()) {
let a_char = a_chars.get(pos);
let b_char = b_chars.get(pos);
match (a_char, b_char) {
(Some(a_char), None) if *a_char == '~' => {
return Ordering::Less;
}
(Some(_), None) => {
return Ordering::Greater;
}
(None, Some(b_char)) if *b_char == '~' => {
return Ordering::Greater;
}
(None, Some(_)) => {
return Ordering::Less;
}
(Some(a_char), Some(b_char)) => match compare_char(a_char, b_char) {
Ordering::Equal => {}
res => {
return res;
}
},
(None, None) => {
panic!("None, None variant should never happen");
}
}
}
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> {
match self.epoch_assumed().cmp(&other.epoch_assumed()) {
Ordering::Less => Some(Ordering::Less),
Ordering::Greater => Some(Ordering::Greater),
Ordering::Equal => {
match compare_component(&self.upstream_version, &other.upstream_version) {
Ordering::Less => Some(Ordering::Less),
Ordering::Greater => Some(Ordering::Greater),
Ordering::Equal => {
let a = self.debian_revision.as_deref().unwrap_or("0");
let b = other.debian_revision.as_deref().unwrap_or("0");
Some(compare_component(a, b))
}
}
}
}
}
}
impl Ord for PackageVersion {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
#[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);
}
#[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
);
}
}