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(Clone, Debug, 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.
506#[derive(Debug)]
507pub struct VersionSegments<'a> {
508    /// The original version string. We need that reference so we can get some string
509    /// slices based on indices later on.
510    version: &'a str,
511    /// An iterator over the version's chars and their respective start byte's index.
512    version_chars: Peekable<CharIndices<'a>>,
513    /// Check if the cursor is currently in a segment.
514    /// This is necessary to detect whether the next segment should be a sub-segment or a new
515    /// segment.
516    in_segment: bool,
517}
518
519impl<'a> VersionSegments<'a> {
520    /// Create a new instance of a VersionSegments iterator.
521    pub fn new(version: &'a str) -> Self {
522        VersionSegments {
523            version,
524            version_chars: version.char_indices().peekable(),
525            in_segment: false,
526        }
527    }
528}
529
530impl<'a> Iterator for VersionSegments<'a> {
531    type Item = VersionSegment<'a>;
532
533    /// Get the next [VersionSegment] of this version string.
534    fn next(&mut self) -> Option<VersionSegment<'a>> {
535        // Used to track the number of delimiters the next segment is prefixed with.
536        let mut delimiter_count = 0;
537
538        // First up, get the delimiters out of the way.
539        // Peek at the next char, if it's a delimiter, consume it and increase the delimiter count.
540        while let Some((_, char)) = self.version_chars.peek() {
541            // An alphanumeric char indicates that we reached the next segment.
542            if char.is_alphanumeric() {
543                break;
544            }
545
546            self.version_chars.next();
547            delimiter_count += 1;
548
549            // As soon as we hit a delimiter, we know that a new segment is about to start.
550            self.in_segment = false;
551            continue;
552        }
553
554        // Get the next char. If there's no further char, we reached the end of the version string.
555        let Some((first_index, first_char)) = self.version_chars.next() else {
556            // We're at the end of the string and now have to differentiate between two cases:
557
558            // 1. There are no trailing delimiters. We can just return `None` as we truly reached
559            //    the end.
560            if delimiter_count == 0 {
561                return None;
562            }
563
564            // 2. There's no further segment, but there were some trailing delimiters. The
565            //    comparison algorithm considers this case which is why we have to somehow encode
566            //    it. We do so by returning an empty segment.
567            return Some(VersionSegment::Segment {
568                text: "",
569                delimiter_count,
570            });
571        };
572
573        // Cache the last valid char + index that was checked. We need this to
574        // calculate the offset in case the last char is a multi-byte UTF-8 char.
575        let mut last_char = first_char;
576        let mut last_char_index = first_index;
577
578        // The following section now handles the splitting of an alphanumeric string into its
579        // sub-segments. As described in the [VersionSegment] docs, the string needs to be split
580        // every time a switch from alphabetic to numeric or vice versa is detected.
581
582        let is_numeric = first_char.is_numeric();
583
584        if is_numeric {
585            // Go through chars until we hit a non-numeric char or reached the end of the string.
586            #[allow(clippy::while_let_on_iterator)]
587            while let Some((index, next_char)) =
588                self.version_chars.next_if(|(_, peek)| peek.is_numeric())
589            {
590                last_char_index = index;
591                last_char = next_char;
592            }
593        } else {
594            // Go through chars until we hit a non-alphabetic char or reached the end of the string.
595            #[allow(clippy::while_let_on_iterator)]
596            while let Some((index, next_char)) =
597                self.version_chars.next_if(|(_, peek)| peek.is_alphabetic())
598            {
599                last_char_index = index;
600                last_char = next_char;
601            }
602        }
603
604        // Create a subslice based on the indices of the first and last char.
605        // The last char might be multi-byte, which is why we add its length.
606        let segment_slice = &self.version[first_index..(last_char_index + last_char.len_utf8())];
607
608        if !self.in_segment {
609            // Any further segments should be sub-segments, unless we hit a delimiter in which
610            // case this variable will reset to false.
611            self.in_segment = true;
612            Some(VersionSegment::Segment {
613                text: segment_slice,
614                delimiter_count,
615            })
616        } else {
617            Some(VersionSegment::SubSegment {
618                text: segment_slice,
619            })
620        }
621    }
622}
623
624impl Ord for PackageVersion {
625    /// This block implements the logic to determine which of two package versions is newer or
626    /// whether they're considered equal.
627    ///
628    /// This logic is surprisingly complex as it mirrors the current C-alpmlib implementation for
629    /// backwards compatibility reasons.
630    /// <https://gitlab.archlinux.org/pacman/pacman/-/blob/a2d029388c7c206f5576456f91bfbea2dca98c96/lib/libalpm/version.c#L83-217>
631    fn cmp(&self, other: &Self) -> Ordering {
632        // Equal strings are considered equal versions.
633        if self.inner() == other.inner() {
634            return Ordering::Equal;
635        }
636
637        let mut self_segments = self.segments();
638        let mut other_segments = other.segments();
639
640        // Loop through both versions' segments and compare them.
641        loop {
642            // Try to get the next segments
643            let self_segment = self_segments.next();
644            let other_segment = other_segments.next();
645
646            // Make sure that there's a next segment for both versions.
647            let (self_segment, other_segment) = match (self_segment, other_segment) {
648                // Both segments exist, we continue after match.
649                (Some(self_seg), Some(other_seg)) => (self_seg, other_seg),
650
651                // Both versions reached their end and are thereby equal.
652                (None, None) => return Ordering::Equal,
653
654                // One version is longer than the other and both are equal until now.
655                //
656                // ## Case 1
657                //
658                // The longer version is one or more **segment**s longer.
659                // In this case, the longer version is always considered newer.
660                //   `1.0` > `1`
661                // `1.0.0` > `1.0`
662                // `1.0.a` > `1.0`
663                //     ⤷ New segment exists, thereby newer
664                //
665                // ## Case 2
666                //
667                // The current **segment** has one or more sub-segments and the next sub-segment is
668                // alphabetic.
669                // In this case, the shorter version is always newer.
670                // The reason for this is to handle pre-releases (e.g. alpha/beta).
671                // `1.0alpha` < `1.0`
672                // `1.0alpha.0` < `1.0`
673                // `1.0alpha12.0` < `1.0`
674                //     ⤷ Next sub-segment is alphabetic.
675                //
676                // ## Case 3
677                //
678                // The current **segment** has one or more sub-segments and the next sub-segment is
679                // numeric. In this case, the longer version is always newer.
680                // `1.alpha0` > `1.alpha`
681                // `1.alpha0.1` > `1.alpha`
682                //         ⤷ Next sub-segment is numeric.
683                (Some(seg), None) => {
684                    // If the current segment is the start of a segment, it's always considered
685                    // newer.
686                    let text = match seg {
687                        VersionSegment::Segment { .. } => return Ordering::Greater,
688                        VersionSegment::SubSegment { text } => text,
689                    };
690
691                    // If it's a sub-segment, we have to check for the edge-case explained above
692                    // If all chars are alphabetic, `self` is consider older.
693                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
694                        return Ordering::Less;
695                    }
696
697                    return Ordering::Greater;
698                }
699
700                // This is the same logic as above, but inverted.
701                (None, Some(seg)) => {
702                    let text = match seg {
703                        VersionSegment::Segment { .. } => return Ordering::Less,
704                        VersionSegment::SubSegment { text } => text,
705                    };
706                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
707                        return Ordering::Greater;
708                    }
709                    if !text.is_empty() && text.chars().all(char::is_alphabetic) {
710                        return Ordering::Greater;
711                    }
712
713                    return Ordering::Less;
714                }
715            };
716
717            // At this point, we have two sub-/segments.
718            //
719            // We start with the special case where one or both of the segments are empty.
720            // That means that the end of the version string has been reached, but there were one
721            // or more trailing delimiters, e.g.:
722            //
723            // `1.0.`
724            // `1.0...`
725            if other_segment.is_empty() && self_segment.is_empty() {
726                // Both reached the end of their version with a trailing delimiter.
727                // Counterintuitively, the trailing delimiter count is not considered and both
728                // versions are considered equal
729                // `1.0....` == `1.0.`
730                //       ⤷ Length of delimiters is ignored.
731                return Ordering::Equal;
732            } else if self_segment.is_empty() {
733                // Now we have to consider the special case where `other` is alphabetic.
734                // If that's the case, `self` will be considered newer, as the alphabetic string
735                // indicates a pre-release,
736                // `1.0.` > `1.0alpha0`
737                // `1.0.` > `1.0.alpha.0`
738                //                ⤷ Alphabetic sub-/segment and thereby always older.
739                //
740                // Also, we know that `other_segment` isn't empty at this point.
741                // It's noteworthy that this logic does not differentiated between segments and
742                // sub-segments.
743                if other_segment.chars().all(char::is_alphabetic) {
744                    return Ordering::Greater;
745                }
746
747                // In all other cases, `other` is newer.
748                // `1.0.` < `1.0.0`
749                // `1.0.` < `1.0.2.0`
750                return Ordering::Less;
751            } else if other_segment.is_empty() {
752                // Check docs above, as it's the same logic as above, just inverted.
753                if self_segment.chars().all(char::is_alphabetic) {
754                    return Ordering::Less;
755                }
756
757                return Ordering::Greater;
758            }
759
760            // We finally reached the end handling special cases when the version string ended.
761            // From now on, we know that we have two actual sub-/segments that might be prefixed by
762            // some delimiters.
763            //
764            // However, it is possible that one version has a segment and while the other has a
765            // sub-segment. This special case is what is handled next.
766            //
767            // We purposefully give up ownership of both segments.
768            // This is to ensure that following this match block, we finally only have to
769            // consider the actual text of the segments, as we'll know that both sub-/segments are
770            // of the same type.
771            let (self_text, other_text) = match (self_segment, other_segment) {
772                (
773                    VersionSegment::Segment {
774                        delimiter_count: self_count,
775                        text: self_text,
776                    },
777                    VersionSegment::Segment {
778                        delimiter_count: other_count,
779                        text: other_text,
780                    },
781                ) => {
782                    // Special case:
783                    // If one of the segments has more leading delimiters than the other, it is
784                    // always considered newer, no matter what follows after the delimiters.
785                    // `1..0.0` > `1.2.0`
786                    //    ⤷ Two delimiters, thereby always newer.
787                    // `1..0.0` < `1..2.0`
788                    //               ⤷ Same amount of delimiters, now `2 > 0`
789                    if self_count != other_count {
790                        return self_count.cmp(&other_count);
791                    }
792                    (self_text, other_text)
793                }
794                // If one is the start of a new segment, while the other is still a sub-segment,
795                // we can return early as a new segment always overrules a sub-segment.
796                // `1.alpha0.0` < `1.alpha.0`
797                //         ⤷ sub-segment  ⤷ segment
798                //         In the third iteration there's a sub-segment on the left side while
799                //         there's a segment on the right side.
800                (VersionSegment::Segment { .. }, VersionSegment::SubSegment { .. }) => {
801                    return Ordering::Greater;
802                }
803                (VersionSegment::SubSegment { .. }, VersionSegment::Segment { .. }) => {
804                    return Ordering::Less;
805                }
806                (
807                    VersionSegment::SubSegment { text: self_text },
808                    VersionSegment::SubSegment { text: other_text },
809                ) => (self_text, other_text),
810            };
811
812            // At this point, we know that we are dealing with two identical types of sub-/segments.
813            // Thereby, we now only have to compare the text of those sub-/segments.
814
815            // Check whether any of the texts are numeric.
816            // Numeric sub-/segments are always considered newer than non-numeric sub-/segments.
817            // E.g.: `1.0.0` > `1.foo.0`
818            //          ⤷ `0` vs `foo`.
819            //            `0` is numeric and therebynewer than a alphanumeric one.
820            let self_is_numeric = !self_text.is_empty() && self_text.chars().all(char::is_numeric);
821            let other_is_numeric =
822                !other_text.is_empty() && other_text.chars().all(char::is_numeric);
823
824            if self_is_numeric && !other_is_numeric {
825                return Ordering::Greater;
826            } else if !self_is_numeric && other_is_numeric {
827                return Ordering::Less;
828            } else if self_is_numeric && other_is_numeric {
829                // In case both are numeric, we do a number comparison.
830                // We can parse the string as we know that they only consist of digits, hence the
831                // unwrap.
832                //
833                // Preceding zeroes are to be ignored, which is automatically done by Rust's number
834                // parser.
835                // E.g. `1.0001.1` == `1.1.1`
836                //          ⤷ `000` is ignored in the comparison.
837                let ordering = self_text
838                    .parse::<usize>()
839                    .unwrap()
840                    .cmp(&other_text.parse::<usize>().unwrap());
841
842                match ordering {
843                    Ordering::Less => return Ordering::Less,
844                    Ordering::Greater => return Ordering::Greater,
845                    // If both numbers are equal we check the next sub-/segment.
846                    Ordering::Equal => continue,
847                }
848            }
849
850            // At this point, we know that both sub-/segments are alphabetic.
851            // We do a simple string comparison to determine the newer version.
852            match self_text.cmp(other_text) {
853                Ordering::Less => return Ordering::Less,
854                Ordering::Greater => return Ordering::Greater,
855                // If the strings are equal, we check the next sub-/segment.
856                Ordering::Equal => continue,
857            }
858        }
859    }
860}
861
862impl PartialOrd for PackageVersion {
863    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
864        Some(self.cmp(other))
865    }
866}
867
868impl PartialEq for PackageVersion {
869    fn eq(&self, other: &Self) -> bool {
870        self.cmp(other).is_eq()
871    }
872}
873
874/// The schema version of a type
875///
876/// A `SchemaVersion` wraps a `semver::Version`, which means that the tracked version should follow [semver](https://semver.org).
877/// However, for backwards compatibility reasons it is possible to initialize a `SchemaVersion`
878/// using a non-semver compatible string, *if* it can be parsed to a single `u64` (e.g. `"1"`).
879///
880/// ## Examples
881/// ```
882/// use std::str::FromStr;
883///
884/// use alpm_types::SchemaVersion;
885///
886/// # fn main() -> Result<(), alpm_types::Error> {
887/// // create SchemaVersion from str
888/// let version_one = SchemaVersion::from_str("1.0.0")?;
889/// let version_also_one = SchemaVersion::from_str("1")?;
890/// assert_eq!(version_one, version_also_one);
891///
892/// // format as String
893/// assert_eq!("1.0.0", format!("{}", version_one));
894/// assert_eq!("1.0.0", format!("{}", version_also_one));
895/// # Ok(())
896/// # }
897/// ```
898#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
899pub struct SchemaVersion(SemverVersion);
900
901impl SchemaVersion {
902    /// Create a new SchemaVersion
903    pub fn new(version: SemverVersion) -> Self {
904        SchemaVersion(version)
905    }
906
907    /// Return a reference to the inner type
908    pub fn inner(&self) -> &SemverVersion {
909        &self.0
910    }
911}
912
913impl FromStr for SchemaVersion {
914    type Err = Error;
915    /// Create a new SchemaVersion from a string
916    ///
917    /// When providing a non-semver string with only a number (i.e. no minor or patch version), the
918    /// number is treated as the major version (e.g. `"23"` -> `"23.0.0"`).
919    fn from_str(s: &str) -> Result<SchemaVersion, Self::Err> {
920        if !s.contains('.') {
921            match s.parse() {
922                Ok(major) => Ok(SchemaVersion(SemverVersion::new(major, 0, 0))),
923                Err(e) => Err(Error::InvalidInteger {
924                    kind: e.kind().clone(),
925                }),
926            }
927        } else {
928            match SemverVersion::parse(s) {
929                Ok(version) => Ok(SchemaVersion(version)),
930                Err(e) => Err(Error::InvalidSemver {
931                    kind: e.to_string(),
932                }),
933            }
934        }
935    }
936}
937
938impl Display for SchemaVersion {
939    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
940        write!(fmt, "{}", self.0)
941    }
942}
943
944/// A version of a package
945///
946/// A `Version` tracks an optional `Epoch`, a `PackageVersion` and an optional `PackageRelease`.
947/// See [alpm-package-version] for details on the format.
948///
949/// ## Examples
950/// ```
951/// use std::str::FromStr;
952///
953/// use alpm_types::{Epoch, PackageRelease, PackageVersion, Version};
954///
955/// # fn main() -> Result<(), alpm_types::Error> {
956///
957/// let version = Version::from_str("1:2-3")?;
958/// assert_eq!(version.epoch, Some(Epoch::from_str("1")?));
959/// assert_eq!(version.pkgver, PackageVersion::new("2".to_string())?);
960/// assert_eq!(version.pkgrel, Some(PackageRelease::new(3, None)));
961/// # Ok(())
962/// # }
963/// ```
964///
965/// [alpm-package-version]: https://alpm.archlinux.page/specifications/alpm-package-version.7.html
966#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
967pub struct Version {
968    /// The version of the package
969    pub pkgver: PackageVersion,
970    /// The epoch of the package
971    pub epoch: Option<Epoch>,
972    /// The release of the package
973    pub pkgrel: Option<PackageRelease>,
974}
975
976impl Version {
977    /// Create a new Version
978    pub fn new(
979        pkgver: PackageVersion,
980        epoch: Option<Epoch>,
981        pkgrel: Option<PackageRelease>,
982    ) -> Self {
983        Version {
984            pkgver,
985            epoch,
986            pkgrel,
987        }
988    }
989
990    /// Create a new Version, which is guaranteed to have a PackageRelease
991    pub fn with_pkgrel(version: &str) -> Result<Self, Error> {
992        let version = Version::from_str(version)?;
993        if version.pkgrel.is_some() {
994            Ok(version)
995        } else {
996            Err(Error::MissingComponent {
997                component: "pkgrel",
998            })
999        }
1000    }
1001
1002    /// Compare two Versions and return a number
1003    ///
1004    /// The comparison algorithm is based on libalpm/ pacman's vercmp behavior.
1005    ///
1006    /// * `1` if `a` is newer than `b`
1007    /// * `0` if `a` and `b` are considered to be the same version
1008    /// * `-1` if `a` is older than `b`
1009    ///
1010    /// ## Examples
1011    /// ```
1012    /// use std::str::FromStr;
1013    ///
1014    /// use alpm_types::Version;
1015    ///
1016    /// # fn main() -> Result<(), alpm_types::Error> {
1017    ///
1018    /// assert_eq!(
1019    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("0.1.0")?),
1020    ///     1
1021    /// );
1022    /// assert_eq!(
1023    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("1.0.0")?),
1024    ///     0
1025    /// );
1026    /// assert_eq!(
1027    ///     Version::vercmp(&Version::from_str("0.1.0")?, &Version::from_str("1.0.0")?),
1028    ///     -1
1029    /// );
1030    /// # Ok(())
1031    /// # }
1032    /// ```
1033    pub fn vercmp(a: &Version, b: &Version) -> i8 {
1034        match a.cmp(b) {
1035            Ordering::Less => -1,
1036            Ordering::Equal => 0,
1037            Ordering::Greater => 1,
1038        }
1039    }
1040
1041    /// Recognizes a [`Version`] in a string slice.
1042    ///
1043    /// Consumes all of its input.
1044    ///
1045    /// # Errors
1046    ///
1047    /// Returns an error if `input` is not a valid _alpm-package-version_.
1048    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1049        let mut epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
1050            // cut_err now that we've found a pattern with ':'
1051            cut_err(Epoch::parser),
1052        ))
1053        .context(StrContext::Expected(StrContextValue::Description(
1054            "followed by a ':'",
1055        )));
1056
1057        seq!(Self {
1058            epoch: epoch,
1059            pkgver: take_till(1.., '-')
1060                // this context will trigger on empty pkgver due to 1.. above
1061                .context(StrContext::Expected(StrContextValue::Description("pkgver string")))
1062                .and_then(PackageVersion::parser),
1063            pkgrel: opt(preceded('-', cut_err(PackageRelease::parser))),
1064            _: eof.context(StrContext::Expected(StrContextValue::Description("end of version string"))),
1065        })
1066        .parse_next(input)
1067    }
1068}
1069
1070impl FromStr for Version {
1071    type Err = Error;
1072    /// Creates a new [`Version`] from a string slice.
1073    ///
1074    /// Delegates to [`Version::parser`].
1075    ///
1076    /// # Errors
1077    ///
1078    /// Returns an error if [`Version::parser`] fails.
1079    fn from_str(s: &str) -> Result<Version, Self::Err> {
1080        Ok(Self::parser.parse(s)?)
1081    }
1082}
1083
1084impl Display for Version {
1085    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
1086        if let Some(epoch) = self.epoch {
1087            write!(fmt, "{epoch}:")?;
1088        }
1089
1090        write!(fmt, "{}", self.pkgver)?;
1091
1092        if let Some(pkgrel) = &self.pkgrel {
1093            write!(fmt, "-{pkgrel}")?;
1094        }
1095
1096        Ok(())
1097    }
1098}
1099
1100impl Ord for Version {
1101    fn cmp(&self, other: &Self) -> Ordering {
1102        match (self.epoch, other.epoch) {
1103            (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
1104                return self_epoch.cmp(&other_epoch);
1105            }
1106            (Some(_), None) => return Ordering::Greater,
1107            (None, Some(_)) => return Ordering::Less,
1108            (_, _) => {}
1109        }
1110
1111        let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
1112        if pkgver_cmp.is_ne() {
1113            return pkgver_cmp;
1114        }
1115
1116        self.pkgrel.cmp(&other.pkgrel)
1117    }
1118}
1119
1120impl PartialOrd for Version {
1121    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1122        Some(self.cmp(other))
1123    }
1124}
1125
1126/// Specifies the comparison function for a [`VersionRequirement`].
1127///
1128/// The package version can be required to be:
1129/// - less than (`<`)
1130/// - less than or equal to (`<=`)
1131/// - equal to (`=`)
1132/// - greater than or equal to (`>=`)
1133/// - greater than (`>`)
1134///
1135/// the specified version.
1136///
1137/// See [alpm-comparison] for details on the format.
1138///
1139/// ## Note
1140///
1141/// The variants of this enum are sorted in a way, that prefers the two-letter comparators over
1142/// the one-letter ones.
1143/// This is because when splitting a string on the string representation of [`VersionComparison`]
1144/// variant and relying on the ordering of [`strum::EnumIter`], the two-letter comparators must be
1145/// checked before checking the one-letter ones to yield robust results.
1146///
1147/// [alpm-comparison]: https://alpm.archlinux.page/specifications/alpm-comparison.7.html
1148#[derive(
1149    strum::AsRefStr,
1150    Clone,
1151    Copy,
1152    Debug,
1153    strum::Display,
1154    strum::EnumIter,
1155    PartialEq,
1156    Eq,
1157    strum::VariantNames,
1158    Serialize,
1159    Deserialize,
1160)]
1161pub enum VersionComparison {
1162    /// Less than or equal to
1163    #[strum(to_string = "<=")]
1164    LessOrEqual,
1165
1166    /// Greater than or equal to
1167    #[strum(to_string = ">=")]
1168    GreaterOrEqual,
1169
1170    /// Equal to
1171    #[strum(to_string = "=")]
1172    Equal,
1173
1174    /// Less than
1175    #[strum(to_string = "<")]
1176    Less,
1177
1178    /// Greater than
1179    #[strum(to_string = ">")]
1180    Greater,
1181}
1182
1183impl VersionComparison {
1184    /// Returns `true` if the result of a comparison between the actual and required package
1185    /// versions satisfies the comparison function.
1186    fn is_compatible_with(self, ord: Ordering) -> bool {
1187        match (self, ord) {
1188            (VersionComparison::Less, Ordering::Less)
1189            | (VersionComparison::LessOrEqual, Ordering::Less | Ordering::Equal)
1190            | (VersionComparison::Equal, Ordering::Equal)
1191            | (VersionComparison::GreaterOrEqual, Ordering::Greater | Ordering::Equal)
1192            | (VersionComparison::Greater, Ordering::Greater) => true,
1193
1194            (VersionComparison::Less, Ordering::Equal | Ordering::Greater)
1195            | (VersionComparison::LessOrEqual, Ordering::Greater)
1196            | (VersionComparison::Equal, Ordering::Less | Ordering::Greater)
1197            | (VersionComparison::GreaterOrEqual, Ordering::Less)
1198            | (VersionComparison::Greater, Ordering::Less | Ordering::Equal) => false,
1199        }
1200    }
1201
1202    /// Recognizes a [`VersionComparison`] in a string slice.
1203    ///
1204    /// Consumes all of its input.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns an error if `input` is not a valid _alpm-comparison_.
1209    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1210        alt((
1211            // insert eofs here (instead of after alt call) so correct error message is thrown
1212            ("<=", eof).value(Self::LessOrEqual),
1213            (">=", eof).value(Self::GreaterOrEqual),
1214            ("=", eof).value(Self::Equal),
1215            ("<", eof).value(Self::Less),
1216            (">", eof).value(Self::Greater),
1217            fail.context(StrContext::Label("comparison operator"))
1218                .context_with(iter_str_context!([VersionComparison::VARIANTS])),
1219        ))
1220        .parse_next(input)
1221    }
1222}
1223
1224impl FromStr for VersionComparison {
1225    type Err = Error;
1226
1227    /// Creates a new [`VersionComparison`] from a string slice.
1228    ///
1229    /// Delegates to [`VersionComparison::parser`].
1230    ///
1231    /// # Errors
1232    ///
1233    /// Returns an error if [`VersionComparison::parser`] fails.
1234    fn from_str(s: &str) -> Result<Self, Self::Err> {
1235        Ok(Self::parser.parse(s)?)
1236    }
1237}
1238
1239/// A version requirement, e.g. for a dependency package.
1240///
1241/// It consists of a target version and a comparison function. A version requirement of `>=1.5` has
1242/// a target version of `1.5` and a comparison function of [`VersionComparison::GreaterOrEqual`].
1243/// See [alpm-comparison] for details on the format.
1244///
1245/// ## Examples
1246///
1247/// ```
1248/// use std::str::FromStr;
1249///
1250/// use alpm_types::{Version, VersionComparison, VersionRequirement};
1251///
1252/// # fn main() -> Result<(), alpm_types::Error> {
1253/// let requirement = VersionRequirement::from_str(">=1.5")?;
1254///
1255/// assert_eq!(requirement.comparison, VersionComparison::GreaterOrEqual);
1256/// assert_eq!(requirement.version, Version::from_str("1.5")?);
1257/// # Ok(())
1258/// # }
1259/// ```
1260///
1261/// [alpm-comparison]: https://alpm.archlinux.page/specifications/alpm-comparison.7.html
1262#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1263pub struct VersionRequirement {
1264    /// Version comparison function
1265    pub comparison: VersionComparison,
1266    /// Target version
1267    pub version: Version,
1268}
1269
1270impl VersionRequirement {
1271    /// Create a new `VersionRequirement`
1272    pub fn new(comparison: VersionComparison, version: Version) -> Self {
1273        VersionRequirement {
1274            comparison,
1275            version,
1276        }
1277    }
1278
1279    /// Returns `true` if the requirement is satisfied by the given package version.
1280    ///
1281    /// ## Examples
1282    ///
1283    /// ```
1284    /// use std::str::FromStr;
1285    ///
1286    /// use alpm_types::{Version, VersionRequirement};
1287    ///
1288    /// # fn main() -> Result<(), alpm_types::Error> {
1289    /// let requirement = VersionRequirement::from_str(">=1.5-3")?;
1290    ///
1291    /// assert!(!requirement.is_satisfied_by(&Version::from_str("1.5")?));
1292    /// assert!(requirement.is_satisfied_by(&Version::from_str("1.5-3")?));
1293    /// assert!(requirement.is_satisfied_by(&Version::from_str("1.6")?));
1294    /// assert!(requirement.is_satisfied_by(&Version::from_str("2:1.0")?));
1295    /// assert!(!requirement.is_satisfied_by(&Version::from_str("1.0")?));
1296    /// # Ok(())
1297    /// # }
1298    /// ```
1299    pub fn is_satisfied_by(&self, ver: &Version) -> bool {
1300        self.comparison.is_compatible_with(ver.cmp(&self.version))
1301    }
1302
1303    /// Recognizes a [`VersionRequirement`] in a string slice.
1304    ///
1305    /// Consumes all of its input.
1306    ///
1307    /// # Errors
1308    ///
1309    /// Returns an error if `input` is not a valid _alpm-comparison_.
1310    pub fn parser(input: &mut &str) -> ModalResult<Self> {
1311        seq!(Self {
1312            comparison: take_while(1.., ('<', '>', '='))
1313                // add context here because otherwise take_while can fail and provide no information
1314                .context(StrContext::Expected(StrContextValue::Description(
1315                    "version comparison operator"
1316                )))
1317                .and_then(VersionComparison::parser),
1318            version: Version::parser,
1319        })
1320        .parse_next(input)
1321    }
1322}
1323
1324impl Display for VersionRequirement {
1325    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1326        write!(f, "{}{}", self.comparison, self.version)
1327    }
1328}
1329
1330impl FromStr for VersionRequirement {
1331    type Err = Error;
1332
1333    /// Creates a new [`VersionRequirement`] from a string slice.
1334    ///
1335    /// Delegates to [`VersionRequirement::parser`].
1336    ///
1337    /// # Errors
1338    ///
1339    /// Returns an error if [`VersionRequirement::parser`] fails.
1340    fn from_str(s: &str) -> Result<Self, Self::Err> {
1341        Ok(Self::parser.parse(s)?)
1342    }
1343}
1344
1345#[cfg(test)]
1346mod tests {
1347    use rstest::rstest;
1348
1349    use super::*;
1350
1351    #[rstest]
1352    #[case("1.0.0", Ok(SchemaVersion(SemverVersion::new(1, 0, 0))))]
1353    #[case("1", Ok(SchemaVersion(SemverVersion::new(1, 0, 0))))]
1354    #[case("-1.0.0", Err(Error::InvalidSemver { kind: String::from("unexpected character '-' while parsing major version number") }))]
1355    fn schema_version(#[case] version: &str, #[case] result: Result<SchemaVersion, Error>) {
1356        assert_eq!(result, SchemaVersion::from_str(version))
1357    }
1358
1359    /// Ensure that valid buildtool version strings are parsed as expected.
1360    #[rstest]
1361    #[case(
1362        "1.0.0-1-any",
1363        BuildToolVersion::new(Version::from_str("1.0.0-1").unwrap(), Some(Architecture::from_str("any").unwrap())),
1364    )]
1365    #[case(
1366        "1:1.0.0-1-any",
1367        BuildToolVersion::new(Version::from_str("1:1.0.0-1").unwrap(), Some(Architecture::from_str("any").unwrap())),
1368    )]
1369    #[case(
1370        "1.0.0",
1371        BuildToolVersion::new(Version::from_str("1.0.0").unwrap(), None),
1372    )]
1373    fn valid_buildtoolver_new(#[case] buildtoolver: &str, #[case] expected: BuildToolVersion) {
1374        assert_eq!(
1375            BuildToolVersion::from_str(buildtoolver),
1376            Ok(expected),
1377            "Expected valid parse of buildtoolver '{buildtoolver}'"
1378        );
1379    }
1380
1381    /// Ensure that invalid buildtool version strings produce the respective errors.
1382    #[rstest]
1383    #[case("1.0.0-any", Error::MissingComponent { component: "pkgrel" })]
1384    #[case("1.0.0-1-foo", strum::ParseError::VariantNotFound.into())]
1385    fn invalid_buildtoolver_new(#[case] buildtoolver: &str, #[case] expected: Error) {
1386        assert_eq!(
1387            BuildToolVersion::from_str(buildtoolver),
1388            Err(expected),
1389            "Expected error during parse of buildtoolver '{buildtoolver}'"
1390        );
1391    }
1392
1393    #[rstest]
1394    #[case(".1.0.0-1-any", "invalid first pkgver character")]
1395    fn invalid_buildtoolver_badpkgver(#[case] buildtoolver: &str, #[case] err_snippet: &str) {
1396        let Err(Error::ParseError(err_msg)) = BuildToolVersion::from_str(buildtoolver) else {
1397            panic!("'{buildtoolver}' erroneously parsed as BuildToolVersion")
1398        };
1399        assert!(
1400            err_msg.contains(err_snippet),
1401            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1402        );
1403    }
1404
1405    #[rstest]
1406    #[case(
1407        SchemaVersion(SemverVersion::new(1, 0, 0)),
1408        SchemaVersion(SemverVersion::new(0, 1, 0))
1409    )]
1410    fn compare_schema_version(#[case] version_a: SchemaVersion, #[case] version_b: SchemaVersion) {
1411        assert!(version_a > version_b);
1412    }
1413
1414    /// Ensure that valid version strings are parsed as expected.
1415    #[rstest]
1416    #[case(
1417        "foo",
1418        Version {
1419            epoch: None,
1420            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1421            pkgrel: None
1422        },
1423    )]
1424    #[case(
1425        "1:foo-1",
1426        Version {
1427            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1428            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
1429            pkgrel: Some(PackageRelease::new(1, None))
1430        },
1431    )]
1432    #[case(
1433        "1:foo",
1434        Version {
1435            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1436            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
1437            pkgrel: None,
1438        },
1439    )]
1440    #[case(
1441        "foo-1",
1442        Version {
1443            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
1444            epoch: None,
1445            pkgrel: Some(PackageRelease::new(1, None))
1446        }
1447    )]
1448    fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
1449        assert_eq!(
1450            Version::from_str(version),
1451            Ok(expected),
1452            "Expected valid parsing for version {version}"
1453        )
1454    }
1455
1456    /// Ensure that invalid version strings produce the respective errors.
1457    #[rstest]
1458    #[case::two_pkgrel("1:foo-1-1", "expected end of package release value")]
1459    #[case::two_epoch("1:1:foo-1", "invalid pkgver character")]
1460    #[case::no_version("", "expected pkgver string")]
1461    #[case::no_version(":", "invalid first pkgver character")]
1462    #[case::no_version(".", "invalid first pkgver character")]
1463    #[case::invalid_integer(
1464        "-1foo:1",
1465        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
1466    )]
1467    #[case::invalid_integer(
1468        "1-foo:1",
1469        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
1470    )]
1471    fn parse_error_in_version_from_string(#[case] version: &str, #[case] err_snippet: &str) {
1472        let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
1473            panic!("parsing '{version}' did not fail as expected")
1474        };
1475        assert!(
1476            err_msg.contains(err_snippet),
1477            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1478        );
1479    }
1480
1481    /// Test that version parsing works/fails for the special case where a pkgrel is expected.
1482    /// This is done by calling the `with_pkgrel` function directly.
1483    #[rstest]
1484    #[case(
1485        "1.0.0-1",
1486        Ok(Version{
1487            pkgver: PackageVersion::new("1.0.0".to_string()).unwrap(),
1488            pkgrel: Some(PackageRelease::new(1, None)),
1489            epoch: None,
1490        })
1491    )]
1492    #[case("1.0.0", Err(Error::MissingComponent { component: "pkgrel" }))]
1493    fn version_with_pkgrel(#[case] version: &str, #[case] result: Result<Version, Error>) {
1494        assert_eq!(result, Version::with_pkgrel(version));
1495    }
1496
1497    #[rstest]
1498    #[case("1", Ok(Epoch(NonZeroUsize::new(1).unwrap())))]
1499    fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
1500        assert_eq!(result, Epoch::from_str(version));
1501    }
1502
1503    #[rstest]
1504    #[case("0", "expected positive non-zero decimal integer")]
1505    #[case("-0", "expected positive non-zero decimal integer")]
1506    #[case("z", "expected positive non-zero decimal integer")]
1507    fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
1508        let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
1509            panic!("'{input}' erroneously parsed as Epoch")
1510        };
1511        assert!(
1512            err_msg.contains(err_snippet),
1513            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1514        );
1515    }
1516
1517    /// Make sure that we can parse valid **pkgver** strings.
1518    #[rstest]
1519    #[case("foo")]
1520    #[case("1.0.0")]
1521    fn valid_pkgver(#[case] pkgver: &str) {
1522        let parsed = PackageVersion::new(pkgver.to_string());
1523        assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
1524        assert_eq!(
1525            parsed.as_ref().unwrap().to_string(),
1526            pkgver,
1527            "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
1528            parsed.unwrap(),
1529            pkgver
1530        );
1531    }
1532
1533    /// Ensure that invalid **pkgver**s are throwing errors.
1534    #[rstest]
1535    #[case("1:foo", "invalid pkgver character")]
1536    #[case("foo-1", "invalid pkgver character")]
1537    #[case("foo,1", "invalid pkgver character")]
1538    #[case(".foo", "invalid first pkgver character")]
1539    #[case("_foo", "invalid first pkgver character")]
1540    // ß is not in [:alnum:]
1541    #[case("ß", "invalid first pkgver character")]
1542    #[case("1.ß", "invalid pkgver character")]
1543    fn invalid_pkgver(#[case] pkgver: &str, #[case] err_snippet: &str) {
1544        let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
1545            panic!("Expected pkgver {pkgver} to be invalid.")
1546        };
1547        assert!(
1548            err_msg.contains(err_snippet),
1549            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1550        );
1551    }
1552
1553    /// Make sure that we can parse valid **pkgrel** strings.
1554    #[rstest]
1555    #[case("0")]
1556    #[case("1")]
1557    #[case("10")]
1558    #[case("1.0")]
1559    #[case("10.5")]
1560    #[case("0.1")]
1561    fn valid_pkgrel(#[case] pkgrel: &str) {
1562        let parsed = PackageRelease::from_str(pkgrel);
1563        assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
1564        assert_eq!(
1565            parsed.as_ref().unwrap().to_string(),
1566            pkgrel,
1567            "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
1568            parsed.unwrap(),
1569            pkgrel
1570        );
1571    }
1572
1573    /// Ensure that invalid **pkgrel**s are throwing errors.
1574    #[rstest]
1575    #[case(".1", "expected positive decimal integer")]
1576    #[case("1.", "expected single '.' followed by positive decimal integer")]
1577    #[case("1..1", "expected single '.' followed by positive decimal integer")]
1578    #[case("-1", "expected positive decimal integer")]
1579    #[case("a", "expected positive decimal integer")]
1580    #[case("1.a", "expected single '.' followed by positive decimal integer")]
1581    #[case("1.0.0", "expected end of package release")]
1582    #[case("", "expected positive decimal integer")]
1583    fn invalid_pkgrel(#[case] pkgrel: &str, #[case] err_snippet: &str) {
1584        let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
1585            panic!("'{pkgrel}' erroneously parsed as PackageRelease")
1586        };
1587        assert!(
1588            err_msg.contains(err_snippet),
1589            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1590        );
1591    }
1592
1593    /// Test that pkgrel ordering works as intended
1594    #[rstest]
1595    #[case("1", "1.0", Ordering::Less)]
1596    #[case("1.0", "2", Ordering::Less)]
1597    #[case("1", "1.1", Ordering::Less)]
1598    #[case("1.0", "1.1", Ordering::Less)]
1599    #[case("0", "1.1", Ordering::Less)]
1600    #[case("1", "11", Ordering::Less)]
1601    #[case("1", "1", Ordering::Equal)]
1602    #[case("1.2", "1.2", Ordering::Equal)]
1603    #[case("2.0", "2.0", Ordering::Equal)]
1604    #[case("2", "1.0", Ordering::Greater)]
1605    #[case("1.1", "1", Ordering::Greater)]
1606    #[case("1.1", "1.0", Ordering::Greater)]
1607    #[case("1.1", "0", Ordering::Greater)]
1608    #[case("11", "1", Ordering::Greater)]
1609    fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
1610        let first = PackageRelease::from_str(first).unwrap();
1611        let second = PackageRelease::from_str(second).unwrap();
1612        assert_eq!(
1613            first.cmp(&second),
1614            order,
1615            "{first} should be {order:?} to {second}"
1616        );
1617    }
1618
1619    /// Ensure that versions are properly serialized back to their string representation.
1620    #[rstest]
1621    #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
1622    #[case(Version::from_str("1-1").unwrap(), "1-1")]
1623    #[case(Version::from_str("1").unwrap(), "1")]
1624    #[case(Version::from_str("1:1").unwrap(), "1:1")]
1625    fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
1626        assert_eq!(format!("{version}"), to_str);
1627    }
1628
1629    #[rstest]
1630    // Major version comparisons
1631    #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
1632    #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
1633    #[case(
1634        Version::from_str("20220102"),
1635        Version::from_str("20220202"),
1636        Ordering::Less
1637    )]
1638    // Major vs Major.Minor
1639    #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
1640    #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
1641    #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
1642    #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
1643    #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
1644    // Major.Minor version comparisons
1645    #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
1646    #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
1647    #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
1648    #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
1649    #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
1650    #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
1651    #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
1652    // Major.Minor version comparisons with alphanumerics
1653    #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
1654    #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
1655    #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
1656    #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
1657    #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
1658    #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
1659    #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
1660    #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
1661    #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
1662    #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
1663    #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
1664    #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
1665    #[case(
1666        Version::from_str("1.alpha0.0"),
1667        Version::from_str("1.alpha.0"),
1668        Ordering::Less
1669    )]
1670    // Major.Minor vs Major.Minor.Patch
1671    #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
1672    // Major.Minor.Patch
1673    #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
1674    #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
1675    #[case(
1676        Version::from_str("1.0.alpha.0"),
1677        Version::from_str("1.0."),
1678        Ordering::Less
1679    )]
1680    #[case(
1681        Version::from_str("1.a001a.1"),
1682        Version::from_str("1.a1a.1"),
1683        Ordering::Equal
1684    )]
1685    fn version_cmp(
1686        #[case] version_a: Result<Version, Error>,
1687        #[case] version_b: Result<Version, Error>,
1688        #[case] expected: Ordering,
1689    ) {
1690        // Simply unwrap the Version as we expect all test strings to be valid.
1691        let version_a = version_a.unwrap();
1692        let version_b = version_b.unwrap();
1693
1694        // Derive the expected vercmp binary exitcode from the expected Ordering.
1695        let vercmp_result = match &expected {
1696            Ordering::Equal => 0,
1697            Ordering::Greater => 1,
1698            Ordering::Less => -1,
1699        };
1700
1701        let ordering = version_a.cmp(&version_b);
1702        assert_eq!(
1703            ordering, expected,
1704            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
1705        );
1706
1707        assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
1708
1709        // If we find the `vercmp` binary, also run the test against the actual binary.
1710        #[cfg(feature = "compatibility_tests")]
1711        {
1712            let output = std::process::Command::new("vercmp")
1713                .arg(version_a.to_string())
1714                .arg(version_b.to_string())
1715                .output()
1716                .unwrap();
1717            let result = String::from_utf8_lossy(&output.stdout);
1718            assert_eq!(result.trim(), vercmp_result.to_string());
1719        }
1720
1721        // Now check that the opposite holds true as well.
1722        let reverse_vercmp_result = match &expected {
1723            Ordering::Equal => 0,
1724            Ordering::Greater => -1,
1725            Ordering::Less => 1,
1726        };
1727        let reverse_expected = match &expected {
1728            Ordering::Equal => Ordering::Equal,
1729            Ordering::Greater => Ordering::Less,
1730            Ordering::Less => Ordering::Greater,
1731        };
1732
1733        let reverse_ordering = version_b.cmp(&version_a);
1734        assert_eq!(
1735            reverse_ordering, reverse_expected,
1736            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
1737        );
1738
1739        assert_eq!(
1740            Version::vercmp(&version_b, &version_a),
1741            reverse_vercmp_result
1742        );
1743    }
1744
1745    /// Ensure that valid version comparison strings can be parsed.
1746    #[rstest]
1747    #[case("<", VersionComparison::Less)]
1748    #[case("<=", VersionComparison::LessOrEqual)]
1749    #[case("=", VersionComparison::Equal)]
1750    #[case(">=", VersionComparison::GreaterOrEqual)]
1751    #[case(">", VersionComparison::Greater)]
1752    fn valid_version_comparison(#[case] comparison: &str, #[case] expected: VersionComparison) {
1753        assert_eq!(comparison.parse(), Ok(expected));
1754    }
1755
1756    /// Ensure that invalid version comparisons will throw an error.
1757    #[rstest]
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("= ", "invalid comparison operator")]
1764    #[case("<1", "invalid comparison operator")]
1765    fn invalid_version_comparison(#[case] comparison: &str, #[case] err_snippet: &str) {
1766        let Err(Error::ParseError(err_msg)) = VersionComparison::from_str(comparison) else {
1767            panic!("'{comparison}' did not fail as expected")
1768        };
1769        assert!(
1770            err_msg.contains(err_snippet),
1771            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1772        );
1773    }
1774
1775    /// Test successful parsing for version requirement strings.
1776    #[rstest]
1777    #[case("=1", VersionRequirement {
1778        comparison: VersionComparison::Equal,
1779        version: Version::from_str("1").unwrap(),
1780    })]
1781    #[case("<=42:abcd-2.4", VersionRequirement {
1782        comparison: VersionComparison::LessOrEqual,
1783        version: Version::from_str("42:abcd-2.4").unwrap(),
1784    })]
1785    #[case(">3.1", VersionRequirement {
1786        comparison: VersionComparison::Greater,
1787        version: Version::from_str("3.1").unwrap(),
1788    })]
1789    fn valid_version_requirement(#[case] requirement: &str, #[case] expected: VersionRequirement) {
1790        assert_eq!(
1791            requirement.parse(),
1792            Ok(expected),
1793            "Expected successful parse for version requirement '{requirement}'"
1794        );
1795    }
1796
1797    #[rstest]
1798    #[case::bad_operator("<>3.1", "invalid comparison operator")]
1799    #[case::no_operator("3.1", "expected version comparison operator")]
1800    #[case::arrow_operator("=>3.1", "invalid comparison operator")]
1801    #[case::no_version("<=", "expected pkgver string")]
1802    fn invalid_version_requirement(#[case] requirement: &str, #[case] err_snippet: &str) {
1803        let Err(Error::ParseError(err_msg)) = VersionRequirement::from_str(requirement) else {
1804            panic!("'{requirement}' erroneously parsed as VersionRequirement")
1805        };
1806        assert!(
1807            err_msg.contains(err_snippet),
1808            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1809        );
1810    }
1811
1812    #[rstest]
1813    #[case("<3.1>3.2", "invalid pkgver character")]
1814    fn invalid_version_requirement_pkgver_parse(
1815        #[case] requirement: &str,
1816        #[case] err_snippet: &str,
1817    ) {
1818        let Err(Error::ParseError(err_msg)) = VersionRequirement::from_str(requirement) else {
1819            panic!("'{requirement}' erroneously parsed as VersionRequirement")
1820        };
1821        assert!(
1822            err_msg.contains(err_snippet),
1823            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
1824        );
1825    }
1826
1827    /// Check whether a version requirement (>= 1.0) is fulfilled by a given version string.
1828    #[rstest]
1829    #[case("=1", "1", true)]
1830    #[case("=1", "1.0", false)]
1831    #[case("=1", "1-1", false)]
1832    #[case("=1", "1:1", false)]
1833    #[case("=1", "0.9", false)]
1834    #[case("<42", "41", true)]
1835    #[case("<42", "42", false)]
1836    #[case("<42", "43", false)]
1837    #[case("<=42", "41", true)]
1838    #[case("<=42", "42", true)]
1839    #[case("<=42", "43", false)]
1840    #[case(">42", "41", false)]
1841    #[case(">42", "42", false)]
1842    #[case(">42", "43", true)]
1843    #[case(">=42", "41", false)]
1844    #[case(">=42", "42", true)]
1845    #[case(">=42", "43", true)]
1846    fn version_requirement_satisfied(
1847        #[case] requirement: &str,
1848        #[case] version: &str,
1849        #[case] result: bool,
1850    ) {
1851        let requirement = VersionRequirement::from_str(requirement).unwrap();
1852        let version = Version::from_str(version).unwrap();
1853        assert_eq!(requirement.is_satisfied_by(&version), result);
1854    }
1855
1856    #[rstest]
1857    #[case("1.0.0", vec![
1858        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1859        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1860        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1861    ])]
1862    #[case("1..0", vec![
1863        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1864        VersionSegment::Segment{ text:"0", delimiter_count: 2},
1865    ])]
1866    #[case("1.0.", vec![
1867        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1868        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1869        VersionSegment::Segment{ text:"", delimiter_count: 1},
1870    ])]
1871    #[case("1..", vec![
1872        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1873        VersionSegment::Segment{ text:"", delimiter_count: 2},
1874    ])]
1875    #[case("1...", vec![
1876        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1877        VersionSegment::Segment{ text:"", delimiter_count: 3},
1878    ])]
1879    #[case("1.🗻lol.0", vec![
1880        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1881        VersionSegment::Segment{ text:"lol", delimiter_count: 2},
1882        VersionSegment::Segment{ text:"0", delimiter_count: 1},
1883    ])]
1884    #[case("1.🗻lol.", vec![
1885        VersionSegment::Segment{ text:"1", delimiter_count: 0},
1886        VersionSegment::Segment{ text:"lol", delimiter_count: 2},
1887        VersionSegment::Segment{ text:"", delimiter_count: 1},
1888    ])]
1889    #[case("20220202", vec![
1890        VersionSegment::Segment{ text:"20220202", delimiter_count: 0},
1891    ])]
1892    #[case("some_string", vec![
1893        VersionSegment::Segment{ text:"some", delimiter_count: 0},
1894        VersionSegment::Segment{ text:"string", delimiter_count: 1}
1895    ])]
1896    #[case("alpha7654numeric321", vec![
1897        VersionSegment::Segment{ text:"alpha", delimiter_count: 0},
1898        VersionSegment::SubSegment{ text:"7654"},
1899        VersionSegment::SubSegment{ text:"numeric"},
1900        VersionSegment::SubSegment{ text:"321"},
1901    ])]
1902    fn version_segment_iterator(
1903        #[case] version: &str,
1904        #[case] expected_segments: Vec<VersionSegment>,
1905    ) {
1906        let version = PackageVersion(version.to_string());
1907        // Convert the simplified definition above into actual VersionSegment instances.
1908        let mut segments_iter = version.segments();
1909        let mut expected_iter = expected_segments.clone().into_iter();
1910
1911        // Iterate over both iterators.
1912        // We do it manually to ensure that they both end at the same time.
1913        loop {
1914            let next_segment = segments_iter.next();
1915            assert_eq!(
1916                next_segment,
1917                expected_iter.next(),
1918                "Failed for segment {next_segment:?} in version string {version}:\nsegments: {:?}\n expected: {:?}",
1919                version.segments().collect::<Vec<VersionSegment>>(),
1920                expected_segments,
1921            );
1922            if next_segment.is_none() {
1923                break;
1924            }
1925        }
1926    }
1927}