alpm_types/
version.rs

1use std::{
2    cmp::Ordering,
3    fmt::{Display, Formatter},
4    iter::Peekable,
5    num::NonZeroUsize,
6    str::{CharIndices, Chars, FromStr},
7};
8
9use alpm_parsers::{iter_char_context, iter_str_context};
10use semver::Version as SemverVersion;
11use serde::{Deserialize, Serialize};
12use strum::VariantNames;
13use winnow::{
14    ModalResult,
15    Parser,
16    ascii::{dec_uint, digit1},
17    combinator::{Repeat, alt, cut_err, eof, fail, opt, preceded, repeat, seq, terminated},
18    error::{StrContext, StrContextValue},
19    token::{one_of, take_till, take_while},
20};
21
22use crate::{Architecture, error::Error};
23
24/// The version and architecture of a build tool
25///
26/// `BuildToolVersion` is used in conjunction with `BuildTool` to denote the specific build tool a
27/// package is built with. A `BuildToolVersion` wraps a `Version` (that is guaranteed to have a
28/// `PackageRelease`) and an `Architecture`.
29///
30/// ## Examples
31/// ```
32/// use std::str::FromStr;
33///
34/// use alpm_types::BuildToolVersion;
35///
36/// assert!(BuildToolVersion::from_str("1-1-any").is_ok());
37/// assert!(BuildToolVersion::from_str("1").is_ok());
38/// assert!(BuildToolVersion::from_str("1-1").is_err());
39/// assert!(BuildToolVersion::from_str("1-1-foo").is_err());
40/// ```
41#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
42pub struct BuildToolVersion {
43    version: Version,
44    architecture: Option<Architecture>,
45}
46
47impl BuildToolVersion {
48    /// Create a new BuildToolVersion
49    pub fn new(version: Version, architecture: Option<Architecture>) -> Self {
50        BuildToolVersion {
51            version,
52            architecture,
53        }
54    }
55
56    /// Return a reference to the Architecture
57    pub fn architecture(&self) -> &Option<Architecture> {
58        &self.architecture
59    }
60
61    /// Return a reference to the Version
62    pub fn version(&self) -> &Version {
63        &self.version
64    }
65}
66
67impl FromStr for BuildToolVersion {
68    type Err = Error;
69    /// Create an BuildToolVersion from a string and return it in a Result
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        const VERSION_DELIMITER: char = '-';
72        match s.rsplit_once(VERSION_DELIMITER) {
73            Some((version, architecture)) => match Architecture::from_str(architecture) {
74                Ok(architecture) => Ok(BuildToolVersion {
75                    version: Version::with_pkgrel(version)?,
76                    architecture: Some(architecture),
77                }),
78                Err(err) => Err(err.into()),
79            },
80            None => Ok(BuildToolVersion {
81                version: Version::from_str(s)?,
82                architecture: None,
83            }),
84        }
85    }
86}
87
88impl Display for BuildToolVersion {
89    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
90        if let Some(architecture) = &self.architecture {
91            write!(fmt, "{}-{}", self.version, architecture)
92        } else {
93            write!(fmt, "{}", self.version)
94        }
95    }
96}
97
98/// An epoch of a package
99///
100/// Epoch is used to indicate the downgrade of a package and is prepended to a version, delimited by
101/// a `":"` (e.g. `1:` is added to `0.10.0-1` to form `1:0.10.0-1` which then orders newer than
102/// `1.0.0-1`).
103/// See [alpm-epoch] for details on the format.
104///
105/// An Epoch wraps a usize that is guaranteed to be greater than `0`.
106///
107/// ## Examples
108/// ```
109/// use std::str::FromStr;
110///
111/// use alpm_types::Epoch;
112///
113/// assert!(Epoch::from_str("1").is_ok());
114/// assert!(Epoch::from_str("0").is_err());
115/// ```
116///
117/// [alpm-epoch]: https://alpm.archlinux.page/specifications/alpm-epoch.7.html
118#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
119pub struct Epoch(pub NonZeroUsize);
120
121impl Epoch {
122    /// Create a new Epoch
123    pub fn new(epoch: NonZeroUsize) -> Self {
124        Epoch(epoch)
125    }
126
127    /// Recognizes an [`Epoch`] in a string slice.
128    ///
129    /// Consumes all of its input.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error if `input` is not a valid _alpm_epoch_.
134    pub fn parser(input: &mut &str) -> ModalResult<Self> {
135        terminated(dec_uint, eof)
136            .verify_map(NonZeroUsize::new)
137            .context(StrContext::Label("package epoch"))
138            .context(StrContext::Expected(StrContextValue::Description(
139                "positive non-zero decimal integer",
140            )))
141            .map(Self)
142            .parse_next(input)
143    }
144}
145
146impl FromStr for Epoch {
147    type Err = Error;
148    /// Create an Epoch from a string and return it in a Result
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        Ok(Self::parser.parse(s)?)
151    }
152}
153
154impl Display for Epoch {
155    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
156        write!(fmt, "{}", self.0)
157    }
158}
159
160/// The release version of a package.
161///
162/// A [`PackageRelease`] wraps a [`usize`] for its `major` version and an optional [`usize`] for its
163/// `minor` version.
164///
165/// [`PackageRelease`] is used to indicate the build version of a package.
166/// It is mostly useful in conjunction with a [`PackageVersion`] (see [`Version`]).
167/// Refer to [alpm-pkgrel] for more details on the format.
168///
169/// ## Examples
170/// ```
171/// use std::str::FromStr;
172///
173/// use alpm_types::PackageRelease;
174///
175/// assert!(PackageRelease::from_str("1").is_ok());
176/// assert!(PackageRelease::from_str("1.1").is_ok());
177/// assert!(PackageRelease::from_str("0").is_ok());
178/// assert!(PackageRelease::from_str("a").is_err());
179/// assert!(PackageRelease::from_str("1.a").is_err());
180/// ```
181///
182/// [alpm-pkgrel]: https://alpm.archlinux.page/specifications/alpm-pkgrel.7.html
183#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
184pub struct PackageRelease {
185    /// The major version of this package release.
186    pub major: usize,
187    /// The optional minor version of this package release.
188    pub minor: Option<usize>,
189}
190
191impl PackageRelease {
192    /// Creates a new [`PackageRelease`] from a `major` and optional `minor` integer version.
193    ///
194    /// ## Examples
195    /// ```
196    /// use alpm_types::PackageRelease;
197    ///
198    /// # fn main() {
199    /// let release = PackageRelease::new(1, Some(2));
200    /// assert_eq!(format!("{release}"), "1.2");
201    /// # }
202    /// ```
203    pub fn new(major: usize, minor: Option<usize>) -> Self {
204        PackageRelease { major, minor }
205    }
206
207    /// Recognizes a [`PackageRelease`] in a string slice.
208    ///
209    /// Consumes all of its input.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if `input` does not contain a valid [`PackageRelease`].
214    pub fn parser(input: &mut &str) -> ModalResult<Self> {
215        seq!(Self {
216            major: digit1.try_map(FromStr::from_str)
217                .context(StrContext::Label("package release"))
218                .context(StrContext::Expected(StrContextValue::Description(
219                    "positive decimal integer",
220                ))),
221            minor: opt(preceded('.', cut_err(digit1.try_map(FromStr::from_str))))
222                .context(StrContext::Label("package release"))
223                .context(StrContext::Expected(StrContextValue::Description(
224                    "single '.' followed by positive decimal integer",
225                ))),
226            _: eof.context(StrContext::Expected(StrContextValue::Description(
227                "end of package release value",
228            ))),
229        })
230        .parse_next(input)
231    }
232}
233
234impl FromStr for PackageRelease {
235    type Err = Error;
236    /// Creates a [`PackageRelease`] from a string slice.
237    ///
238    /// Delegates to [`PackageRelease::parser`].
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if [`PackageRelease::parser`] fails.
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        Ok(Self::parser.parse(s)?)
245    }
246}
247
248impl Display for PackageRelease {
249    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
250        write!(fmt, "{}", self.major)?;
251        if let Some(minor) = self.minor {
252            write!(fmt, ".{minor}")?;
253        }
254        Ok(())
255    }
256}
257
258impl PartialOrd for PackageRelease {
259    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
260        Some(self.cmp(other))
261    }
262}
263
264impl Ord for PackageRelease {
265    fn cmp(&self, other: &Self) -> Ordering {
266        let major_order = self.major.cmp(&other.major);
267        if major_order != Ordering::Equal {
268            return major_order;
269        }
270
271        match (self.minor, other.minor) {
272            (None, None) => Ordering::Equal,
273            (None, Some(_)) => Ordering::Less,
274            (Some(_), None) => Ordering::Greater,
275            (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
276        }
277    }
278}
279
280/// A pkgver of a package
281///
282/// PackageVersion is used to denote the upstream version of a package.
283///
284/// A PackageVersion wraps a `String`, which is guaranteed to only contain alphanumeric characters,
285/// `"_"`, `"+"` or `"."`, but to not start with a `"_"`, a `"+"` or a `"."` character and to be at
286/// least one char long.
287///
288/// NOTE: This implementation of PackageVersion is stricter than that of libalpm/pacman. It does not
289/// allow empty strings `""`, or chars that are not in the allowed set, or `"."` as the first
290/// character.
291///
292/// ## Examples
293/// ```
294/// use std::str::FromStr;
295///
296/// use alpm_types::PackageVersion;
297///
298/// assert!(PackageVersion::new("1".to_string()).is_ok());
299/// assert!(PackageVersion::new("1.1".to_string()).is_ok());
300/// assert!(PackageVersion::new("foo".to_string()).is_ok());
301/// assert!(PackageVersion::new("0".to_string()).is_ok());
302/// assert!(PackageVersion::new(".0.1".to_string()).is_err());
303/// assert!(PackageVersion::new("_1.0".to_string()).is_err());
304/// assert!(PackageVersion::new("+1.0".to_string()).is_err());
305/// ```
306#[derive(Clone, Debug, Deserialize, Eq, Serialize)]
307pub struct PackageVersion(pub(crate) String);
308
309impl PackageVersion {
310    /// Create a new PackageVersion from a string and return it in a Result
311    pub fn new(pkgver: String) -> Result<Self, Error> {
312        PackageVersion::from_str(pkgver.as_str())
313    }
314
315    /// Return a reference to the inner type
316    pub fn inner(&self) -> &str {
317        &self.0
318    }
319
320    /// Return an iterator over all segments of this version.
321    pub fn segments(&self) -> VersionSegments {
322        VersionSegments::new(&self.0)
323    }
324
325    /// Recognizes a [`PackageVersion`] in a string slice.
326    ///
327    /// Consumes all of its input.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if `input` is not a valid _alpm-pkgrel_.
332    pub fn parser(input: &mut &str) -> ModalResult<Self> {
333        let alnum = |c: char| c.is_ascii_alphanumeric();
334
335        let first_character = one_of(alnum)
336            .context(StrContext::Label("first pkgver character"))
337            .context(StrContext::Expected(StrContextValue::Description(
338                "ASCII alphanumeric character",
339            )));
340        let special_tail_character = ['_', '+', '.'];
341        let tail_character = one_of((alnum, special_tail_character));
342
343        // no error context because this is infallible due to `0..`
344        // note the empty tuple collection to avoid allocation
345        let tail: Repeat<_, _, _, (), _> = repeat(0.., tail_character);
346
347        (
348            first_character,
349            tail,
350            eof.context(StrContext::Label("pkgver character"))
351                .context(StrContext::Expected(StrContextValue::Description(
352                    "ASCII alphanumeric character",
353                )))
354                .context_with(iter_char_context!(special_tail_character)),
355        )
356            .take()
357            .map(|s: &str| Self(s.to_string()))
358            .parse_next(input)
359    }
360}
361
362impl FromStr for PackageVersion {
363    type Err = Error;
364    /// Create a PackageVersion from a string and return it in a Result
365    fn from_str(s: &str) -> Result<Self, Self::Err> {
366        Ok(Self::parser.parse(s)?)
367    }
368}
369
370impl Display for PackageVersion {
371    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
372        write!(fmt, "{}", self.inner())
373    }
374}
375
376/// This enum represents a single segment in a version string.
377/// [`VersionSegment`]s are returned by the [`VersionSegments`] iterator, which is responsible for
378/// splitting a version string into its segments.
379///
380/// Version strings are split according to the following rules:
381///
382/// - Non-alphanumeric characters always count as delimiters (`.`, `-`, `$`, etc.).
383/// - There's no differentiation between delimiters represented by different characters (e.g. `'$$$'
384///   == '...' == '.$-'`).
385/// - Each segment contains the info about the amount of leading delimiters for that segment.
386///   Leading delimiters that directly follow after one another are grouped together. The length of
387///   the delimiters is important, as it plays a crucial role in the algorithm that determines which
388///   version is newer.
389///
390///   `1...a` would be represented as:
391///
392///   ```
393///   use alpm_types::VersionSegment::*;
394///   vec![
395///     Segment { text: "1", delimiter_count: 0},
396///     Segment { text: "a", delimiter_count: 3},
397///   ];
398///   ```
399/// - Alphanumeric strings are also split into individual sub-segments. This is done by walking over
400///   the string and splitting it every time a switch from alphabetic to numeric is detected or vice
401///   versa.
402///
403///   `1.1foo123.0` would be represented as:
404///
405///   ```
406///   use alpm_types::VersionSegment::*;
407///   vec![
408///     Segment { text: "1", delimiter_count: 0},
409///     Segment { text: "1", delimiter_count: 1},
410///     SubSegment { text: "foo" },
411///     SubSegment { text: "123" },
412///     Segment { text: "0", delimiter_count: 1},
413///   ];
414///   ```
415/// - Trailing delimiters are encoded as an empty string.
416///
417///   `1...` would be represented as:
418///
419///   ```
420///   use alpm_types::VersionSegment::*;
421///   vec![
422///     Segment { text: "1", delimiter_count: 0},
423///     Segment { text: "", delimiter_count: 3},
424///   ];
425///   ```
426#[derive(Debug, Clone, Eq, PartialEq)]
427pub enum VersionSegment<'a> {
428    /// The start of a new segment.
429    /// If the current segment can be split into multiple sub-segments, this variant only contains
430    /// the **first** sub-segment.
431    ///
432    /// To figure out whether this is sub-segment, peek at the next element in the
433    /// [`VersionSegments`] iterator, whether it's a [`VersionSegment::SubSegment`].
434    Segment {
435        /// The string representation of this segment
436        text: &'a str,
437        /// The amount of leading delimiters that were found for this segment
438        delimiter_count: usize,
439    },
440    /// A sub-segment of a version string's segment.
441    ///
442    /// Note that the first sub-segment of a segment that can be split into sub-segments is
443    /// counterintuitively represented by [VersionSegment::Segment]. This implementation detail
444    /// is due to the way the comparison algorithm works, as it does not always differentiate
445    /// between segments and sub-segments.
446    SubSegment {
447        /// The string representation of this sub-segment
448        text: &'a str,
449    },
450}
451
452impl<'a> VersionSegment<'a> {
453    /// Returns the inner string slice independent of [`VersionSegment`] variant.
454    pub fn text(&self) -> &str {
455        match self {
456            VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => text,
457        }
458    }
459
460    /// Returns whether the inner string slice is empty, independent of [`VersionSegment`] variant
461    pub fn is_empty(&self) -> bool {
462        match self {
463            VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
464                text.is_empty()
465            }
466        }
467    }
468
469    /// Returns an iterator over the chars of the inner string slice.
470    pub fn chars(&self) -> Chars<'a> {
471        match self {
472            VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
473                text.chars()
474            }
475        }
476    }
477
478    /// Creates a type `T` from the inner string slice by relying on `T`'s [`FromStr::from_str`]
479    /// implementation.
480    pub fn parse<T: FromStr>(&self) -> Result<T, T::Err> {
481        match self {
482            VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
483                FromStr::from_str(text)
484            }
485        }
486    }
487
488    /// Compares the inner string slice with that of another [`VersionSegment`].
489    pub fn str_cmp(&self, other: &VersionSegment) -> Ordering {
490        match self {
491            VersionSegment::Segment { text, .. } | VersionSegment::SubSegment { text } => {
492                text.cmp(&other.text())
493            }
494        }
495    }
496}
497
498/// An [Iterator] over all [VersionSegment]s of an upstream version string.
499/// Check the documentation on [VersionSegment] to see how a string is split into segments.
500///
501/// Important note:
502/// Trailing delimiters will also produce a trailing [VersionSegment] with an empty string.
503///
504/// This iterator is capable of handling utf-8 strings.
505/// However, non alphanumeric chars are still interpreted as delimiters.
506pub struct VersionSegments<'a> {
507    /// The original version string. We need that reference so we can get some string
508    /// slices based on indices later on.
509    version: &'a str,
510    /// An iterator over the version's chars and their respective start byte's index.
511    version_chars: Peekable<CharIndices<'a>>,
512    /// Check if the cursor is currently in a segment.
513    /// This is necessary to detect whether the next segment should be a sub-segment or a new
514    /// segment.
515    in_segment: bool,
516}
517
518impl<'a> VersionSegments<'a> {
519    /// Create a new instance of a VersionSegments iterator.
520    pub fn new(version: &'a str) -> Self {
521        VersionSegments {
522            version,
523            version_chars: version.char_indices().peekable(),
524            in_segment: false,
525        }
526    }
527}
528
529impl<'a> Iterator for VersionSegments<'a> {
530    type Item = VersionSegment<'a>;
531
532    /// Get the next [VersionSegment] of this version string.
533    fn next(&mut self) -> Option<VersionSegment<'a>> {
534        // Used to track the number of delimiters the next segment is prefixed with.
535        let mut delimiter_count = 0;
536
537        // First up, get the delimiters out of the way.
538        // Peek at the next char, if it's a delimiter, consume it and increase the delimiter count.
539        while let Some((_, char)) = self.version_chars.peek() {
540            // An alphanumeric char indicates that we reached the next segment.
541            if char.is_alphanumeric() {
542                break;
543            }
544
545            self.version_chars.next();
546            delimiter_count += 1;
547
548            // As soon as we hit a delimiter, we know that a new segment is about to start.
549            self.in_segment = false;
550            continue;
551        }
552
553        // Get the next char. If there's no further char, we reached the end of the version string.
554        let Some((first_index, first_char)) = self.version_chars.next() else {
555            // We're at the end of the string and now have to differentiate between two cases:
556
557            // 1. There are no trailing delimiters. We can just return `None` as we truly reached
558            //    the end.
559            if delimiter_count == 0 {
560                return None;
561            }
562
563            // 2. There's no further segment, but there were some trailing delimiters. The
564            //    comparison algorithm considers this case which is why we have to somehow encode
565            //    it. We do so by returning an empty segment.
566            return Some(VersionSegment::Segment {
567                text: "",
568                delimiter_count,
569            });
570        };
571
572        // Cache the last valid char + index that was checked. We need this to
573        // calculate the offset in case the last char is a multi-byte UTF-8 char.
574        let mut last_char = first_char;
575        let mut last_char_index = first_index;
576
577        // The following section now handles the splitting of an alphanumeric string into its
578        // sub-segments. As described in the [VersionSegment] docs, the string needs to be split
579        // every time a switch from alphabetic to numeric or vice versa is detected.
580
581        let is_numeric = first_char.is_numeric();
582
583        if is_numeric {
584            // Go through chars until we hit a non-numeric char or reached the end of the string.
585            #[allow(clippy::while_let_on_iterator)]
586            while let Some((index, next_char)) =
587                self.version_chars.next_if(|(_, peek)| peek.is_numeric())
588            {
589                last_char_index = index;
590                last_char = next_char;
591            }
592        } else {
593            // Go through chars until we hit a non-alphabetic char or reached the end of the string.
594            #[allow(clippy::while_let_on_iterator)]
595            while let Some((index, next_char)) =
596                self.version_chars.next_if(|(_, peek)| peek.is_alphabetic())
597            {
598                last_char_index = index;
599                last_char = next_char;
600            }
601        }
602
603        // Create a subslice based on the indices of the first and last char.
604        // The last char might be multi-byte, which is why we add its length.
605        let segment_slice = &self.version[first_index..(last_char_index + last_char.len_utf8())];
606
607        if !self.in_segment {
608            // Any further segments should be sub-segments, unless we hit a delimiter in which
609            // case this variable will reset to false.
610            self.in_segment = true;
611            Some(VersionSegment::Segment {
612                text: segment_slice,
613                delimiter_count,
614            })
615        } else {
616            Some(VersionSegment::SubSegment {
617                text: segment_slice,
618            })
619        }
620    }
621}
622
623impl Ord for PackageVersion {
624    /// This block implements the logic to determine which of two package versions is newer or
625    /// whether they're considered equal.
626    ///
627    /// This logic is surprisingly complex as it mirrors the current C-alpmlib implementation for
628    /// backwards compatibility reasons.
629    /// <https://gitlab.archlinux.org/pacman/pacman/-/blob/a2d029388c7c206f5576456f91bfbea2dca98c96/lib/libalpm/version.c#L83-217>
630    fn cmp(&self, other: &Self) -> Ordering {
631        // Equal strings are considered equal versions.
632        if self.inner() == other.inner() {
633            return Ordering::Equal;
634        }
635
636        let mut self_segments = self.segments();
637        let mut other_segments = other.segments();
638
639        // Loop through both versions' segments and compare them.
640        loop {
641            // Try to get the next segments
642            let self_segment = self_segments.next();
643            let other_segment = other_segments.next();
644
645            // Make sure that there's a next segment for both versions.
646            let (self_segment, other_segment) = match (self_segment, other_segment) {
647                // Both segments exist, we continue after match.
648                (Some(self_seg), Some(other_seg)) => (self_seg, other_seg),
649
650                // Both versions reached their end and are thereby equal.
651                (None, None) => return Ordering::Equal,
652
653                // One version is longer than the other and both are equal until now.
654                //
655                // ## Case 1
656                //
657                // The longer version is one or more **segment**s longer.
658                // In this case, the longer version is always considered newer.
659                //   `1.0` > `1`
660                // `1.0.0` > `1.0`
661                // `1.0.a` > `1.0`
662                //     ⤷ New segment exists, thereby newer
663                //
664                // ## Case 2
665                //
666                // The current **segment** has one or more sub-segments and the next sub-segment is
667                // alphabetic.
668                // In this case, the shorter version is always newer.
669                // The reason for this is to handle pre-releases (e.g. alpha/beta).
670                // `1.0alpha` < `1.0`
671                // `1.0alpha.0` < `1.0`
672                // `1.0alpha12.0` < `1.0`
673                //     ⤷ Next sub-segment is alphabetic.
674                //
675                // ## Case 3
676                //
677                // The current **segment** has one or more sub-segments and the next sub-segment is
678                // numeric. In this case, the longer version is always newer.
679                // `1.alpha0` > `1.alpha`
680                // `1.alpha0.1` > `1.alpha`
681                //         ⤷ Next sub-segment is numeric.
682                (Some(seg), None) => {
683                    // If the current segment is the start of a segment, it's always considered
684                    // newer.
685                    let text = match seg {
686                        VersionSegment::Segment { .. } => return Ordering::Greater,
687                        VersionSegment::SubSegment { text } => text,
688                    };
689
690                    // If it's a sub-segment, we have to check for the edge-case explained above
691                    // If all chars are alphabetic, `self` is consider older.
692                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
693                        return Ordering::Less;
694                    }
695
696                    return Ordering::Greater;
697                }
698
699                // This is the same logic as above, but inverted.
700                (None, Some(seg)) => {
701                    let text = match seg {
702                        VersionSegment::Segment { .. } => return Ordering::Less,
703                        VersionSegment::SubSegment { text } => text,
704                    };
705                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
706                        return Ordering::Greater;
707                    }
708                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
709                        return Ordering::Greater;
710                    }
711
712                    return Ordering::Less;
713                }
714            };
715
716            // At this point, we have two sub-/segments.
717            //
718            // We start with the special case where one or both of the segments are empty.
719            // That means that the end of the version string has been reached, but there were one
720            // or more trailing delimiters, e.g.:
721            //
722            // `1.0.`
723            // `1.0...`
724            if other_segment.is_empty() && self_segment.is_empty() {
725                // Both reached the end of their version with a trailing delimiter.
726                // Counterintuitively, the trailing delimiter count is not considered and both
727                // versions are considered equal
728                // `1.0....` == `1.0.`
729                //       ⤷ Length of delimiters is ignored.
730                return Ordering::Equal;
731            } else if self_segment.is_empty() {
732                // Now we have to consider the special case where `other` is alphabetic.
733                // If that's the case, `self` will be considered newer, as the alphabetic string
734                // indicates a pre-release,
735                // `1.0.` > `1.0alpha0`
736                // `1.0.` > `1.0.alpha.0`
737                //                ⤷ Alphabetic sub-/segment and thereby always older.
738                //
739                // Also, we know that `other_segment` isn't empty at this point.
740                // It's noteworthy that this logic does not differentiated between segments and
741                // sub-segments.
742                if other_segment.chars().all(char::is_alphabetic) {
743                    return Ordering::Greater;
744                }
745
746                // In all other cases, `other` is newer.
747                // `1.0.` < `1.0.0`
748                // `1.0.` < `1.0.2.0`
749                return Ordering::Less;
750            } else if other_segment.is_empty() {
751                // Check docs above, as it's the same logic as above, just inverted.
752                if self_segment.chars().all(char::is_alphabetic) {
753                    return Ordering::Less;
754                }
755
756                return Ordering::Greater;
757            }
758
759            // We finally reached the end handling special cases when the version string ended.
760            // From now on, we know that we have two actual sub-/segments that might be prefixed by
761            // some delimiters.
762            //
763            // However, it is possible that one version has a segment and while the other has a
764            // sub-segment. This special case is what is handled next.
765            //
766            // We purposefully give up ownership of both segments.
767            // This is to ensure that following this match block, we finally only have to
768            // consider the actual text of the segments, as we'll know that both sub-/segments are
769            // of the same type.
770            let (self_text, other_text) = match (self_segment, other_segment) {
771                (
772                    VersionSegment::Segment {
773                        delimiter_count: self_count,
774                        text: self_text,
775                    },
776                    VersionSegment::Segment {
777                        delimiter_count: other_count,
778                        text: other_text,
779                    },
780                ) => {
781                    // Special case:
782                    // If one of the segments has more leading delimiters than the other, it is
783                    // always considered newer, no matter what follows after the delimiters.
784                    // `1..0.0` > `1.2.0`
785                    //    ⤷ Two delimiters, thereby always newer.
786                    // `1..0.0` < `1..2.0`
787                    //               ⤷ Same amount of delimiters, now `2 > 0`
788                    if self_count != other_count {
789                        return self_count.cmp(&other_count);
790                    }
791                    (self_text, other_text)
792                }
793                // If one is the start of a new segment, while the other is still a sub-segment,
794                // we can return early as a new segment always overrules a sub-segment.
795                // `1.alpha0.0` < `1.alpha.0`
796                //         ⤷ sub-segment  ⤷ segment
797                //         In the third iteration there's a sub-segment on the left side while
798                //         there's a segment on the right side.
799                (VersionSegment::Segment { .. }, VersionSegment::SubSegment { .. }) => {
800                    return Ordering::Greater;
801                }
802                (VersionSegment::SubSegment { .. }, VersionSegment::Segment { .. }) => {
803                    return Ordering::Less;
804                }
805                (
806                    VersionSegment::SubSegment { text: self_text },
807                    VersionSegment::SubSegment { text: other_text },
808                ) => (self_text, other_text),
809            };
810
811            // At this point, we know that we are dealing with two identical types of sub-/segments.
812            // Thereby, we now only have to compare the text of those sub-/segments.
813
814            // Check whether any of the texts are numeric.
815            // Numeric sub-/segments are always considered newer than non-numeric sub-/segments.
816            // E.g.: `1.0.0` > `1.foo.0`
817            //          ⤷ `0` vs `foo`.
818            //            `0` is numeric and therebynewer than a alphanumeric one.
819            let self_is_numeric = !self_text.is_empty() && self_text.chars().all(char::is_numeric);
820            let other_is_numeric =
821                !other_text.is_empty() && other_text.chars().all(char::is_numeric);
822
823            if self_is_numeric && !other_is_numeric {
824                return Ordering::Greater;
825            } else if !self_is_numeric && other_is_numeric {
826                return Ordering::Less;
827            } else if self_is_numeric && other_is_numeric {
828                // In case both are numeric, we do a number comparison.
829                // We can parse the string as we know that they only consist of digits, hence the
830                // unwrap.
831                //
832                // Preceding zeroes are to be ignored, which is automatically done by Rust's number
833                // parser.
834                // E.g. `1.0001.1` == `1.1.1`
835                //          ⤷ `000` is ignored in the comparison.
836                let ordering = self_text
837                    .parse::<usize>()
838                    .unwrap()
839                    .cmp(&other_text.parse::<usize>().unwrap());
840
841                match ordering {
842                    Ordering::Less => return Ordering::Less,
843                    Ordering::Greater => return Ordering::Greater,
844                    // If both numbers are equal we check the next sub-/segment.
845                    Ordering::Equal => continue,
846                }
847            }
848
849            // At this point, we know that both sub-/segments are alphabetic.
850            // We do a simple string comparison to determine the newer version.
851            match self_text.cmp(other_text) {
852                Ordering::Less => return Ordering::Less,
853                Ordering::Greater => return Ordering::Greater,
854                // If the strings are equal, we check the next sub-/segment.
855                Ordering::Equal => continue,
856            }
857        }
858    }
859}
860
861impl PartialOrd for PackageVersion {
862    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
863        Some(self.cmp(other))
864    }
865}
866
867impl PartialEq for PackageVersion {
868    fn eq(&self, other: &Self) -> bool {
869        self.cmp(other).is_eq()
870    }
871}
872
873/// The schema version of a type
874///
875/// A `SchemaVersion` wraps a `semver::Version`, which means that the tracked version should follow [semver](https://semver.org).
876/// However, for backwards compatibility reasons it is possible to initialize a `SchemaVersion`
877/// using a non-semver compatible string, *if* it can be parsed to a single `u64` (e.g. `"1"`).
878///
879/// ## Examples
880/// ```
881/// use std::str::FromStr;
882///
883/// use alpm_types::SchemaVersion;
884///
885/// # fn main() -> Result<(), alpm_types::Error> {
886/// // create SchemaVersion from str
887/// let version_one = SchemaVersion::from_str("1.0.0")?;
888/// let version_also_one = SchemaVersion::from_str("1")?;
889/// assert_eq!(version_one, version_also_one);
890///
891/// // format as String
892/// assert_eq!("1.0.0", format!("{}", version_one));
893/// assert_eq!("1.0.0", format!("{}", version_also_one));
894/// # Ok(())
895/// # }
896/// ```
897#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
898pub struct SchemaVersion(SemverVersion);
899
900impl SchemaVersion {
901    /// Create a new SchemaVersion
902    pub fn new(version: SemverVersion) -> Self {
903        SchemaVersion(version)
904    }
905
906    /// Return a reference to the inner type
907    pub fn inner(&self) -> &SemverVersion {
908        &self.0
909    }
910}
911
912impl FromStr for SchemaVersion {
913    type Err = Error;
914    /// Create a new SchemaVersion from a string
915    ///
916    /// When providing a non-semver string with only a number (i.e. no minor or patch version), the
917    /// number is treated as the major version (e.g. `"23"` -> `"23.0.0"`).
918    fn from_str(s: &str) -> Result<SchemaVersion, Self::Err> {
919        if !s.contains('.') {
920            match s.parse() {
921                Ok(major) => Ok(SchemaVersion(SemverVersion::new(major, 0, 0))),
922                Err(e) => Err(Error::InvalidInteger {
923                    kind: e.kind().clone(),
924                }),
925            }
926        } else {
927            match SemverVersion::parse(s) {
928                Ok(version) => Ok(SchemaVersion(version)),
929                Err(e) => Err(Error::InvalidSemver {
930                    kind: e.to_string(),
931                }),
932            }
933        }
934    }
935}
936
937impl Display for SchemaVersion {
938    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
939        write!(fmt, "{}", self.0)
940    }
941}
942
943/// A version of a package
944///
945/// A `Version` tracks an optional `Epoch`, a `PackageVersion` and an optional `PackageRelease`.
946/// See [alpm-package-version] for details on the format.
947///
948/// ## Examples
949/// ```
950/// use std::str::FromStr;
951///
952/// use alpm_types::{Epoch, PackageRelease, PackageVersion, Version};
953///
954/// # fn main() -> Result<(), alpm_types::Error> {
955///
956/// let version = Version::from_str("1:2-3")?;
957/// assert_eq!(version.epoch, Some(Epoch::from_str("1")?));
958/// assert_eq!(version.pkgver, PackageVersion::new("2".to_string())?);
959/// assert_eq!(version.pkgrel, Some(PackageRelease::new(3, None)));
960/// # Ok(())
961/// # }
962/// ```
963///
964/// [alpm-package-version]: https://alpm.archlinux.page/specifications/alpm-package-version.7.html
965#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
966pub struct Version {
967    /// The version of the package
968    pub pkgver: PackageVersion,
969    /// The epoch of the package
970    pub epoch: Option<Epoch>,
971    /// The release of the package
972    pub pkgrel: Option<PackageRelease>,
973}
974
975impl Version {
976    /// Create a new Version
977    pub fn new(
978        pkgver: PackageVersion,
979        epoch: Option<Epoch>,
980        pkgrel: Option<PackageRelease>,
981    ) -> Self {
982        Version {
983            pkgver,
984            epoch,
985            pkgrel,
986        }
987    }
988
989    /// Create a new Version, which is guaranteed to have a PackageRelease
990    pub fn with_pkgrel(version: &str) -> Result<Self, Error> {
991        let version = Version::from_str(version)?;
992        if version.pkgrel.is_some() {
993            Ok(version)
994        } else {
995            Err(Error::MissingComponent {
996                component: "pkgrel",
997            })
998        }
999    }
1000
1001    /// Compare two Versions and return a number
1002    ///
1003    /// The comparison algorithm is based on libalpm/ pacman's vercmp behavior.
1004    ///
1005    /// * `1` if `a` is newer than `b`
1006    /// * `0` if `a` and `b` are considered to be the same version
1007    /// * `-1` if `a` is older than `b`
1008    ///
1009    /// ## Examples
1010    /// ```
1011    /// use std::str::FromStr;
1012    ///
1013    /// use alpm_types::Version;
1014    ///
1015    /// # fn main() -> Result<(), alpm_types::Error> {
1016    ///
1017    /// assert_eq!(
1018    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("0.1.0")?),
1019    ///     1
1020    /// );
1021    /// assert_eq!(
1022    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("1.0.0")?),
1023    ///     0
1024    /// );
1025    /// assert_eq!(
1026    ///     Version::vercmp(&Version::from_str("0.1.0")?, &Version::from_str("1.0.0")?),
1027    ///     -1
1028    /// );
1029    /// # Ok(())
1030    /// # }
1031    /// ```
1032    pub fn vercmp(a: &Version, b: &Version) -> i8 {
1033        match a.cmp(b) {
1034            Ordering::Less => -1,
1035            Ordering::Equal => 0,
1036            Ordering::Greater => 1,
1037        }
1038    }
1039
1040    /// Recognizes a [`Version`] in a string slice.
1041    ///
1042    /// Consumes all of its input.
1043    ///
1044    /// # Errors
1045    ///
1046    /// Returns an error if `input` is not a valid _alpm-package-version_.
1047    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1048        let mut epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
1049            // cut_err now that we've found a pattern with ':'
1050            cut_err(Epoch::parser),
1051        ))
1052        .context(StrContext::Expected(StrContextValue::Description(
1053            "followed by a ':'",
1054        )));
1055
1056        seq!(Self {
1057            epoch: epoch,
1058            pkgver: take_till(1.., '-')
1059                // this context will trigger on empty pkgver due to 1.. above
1060                .context(StrContext::Expected(StrContextValue::Description("pkgver string")))
1061                .and_then(PackageVersion::parser),
1062            pkgrel: opt(preceded('-', cut_err(PackageRelease::parser))),
1063            _: eof.context(StrContext::Expected(StrContextValue::Description("end of version string"))),
1064        })
1065        .parse_next(input)
1066    }
1067}
1068
1069impl FromStr for Version {
1070    type Err = Error;
1071    /// Creates a new [`Version`] from a string slice.
1072    ///
1073    /// Delegates to [`Version::parser`].
1074    ///
1075    /// # Errors
1076    ///
1077    /// Returns an error if [`Version::parser`] fails.
1078    fn from_str(s: &str) -> Result<Version, Self::Err> {
1079        Ok(Self::parser.parse(s)?)
1080    }
1081}
1082
1083impl Display for Version {
1084    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
1085        if let Some(epoch) = self.epoch {
1086            write!(fmt, "{}:", epoch)?;
1087        }
1088
1089        write!(fmt, "{}", self.pkgver)?;
1090
1091        if let Some(pkgrel) = &self.pkgrel {
1092            write!(fmt, "-{}", pkgrel)?;
1093        }
1094
1095        Ok(())
1096    }
1097}
1098
1099impl Ord for Version {
1100    fn cmp(&self, other: &Self) -> Ordering {
1101        match (self.epoch, other.epoch) {
1102            (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
1103                return self_epoch.cmp(&other_epoch);
1104            }
1105            (Some(_), None) => return Ordering::Greater,
1106            (None, Some(_)) => return Ordering::Less,
1107            (_, _) => {}
1108        }
1109
1110        let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
1111        if pkgver_cmp.is_ne() {
1112            return pkgver_cmp;
1113        }
1114
1115        self.pkgrel.cmp(&other.pkgrel)
1116    }
1117}
1118
1119impl PartialOrd for Version {
1120    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1121        Some(self.cmp(other))
1122    }
1123}
1124
1125/// Specifies the comparison function for a [`VersionRequirement`].
1126///
1127/// The package version can be required to be:
1128/// - less than (`<`)
1129/// - less than or equal to (`<=`)
1130/// - equal to (`=`)
1131/// - greater than or equal to (`>=`)
1132/// - greater than (`>`)
1133///
1134/// the specified version.
1135///
1136/// See [alpm-comparison] for details on the format.
1137///
1138/// ## Note
1139///
1140/// The variants of this enum are sorted in a way, that prefers the two-letter comparators over
1141/// the one-letter ones.
1142/// This is because when splitting a string on the string representation of [`VersionComparison`]
1143/// variant and relying on the ordering of [`strum::EnumIter`], the two-letter comparators must be
1144/// checked before checking the one-letter ones to yield robust results.
1145///
1146/// [alpm-comparison]: https://alpm.archlinux.page/specifications/alpm-comparison.7.html
1147#[derive(
1148    strum::AsRefStr,
1149    Clone,
1150    Copy,
1151    Debug,
1152    strum::Display,
1153    strum::EnumIter,
1154    PartialEq,
1155    Eq,
1156    strum::VariantNames,
1157    Serialize,
1158    Deserialize,
1159)]
1160pub enum VersionComparison {
1161    /// Less than or equal to
1162    #[strum(to_string = "<=")]
1163    LessOrEqual,
1164
1165    /// Greater than or equal to
1166    #[strum(to_string = ">=")]
1167    GreaterOrEqual,
1168
1169    /// Equal to
1170    #[strum(to_string = "=")]
1171    Equal,
1172
1173    /// Less than
1174    #[strum(to_string = "<")]
1175    Less,
1176
1177    /// Greater than
1178    #[strum(to_string = ">")]
1179    Greater,
1180}
1181
1182impl VersionComparison {
1183    /// Returns `true` if the result of a comparison between the actual and required package
1184    /// versions satisfies the comparison function.
1185    fn is_compatible_with(self, ord: Ordering) -> bool {
1186        match (self, ord) {
1187            (VersionComparison::Less, Ordering::Less)
1188            | (VersionComparison::LessOrEqual, Ordering::Less | Ordering::Equal)
1189            | (VersionComparison::Equal, Ordering::Equal)
1190            | (VersionComparison::GreaterOrEqual, Ordering::Greater | Ordering::Equal)
1191            | (VersionComparison::Greater, Ordering::Greater) => true,
1192
1193            (VersionComparison::Less, Ordering::Equal | Ordering::Greater)
1194            | (VersionComparison::LessOrEqual, Ordering::Greater)
1195            | (VersionComparison::Equal, Ordering::Less | Ordering::Greater)
1196            | (VersionComparison::GreaterOrEqual, Ordering::Less)
1197            | (VersionComparison::Greater, Ordering::Less | Ordering::Equal) => false,
1198        }
1199    }
1200
1201    /// Recognizes a [`VersionComparison`] in a string slice.
1202    ///
1203    /// Consumes all of its input.
1204    ///
1205    /// # Errors
1206    ///
1207    /// Returns an error if `input` is not a valid _alpm-comparison_.
1208    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1209        alt((
1210            // insert eofs here (instead of after alt call) so correct error message is thrown
1211            ("<=", eof).value(Self::LessOrEqual),
1212            (">=", eof).value(Self::GreaterOrEqual),
1213            ("=", eof).value(Self::Equal),
1214            ("<", eof).value(Self::Less),
1215            (">", eof).value(Self::Greater),
1216            fail.context(StrContext::Label("comparison operator"))
1217                .context_with(iter_str_context!([VersionComparison::VARIANTS])),
1218        ))
1219        .parse_next(input)
1220    }
1221}
1222
1223impl FromStr for VersionComparison {
1224    type Err = Error;
1225
1226    /// Creates a new [`VersionComparison`] from a string slice.
1227    ///
1228    /// Delegates to [`VersionComparison::parser`].
1229    ///
1230    /// # Errors
1231    ///
1232    /// Returns an error if [`VersionComparison::parser`] fails.
1233    fn from_str(s: &str) -> Result<Self, Self::Err> {
1234        Ok(Self::parser.parse(s)?)
1235    }
1236}
1237
1238/// A version requirement, e.g. for a dependency package.
1239///
1240/// It consists of a target version and a comparison function. A version requirement of `>=1.5` has
1241/// a target version of `1.5` and a comparison function of [`VersionComparison::GreaterOrEqual`].
1242/// See [alpm-comparison] for details on the format.
1243///
1244/// ## Examples
1245///
1246/// ```
1247/// use std::str::FromStr;
1248///
1249/// use alpm_types::{Version, VersionComparison, VersionRequirement};
1250///
1251/// # fn main() -> Result<(), alpm_types::Error> {
1252/// let requirement = VersionRequirement::from_str(">=1.5")?;
1253///
1254/// assert_eq!(requirement.comparison, VersionComparison::GreaterOrEqual);
1255/// assert_eq!(requirement.version, Version::from_str("1.5")?);
1256/// # Ok(())
1257/// # }
1258/// ```
1259///
1260/// [alpm-comparison]: https://alpm.archlinux.page/specifications/alpm-comparison.7.html
1261#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1262pub struct VersionRequirement {
1263    /// Version comparison function
1264    pub comparison: VersionComparison,
1265    /// Target version
1266    pub version: Version,
1267}
1268
1269impl VersionRequirement {
1270    /// Create a new `VersionRequirement`
1271    pub fn new(comparison: VersionComparison, version: Version) -> Self {
1272        VersionRequirement {
1273            comparison,
1274            version,
1275        }
1276    }
1277
1278    /// Returns `true` if the requirement is satisfied by the given package version.
1279    ///
1280    /// ## Examples
1281    ///
1282    /// ```
1283    /// use std::str::FromStr;
1284    ///
1285    /// use alpm_types::{Version, VersionRequirement};
1286    ///
1287    /// # fn main() -> Result<(), alpm_types::Error> {
1288    /// let requirement = VersionRequirement::from_str(">=1.5-3")?;
1289    ///
1290    /// assert!(!requirement.is_satisfied_by(&Version::from_str("1.5")?));
1291    /// assert!(requirement.is_satisfied_by(&Version::from_str("1.5-3")?));
1292    /// assert!(requirement.is_satisfied_by(&Version::from_str("1.6")?));
1293    /// assert!(requirement.is_satisfied_by(&Version::from_str("2:1.0")?));
1294    /// assert!(!requirement.is_satisfied_by(&Version::from_str("1.0")?));
1295    /// # Ok(())
1296    /// # }
1297    /// ```
1298    pub fn is_satisfied_by(&self, ver: &Version) -> bool {
1299        self.comparison.is_compatible_with(ver.cmp(&self.version))
1300    }
1301
1302    /// Recognizes a [`VersionRequirement`] in a string slice.
1303    ///
1304    /// Consumes all of its input.
1305    ///
1306    /// # Errors
1307    ///
1308    /// Returns an error if `input` is not a valid _alpm-comparison_.
1309    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1310        seq!(Self {
1311            comparison: take_while(1.., ('<', '>', '='))
1312                // add context here because otherwise take_while can fail and provide no information
1313                .context(StrContext::Expected(StrContextValue::Description(
1314                    "version comparison operator"
1315                )))
1316                .and_then(VersionComparison::parser),
1317            version: Version::parser,
1318        })
1319        .parse_next(input)
1320    }
1321}
1322
1323impl Display for VersionRequirement {
1324    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1325        write!(f, "{}{}", self.comparison, self.version)
1326    }
1327}
1328
1329impl FromStr for VersionRequirement {
1330    type Err = Error;
1331
1332    /// Creates a new [`VersionRequirement`] from a string slice.
1333    ///
1334    /// Delegates to [`VersionRequirement::parser`].
1335    ///
1336    /// # Errors
1337    ///
1338    /// Returns an error if [`VersionRequirement::parser`] fails.
1339    fn from_str(s: &str) -> Result<Self, Self::Err> {
1340        Ok(Self::parser.parse(s)?)
1341    }
1342}
1343
1344#[cfg(test)]
1345mod tests {
1346    use rstest::rstest;
1347
1348    use super::*;
1349
1350    #[rstest]
1351    #[case("1.0.0", Ok(SchemaVersion(SemverVersion::new(1, 0, 0))))]
1352    #[case("1", Ok(SchemaVersion(SemverVersion::new(1, 0, 0))))]
1353    #[case("-1.0.0", Err(Error::InvalidSemver { kind: String::from("unexpected character '-' while parsing major version number") }))]
1354    fn schema_version(#[case] version: &str, #[case] result: Result<SchemaVersion, Error>) {
1355        assert_eq!(result, SchemaVersion::from_str(version))
1356    }
1357
1358    /// Ensure that valid buildtool version strings are parsed as expected.
1359    #[rstest]
1360    #[case(
1361        "1.0.0-1-any",
1362        BuildToolVersion::new(Version::from_str("1.0.0-1").unwrap(), Some(Architecture::from_str("any").unwrap())),
1363    )]
1364    #[case(
1365        "1:1.0.0-1-any",
1366        BuildToolVersion::new(Version::from_str("1:1.0.0-1").unwrap(), Some(Architecture::from_str("any").unwrap())),
1367    )]
1368    #[case(
1369        "1.0.0",
1370        BuildToolVersion::new(Version::from_str("1.0.0").unwrap(), None),
1371    )]
1372    fn valid_buildtoolver_new(#[case] buildtoolver: &str, #[case] expected: BuildToolVersion) {
1373        assert_eq!(
1374            BuildToolVersion::from_str(buildtoolver),
1375            Ok(expected),
1376            "Expected valid parse of buildtoolver '{buildtoolver}'"
1377        );
1378    }
1379
1380    /// Ensure that invalid buildtool version strings produce the respective errors.
1381    #[rstest]
1382    #[case("1.0.0-any", Error::MissingComponent { component: "pkgrel" })]
1383    #[case("1.0.0-1-foo", strum::ParseError::VariantNotFound.into())]
1384    fn invalid_buildtoolver_new(#[case] buildtoolver: &str, #[case] expected: Error) {
1385        assert_eq!(
1386            BuildToolVersion::from_str(buildtoolver),
1387            Err(expected),
1388            "Expected error during parse of buildtoolver '{buildtoolver}'"
1389        );
1390    }
1391
1392    #[rstest]
1393    #[case(".1.0.0-1-any", "invalid first pkgver character")]
1394    fn invalid_buildtoolver_badpkgver(#[case] buildtoolver: &str, #[case] err_snippet: &str) {
1395        let Err(Error::ParseError(err_msg)) = BuildToolVersion::from_str(buildtoolver) else {
1396            panic!("'{buildtoolver}' erroneously parsed as BuildToolVersion")
1397        };
1398        assert!(
1399            err_msg.contains(err_snippet),
1400            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1401        );
1402    }
1403
1404    #[rstest]
1405    #[case(
1406        SchemaVersion(SemverVersion::new(1, 0, 0)),
1407        SchemaVersion(SemverVersion::new(0, 1, 0))
1408    )]
1409    fn compare_schema_version(#[case] version_a: SchemaVersion, #[case] version_b: SchemaVersion) {
1410        assert!(version_a > version_b);
1411    }
1412
1413    /// Ensure that valid version strings are parsed as expected.
1414    #[rstest]
1415    #[case(
1416        "foo",
1417        Version {
1418            epoch: None,
1419            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1420            pkgrel: None
1421        },
1422    )]
1423    #[case(
1424        "1:foo-1",
1425        Version {
1426            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1427            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
1428            pkgrel: Some(PackageRelease::new(1, None))
1429        },
1430    )]
1431    #[case(
1432        "1:foo",
1433        Version {
1434            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1435            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
1436            pkgrel: None,
1437        },
1438    )]
1439    #[case(
1440        "foo-1",
1441        Version {
1442            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1443            epoch: None,
1444            pkgrel: Some(PackageRelease::new(1, None))
1445        }
1446    )]
1447    fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
1448        assert_eq!(
1449            Version::from_str(version),
1450            Ok(expected),
1451            "Expected valid parsing for version {version}"
1452        )
1453    }
1454
1455    /// Ensure that invalid version strings produce the respective errors.
1456    #[rstest]
1457    #[case::two_pkgrel("1:foo-1-1", "expected end of package release value")]
1458    #[case::two_epoch("1:1:foo-1", "invalid pkgver character")]
1459    #[case::no_version("", "expected pkgver string")]
1460    #[case::no_version(":", "invalid first pkgver character")]
1461    #[case::no_version(".", "invalid first pkgver character")]
1462    #[case::invalid_integer(
1463        "-1foo:1",
1464        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
1465    )]
1466    #[case::invalid_integer(
1467        "1-foo:1",
1468        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
1469    )]
1470    fn parse_error_in_version_from_string(#[case] version: &str, #[case] err_snippet: &str) {
1471        let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
1472            panic!("parsing '{version}' did not fail as expected")
1473        };
1474        assert!(
1475            err_msg.contains(err_snippet),
1476            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1477        );
1478    }
1479
1480    /// Test that version parsing works/fails for the special case where a pkgrel is expected.
1481    /// This is done by calling the `with_pkgrel` function directly.
1482    #[rstest]
1483    #[case(
1484        "1.0.0-1",
1485        Ok(Version{
1486            pkgver: PackageVersion::new("1.0.0".to_string()).unwrap(),
1487            pkgrel: Some(PackageRelease::new(1, None)),
1488            epoch: None,
1489        })
1490    )]
1491    #[case("1.0.0", Err(Error::MissingComponent { component: "pkgrel" }))]
1492    fn version_with_pkgrel(#[case] version: &str, #[case] result: Result<Version, Error>) {
1493        assert_eq!(result, Version::with_pkgrel(version));
1494    }
1495
1496    #[rstest]
1497    #[case("1", Ok(Epoch(NonZeroUsize::new(1).unwrap())))]
1498    fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
1499        assert_eq!(result, Epoch::from_str(version));
1500    }
1501
1502    #[rstest]
1503    #[case("0", "expected positive non-zero decimal integer")]
1504    #[case("-0", "expected positive non-zero decimal integer")]
1505    #[case("z", "expected positive non-zero decimal integer")]
1506    fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
1507        let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
1508            panic!("'{input}' erroneously parsed as Epoch")
1509        };
1510        assert!(
1511            err_msg.contains(err_snippet),
1512            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1513        );
1514    }
1515
1516    /// Make sure that we can parse valid **pkgver** strings.
1517    #[rstest]
1518    #[case("foo")]
1519    #[case("1.0.0")]
1520    fn valid_pkgver(#[case] pkgver: &str) {
1521        let parsed = PackageVersion::new(pkgver.to_string());
1522        assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
1523        assert_eq!(
1524            parsed.as_ref().unwrap().to_string(),
1525            pkgver,
1526            "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
1527            parsed.unwrap(),
1528            pkgver
1529        );
1530    }
1531
1532    /// Ensure that invalid **pkgver**s are throwing errors.
1533    #[rstest]
1534    #[case("1:foo", "invalid pkgver character")]
1535    #[case("foo-1", "invalid pkgver character")]
1536    #[case("foo,1", "invalid pkgver character")]
1537    #[case(".foo", "invalid first pkgver character")]
1538    #[case("_foo", "invalid first pkgver character")]
1539    // ß is not in [:alnum:]
1540    #[case("ß", "invalid first pkgver character")]
1541    #[case("1.ß", "invalid pkgver character")]
1542    fn invalid_pkgver(#[case] pkgver: &str, #[case] err_snippet: &str) {
1543        let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
1544            panic!("Expected pkgver {pkgver} to be invalid.")
1545        };
1546        assert!(
1547            err_msg.contains(err_snippet),
1548            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1549        );
1550    }
1551
1552    /// Make sure that we can parse valid **pkgrel** strings.
1553    #[rstest]
1554    #[case("0")]
1555    #[case("1")]
1556    #[case("10")]
1557    #[case("1.0")]
1558    #[case("10.5")]
1559    #[case("0.1")]
1560    fn valid_pkgrel(#[case] pkgrel: &str) {
1561        let parsed = PackageRelease::from_str(pkgrel);
1562        assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
1563        assert_eq!(
1564            parsed.as_ref().unwrap().to_string(),
1565            pkgrel,
1566            "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
1567            parsed.unwrap(),
1568            pkgrel
1569        );
1570    }
1571
1572    /// Ensure that invalid **pkgrel**s are throwing errors.
1573    #[rstest]
1574    #[case(".1", "expected positive decimal integer")]
1575    #[case("1.", "expected single '.' followed by positive decimal integer")]
1576    #[case("1..1", "expected single '.' followed by positive decimal integer")]
1577    #[case("-1", "expected positive decimal integer")]
1578    #[case("a", "expected positive decimal integer")]
1579    #[case("1.a", "expected single '.' followed by positive decimal integer")]
1580    #[case("1.0.0", "expected end of package release")]
1581    #[case("", "expected positive decimal integer")]
1582    fn invalid_pkgrel(#[case] pkgrel: &str, #[case] err_snippet: &str) {
1583        let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
1584            panic!("'{pkgrel}' erroneously parsed as PackageRelease")
1585        };
1586        assert!(
1587            err_msg.contains(err_snippet),
1588            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1589        );
1590    }
1591
1592    /// Test that pkgrel ordering works as intended
1593    #[rstest]
1594    #[case("1", "1.0", Ordering::Less)]
1595    #[case("1.0", "2", Ordering::Less)]
1596    #[case("1", "1.1", Ordering::Less)]
1597    #[case("1.0", "1.1", Ordering::Less)]
1598    #[case("0", "1.1", Ordering::Less)]
1599    #[case("1", "11", Ordering::Less)]
1600    #[case("1", "1", Ordering::Equal)]
1601    #[case("1.2", "1.2", Ordering::Equal)]
1602    #[case("2.0", "2.0", Ordering::Equal)]
1603    #[case("2", "1.0", Ordering::Greater)]
1604    #[case("1.1", "1", Ordering::Greater)]
1605    #[case("1.1", "1.0", Ordering::Greater)]
1606    #[case("1.1", "0", Ordering::Greater)]
1607    #[case("11", "1", Ordering::Greater)]
1608    fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
1609        let first = PackageRelease::from_str(first).unwrap();
1610        let second = PackageRelease::from_str(second).unwrap();
1611        assert_eq!(
1612            first.cmp(&second),
1613            order,
1614            "{first} should be {order:?} to {second}"
1615        );
1616    }
1617
1618    /// Ensure that versions are properly serialized back to their string representation.
1619    #[rstest]
1620    #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
1621    #[case(Version::from_str("1-1").unwrap(), "1-1")]
1622    #[case(Version::from_str("1").unwrap(), "1")]
1623    #[case(Version::from_str("1:1").unwrap(), "1:1")]
1624    fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
1625        assert_eq!(format!("{}", version), to_str);
1626    }
1627
1628    #[rstest]
1629    // Major version comparisons
1630    #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
1631    #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
1632    #[case(
1633        Version::from_str("20220102"),
1634        Version::from_str("20220202"),
1635        Ordering::Less
1636    )]
1637    // Major vs Major.Minor
1638    #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
1639    #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
1640    #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
1641    #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
1642    #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
1643    // Major.Minor version comparisons
1644    #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
1645    #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
1646    #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
1647    #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
1648    #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
1649    #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
1650    #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
1651    // Major.Minor version comparisons with alphanumerics
1652    #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
1653    #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
1654    #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
1655    #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
1656    #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
1657    #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
1658    #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
1659    #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
1660    #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
1661    #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
1662    #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
1663    #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
1664    #[case(
1665        Version::from_str("1.alpha0.0"),
1666        Version::from_str("1.alpha.0"),
1667        Ordering::Less
1668    )]
1669    // Major.Minor vs Major.Minor.Patch
1670    #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
1671    // Major.Minor.Patch
1672    #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
1673    #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
1674    #[case(
1675        Version::from_str("1.0.alpha.0"),
1676        Version::from_str("1.0."),
1677        Ordering::Less
1678    )]
1679    #[case(
1680        Version::from_str("1.a001a.1"),
1681        Version::from_str("1.a1a.1"),
1682        Ordering::Equal
1683    )]
1684    fn version_cmp(
1685        #[case] version_a: Result<Version, Error>,
1686        #[case] version_b: Result<Version, Error>,
1687        #[case] expected: Ordering,
1688    ) {
1689        // Simply unwrap the Version as we expect all test strings to be valid.
1690        let version_a = version_a.unwrap();
1691        let version_b = version_b.unwrap();
1692
1693        // Derive the expected vercmp binary exitcode from the expected Ordering.
1694        let vercmp_result = match &expected {
1695            Ordering::Equal => 0,
1696            Ordering::Greater => 1,
1697            Ordering::Less => -1,
1698        };
1699
1700        let ordering = version_a.cmp(&version_b);
1701        assert_eq!(
1702            ordering, expected,
1703            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
1704        );
1705
1706        assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
1707
1708        // If we find the `vercmp` binary, also run the test against the actual binary.
1709        #[cfg(feature = "compatibility_tests")]
1710        {
1711            let output = std::process::Command::new("vercmp")
1712                .arg(version_a.to_string())
1713                .arg(version_b.to_string())
1714                .output()
1715                .unwrap();
1716            let result = String::from_utf8_lossy(&output.stdout);
1717            assert_eq!(result.trim(), vercmp_result.to_string());
1718        }
1719
1720        // Now check that the opposite holds true as well.
1721        let reverse_vercmp_result = match &expected {
1722            Ordering::Equal => 0,
1723            Ordering::Greater => -1,
1724            Ordering::Less => 1,
1725        };
1726        let reverse_expected = match &expected {
1727            Ordering::Equal => Ordering::Equal,
1728            Ordering::Greater => Ordering::Less,
1729            Ordering::Less => Ordering::Greater,
1730        };
1731
1732        let reverse_ordering = version_b.cmp(&version_a);
1733        assert_eq!(
1734            reverse_ordering, reverse_expected,
1735            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
1736        );
1737
1738        assert_eq!(
1739            Version::vercmp(&version_b, &version_a),
1740            reverse_vercmp_result
1741        );
1742    }
1743
1744    /// Ensure that valid version comparison strings can be parsed.
1745    #[rstest]
1746    #[case("<", VersionComparison::Less)]
1747    #[case("<=", VersionComparison::LessOrEqual)]
1748    #[case("=", VersionComparison::Equal)]
1749    #[case(">=", VersionComparison::GreaterOrEqual)]
1750    #[case(">", VersionComparison::Greater)]
1751    fn valid_version_comparison(#[case] comparison: &str, #[case] expected: VersionComparison) {
1752        assert_eq!(comparison.parse(), Ok(expected));
1753    }
1754
1755    /// Ensure that invalid version comparisons will throw an error.
1756    #[rstest]
1757    #[case("", "invalid comparison operator")]
1758    #[case("<<", "invalid comparison operator")]
1759    #[case("==", "invalid comparison operator")]
1760    #[case("!=", "invalid comparison operator")]
1761    #[case(" =", "invalid comparison operator")]
1762    #[case("= ", "invalid comparison operator")]
1763    #[case("<1", "invalid comparison operator")]
1764    fn invalid_version_comparison(#[case] comparison: &str, #[case] err_snippet: &str) {
1765        let Err(Error::ParseError(err_msg)) = VersionComparison::from_str(comparison) else {
1766            panic!("'{comparison}' did not fail as expected")
1767        };
1768        assert!(
1769            err_msg.contains(err_snippet),
1770            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1771        );
1772    }
1773
1774    /// Test successful parsing for version requirement strings.
1775    #[rstest]
1776    #[case("=1", VersionRequirement {
1777        comparison: VersionComparison::Equal,
1778        version: Version::from_str("1").unwrap(),
1779    })]
1780    #[case("<=42:abcd-2.4", VersionRequirement {
1781        comparison: VersionComparison::LessOrEqual,
1782        version: Version::from_str("42:abcd-2.4").unwrap(),
1783    })]
1784    #[case(">3.1", VersionRequirement {
1785        comparison: VersionComparison::Greater,
1786        version: Version::from_str("3.1").unwrap(),
1787    })]
1788    fn valid_version_requirement(#[case] requirement: &str, #[case] expected: VersionRequirement) {
1789        assert_eq!(
1790            requirement.parse(),
1791            Ok(expected),
1792            "Expected successful parse for version requirement '{requirement}'"
1793        );
1794    }
1795
1796    #[rstest]
1797    #[case::bad_operator("<>3.1", "invalid comparison operator")]
1798    #[case::no_operator("3.1", "expected version comparison operator")]
1799    #[case::arrow_operator("=>3.1", "invalid comparison operator")]
1800    #[case::no_version("<=", "expected pkgver string")]
1801    fn invalid_version_requirement(#[case] requirement: &str, #[case] err_snippet: &str) {
1802        let Err(Error::ParseError(err_msg)) = VersionRequirement::from_str(requirement) else {
1803            panic!("'{requirement}' erroneously parsed as VersionRequirement")
1804        };
1805        assert!(
1806            err_msg.contains(err_snippet),
1807            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1808        );
1809    }
1810
1811    #[rstest]
1812    #[case("<3.1>3.2", "invalid pkgver character")]
1813    fn invalid_version_requirement_pkgver_parse(
1814        #[case] requirement: &str,
1815        #[case] err_snippet: &str,
1816    ) {
1817        let Err(Error::ParseError(err_msg)) = VersionRequirement::from_str(requirement) else {
1818            panic!("'{requirement}' erroneously parsed as VersionRequirement")
1819        };
1820        assert!(
1821            err_msg.contains(err_snippet),
1822            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1823        );
1824    }
1825
1826    /// Check whether a version requirement (>= 1.0) is fulfilled by a given version string.
1827    #[rstest]
1828    #[case("=1", "1", true)]
1829    #[case("=1", "1.0", false)]
1830    #[case("=1", "1-1", false)]
1831    #[case("=1", "1:1", false)]
1832    #[case("=1", "0.9", false)]
1833    #[case("<42", "41", true)]
1834    #[case("<42", "42", false)]
1835    #[case("<42", "43", false)]
1836    #[case("<=42", "41", true)]
1837    #[case("<=42", "42", true)]
1838    #[case("<=42", "43", false)]
1839    #[case(">42", "41", false)]
1840    #[case(">42", "42", false)]
1841    #[case(">42", "43", true)]
1842    #[case(">=42", "41", false)]
1843    #[case(">=42", "42", true)]
1844    #[case(">=42", "43", true)]
1845    fn version_requirement_satisfied(
1846        #[case] requirement: &str,
1847        #[case] version: &str,
1848        #[case] result: bool,
1849    ) {
1850        let requirement = VersionRequirement::from_str(requirement).unwrap();
1851        let version = Version::from_str(version).unwrap();
1852        assert_eq!(requirement.is_satisfied_by(&version), result);
1853    }
1854
1855    #[rstest]
1856    #[case("1.0.0", vec![
1857        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1858        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1859        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1860    ])]
1861    #[case("1..0", vec![
1862        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1863        VersionSegment::Segment{ text:"0", delimiter_count: 2},
1864    ])]
1865    #[case("1.0.", vec![
1866        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1867        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1868        VersionSegment::Segment{ text:"", delimiter_count: 1},
1869    ])]
1870    #[case("1..", vec![
1871        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1872        VersionSegment::Segment{ text:"", delimiter_count: 2},
1873    ])]
1874    #[case("1...", vec![
1875        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1876        VersionSegment::Segment{ text:"", delimiter_count: 3},
1877    ])]
1878    #[case("1.🗻lol.0", vec![
1879        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1880        VersionSegment::Segment{ text:"lol", delimiter_count: 2},
1881        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1882    ])]
1883    #[case("1.🗻lol.", vec![
1884        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1885        VersionSegment::Segment{ text:"lol", delimiter_count: 2},
1886        VersionSegment::Segment{ text:"", delimiter_count: 1},
1887    ])]
1888    #[case("20220202", vec![
1889        VersionSegment::Segment{ text:"20220202", delimiter_count: 0},
1890    ])]
1891    #[case("some_string", vec![
1892        VersionSegment::Segment{ text:"some", delimiter_count: 0},
1893        VersionSegment::Segment{ text:"string", delimiter_count: 1}
1894    ])]
1895    #[case("alpha7654numeric321", vec![
1896        VersionSegment::Segment{ text:"alpha", delimiter_count: 0},
1897        VersionSegment::SubSegment{ text:"7654"},
1898        VersionSegment::SubSegment{ text:"numeric"},
1899        VersionSegment::SubSegment{ text:"321"},
1900    ])]
1901    fn version_segment_iterator(
1902        #[case] version: &str,
1903        #[case] expected_segments: Vec<VersionSegment>,
1904    ) {
1905        let version = PackageVersion(version.to_string());
1906        // Convert the simplified definition above into actual VersionSegment instances.
1907        let mut segments_iter = version.segments();
1908        let mut expected_iter = expected_segments.clone().into_iter();
1909
1910        // Iterate over both iterators.
1911        // We do it manually to ensure that they both end at the same time.
1912        loop {
1913            let next_segment = segments_iter.next();
1914            assert_eq!(
1915                next_segment,
1916                expected_iter.next(),
1917                "Failed for segment {next_segment:?} in version string {version}:\nsegments: {:?}\n expected: {:?}",
1918                version.segments().collect::<Vec<VersionSegment>>(),
1919                expected_segments,
1920            );
1921            if next_segment.is_none() {
1922                break;
1923            }
1924        }
1925    }
1926}