use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PkgVersion(String);
impl PkgVersion {
pub fn new(version: &str) -> Self {
Self(version.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PkgVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for PkgVersion {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s))
}
}
impl From<String> for PkgVersion {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for PkgVersion {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl AsRef<str> for PkgVersion {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialOrd for PkgVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PkgVersion {
fn cmp(&self, other: &Self) -> Ordering {
compare(&self.0, &other.0)
}
}
fn compare(a: &str, b: &str) -> Ordering {
if a.eq_ignore_ascii_case(b) {
return Ordering::Equal;
}
let mut one = a.as_bytes();
let mut two = b.as_bytes();
while !one.is_empty() || !two.is_empty() {
one = skip_separators(one);
two = skip_separators(two);
if one.first() == Some(&b'~') || two.first() == Some(&b'~') {
match (one.first(), two.first()) {
(Some(b'~'), Some(b'~')) => {
one = &one[1..];
two = &two[1..];
continue;
}
(Some(b'~'), _) => return Ordering::Less,
_ => return Ordering::Greater,
}
}
if one.is_empty() || two.is_empty() {
break;
}
let isnum = one[0].is_ascii_digit();
let (seg1, rest1) = split_run(one, isnum);
let (seg2, rest2) = split_run(two, isnum);
if seg2.is_empty() {
return if isnum {
Ordering::Greater
} else {
Ordering::Less
};
}
let ord = if isnum {
compare_numeric(seg1, seg2)
} else {
seg1.cmp(seg2)
};
if ord != Ordering::Equal {
return ord;
}
one = rest1;
two = rest2;
}
match (one.is_empty(), two.is_empty()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, _) => Ordering::Greater,
}
}
fn skip_separators(s: &[u8]) -> &[u8] {
let end = s
.iter()
.position(|c| c.is_ascii_alphanumeric() || *c == b'~')
.unwrap_or(s.len());
&s[end..]
}
fn split_run(s: &[u8], isnum: bool) -> (&[u8], &[u8]) {
let pred = if isnum {
u8::is_ascii_digit
} else {
u8::is_ascii_alphabetic
};
let end = s.iter().position(|c| !pred(c)).unwrap_or(s.len());
s.split_at(end)
}
fn compare_numeric(a: &[u8], b: &[u8]) -> Ordering {
let a = strip_leading_zeroes(a);
let b = strip_leading_zeroes(b);
a.len().cmp(&b.len()).then_with(|| a.cmp(b))
}
fn strip_leading_zeroes(s: &[u8]) -> &[u8] {
let start = s.iter().position(|c| *c != b'0').unwrap_or(s.len());
&s[start..]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PkgComparison {
Less,
LessEqual,
Equal,
NotEqual,
GreaterEqual,
Greater,
}
impl PkgComparison {
pub fn as_str(&self) -> &'static str {
match self {
Self::Less => "<",
Self::LessEqual => "<=",
Self::Equal => "=",
Self::NotEqual => "!=",
Self::GreaterEqual => ">=",
Self::Greater => ">",
}
}
fn matches(&self, ordering: Ordering) -> bool {
match self {
Self::Less => ordering == Ordering::Less,
Self::LessEqual => ordering != Ordering::Greater,
Self::Equal => ordering == Ordering::Equal,
Self::NotEqual => ordering != Ordering::Equal,
Self::GreaterEqual => ordering != Ordering::Less,
Self::Greater => ordering == Ordering::Greater,
}
}
}
impl fmt::Display for PkgComparison {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsePkgComparisonError(String);
impl fmt::Display for ParsePkgComparisonError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid pkg-config comparison operator: {}", self.0)
}
}
impl std::error::Error for ParsePkgComparisonError {}
impl FromStr for PkgComparison {
type Err = ParsePkgComparisonError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"<" => Ok(Self::Less),
"<=" => Ok(Self::LessEqual),
"=" | "==" => Ok(Self::Equal),
"!=" => Ok(Self::NotEqual),
">=" => Ok(Self::GreaterEqual),
">" => Ok(Self::Greater),
_ => Err(ParsePkgComparisonError(s.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PkgConstraint {
pub comparison: PkgComparison,
pub version: PkgVersion,
}
impl PkgConstraint {
pub fn new(comparison: PkgComparison, version: PkgVersion) -> Self {
Self {
comparison,
version,
}
}
pub fn at_least(version: PkgVersion) -> Self {
Self::new(PkgComparison::GreaterEqual, version)
}
pub fn matches(&self, version: &PkgVersion) -> bool {
self.comparison.matches(version.cmp(&self.version))
}
}
impl fmt::Display for PkgConstraint {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}", self.comparison, self.version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsePkgConstraintError {
Comparison(ParsePkgComparisonError),
MissingVersion,
}
impl fmt::Display for ParsePkgConstraintError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Comparison(e) => e.fmt(f),
Self::MissingVersion => f.write_str("missing version in pkg-config constraint"),
}
}
}
impl std::error::Error for ParsePkgConstraintError {}
impl From<ParsePkgComparisonError> for ParsePkgConstraintError {
fn from(e: ParsePkgComparisonError) -> Self {
Self::Comparison(e)
}
}
impl FromStr for PkgConstraint {
type Err = ParsePkgConstraintError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
let split = s
.find(|c: char| !"<>=!".contains(c))
.ok_or(ParsePkgConstraintError::MissingVersion)?;
let (comparison, version) = s.split_at(split);
let version = version.trim();
if version.is_empty() {
return Err(ParsePkgConstraintError::MissingVersion);
}
Ok(Self::new(comparison.parse()?, PkgVersion::new(version)))
}
}
#[cfg(feature = "debian")]
pub fn pkg_comparison_to_debian(
comparison: PkgComparison,
) -> Option<debian_control::relations::VersionConstraint> {
use debian_control::relations::VersionConstraint as Deb;
Some(match comparison {
PkgComparison::Less => Deb::LessThan,
PkgComparison::LessEqual => Deb::LessThanEqual,
PkgComparison::Equal => Deb::Equal,
PkgComparison::GreaterEqual => Deb::GreaterThanEqual,
PkgComparison::Greater => Deb::GreaterThan,
PkgComparison::NotEqual => return None,
})
}
#[cfg(feature = "debian")]
pub fn pkg_constraint_to_debian(
constraint: &PkgConstraint,
) -> Option<(
debian_control::relations::VersionConstraint,
debversion::Version,
)> {
Some((
pkg_comparison_to_debian(constraint.comparison)?,
pkg_version_to_debian(&constraint.version)?,
))
}
#[cfg(feature = "debian")]
pub fn pkg_version_to_debian(version: &PkgVersion) -> Option<debversion::Version> {
if !valid_debian_upstream_version(version.as_str()) {
return None;
}
Some(debversion::Version {
epoch: None,
upstream_version: version.as_str().to_string(),
debian_revision: None,
})
}
#[cfg(feature = "debian")]
fn valid_debian_upstream_version(s: &str) -> bool {
s.starts_with(|c: char| c.is_ascii_digit())
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~'))
}
#[cfg(feature = "debian")]
pub fn debian_version_to_pkg(version: &debversion::Version) -> PkgVersion {
PkgVersion::new(&version.upstream_version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display_and_parse() {
let v: PkgVersion = "1.2.3".parse().unwrap();
assert_eq!(v.to_string(), "1.2.3");
assert_eq!(v.as_str(), "1.2.3");
}
#[rustfmt::skip]
const ASCENDING: &[&str] = &[
"0.9~~",
"0.9~",
"0.9",
"1.a",
"1.0~~",
"1.0~",
"1.0~a",
"1.0~rc1",
"1.0~rc2",
"1.0",
"1.0a",
"1.0a1",
"1.0a2",
"1.0b",
"1.0rc1",
"1.0.1",
"1.0.2",
"1.9",
"1.10",
"1.100",
"2.0",
"10.0",
];
#[test]
fn test_ordering() {
for (i, lesser) in ASCENDING.iter().enumerate() {
for greater in &ASCENDING[i + 1..] {
assert_eq!(
PkgVersion::new(lesser).cmp(&PkgVersion::new(greater)),
Ordering::Less,
"{lesser} should sort before {greater}"
);
assert_eq!(
PkgVersion::new(greater).cmp(&PkgVersion::new(lesser)),
Ordering::Greater,
"{greater} should sort after {lesser}"
);
}
}
}
const EQUIVALENT: &[(&str, &str)] = &[
("1.0.0", "1_0-0"),
("1.0.1", "1:0:1"),
("1.0", "1..0"),
("1.0", "-1.0-"),
("1.0a", "1.0-a"),
("1.0.", "1.0"),
("1.0^1", "1.0.1"),
("1.0^", "1.0"),
("1.0~rc1", "1.0~rc-1"),
("1.01", "1.1"),
("1.0007", "1.7"),
("1.0RC1", "1.0rc1"),
("ALPHA", "alpha"),
];
#[test]
fn test_equivalent() {
for (a, b) in EQUIVALENT {
assert_eq!(
PkgVersion::new(a).cmp(&PkgVersion::new(b)),
Ordering::Equal,
"{a} should compare equal to {b}"
);
}
}
#[test]
fn test_equal_to_itself() {
for v in ASCENDING {
assert_eq!(
PkgVersion::new(v).cmp(&PkgVersion::new(v)),
Ordering::Equal,
"{v} should compare equal to itself"
);
}
}
#[test]
fn test_sort() {
let mut versions = vec![
PkgVersion::new("1.10"),
PkgVersion::new("1.0~rc1"),
PkgVersion::new("1.9"),
PkgVersion::new("1.0"),
];
versions.sort();
assert_eq!(
versions,
vec![
PkgVersion::new("1.0~rc1"),
PkgVersion::new("1.0"),
PkgVersion::new("1.9"),
PkgVersion::new("1.10"),
]
);
}
#[test]
fn test_parse_constraint() {
for (text, comparison, version) in [
(">= 1.2", PkgComparison::GreaterEqual, "1.2"),
(">=1.2", PkgComparison::GreaterEqual, "1.2"),
("> 1.2", PkgComparison::Greater, "1.2"),
("<2.0", PkgComparison::Less, "2.0"),
("<= 2.0", PkgComparison::LessEqual, "2.0"),
("= 1.4", PkgComparison::Equal, "1.4"),
("== 1.4", PkgComparison::Equal, "1.4"),
("!= 1.4", PkgComparison::NotEqual, "1.4"),
(" >= 1.2 ", PkgComparison::GreaterEqual, "1.2"),
] {
let constraint: PkgConstraint = text.parse().unwrap();
assert_eq!(
constraint,
PkgConstraint::new(comparison, PkgVersion::new(version)),
"parsing {text}"
);
}
}
#[test]
fn test_parse_constraint_errors() {
assert_eq!(
">= ".parse::<PkgConstraint>(),
Err(ParsePkgConstraintError::MissingVersion)
);
assert_eq!(
">=".parse::<PkgConstraint>(),
Err(ParsePkgConstraintError::MissingVersion)
);
assert_eq!(
"=> 1.2".parse::<PkgConstraint>(),
Err(ParsePkgConstraintError::Comparison(
ParsePkgComparisonError("=>".to_string())
))
);
assert_eq!(
"1.2".parse::<PkgConstraint>(),
Err(ParsePkgConstraintError::Comparison(
ParsePkgComparisonError("".to_string())
))
);
}
#[test]
fn test_constraint_display() {
let constraint: PkgConstraint = ">=1.2".parse().unwrap();
assert_eq!(constraint.to_string(), ">= 1.2");
}
#[test]
fn test_constraint_matches() {
let cases = [
(">= 1.2", "1.2", true),
(">= 1.2", "1.3", true),
(">= 1.2", "1.1", false),
("> 1.2", "1.2", false),
("> 1.2", "1.3", true),
("< 2.0", "1.9", true),
("< 2.0", "2.0", false),
("<= 2.0", "2.0", true),
("<= 2.0", "2.1", false),
("= 1.4", "1.4", true),
("= 1.4", "1.5", false),
("!= 1.4", "1.4", false),
("!= 1.4", "1.5", true),
(">= 1.0", "1.0~rc1", false),
("< 1.0", "1.0~rc1", true),
];
for (text, version, expected) in cases {
let constraint: PkgConstraint = text.parse().unwrap();
assert_eq!(
constraint.matches(&PkgVersion::new(version)),
expected,
"{version} against {text}"
);
}
}
#[cfg(feature = "debian")]
#[test]
fn test_pkg_constraint_to_debian() {
use debian_control::relations::VersionConstraint as Deb;
for (text, expected) in [
(">= 1.2", Some(Deb::GreaterThanEqual)),
("> 1.2", Some(Deb::GreaterThan)),
("<= 1.2", Some(Deb::LessThanEqual)),
("< 1.2", Some(Deb::LessThan)),
("= 1.2", Some(Deb::Equal)),
("!= 1.2", None),
] {
let constraint: PkgConstraint = text.parse().unwrap();
let converted = pkg_constraint_to_debian(&constraint);
assert_eq!(
converted.map(|(comparison, _)| comparison),
expected,
"converting {text}"
);
}
}
#[cfg(feature = "debian")]
#[test]
fn test_pkg_constraint_to_debian_unrepresentable_version() {
let constraint: PkgConstraint = ">= 1_0".parse().unwrap();
assert_eq!(pkg_constraint_to_debian(&constraint), None);
}
#[cfg(feature = "debian")]
#[test]
fn test_pkg_version_to_debian() {
for (pkg, deb) in [("1.0", "1.0"), ("1.2.3", "1.2.3"), ("1.0~rc1", "1.0~rc1")] {
let version = pkg_version_to_debian(&PkgVersion::new(pkg)).unwrap();
assert_eq!(version.to_string(), deb);
}
}
#[cfg(feature = "debian")]
#[test]
fn test_pkg_version_to_debian_unrepresentable() {
assert_eq!(pkg_version_to_debian(&PkgVersion::new("v1.0")), None);
assert_eq!(pkg_version_to_debian(&PkgVersion::new("")), None);
assert_eq!(pkg_version_to_debian(&PkgVersion::new("1.0-2")), None);
assert_eq!(pkg_version_to_debian(&PkgVersion::new("1:0")), None);
assert_eq!(pkg_version_to_debian(&PkgVersion::new("1_0")), None);
}
#[cfg(feature = "debian")]
#[test]
fn test_debian_version_to_pkg() {
let debian: debversion::Version = "2:1.2.3-1".parse().unwrap();
assert_eq!(debian_version_to_pkg(&debian), PkgVersion::new("1.2.3"));
}
#[cfg(feature = "debian")]
#[test]
fn test_debian_round_trip() {
for pkg in ["1.0", "1.2.3", "1.0~rc1", "1.0+dfsg"] {
let version = PkgVersion::new(pkg);
let debian = pkg_version_to_debian(&version).unwrap();
assert_eq!(debian_version_to_pkg(&debian), version);
}
}
#[cfg(feature = "debian")]
#[test]
fn test_debian_ordering_diverges_on_alpha_segment() {
let alpha = pkg_version_to_debian(&PkgVersion::new("1.a")).unwrap();
let numeric = pkg_version_to_debian(&PkgVersion::new("1.0")).unwrap();
assert_eq!(
PkgVersion::new("1.a").cmp(&PkgVersion::new("1.0")),
Ordering::Less,
"pkgconf sorts a letter run below a digit run"
);
assert!(alpha > numeric, "dpkg sorts it above");
}
#[cfg(feature = "debian")]
#[test]
fn test_debian_ordering_agrees() {
let convertible: Vec<&str> = ASCENDING
.iter()
.copied()
.filter(|v| pkg_version_to_debian(&PkgVersion::new(v)).is_some())
.filter(|v| *v != "1.a")
.collect();
for (i, lesser) in convertible.iter().enumerate() {
for greater in &convertible[i + 1..] {
let lesser = pkg_version_to_debian(&PkgVersion::new(lesser)).unwrap();
let greater = pkg_version_to_debian(&PkgVersion::new(greater)).unwrap();
assert!(lesser < greater, "{lesser} should sort before {greater}");
}
}
}
}