use std::cmp::Ordering;
use std::fmt::{self, Write};
use std::hash::{Hash, Hasher};
use std::str;
use std::str::FromStr;
use itertools::EitherOrBoth::{Both, Left, Right};
use itertools::Itertools;
use strum::{AsRefStr, Display, EnumIter, EnumString, IntoEnumIterator};
use crate::Error;
use crate::macros::cmp_not_equal;
use crate::traits::Intersects;
use super::parse;
pub trait WithOp {
type WithOp;
fn with_op(self, op: Operator) -> Result<Self::WithOp, &'static str>;
}
#[derive(Debug, Default, Clone)]
pub(crate) struct Number {
pub(crate) raw: String,
pub(crate) value: u64,
}
impl Number {
pub(crate) fn is_empty(&self) -> bool {
self.raw.is_empty()
}
pub(crate) fn as_str(&self) -> &str {
&self.raw
}
fn starts_with(&self, other: &Number) -> bool {
self.raw.starts_with(&other.raw)
}
}
impl PartialEq for Number {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl Eq for Number {}
impl Hash for Number {
fn hash<H: Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl Ord for Number {
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
impl PartialOrd for Number {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for Number {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.raw)
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub struct Revision(pub(crate) Number);
impl Revision {
pub fn try_new(s: &str) -> crate::Result<Self> {
if s.is_empty() {
Ok(Self::default())
} else {
Ok(parse::revision(s)?)
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
fn starts_with(&self, other: &Revision) -> bool {
self.0.starts_with(&other.0)
}
}
impl FromStr for Revision {
type Err = Error;
fn from_str(s: &str) -> crate::Result<Self> {
Self::try_new(s)
}
}
impl fmt::Display for Revision {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum SuffixKind {
Alpha,
Beta,
Pre,
Rc,
P,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub(crate) struct Suffix {
pub(crate) kind: SuffixKind,
pub(crate) version: Option<Number>,
}
impl fmt::Display for Suffix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(ver) = self.version.as_ref() {
write!(f, "{ver}")?;
}
Ok(())
}
}
#[repr(C)]
#[derive(
AsRefStr,
Display,
EnumString,
EnumIter,
Debug,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Copy,
Clone,
)]
pub enum Operator {
#[strum(serialize = "<")]
Less = 1,
#[strum(serialize = "<=")]
LessOrEqual,
#[strum(serialize = "=")]
Equal,
#[strum(serialize = "=*")]
EqualGlob,
#[strum(serialize = "~")]
Approximate,
#[strum(serialize = ">=")]
GreaterOrEqual,
#[strum(serialize = ">")]
Greater,
}
impl Operator {
fn intersects(&self, lhs: &Version, rhs: &Version) -> bool {
match self {
Self::Less => NonOpVersion(rhs) < NonOpVersion(lhs),
Self::LessOrEqual => NonOpVersion(rhs) <= NonOpVersion(lhs),
Self::Equal => NonOpVersion(rhs) == NonOpVersion(lhs),
Self::EqualGlob => rhs.starts_with(lhs),
Self::Approximate => NonRevisionVersion(rhs) == NonRevisionVersion(lhs),
Self::GreaterOrEqual => NonOpVersion(rhs) >= NonOpVersion(lhs),
Self::Greater => NonOpVersion(rhs) > NonOpVersion(lhs),
}
}
}
#[derive(Clone)]
pub struct Version {
pub(crate) op: Option<Operator>,
pub(crate) numbers: Vec<Number>,
pub(crate) letter: Option<char>,
pub(crate) suffixes: Vec<Suffix>,
pub(crate) revision: Revision,
}
impl fmt::Debug for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Version {{ {self} }}")
}
}
impl WithOp for Version {
type WithOp = Version;
fn with_op(mut self, op: Operator) -> Result<Self::WithOp, &'static str> {
if op == Operator::Approximate && !self.revision.is_empty() {
Err("~ version operator can't be used with a revision")
} else {
self.op = Some(op);
Ok(self)
}
}
}
impl Version {
pub fn try_new<S: AsRef<str>>(s: S) -> crate::Result<Self> {
let s = s.as_ref();
if s.starts_with(|c| Operator::iter().any(|op| op.as_ref().starts_with(c))) {
parse::version_with_op(s)
} else {
parse::version(s)
}
}
pub fn try_new_with_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
parse::version_with_op(s.as_ref())
}
pub fn try_new_without_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
parse::version(s.as_ref())
}
}
impl Version {
pub fn op(&self) -> Option<Operator> {
self.op
}
pub fn revision(&self) -> Option<&Revision> {
if self.revision.is_empty() {
None
} else {
Some(&self.revision)
}
}
pub fn without_op(&self) -> String {
NonOpVersion(self).to_string()
}
pub fn base(&self) -> String {
NonRevisionVersion(self).to_string()
}
fn starts_with(&self, other: &Version) -> bool {
let mut unmatched = false;
for numbers in self.numbers.iter().zip_longest(&other.numbers) {
match numbers {
Both(n1, n2) => {
if !n1.starts_with(n2) {
return false;
}
}
Left(_) => {
unmatched = true;
break;
}
Right(_) => return false,
}
}
match (&self.letter, &other.letter) {
(Some(c1), Some(c2)) => {
if unmatched || c1 != c2 {
return false;
}
}
(None, Some(_)) => return false,
(Some(_), None) => unmatched = true,
(None, None) => (),
}
for suffixes in self.suffixes.iter().zip_longest(&other.suffixes) {
match suffixes {
Both(s1, s2) => {
if unmatched || s1.kind != s2.kind {
return false;
}
match (&s1.version, &s2.version) {
(Some(v1), Some(v2)) => {
if !v1.starts_with(v2) {
return false;
}
}
(None, Some(_)) => return false,
(Some(_), None) => unmatched = true,
(None, None) => (),
}
}
Left(_) => {
unmatched = true;
break;
}
Right(_) => return false,
}
}
match (self.revision(), other.revision()) {
(Some(r1), Some(r2)) if unmatched || !r1.starts_with(r2) => false,
(None, Some(_)) => false,
_ => true,
}
}
}
macro_rules! unbounded {
() => {
Operator::Less | Operator::LessOrEqual | Operator::Greater | Operator::GreaterOrEqual
};
}
macro_rules! ranged {
($ranged:expr, $ranged_op:expr, $other:expr, $other_op:expr) => {
match ($ranged_op, $other_op) {
(op, Approximate | EqualGlob) if op.intersects($ranged, $other) => true,
(Greater | GreaterOrEqual, Approximate) => $other_op.intersects($other, $ranged),
(_, Approximate) => false,
(Less | LessOrEqual, EqualGlob) => match $other.revision().map(|r| r.as_str()) {
None | Some("0") => $ranged.starts_with($other),
_ => false,
},
(_, EqualGlob) => $ranged.starts_with($other),
(_, op) => unreachable!("operator should be previously handled: {op:?}"),
}
};
}
impl Intersects<Self> for Version {
fn intersects(&self, other: &Self) -> bool {
use Operator::*;
match (self.op, other.op) {
(Some(Less | LessOrEqual), Some(Less | LessOrEqual)) => true,
(Some(Greater | GreaterOrEqual), Some(Greater | GreaterOrEqual)) => true,
(Some(lhs @ unbounded!()), Some(rhs @ unbounded!())) => {
lhs.intersects(self, other) && rhs.intersects(other, self)
}
(None, None) | (Some(Approximate), Some(Approximate)) => self == other,
(Some(op), None | Some(Equal)) => op.intersects(self, other),
(None | Some(Equal), Some(op)) => op.intersects(other, self),
(Some(op @ EqualGlob), Some(EqualGlob)) => {
op.intersects(self, other) || op.intersects(other, self)
}
(Some(EqualGlob), Some(Approximate)) => other.starts_with(self),
(Some(Approximate), Some(EqualGlob)) => self.starts_with(other),
(Some(lhs @ unbounded!()), Some(rhs)) => ranged!(self, lhs, other, rhs),
(Some(lhs), Some(rhs @ unbounded!())) => ranged!(other, rhs, self, lhs),
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt(f, self, true, true)
}
}
fn fmt(f: &mut fmt::Formatter, v: &Version, rev: bool, op: bool) -> fmt::Result {
let mut s = String::new();
write!(s, "{}", v.numbers.iter().join("."))?;
if let Some(c) = &v.letter {
write!(s, "{c}")?;
}
for suffix in &v.suffixes {
write!(s, "_{suffix}")?;
}
if rev && let Some(rev) = v.revision() {
write!(s, "-r{rev}")?;
}
if op {
match &v.op {
None => write!(f, "{s}"),
Some(Operator::Less) => write!(f, "<{s}"),
Some(Operator::LessOrEqual) => write!(f, "<={s}"),
Some(Operator::Equal) => write!(f, "={s}"),
Some(Operator::EqualGlob) => write!(f, "={s}*"),
Some(Operator::Approximate) => write!(f, "~{s}"),
Some(Operator::GreaterOrEqual) => write!(f, ">={s}"),
Some(Operator::Greater) => write!(f, ">{s}"),
}
} else {
write!(f, "{s}")
}
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
cmp(self, other, true, true) == Ordering::Equal
}
}
impl Eq for Version {}
impl Hash for Version {
fn hash<H: Hasher>(&self, state: &mut H) {
self.numbers[0].hash(state);
for n in &self.numbers[1..] {
if n.as_str().starts_with('0') {
n.as_str().trim_end_matches('0').hash(state);
} else {
n.value.hash(state);
}
}
self.letter.hash(state);
self.suffixes.hash(state);
self.revision.hash(state);
}
}
fn cmp(v1: &Version, v2: &Version, rev: bool, op: bool) -> Ordering {
cmp_not_equal!(&v1.numbers[0], &v2.numbers[0]);
for numbers in v1.numbers[1..].iter().zip_longest(&v2.numbers[1..]) {
match numbers {
Both(n1, n2) => {
let (s1, s2) = (n1.as_str(), n2.as_str());
if s1.starts_with('0') || s2.starts_with('0') {
cmp_not_equal!(s1.trim_end_matches('0'), s2.trim_end_matches('0'));
} else {
cmp_not_equal!(n1, n2);
}
}
Left(_) => return Ordering::Greater,
Right(_) => return Ordering::Less,
}
}
cmp_not_equal!(&v1.letter, &v2.letter);
for suffixes in v1.suffixes.iter().zip_longest(&v2.suffixes) {
match suffixes {
Both(s1, s2) => cmp_not_equal!(s1, s2),
Left(s) if s.kind == SuffixKind::P => return Ordering::Greater,
Left(_) => return Ordering::Less,
Right(s) if s.kind == SuffixKind::P => return Ordering::Less,
Right(_) => return Ordering::Greater,
}
}
if rev {
cmp_not_equal!(&v1.revision, &v2.revision);
}
if op {
cmp_not_equal!(&v1.op, &v2.op);
}
Ordering::Equal
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
cmp(self, other, true, true)
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl FromStr for Version {
type Err = Error;
fn from_str(s: &str) -> crate::Result<Self> {
Self::try_new(s)
}
}
struct NonRevisionVersion<'a>(&'a Version);
impl PartialEq for NonRevisionVersion<'_> {
fn eq(&self, other: &Self) -> bool {
cmp(self.0, other.0, false, false) == Ordering::Equal
}
}
impl Eq for NonRevisionVersion<'_> {}
impl fmt::Display for NonRevisionVersion<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt(f, self.0, false, false)
}
}
struct NonOpVersion<'a>(&'a Version);
impl PartialEq for NonOpVersion<'_> {
fn eq(&self, other: &Self) -> bool {
cmp(self.0, other.0, true, false) == Ordering::Equal
}
}
impl Eq for NonOpVersion<'_> {}
impl Ord for NonOpVersion<'_> {
fn cmp(&self, other: &Self) -> Ordering {
cmp(self.0, other.0, true, false)
}
}
impl PartialOrd for NonOpVersion<'_> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for NonOpVersion<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt(f, self.0, true, false)
}
}
#[cfg(test)]
mod tests {
use std::collections::{HashMap, HashSet};
use itertools::Itertools;
use crate::test::test_data;
use crate::utils::hash;
use super::*;
#[test]
fn new_and_parse() {
let data = test_data();
for s in &data.version_toml.invalid {
let result = Version::try_new(s);
assert!(result.is_err(), "{s:?} didn't fail");
}
for s in &data.version_toml.valid {
let result = Version::try_new(s);
assert!(result.is_ok(), "{s:?} failed");
let ver = result.unwrap();
assert_eq!(ver.to_string(), s.as_str());
assert!(format!("{ver:?}").contains(s));
}
assert!(Version::try_new_with_op(">1").is_ok());
assert!(Version::try_new_with_op("1").is_err());
assert!(Version::try_new_without_op(">1").is_err());
assert!(Version::try_new_without_op("1").is_ok());
}
#[test]
fn op() {
let ver = Version::try_new("1").unwrap();
assert!(ver.op().is_none());
for op in Operator::iter() {
let ver = Version::try_new("1").unwrap().with_op(op).unwrap();
assert_eq!(ver.op(), Some(op));
}
}
#[test]
fn rev_new_and_parse() {
for s in ["a", "a1", "1.1", ".1"] {
assert!(s.parse::<Revision>().is_err());
assert!(Revision::try_new(s).is_err());
}
for s in ["", "1", "01"] {
let rev = Revision::try_new(s).unwrap();
assert_eq!(rev.to_string(), s);
}
}
#[test]
fn compare() {
let op_map: HashMap<_, _> =
[("<", Ordering::Less), ("==", Ordering::Equal), (">", Ordering::Greater)]
.into_iter()
.collect();
let data = test_data();
for (expr, (s1, op, s2)) in data.version_toml.compares() {
let v1 = Version::try_new(s1).unwrap();
let v2 = Version::try_new(s2).unwrap();
if op == "!=" {
assert_ne!(v1, v2, "failed comparing: {expr}");
assert_ne!(v2, v1, "failed comparing: {expr}");
} else {
let op = op_map[op];
assert_eq!(v1.cmp(&v2), op, "failed comparing: {expr}");
assert_eq!(v2.cmp(&v1), op.reverse(), "failed comparing inverted: {expr}");
if op == Ordering::Equal {
assert_eq!(hash(&v1), hash(&v2), "failed hash: {expr}");
}
}
}
}
#[test]
fn intersects() {
let data = test_data();
for d in &data.version_toml.intersects {
let permutations = d
.vals
.iter()
.map(|s| s.as_str())
.permutations(2)
.map(|val| val.into_iter().collect_tuple().unwrap());
for (s1, s2) in permutations {
let v1 = Version::try_new(s1).unwrap();
let v2 = Version::try_new(s2).unwrap();
assert!(v1.intersects(&v1), "{s1} doesn't intersect {s2}");
assert!(v2.intersects(&v2), "{s2} doesn't intersect {s2}");
if d.status {
assert!(v1.intersects(&v2), "{s1} doesn't intersect {s2}");
} else {
assert!(!v1.intersects(&v2), "{s1} intersects {s2}");
}
}
}
}
#[test]
fn sorting() {
let data = test_data();
for d in &data.version_toml.sorting {
let mut reversed: Vec<Version> =
d.sorted.iter().map(|s| s.parse().unwrap()).rev().collect();
reversed.sort();
let mut sorted: Vec<_> = reversed.iter().map(|x| x.to_string()).collect();
if d.equal {
sorted = sorted.into_iter().rev().collect();
}
assert_eq!(&sorted, &d.sorted);
}
}
#[test]
fn hashing() {
let data = test_data();
for d in &data.version_toml.hashing {
let set: HashSet<Version> =
d.versions.iter().map(|s| s.parse().unwrap()).collect();
if d.equal {
assert_eq!(set.len(), 1, "failed hashing versions: {set:?}");
} else {
assert_eq!(set.len(), d.versions.len(), "failed hashing versions: {set:?}");
}
}
}
}