pkgcraft/dep/
version.rs

1use std::cmp::Ordering;
2use std::fmt::{self, Write};
3use std::hash::{Hash, Hasher};
4use std::str;
5use std::str::FromStr;
6
7use itertools::EitherOrBoth::{Both, Left, Right};
8use itertools::Itertools;
9use strum::{AsRefStr, Display, EnumIter, EnumString, IntoEnumIterator};
10
11use crate::Error;
12use crate::macros::cmp_not_equal;
13use crate::traits::Intersects;
14
15use super::parse;
16
17/// Modify or create a new type by adding a version operator.
18pub trait WithOp {
19    type WithOp;
20    fn with_op(self, op: Operator) -> Result<Self::WithOp, &'static str>;
21}
22
23#[derive(Debug, Default, Clone)]
24pub(crate) struct Number {
25    pub(crate) raw: String,
26    pub(crate) value: u64,
27}
28
29impl Number {
30    /// Determine if a number is represented by an empty string.
31    pub(crate) fn is_empty(&self) -> bool {
32        self.raw.is_empty()
33    }
34
35    /// Return the raw string value for a number.
36    pub(crate) fn as_str(&self) -> &str {
37        &self.raw
38    }
39
40    /// Determine if a Number starts with another Number using string representation.
41    fn starts_with(&self, other: &Number) -> bool {
42        self.raw.starts_with(&other.raw)
43    }
44}
45
46impl PartialEq for Number {
47    fn eq(&self, other: &Self) -> bool {
48        self.value == other.value
49    }
50}
51
52impl Eq for Number {}
53
54impl Hash for Number {
55    fn hash<H: Hasher>(&self, state: &mut H) {
56        self.value.hash(state);
57    }
58}
59
60impl Ord for Number {
61    fn cmp(&self, other: &Self) -> Ordering {
62        self.value.cmp(&other.value)
63    }
64}
65
66impl PartialOrd for Number {
67    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
68        Some(self.cmp(other))
69    }
70}
71
72impl fmt::Display for Number {
73    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74        write!(f, "{}", self.raw)
75    }
76}
77
78#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
79pub struct Revision(pub(crate) Number);
80
81impl Revision {
82    /// Create a new Revision from a given string.
83    pub fn try_new(s: &str) -> crate::Result<Self> {
84        if s.is_empty() {
85            Ok(Self::default())
86        } else {
87            Ok(parse::revision(s)?)
88        }
89    }
90
91    /// Determine if a revision is represented by an empty string.
92    pub fn is_empty(&self) -> bool {
93        self.0.is_empty()
94    }
95
96    /// Return the raw string value for a revision.
97    pub fn as_str(&self) -> &str {
98        self.0.as_str()
99    }
100
101    /// Determine if a Revision starts with another Revision using string representation.
102    fn starts_with(&self, other: &Revision) -> bool {
103        self.0.starts_with(&other.0)
104    }
105}
106
107impl FromStr for Revision {
108    type Err = Error;
109
110    fn from_str(s: &str) -> crate::Result<Self> {
111        Self::try_new(s)
112    }
113}
114
115impl fmt::Display for Revision {
116    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117        write!(f, "{}", self.0)
118    }
119}
120
121#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122#[strum(serialize_all = "snake_case")]
123pub(crate) enum SuffixKind {
124    Alpha,
125    Beta,
126    Pre,
127    Rc,
128    P,
129}
130
131#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
132pub(crate) struct Suffix {
133    pub(crate) kind: SuffixKind,
134    pub(crate) version: Option<Number>,
135}
136
137impl fmt::Display for Suffix {
138    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139        write!(f, "{}", self.kind)?;
140        if let Some(ver) = self.version.as_ref() {
141            write!(f, "{ver}")?;
142        }
143        Ok(())
144    }
145}
146
147#[repr(C)]
148#[derive(
149    AsRefStr,
150    Display,
151    EnumString,
152    EnumIter,
153    Debug,
154    PartialEq,
155    Eq,
156    PartialOrd,
157    Ord,
158    Hash,
159    Copy,
160    Clone,
161)]
162pub enum Operator {
163    #[strum(serialize = "<")]
164    Less = 1,
165    #[strum(serialize = "<=")]
166    LessOrEqual,
167    #[strum(serialize = "=")]
168    Equal,
169    #[strum(serialize = "=*")]
170    EqualGlob,
171    #[strum(serialize = "~")]
172    Approximate,
173    #[strum(serialize = ">=")]
174    GreaterOrEqual,
175    #[strum(serialize = ">")]
176    Greater,
177}
178
179impl Operator {
180    /// Determine if two versions intersect for an operator.
181    fn intersects(&self, lhs: &Version, rhs: &Version) -> bool {
182        match self {
183            Self::Less => NonOpVersion(rhs) < NonOpVersion(lhs),
184            Self::LessOrEqual => NonOpVersion(rhs) <= NonOpVersion(lhs),
185            Self::Equal => NonOpVersion(rhs) == NonOpVersion(lhs),
186            Self::EqualGlob => rhs.starts_with(lhs),
187            Self::Approximate => NonRevisionVersion(rhs) == NonRevisionVersion(lhs),
188            Self::GreaterOrEqual => NonOpVersion(rhs) >= NonOpVersion(lhs),
189            Self::Greater => NonOpVersion(rhs) > NonOpVersion(lhs),
190        }
191    }
192}
193
194#[derive(Clone)]
195pub struct Version {
196    pub(crate) op: Option<Operator>,
197    pub(crate) numbers: Vec<Number>,
198    pub(crate) letter: Option<char>,
199    pub(crate) suffixes: Vec<Suffix>,
200    pub(crate) revision: Revision,
201}
202
203impl fmt::Debug for Version {
204    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205        write!(f, "Version {{ {self} }}")
206    }
207}
208
209impl WithOp for Version {
210    type WithOp = Version;
211
212    fn with_op(mut self, op: Operator) -> Result<Self::WithOp, &'static str> {
213        if op == Operator::Approximate && !self.revision.is_empty() {
214            Err("~ version operator can't be used with a revision")
215        } else {
216            self.op = Some(op);
217            Ok(self)
218        }
219    }
220}
221
222impl Version {
223    /// Create a [`Version`] from a given string with or without an [`Operator`].
224    pub fn try_new<S: AsRef<str>>(s: S) -> crate::Result<Self> {
225        let s = s.as_ref();
226        if s.starts_with(|c| Operator::iter().any(|op| op.as_ref().starts_with(c))) {
227            parse::version_with_op(s)
228        } else {
229            parse::version(s)
230        }
231    }
232
233    /// Create a [`Version`] with an [`Operator`].
234    pub fn try_new_with_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
235        parse::version_with_op(s.as_ref())
236    }
237
238    /// Create a [`Version`] without an [`Operator`].
239    pub fn try_new_without_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
240        parse::version(s.as_ref())
241    }
242}
243
244impl Version {
245    /// Return a version's operator, if one exists.
246    pub fn op(&self) -> Option<Operator> {
247        self.op
248    }
249
250    /// Return a version's revision.
251    pub fn revision(&self) -> Option<&Revision> {
252        if self.revision.is_empty() {
253            None
254        } else {
255            Some(&self.revision)
256        }
257    }
258
259    /// Return a version's string value without operator.
260    pub fn without_op(&self) -> String {
261        NonOpVersion(self).to_string()
262    }
263
264    /// Return a version's string value without operator or revision.
265    pub fn base(&self) -> String {
266        NonRevisionVersion(self).to_string()
267    }
268
269    /// Determine if a Version starts with another Version, disregarding the operator.
270    fn starts_with(&self, other: &Version) -> bool {
271        // flag denoting the lhs has more components than the rhs
272        let mut unmatched = false;
273
274        // compare components
275        for numbers in self.numbers.iter().zip_longest(&other.numbers) {
276            match numbers {
277                Both(n1, n2) => {
278                    if !n1.starts_with(n2) {
279                        return false;
280                    }
281                }
282                Left(_) => {
283                    unmatched = true;
284                    break;
285                }
286                Right(_) => return false,
287            }
288        }
289
290        // compare letters
291        match (&self.letter, &other.letter) {
292            (Some(c1), Some(c2)) => {
293                if unmatched || c1 != c2 {
294                    return false;
295                }
296            }
297            (None, Some(_)) => return false,
298            (Some(_), None) => unmatched = true,
299            (None, None) => (),
300        }
301
302        // compare suffixes
303        for suffixes in self.suffixes.iter().zip_longest(&other.suffixes) {
304            match suffixes {
305                Both(s1, s2) => {
306                    if unmatched || s1.kind != s2.kind {
307                        return false;
308                    }
309
310                    // compare suffix versions
311                    match (&s1.version, &s2.version) {
312                        (Some(v1), Some(v2)) => {
313                            if !v1.starts_with(v2) {
314                                return false;
315                            }
316                        }
317                        (None, Some(_)) => return false,
318                        (Some(_), None) => unmatched = true,
319                        (None, None) => (),
320                    }
321                }
322                Left(_) => {
323                    unmatched = true;
324                    break;
325                }
326                Right(_) => return false,
327            }
328        }
329
330        // compare revisions
331        match (self.revision(), other.revision()) {
332            (Some(r1), Some(r2)) if unmatched || !r1.starts_with(r2) => false,
333            (None, Some(_)) => false,
334            _ => true,
335        }
336    }
337}
338
339// unbounded operators
340macro_rules! unbounded {
341    () => {
342        Operator::Less | Operator::LessOrEqual | Operator::Greater | Operator::GreaterOrEqual
343    };
344}
345
346/// Determine if two ranged versions intersect.
347macro_rules! ranged {
348    ($ranged:expr, $ranged_op:expr, $other:expr, $other_op:expr) => {
349        match ($ranged_op, $other_op) {
350            // '~' or '=*' -- intersects if range matches
351            (op, Approximate | EqualGlob) if op.intersects($ranged, $other) => true,
352
353            // remaining '~' -- intersects if ranged is '>' or '>=' on other's version with
354            // a nonzero revision, e.g. >1-r1 intersects with ~1
355            (Greater | GreaterOrEqual, Approximate) => $other_op.intersects($other, $ranged),
356            (_, Approximate) => false,
357
358            // '=*' and '<' or '<=' -- intersects if the other revision is 0 or doesn't
359            // exist and glob matches ranged version
360            (Less | LessOrEqual, EqualGlob) => match $other.revision().map(|r| r.as_str()) {
361                None | Some("0") => $ranged.starts_with($other),
362                _ => false,
363            },
364
365            // remaining '=*' -- intersects if glob matches ranged version
366            (_, EqualGlob) => $ranged.starts_with($other),
367
368            // remaining variants should never occur
369            (_, op) => unreachable!("operator should be previously handled: {op:?}"),
370        }
371    };
372}
373
374impl Intersects<Self> for Version {
375    fn intersects(&self, other: &Self) -> bool {
376        use Operator::*;
377        match (self.op, other.op) {
378            // intersects if both are unbounded in the same direction
379            (Some(Less | LessOrEqual), Some(Less | LessOrEqual)) => true,
380            (Some(Greater | GreaterOrEqual), Some(Greater | GreaterOrEqual)) => true,
381
382            // unbounded in opposite directions -- intersects if both match
383            (Some(lhs @ unbounded!()), Some(rhs @ unbounded!())) => {
384                lhs.intersects(self, other) && rhs.intersects(other, self)
385            }
386
387            // both non-op or '~' -- intersects if equal
388            (None, None) | (Some(Approximate), Some(Approximate)) => self == other,
389
390            // either non-op or '=' -- intersects if the other matches it
391            (Some(op), None | Some(Equal)) => op.intersects(self, other),
392            (None | Some(Equal), Some(op)) => op.intersects(other, self),
393
394            // both '=*' -- intersects if either glob matches
395            (Some(op @ EqualGlob), Some(EqualGlob)) => {
396                op.intersects(self, other) || op.intersects(other, self)
397            }
398
399            // '=*' and '~' -- intersects if glob matches unrevisioned version
400            (Some(EqualGlob), Some(Approximate)) => other.starts_with(self),
401            (Some(Approximate), Some(EqualGlob)) => self.starts_with(other),
402
403            // remaining cases must have one op that is unbounded
404            (Some(lhs @ unbounded!()), Some(rhs)) => ranged!(self, lhs, other, rhs),
405            (Some(lhs), Some(rhs @ unbounded!())) => ranged!(other, rhs, self, lhs),
406        }
407    }
408}
409
410impl fmt::Display for Version {
411    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
412        fmt(f, self, true, true)
413    }
414}
415
416/// Format a [`Version`] into a [`String`], optionally ignoring the revision and/or operator.
417fn fmt(f: &mut fmt::Formatter, v: &Version, rev: bool, op: bool) -> fmt::Result {
418    let mut s = String::new();
419
420    write!(s, "{}", v.numbers.iter().join("."))?;
421
422    if let Some(c) = &v.letter {
423        write!(s, "{c}")?;
424    }
425
426    for suffix in &v.suffixes {
427        write!(s, "_{suffix}")?;
428    }
429
430    if rev && let Some(rev) = v.revision() {
431        write!(s, "-r{rev}")?;
432    }
433
434    if op {
435        match &v.op {
436            None => write!(f, "{s}"),
437            Some(Operator::Less) => write!(f, "<{s}"),
438            Some(Operator::LessOrEqual) => write!(f, "<={s}"),
439            Some(Operator::Equal) => write!(f, "={s}"),
440            Some(Operator::EqualGlob) => write!(f, "={s}*"),
441            Some(Operator::Approximate) => write!(f, "~{s}"),
442            Some(Operator::GreaterOrEqual) => write!(f, ">={s}"),
443            Some(Operator::Greater) => write!(f, ">{s}"),
444        }
445    } else {
446        write!(f, "{s}")
447    }
448}
449
450impl PartialEq for Version {
451    fn eq(&self, other: &Self) -> bool {
452        cmp(self, other, true, true) == Ordering::Equal
453    }
454}
455
456impl Eq for Version {}
457
458impl Hash for Version {
459    fn hash<H: Hasher>(&self, state: &mut H) {
460        self.numbers[0].hash(state);
461        for n in &self.numbers[1..] {
462            if n.as_str().starts_with('0') {
463                n.as_str().trim_end_matches('0').hash(state);
464            } else {
465                n.value.hash(state);
466            }
467        }
468        self.letter.hash(state);
469        self.suffixes.hash(state);
470        self.revision.hash(state);
471    }
472}
473
474/// Compare two versions, optionally ignoring the revision and/or operator.
475fn cmp(v1: &Version, v2: &Version, rev: bool, op: bool) -> Ordering {
476    // compare major versions
477    cmp_not_equal!(&v1.numbers[0], &v2.numbers[0]);
478
479    // compare remaining version components
480    for numbers in v1.numbers[1..].iter().zip_longest(&v2.numbers[1..]) {
481        match numbers {
482            Both(n1, n2) => {
483                // compare as strings if a component starts with "0", otherwise as integers
484                let (s1, s2) = (n1.as_str(), n2.as_str());
485                if s1.starts_with('0') || s2.starts_with('0') {
486                    cmp_not_equal!(s1.trim_end_matches('0'), s2.trim_end_matches('0'));
487                } else {
488                    cmp_not_equal!(n1, n2);
489                }
490            }
491            Left(_) => return Ordering::Greater,
492            Right(_) => return Ordering::Less,
493        }
494    }
495
496    // compare letter suffixes
497    cmp_not_equal!(&v1.letter, &v2.letter);
498
499    // compare suffixes
500    for suffixes in v1.suffixes.iter().zip_longest(&v2.suffixes) {
501        match suffixes {
502            Both(s1, s2) => cmp_not_equal!(s1, s2),
503            Left(s) if s.kind == SuffixKind::P => return Ordering::Greater,
504            Left(_) => return Ordering::Less,
505            Right(s) if s.kind == SuffixKind::P => return Ordering::Less,
506            Right(_) => return Ordering::Greater,
507        }
508    }
509
510    // compare revisions
511    if rev {
512        cmp_not_equal!(&v1.revision, &v2.revision);
513    }
514
515    // compare operators
516    if op {
517        cmp_not_equal!(&v1.op, &v2.op);
518    }
519
520    Ordering::Equal
521}
522
523impl Ord for Version {
524    fn cmp(&self, other: &Self) -> Ordering {
525        cmp(self, other, true, true)
526    }
527}
528
529impl PartialOrd for Version {
530    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
531        Some(self.cmp(other))
532    }
533}
534
535impl FromStr for Version {
536    type Err = Error;
537
538    fn from_str(s: &str) -> crate::Result<Self> {
539        Self::try_new(s)
540    }
541}
542
543/// Version wrapper that ignores revisions and operators during comparisons.
544struct NonRevisionVersion<'a>(&'a Version);
545
546impl PartialEq for NonRevisionVersion<'_> {
547    fn eq(&self, other: &Self) -> bool {
548        cmp(self.0, other.0, false, false) == Ordering::Equal
549    }
550}
551
552impl Eq for NonRevisionVersion<'_> {}
553
554impl fmt::Display for NonRevisionVersion<'_> {
555    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
556        fmt(f, self.0, false, false)
557    }
558}
559
560/// Version wrapper that ignores operators during comparisons.
561struct NonOpVersion<'a>(&'a Version);
562
563impl PartialEq for NonOpVersion<'_> {
564    fn eq(&self, other: &Self) -> bool {
565        cmp(self.0, other.0, true, false) == Ordering::Equal
566    }
567}
568
569impl Eq for NonOpVersion<'_> {}
570
571impl Ord for NonOpVersion<'_> {
572    fn cmp(&self, other: &Self) -> Ordering {
573        cmp(self.0, other.0, true, false)
574    }
575}
576
577impl PartialOrd for NonOpVersion<'_> {
578    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
579        Some(self.cmp(other))
580    }
581}
582
583impl fmt::Display for NonOpVersion<'_> {
584    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
585        fmt(f, self.0, true, false)
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use std::collections::{HashMap, HashSet};
592
593    use itertools::Itertools;
594
595    use crate::test::test_data;
596    use crate::utils::hash;
597
598    use super::*;
599
600    #[test]
601    fn new_and_parse() {
602        let data = test_data();
603
604        // invalid
605        for s in &data.version_toml.invalid {
606            let result = Version::try_new(s);
607            assert!(result.is_err(), "{s:?} didn't fail");
608        }
609
610        // valid
611        for s in &data.version_toml.valid {
612            let result = Version::try_new(s);
613            assert!(result.is_ok(), "{s:?} failed");
614            let ver = result.unwrap();
615            assert_eq!(ver.to_string(), s.as_str());
616            assert!(format!("{ver:?}").contains(s));
617        }
618
619        // forced with and without operators
620        assert!(Version::try_new_with_op(">1").is_ok());
621        assert!(Version::try_new_with_op("1").is_err());
622        assert!(Version::try_new_without_op(">1").is_err());
623        assert!(Version::try_new_without_op("1").is_ok());
624    }
625
626    #[test]
627    fn op() {
628        let ver = Version::try_new("1").unwrap();
629        assert!(ver.op().is_none());
630
631        for op in Operator::iter() {
632            let ver = Version::try_new("1").unwrap().with_op(op).unwrap();
633            assert_eq!(ver.op(), Some(op));
634        }
635    }
636
637    #[test]
638    fn rev_new_and_parse() {
639        // invalid
640        for s in ["a", "a1", "1.1", ".1"] {
641            assert!(s.parse::<Revision>().is_err());
642            assert!(Revision::try_new(s).is_err());
643        }
644
645        // valid
646        for s in ["", "1", "01"] {
647            let rev = Revision::try_new(s).unwrap();
648            assert_eq!(rev.to_string(), s);
649        }
650    }
651
652    #[test]
653    fn compare() {
654        let op_map: HashMap<_, _> =
655            [("<", Ordering::Less), ("==", Ordering::Equal), (">", Ordering::Greater)]
656                .into_iter()
657                .collect();
658
659        let data = test_data();
660        for (expr, (s1, op, s2)) in data.version_toml.compares() {
661            let v1 = Version::try_new(s1).unwrap();
662            let v2 = Version::try_new(s2).unwrap();
663            if op == "!=" {
664                assert_ne!(v1, v2, "failed comparing: {expr}");
665                assert_ne!(v2, v1, "failed comparing: {expr}");
666            } else {
667                let op = op_map[op];
668                assert_eq!(v1.cmp(&v2), op, "failed comparing: {expr}");
669                assert_eq!(v2.cmp(&v1), op.reverse(), "failed comparing inverted: {expr}");
670
671                // verify the following property holds since both Hash and Eq are implemented:
672                // k1 == k2 -> hash(k1) == hash(k2)
673                if op == Ordering::Equal {
674                    assert_eq!(hash(&v1), hash(&v2), "failed hash: {expr}");
675                }
676            }
677        }
678    }
679
680    #[test]
681    fn intersects() {
682        let data = test_data();
683        for d in &data.version_toml.intersects {
684            // test intersections between all pairs of distinct values
685            let permutations = d
686                .vals
687                .iter()
688                .map(|s| s.as_str())
689                .permutations(2)
690                .map(|val| val.into_iter().collect_tuple().unwrap());
691            for (s1, s2) in permutations {
692                let v1 = Version::try_new(s1).unwrap();
693                let v2 = Version::try_new(s2).unwrap();
694
695                // self intersection
696                assert!(v1.intersects(&v1), "{s1} doesn't intersect {s2}");
697                assert!(v2.intersects(&v2), "{s2} doesn't intersect {s2}");
698
699                // intersects depending on status
700                if d.status {
701                    assert!(v1.intersects(&v2), "{s1} doesn't intersect {s2}");
702                } else {
703                    assert!(!v1.intersects(&v2), "{s1} intersects {s2}");
704                }
705            }
706        }
707    }
708
709    #[test]
710    fn sorting() {
711        let data = test_data();
712        for d in &data.version_toml.sorting {
713            let mut reversed: Vec<Version> =
714                d.sorted.iter().map(|s| s.parse().unwrap()).rev().collect();
715            reversed.sort();
716            let mut sorted: Vec<_> = reversed.iter().map(|x| x.to_string()).collect();
717            if d.equal {
718                // equal versions aren't sorted so reversing should restore the original order
719                sorted = sorted.into_iter().rev().collect();
720            }
721            assert_eq!(&sorted, &d.sorted);
722        }
723    }
724
725    #[test]
726    fn hashing() {
727        let data = test_data();
728        for d in &data.version_toml.hashing {
729            let set: HashSet<Version> =
730                d.versions.iter().map(|s| s.parse().unwrap()).collect();
731            if d.equal {
732                assert_eq!(set.len(), 1, "failed hashing versions: {set:?}");
733            } else {
734                assert_eq!(set.len(), d.versions.len(), "failed hashing versions: {set:?}");
735            }
736        }
737    }
738}