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 {
431        if let Some(rev) = v.revision() {
432            write!(s, "-r{rev}")?;
433        }
434    }
435
436    if op {
437        match &v.op {
438            None => write!(f, "{s}"),
439            Some(Operator::Less) => write!(f, "<{s}"),
440            Some(Operator::LessOrEqual) => write!(f, "<={s}"),
441            Some(Operator::Equal) => write!(f, "={s}"),
442            Some(Operator::EqualGlob) => write!(f, "={s}*"),
443            Some(Operator::Approximate) => write!(f, "~{s}"),
444            Some(Operator::GreaterOrEqual) => write!(f, ">={s}"),
445            Some(Operator::Greater) => write!(f, ">{s}"),
446        }
447    } else {
448        write!(f, "{s}")
449    }
450}
451
452impl PartialEq for Version {
453    fn eq(&self, other: &Self) -> bool {
454        cmp(self, other, true, true) == Ordering::Equal
455    }
456}
457
458impl Eq for Version {}
459
460impl Hash for Version {
461    fn hash<H: Hasher>(&self, state: &mut H) {
462        self.numbers[0].hash(state);
463        for n in &self.numbers[1..] {
464            if n.as_str().starts_with('0') {
465                n.as_str().trim_end_matches('0').hash(state);
466            } else {
467                n.value.hash(state);
468            }
469        }
470        self.letter.hash(state);
471        self.suffixes.hash(state);
472        self.revision.hash(state);
473    }
474}
475
476/// Compare two versions, optionally ignoring the revision and/or operator.
477fn cmp(v1: &Version, v2: &Version, rev: bool, op: bool) -> Ordering {
478    // compare major versions
479    cmp_not_equal!(&v1.numbers[0], &v2.numbers[0]);
480
481    // compare remaining version components
482    for numbers in v1.numbers[1..].iter().zip_longest(&v2.numbers[1..]) {
483        match numbers {
484            Both(n1, n2) => {
485                // compare as strings if a component starts with "0", otherwise as integers
486                let (s1, s2) = (n1.as_str(), n2.as_str());
487                if s1.starts_with('0') || s2.starts_with('0') {
488                    cmp_not_equal!(s1.trim_end_matches('0'), s2.trim_end_matches('0'));
489                } else {
490                    cmp_not_equal!(n1, n2);
491                }
492            }
493            Left(_) => return Ordering::Greater,
494            Right(_) => return Ordering::Less,
495        }
496    }
497
498    // compare letter suffixes
499    cmp_not_equal!(&v1.letter, &v2.letter);
500
501    // compare suffixes
502    for suffixes in v1.suffixes.iter().zip_longest(&v2.suffixes) {
503        match suffixes {
504            Both(s1, s2) => cmp_not_equal!(s1, s2),
505            Left(s) if s.kind == SuffixKind::P => return Ordering::Greater,
506            Left(_) => return Ordering::Less,
507            Right(s) if s.kind == SuffixKind::P => return Ordering::Less,
508            Right(_) => return Ordering::Greater,
509        }
510    }
511
512    // compare revisions
513    if rev {
514        cmp_not_equal!(&v1.revision, &v2.revision);
515    }
516
517    // compare operators
518    if op {
519        cmp_not_equal!(&v1.op, &v2.op);
520    }
521
522    Ordering::Equal
523}
524
525impl Ord for Version {
526    fn cmp(&self, other: &Self) -> Ordering {
527        cmp(self, other, true, true)
528    }
529}
530
531impl PartialOrd for Version {
532    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
533        Some(self.cmp(other))
534    }
535}
536
537impl FromStr for Version {
538    type Err = Error;
539
540    fn from_str(s: &str) -> crate::Result<Self> {
541        Self::try_new(s)
542    }
543}
544
545/// Version wrapper that ignores revisions and operators during comparisons.
546struct NonRevisionVersion<'a>(&'a Version);
547
548impl PartialEq for NonRevisionVersion<'_> {
549    fn eq(&self, other: &Self) -> bool {
550        cmp(self.0, other.0, false, false) == Ordering::Equal
551    }
552}
553
554impl Eq for NonRevisionVersion<'_> {}
555
556impl fmt::Display for NonRevisionVersion<'_> {
557    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
558        fmt(f, self.0, false, false)
559    }
560}
561
562/// Version wrapper that ignores operators during comparisons.
563struct NonOpVersion<'a>(&'a Version);
564
565impl PartialEq for NonOpVersion<'_> {
566    fn eq(&self, other: &Self) -> bool {
567        cmp(self.0, other.0, true, false) == Ordering::Equal
568    }
569}
570
571impl Eq for NonOpVersion<'_> {}
572
573impl Ord for NonOpVersion<'_> {
574    fn cmp(&self, other: &Self) -> Ordering {
575        cmp(self.0, other.0, true, false)
576    }
577}
578
579impl PartialOrd for NonOpVersion<'_> {
580    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
581        Some(self.cmp(other))
582    }
583}
584
585impl fmt::Display for NonOpVersion<'_> {
586    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
587        fmt(f, self.0, true, false)
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use std::collections::{HashMap, HashSet};
594
595    use itertools::Itertools;
596
597    use crate::test::test_data;
598    use crate::utils::hash;
599
600    use super::*;
601
602    #[test]
603    fn new_and_parse() {
604        let data = test_data();
605
606        // invalid
607        for s in &data.version_toml.invalid {
608            let result = Version::try_new(s);
609            assert!(result.is_err(), "{s:?} didn't fail");
610        }
611
612        // valid
613        for s in &data.version_toml.valid {
614            let result = Version::try_new(s);
615            assert!(result.is_ok(), "{s:?} failed");
616            let ver = result.unwrap();
617            assert_eq!(ver.to_string(), s.as_str());
618            assert!(format!("{ver:?}").contains(s));
619        }
620
621        // forced with and without operators
622        assert!(Version::try_new_with_op(">1").is_ok());
623        assert!(Version::try_new_with_op("1").is_err());
624        assert!(Version::try_new_without_op(">1").is_err());
625        assert!(Version::try_new_without_op("1").is_ok());
626    }
627
628    #[test]
629    fn op() {
630        let ver = Version::try_new("1").unwrap();
631        assert!(ver.op().is_none());
632
633        for op in Operator::iter() {
634            let ver = Version::try_new("1").unwrap().with_op(op).unwrap();
635            assert_eq!(ver.op(), Some(op));
636        }
637    }
638
639    #[test]
640    fn rev_new_and_parse() {
641        // invalid
642        for s in ["a", "a1", "1.1", ".1"] {
643            assert!(s.parse::<Revision>().is_err());
644            assert!(Revision::try_new(s).is_err());
645        }
646
647        // valid
648        for s in ["", "1", "01"] {
649            let rev = Revision::try_new(s).unwrap();
650            assert_eq!(rev.to_string(), s);
651        }
652    }
653
654    #[test]
655    fn compare() {
656        let op_map: HashMap<_, _> =
657            [("<", Ordering::Less), ("==", Ordering::Equal), (">", Ordering::Greater)]
658                .into_iter()
659                .collect();
660
661        let data = test_data();
662        for (expr, (s1, op, s2)) in data.version_toml.compares() {
663            let v1 = Version::try_new(s1).unwrap();
664            let v2 = Version::try_new(s2).unwrap();
665            if op == "!=" {
666                assert_ne!(v1, v2, "failed comparing: {expr}");
667                assert_ne!(v2, v1, "failed comparing: {expr}");
668            } else {
669                let op = op_map[op];
670                assert_eq!(v1.cmp(&v2), op, "failed comparing: {expr}");
671                assert_eq!(v2.cmp(&v1), op.reverse(), "failed comparing inverted: {expr}");
672
673                // verify the following property holds since both Hash and Eq are implemented:
674                // k1 == k2 -> hash(k1) == hash(k2)
675                if op == Ordering::Equal {
676                    assert_eq!(hash(&v1), hash(&v2), "failed hash: {expr}");
677                }
678            }
679        }
680    }
681
682    #[test]
683    fn intersects() {
684        let data = test_data();
685        for d in &data.version_toml.intersects {
686            // test intersections between all pairs of distinct values
687            let permutations = d
688                .vals
689                .iter()
690                .map(|s| s.as_str())
691                .permutations(2)
692                .map(|val| val.into_iter().collect_tuple().unwrap());
693            for (s1, s2) in permutations {
694                let v1 = Version::try_new(s1).unwrap();
695                let v2 = Version::try_new(s2).unwrap();
696
697                // self intersection
698                assert!(v1.intersects(&v1), "{s1} doesn't intersect {s2}");
699                assert!(v2.intersects(&v2), "{s2} doesn't intersect {s2}");
700
701                // intersects depending on status
702                if d.status {
703                    assert!(v1.intersects(&v2), "{s1} doesn't intersect {s2}");
704                } else {
705                    assert!(!v1.intersects(&v2), "{s1} intersects {s2}");
706                }
707            }
708        }
709    }
710
711    #[test]
712    fn sorting() {
713        let data = test_data();
714        for d in &data.version_toml.sorting {
715            let mut reversed: Vec<Version> =
716                d.sorted.iter().map(|s| s.parse().unwrap()).rev().collect();
717            reversed.sort();
718            let mut sorted: Vec<_> = reversed.iter().map(|x| x.to_string()).collect();
719            if d.equal {
720                // equal versions aren't sorted so reversing should restore the original order
721                sorted = sorted.into_iter().rev().collect();
722            }
723            assert_eq!(&sorted, &d.sorted);
724        }
725    }
726
727    #[test]
728    fn hashing() {
729        let data = test_data();
730        for d in &data.version_toml.hashing {
731            let set: HashSet<Version> =
732                d.versions.iter().map(|s| s.parse().unwrap()).collect();
733            if d.equal {
734                assert_eq!(set.len(), 1, "failed hashing versions: {set:?}");
735            } else {
736                assert_eq!(set.len(), d.versions.len(), "failed hashing versions: {set:?}");
737            }
738        }
739    }
740}