Skip to main content

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 insta::assert_snapshot;
203    use rstest::rstest;
204
205    use super::*;
206    use crate::configure_insta;
207
208    /// Ensure that valid version strings are parsed as expected.
209    #[rstest]
210    #[case(
211        "foo",
212        Version {
213            epoch: None,
214            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
215            pkgrel: None
216        },
217    )]
218    #[case(
219        "1:foo-1",
220        Version {
221            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
222            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
223            pkgrel: Some(PackageRelease::new(1, None))
224        },
225    )]
226    #[case(
227        "1:foo",
228        Version {
229            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
230            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
231            pkgrel: None,
232        },
233    )]
234    #[case(
235        "foo-1",
236        Version {
237            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
238            epoch: None,
239            pkgrel: Some(PackageRelease::new(1, None))
240        }
241    )]
242    // yes, this is valid
243    #[case(
244        ".-1",
245        Version {
246            pkgver: PackageVersion::new(".".to_string()).unwrap(),
247            epoch: None,
248            pkgrel: Some(PackageRelease::new(1, None))
249            }
250    )]
251    fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
252        assert_eq!(
253            Version::from_str(version),
254            Ok(expected),
255            "Expected valid parsing for version {version}"
256        )
257    }
258
259    /// Ensure that invalid version strings produce the respective errors.
260    #[rstest]
261    #[case::two_pkgrel("1:foo-1-1")]
262    #[case::two_epoch("1:1:foo-1")]
263    #[case::no_version("")]
264    #[case::no_version(":")]
265    #[case::invalid_integer("-1foo:1")]
266    #[case::invalid_integer("1-foo:1")]
267    fn parse_error_in_version_from_string(#[case] version: &str) {
268        let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
269            panic!("parsing '{version}' did not fail as expected")
270        };
271
272        let (test_name, _guard) = configure_insta();
273        assert_snapshot!(test_name, err_msg.to_string());
274    }
275
276    /// Ensure that versions are properly serialized back to their string representation.
277    #[rstest]
278    #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
279    #[case(Version::from_str("1-1").unwrap(), "1-1")]
280    #[case(Version::from_str("1").unwrap(), "1")]
281    #[case(Version::from_str("1:1").unwrap(), "1:1")]
282    fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
283        assert_eq!(format!("{version}"), to_str);
284    }
285
286    #[rstest]
287    // Major version comparisons
288    #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
289    #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
290    #[case(
291        Version::from_str("20220102"),
292        Version::from_str("20220202"),
293        Ordering::Less
294    )]
295    // Major vs Major.Minor
296    #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
297    #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
298    #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
299    #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
300    #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
301    // Major.Minor version comparisons
302    #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
303    #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
304    #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
305    #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
306    #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
307    #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
308    #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
309    // Major.Minor version comparisons with alphanumerics
310    #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
311    #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
312    #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
313    #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
314    #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
315    #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
316    #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
317    #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
318    #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
319    #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
320    #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
321    #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
322    #[case(
323        Version::from_str("1.alpha0.0"),
324        Version::from_str("1.alpha.0"),
325        Ordering::Less
326    )]
327    // Major.Minor vs Major.Minor.Patch
328    #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
329    // Major.Minor.Patch
330    #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
331    #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
332    #[case(
333        Version::from_str("1.0.alpha.0"),
334        Version::from_str("1.0."),
335        Ordering::Less
336    )]
337    #[case(
338        Version::from_str("1.a001a.1"),
339        Version::from_str("1.a1a.1"),
340        Ordering::Equal
341    )]
342    fn version_cmp(
343        #[case] version_a: Result<Version, Error>,
344        #[case] version_b: Result<Version, Error>,
345        #[case] expected: Ordering,
346    ) {
347        // Simply unwrap the Version as we expect all test strings to be valid.
348        let version_a = version_a.unwrap();
349        let version_b = version_b.unwrap();
350
351        // Derive the expected vercmp binary exitcode from the expected Ordering.
352        let vercmp_result = match &expected {
353            Ordering::Equal => 0,
354            Ordering::Greater => 1,
355            Ordering::Less => -1,
356        };
357
358        let ordering = version_a.cmp(&version_b);
359        assert_eq!(
360            ordering, expected,
361            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
362        );
363
364        assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
365
366        // If we find the `vercmp` binary, also run the test against the actual binary.
367        #[cfg(feature = "compatibility_tests")]
368        {
369            let output = std::process::Command::new("vercmp")
370                .arg(version_a.to_string())
371                .arg(version_b.to_string())
372                .output()
373                .unwrap();
374            let result = String::from_utf8_lossy(&output.stdout);
375            assert_eq!(result.trim(), vercmp_result.to_string());
376        }
377
378        // Now check that the opposite holds true as well.
379        let reverse_vercmp_result = match &expected {
380            Ordering::Equal => 0,
381            Ordering::Greater => -1,
382            Ordering::Less => 1,
383        };
384        let reverse_expected = match &expected {
385            Ordering::Equal => Ordering::Equal,
386            Ordering::Greater => Ordering::Less,
387            Ordering::Less => Ordering::Greater,
388        };
389
390        let reverse_ordering = version_b.cmp(&version_a);
391        assert_eq!(
392            reverse_ordering, reverse_expected,
393            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
394        );
395
396        assert_eq!(
397            Version::vercmp(&version_b, &version_a),
398            reverse_vercmp_result
399        );
400    }
401}