alpm_types/version/
pkg_generic.rs

1//! A flexible and generic package version.
2
3use std::{
4    cmp::Ordering,
5    fmt::{Display, Formatter},
6    str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use winnow::{
11    ModalResult,
12    Parser,
13    combinator::{cut_err, eof, opt, preceded, seq, terminated},
14    error::{StrContext, StrContextValue},
15    token::take_till,
16};
17
18use crate::{Epoch, Error, PackageRelease, PackageVersion};
19#[cfg(doc)]
20use crate::{FullVersion, MinimalVersion};
21
22/// A version of a package
23///
24/// A [`Version`] generically tracks an optional [`Epoch`], a [`PackageVersion`] and an optional
25/// [`PackageRelease`].
26/// See [alpm-package-version] for details on the format.
27///
28/// # Notes
29///
30/// - If [`PackageRelease`] should be mandatory for your use-case, use [`FullVersion`] instead.
31/// - If [`PackageRelease`] should not be used in your use-case, use [`MinimalVersion`] instead.
32///
33/// ## Examples
34/// ```
35/// use std::str::FromStr;
36///
37/// use alpm_types::{Epoch, PackageRelease, PackageVersion, Version};
38///
39/// # fn main() -> Result<(), alpm_types::Error> {
40///
41/// let version = Version::from_str("1:2-3")?;
42/// assert_eq!(version.epoch, Some(Epoch::from_str("1")?));
43/// assert_eq!(version.pkgver, PackageVersion::new("2".to_string())?);
44/// assert_eq!(version.pkgrel, Some(PackageRelease::new(3, None)));
45/// # Ok(())
46/// # }
47/// ```
48///
49/// [alpm-package-version]: https://alpm.archlinux.page/specifications/alpm-package-version.7.html
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct Version {
52    /// The version of the package
53    pub pkgver: PackageVersion,
54    /// The epoch of the package
55    pub epoch: Option<Epoch>,
56    /// The release of the package
57    pub pkgrel: Option<PackageRelease>,
58}
59
60impl Version {
61    /// Create a new Version
62    pub fn new(
63        pkgver: PackageVersion,
64        epoch: Option<Epoch>,
65        pkgrel: Option<PackageRelease>,
66    ) -> Self {
67        Version {
68            pkgver,
69            epoch,
70            pkgrel,
71        }
72    }
73
74    /// Compare two Versions and return a number
75    ///
76    /// The comparison algorithm is based on libalpm/ pacman's vercmp behavior.
77    ///
78    /// * `1` if `a` is newer than `b`
79    /// * `0` if `a` and `b` are considered to be the same version
80    /// * `-1` if `a` is older than `b`
81    ///
82    /// ## Examples
83    /// ```
84    /// use std::str::FromStr;
85    ///
86    /// use alpm_types::Version;
87    ///
88    /// # fn main() -> Result<(), alpm_types::Error> {
89    ///
90    /// assert_eq!(
91    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("0.1.0")?),
92    ///     1
93    /// );
94    /// assert_eq!(
95    ///     Version::vercmp(&Version::from_str("1.0.0")?, &Version::from_str("1.0.0")?),
96    ///     0
97    /// );
98    /// assert_eq!(
99    ///     Version::vercmp(&Version::from_str("0.1.0")?, &Version::from_str("1.0.0")?),
100    ///     -1
101    /// );
102    /// # Ok(())
103    /// # }
104    /// ```
105    pub fn vercmp(a: &Version, b: &Version) -> i8 {
106        match a.cmp(b) {
107            Ordering::Less => -1,
108            Ordering::Equal => 0,
109            Ordering::Greater => 1,
110        }
111    }
112
113    /// Recognizes a [`Version`] in a string slice.
114    ///
115    /// Consumes all of its input.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if `input` is not a valid _alpm-package-version_.
120    pub fn parser(input: &mut &str) -> ModalResult<Self> {
121        let mut epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
122            // cut_err now that we've found a pattern with ':'
123            cut_err(Epoch::parser),
124        ))
125        .context(StrContext::Expected(StrContextValue::Description(
126            "followed by a ':'",
127        )));
128
129        seq!(Self {
130            epoch: epoch,
131            pkgver: take_till(1.., '-')
132                // this context will trigger on empty pkgver due to 1.. above
133                .context(StrContext::Expected(StrContextValue::Description("pkgver string")))
134                .and_then(PackageVersion::parser),
135            pkgrel: opt(preceded('-', cut_err(PackageRelease::parser))),
136            _: eof.context(StrContext::Expected(StrContextValue::Description("end of version string"))),
137        })
138        .parse_next(input)
139    }
140}
141
142impl FromStr for Version {
143    type Err = Error;
144    /// Creates a new [`Version`] from a string slice.
145    ///
146    /// Delegates to [`Version::parser`].
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if [`Version::parser`] fails.
151    fn from_str(s: &str) -> Result<Version, Self::Err> {
152        Ok(Self::parser.parse(s)?)
153    }
154}
155
156impl Display for Version {
157    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
158        if let Some(epoch) = self.epoch {
159            write!(fmt, "{epoch}:")?;
160        }
161
162        write!(fmt, "{}", self.pkgver)?;
163
164        if let Some(pkgrel) = &self.pkgrel {
165            write!(fmt, "-{pkgrel}")?;
166        }
167
168        Ok(())
169    }
170}
171
172impl Ord for Version {
173    fn cmp(&self, other: &Self) -> Ordering {
174        match (self.epoch, other.epoch) {
175            (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
176                return self_epoch.cmp(&other_epoch);
177            }
178            (Some(_), None) => return Ordering::Greater,
179            (None, Some(_)) => return Ordering::Less,
180            (_, _) => {}
181        }
182
183        let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
184        if pkgver_cmp.is_ne() {
185            return pkgver_cmp;
186        }
187
188        self.pkgrel.cmp(&other.pkgrel)
189    }
190}
191
192impl PartialOrd for Version {
193    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
194        Some(self.cmp(other))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use std::num::NonZeroUsize;
201
202    use rstest::rstest;
203
204    use super::*;
205
206    /// Ensure that valid version strings are parsed as expected.
207    #[rstest]
208    #[case(
209        "foo",
210        Version {
211            epoch: None,
212            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
213            pkgrel: None
214        },
215    )]
216    #[case(
217        "1:foo-1",
218        Version {
219            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
220            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
221            pkgrel: Some(PackageRelease::new(1, None))
222        },
223    )]
224    #[case(
225        "1:foo",
226        Version {
227            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
228            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
229            pkgrel: None,
230        },
231    )]
232    #[case(
233        "foo-1",
234        Version {
235            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
236            epoch: None,
237            pkgrel: Some(PackageRelease::new(1, None))
238        }
239    )]
240    fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
241        assert_eq!(
242            Version::from_str(version),
243            Ok(expected),
244            "Expected valid parsing for version {version}"
245        )
246    }
247
248    /// Ensure that invalid version strings produce the respective errors.
249    #[rstest]
250    #[case::two_pkgrel("1:foo-1-1", "expected end of package release value")]
251    #[case::two_epoch("1:1:foo-1", "invalid pkgver character")]
252    #[case::no_version("", "expected pkgver string")]
253    #[case::no_version(":", "invalid first pkgver character")]
254    #[case::no_version(".", "invalid first pkgver character")]
255    #[case::invalid_integer(
256        "-1foo:1",
257        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
258    )]
259    #[case::invalid_integer(
260        "1-foo:1",
261        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
262    )]
263    fn parse_error_in_version_from_string(#[case] version: &str, #[case] err_snippet: &str) {
264        let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
265            panic!("parsing '{version}' did not fail as expected")
266        };
267        assert!(
268            err_msg.contains(err_snippet),
269            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
270        );
271    }
272
273    /// Ensure that versions are properly serialized back to their string representation.
274    #[rstest]
275    #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
276    #[case(Version::from_str("1-1").unwrap(), "1-1")]
277    #[case(Version::from_str("1").unwrap(), "1")]
278    #[case(Version::from_str("1:1").unwrap(), "1:1")]
279    fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
280        assert_eq!(format!("{version}"), to_str);
281    }
282
283    #[rstest]
284    // Major version comparisons
285    #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
286    #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
287    #[case(
288        Version::from_str("20220102"),
289        Version::from_str("20220202"),
290        Ordering::Less
291    )]
292    // Major vs Major.Minor
293    #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
294    #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
295    #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
296    #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
297    #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
298    // Major.Minor version comparisons
299    #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
300    #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
301    #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
302    #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
303    #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
304    #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
305    #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
306    // Major.Minor version comparisons with alphanumerics
307    #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
308    #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
309    #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
310    #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
311    #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
312    #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
313    #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
314    #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
315    #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
316    #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
317    #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
318    #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
319    #[case(
320        Version::from_str("1.alpha0.0"),
321        Version::from_str("1.alpha.0"),
322        Ordering::Less
323    )]
324    // Major.Minor vs Major.Minor.Patch
325    #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
326    // Major.Minor.Patch
327    #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
328    #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
329    #[case(
330        Version::from_str("1.0.alpha.0"),
331        Version::from_str("1.0."),
332        Ordering::Less
333    )]
334    #[case(
335        Version::from_str("1.a001a.1"),
336        Version::from_str("1.a1a.1"),
337        Ordering::Equal
338    )]
339    fn version_cmp(
340        #[case] version_a: Result<Version, Error>,
341        #[case] version_b: Result<Version, Error>,
342        #[case] expected: Ordering,
343    ) {
344        // Simply unwrap the Version as we expect all test strings to be valid.
345        let version_a = version_a.unwrap();
346        let version_b = version_b.unwrap();
347
348        // Derive the expected vercmp binary exitcode from the expected Ordering.
349        let vercmp_result = match &expected {
350            Ordering::Equal => 0,
351            Ordering::Greater => 1,
352            Ordering::Less => -1,
353        };
354
355        let ordering = version_a.cmp(&version_b);
356        assert_eq!(
357            ordering, expected,
358            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
359        );
360
361        assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
362
363        // If we find the `vercmp` binary, also run the test against the actual binary.
364        #[cfg(feature = "compatibility_tests")]
365        {
366            let output = std::process::Command::new("vercmp")
367                .arg(version_a.to_string())
368                .arg(version_b.to_string())
369                .output()
370                .unwrap();
371            let result = String::from_utf8_lossy(&output.stdout);
372            assert_eq!(result.trim(), vercmp_result.to_string());
373        }
374
375        // Now check that the opposite holds true as well.
376        let reverse_vercmp_result = match &expected {
377            Ordering::Equal => 0,
378            Ordering::Greater => -1,
379            Ordering::Less => 1,
380        };
381        let reverse_expected = match &expected {
382            Ordering::Equal => Ordering::Equal,
383            Ordering::Greater => Ordering::Less,
384            Ordering::Less => Ordering::Greater,
385        };
386
387        let reverse_ordering = version_b.cmp(&version_a);
388        assert_eq!(
389            reverse_ordering, reverse_expected,
390            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
391        );
392
393        assert_eq!(
394            Version::vercmp(&version_b, &version_a),
395            reverse_vercmp_result
396        );
397    }
398}