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