Skip to main content

alpm_types/version/
base.rs

1//! The base components for [alpm-package-version].
2//!
3//! An [alpm-package-version] is defined by the [alpm-epoch], [alpm-pkgver] and [alpm-pkgrel]
4//! components.
5//!
6//! [alpm-package-version]: https://alpm.archlinux.page/specifications/alpm-package-version.7.html
7//! [alpm-epoch]: https://alpm.archlinux.page/specifications/alpm-epoch.7.html
8//! [alpm-pkgver]: https://alpm.archlinux.page/specifications/alpm-pkgver.7.html
9//! [alpm-pkgrel]: https://alpm.archlinux.page/specifications/alpm-pkgrel.7.html
10
11use std::{
12    cmp::Ordering,
13    fmt::{Display, Formatter},
14    str::FromStr,
15};
16
17use alpm_parsers::prelude::*;
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20#[cfg(feature = "serde")]
21use serde_with::DeserializeFromStr;
22use winnow::{
23    ascii::{dec_uint, digit1},
24    combinator::opt,
25    error::ErrMode,
26    token::take_while,
27};
28
29#[cfg(doc)]
30use crate::Version;
31use crate::{Error, VersionSegments};
32
33/// An epoch of a package
34///
35/// Epoch is used to indicate the downgrade of a package and is prepended to a version, delimited by
36/// a `":"` (e.g. `1:` is added to `0.10.0-1` to form `1:0.10.0-1` which then orders newer than
37/// `1.0.0-1`).
38/// See [alpm-epoch] for details on the format.
39///
40/// An Epoch wraps a [`usize`].
41///
42/// ## Examples
43/// ```
44/// use std::str::FromStr;
45///
46/// use alpm_types::Epoch;
47///
48/// assert!(Epoch::from_str("0").is_ok());
49/// assert!(Epoch::from_str("1").is_ok());
50/// ```
51///
52/// [alpm-epoch]: https://alpm.archlinux.page/specifications/alpm-epoch.7.html
53#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
54#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
55pub struct Epoch(pub usize);
56
57impl Epoch {
58    /// Create a new Epoch
59    pub fn new(epoch: usize) -> Self {
60        Epoch(epoch)
61    }
62}
63
64impl AlpmParser for Epoch {
65    /// Recognizes an [`Epoch`] in a string slice.
66    ///
67    /// # Errors
68    ///
69    /// Returns an error if `input` does not begin with a valid _alpm_epoch_.
70    fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
71        dec_uint
72            .expected_text("non-negative decimal integer")
73            .map(Self)
74            .layer("alpm-epoch")
75            .parse_next(input)
76    }
77
78    fn delimiter_error_context<'a, O, P>(
79        parser: P,
80    ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
81    where
82        P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
83    {
84        parser
85            .expected_text("positive non-zero decimal integer")
86            .layer("alpm-epoch")
87    }
88}
89
90impl FromStr for Epoch {
91    type Err = Error;
92    /// Create an Epoch from a string and return it in a Result
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        Ok(Self::parser_until_eof.parse(Input::new(s))?)
95    }
96}
97
98impl Display for Epoch {
99    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
100        write!(fmt, "{}", self.0)
101    }
102}
103
104/// The release version of a package.
105///
106/// A [`PackageRelease`] wraps a [`usize`] for its `major` version and an optional [`usize`] for its
107/// `minor` version.
108///
109/// [`PackageRelease`] is used to indicate the build version of a package.
110/// It is mostly useful in conjunction with a [`PackageVersion`] (see [`Version`]).
111/// Refer to [alpm-pkgrel] for more details on the format.
112///
113/// ## Examples
114/// ```
115/// use std::str::FromStr;
116///
117/// use alpm_types::PackageRelease;
118///
119/// assert!(PackageRelease::from_str("1").is_ok());
120/// assert!(PackageRelease::from_str("1.1").is_ok());
121/// assert!(PackageRelease::from_str("0").is_ok());
122/// assert!(PackageRelease::from_str("a").is_err());
123/// assert!(PackageRelease::from_str("1.a").is_err());
124/// ```
125///
126/// [alpm-pkgrel]: https://alpm.archlinux.page/specifications/alpm-pkgrel.7.html
127#[derive(Clone, Debug, Eq, PartialEq)]
128#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
129pub struct PackageRelease {
130    /// The major version of this package release.
131    pub major: usize,
132    /// The optional minor version of this package release.
133    pub minor: Option<usize>,
134}
135
136impl PackageRelease {
137    /// Creates a new [`PackageRelease`] from a `major` and optional `minor` integer version.
138    ///
139    /// ## Examples
140    /// ```
141    /// use alpm_types::PackageRelease;
142    ///
143    /// # fn main() {
144    /// let release = PackageRelease::new(1, Some(2));
145    /// assert_eq!(format!("{release}"), "1.2");
146    /// # }
147    /// ```
148    pub fn new(major: usize, minor: Option<usize>) -> Self {
149        PackageRelease { major, minor }
150    }
151}
152
153impl AlpmParser for PackageRelease {
154    /// Recognizes a [`PackageRelease`] in a string slice.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if `input` does not begin with a valid [`PackageRelease`].
159    fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
160        let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
161            let major = digit1
162                .try_map(FromStr::from_str)
163                .label("package release")
164                .expected_text("positive decimal integer")
165                .parse_next(input)?;
166
167            // If we find a dot, also expect there to be a minor version number
168            let minor = if opt('.').parse_next(input)?.is_some() {
169                let minor = digit1
170                    .try_map(FromStr::from_str)
171                    .label("package release")
172                    .expected_text("single '.' followed by positive decimal integer")
173                    .parse_next(input)?;
174
175                Some(minor)
176            } else {
177                None
178            };
179
180            Ok(Self { major, minor })
181        };
182
183        parser.layer("alpm-pkgrel").parse_next(input)
184    }
185
186    fn delimiter_error_context<'a, O, P>(
187        parser: P,
188    ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
189    where
190        P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
191    {
192        parser
193            .label("package release")
194            .expected_text("single '.' followed by positive decimal integer")
195            .layer("alpm-pkgrel")
196    }
197}
198
199impl FromStr for PackageRelease {
200    type Err = Error;
201    /// Creates a [`PackageRelease`] from a string slice.
202    ///
203    /// Delegates to [`PackageRelease::parser`].
204    ///
205    /// # Errors
206    ///
207    /// Returns an error if [`PackageRelease::parser`] fails.
208    fn from_str(s: &str) -> Result<Self, Self::Err> {
209        Ok(Self::parser_until_eof.parse(Input::new(s))?)
210    }
211}
212
213impl Display for PackageRelease {
214    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
215        write!(fmt, "{}", self.major)?;
216        if let Some(minor) = self.minor {
217            write!(fmt, ".{minor}")?;
218        }
219        Ok(())
220    }
221}
222
223impl PartialOrd for PackageRelease {
224    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225        Some(self.cmp(other))
226    }
227}
228
229impl Ord for PackageRelease {
230    fn cmp(&self, other: &Self) -> Ordering {
231        let major_order = self.major.cmp(&other.major);
232        if major_order != Ordering::Equal {
233            return major_order;
234        }
235
236        match (self.minor, other.minor) {
237            (None, None) => Ordering::Equal,
238            (None, Some(_)) => Ordering::Less,
239            (Some(_), None) => Ordering::Greater,
240            (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
241        }
242    }
243}
244
245/// A pkgver of a package
246///
247/// PackageVersion is used to denote the upstream version of a package.
248///
249/// A PackageVersion wraps a `String`, which is guaranteed to only contain ASCII characters,
250/// excluding the ':', '/', '-', '<', '>', '=', or any whitespace characters and must be at least
251/// one character long.
252///
253/// NOTE: This implementation of PackageVersion is stricter than that of libalpm/pacman. It does not
254/// allow empty strings `""`.
255///
256/// ## Examples
257/// ```
258/// use std::str::FromStr;
259///
260/// use alpm_types::PackageVersion;
261///
262/// assert!(PackageVersion::new("1".to_string()).is_ok());
263/// assert!(PackageVersion::new("1.1".to_string()).is_ok());
264/// assert!(PackageVersion::new("foo".to_string()).is_ok());
265/// assert!(PackageVersion::new("0".to_string()).is_ok());
266/// assert!(PackageVersion::new(".0.1".to_string()).is_ok());
267/// assert!(PackageVersion::new("=1.0".to_string()).is_err());
268/// assert!(PackageVersion::new("1<0".to_string()).is_err());
269/// ```
270#[derive(Clone, Debug, Eq)]
271#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
272pub struct PackageVersion(pub(crate) String);
273
274impl PackageVersion {
275    /// Create a new PackageVersion from a string and return it in a Result
276    pub fn new(pkgver: String) -> Result<Self, Error> {
277        PackageVersion::from_str(pkgver.as_str())
278    }
279
280    /// Return an iterator over all segments of this version.
281    pub fn segments(&self) -> VersionSegments<'_> {
282        VersionSegments::new(&self.0)
283    }
284}
285
286impl AlpmParser for PackageVersion {
287    /// Recognizes a [`PackageVersion`] in a string slice.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if `input` does not begin with a valid [alpm-pkgver].
292    ///
293    /// [alpm-pkgver]: https://alpm.archlinux.page/specifications/alpm-pkgver.7.html
294    fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
295        // General rule for all characters:
296        // only ASCII except for ':', '/', '-', '<', '>', '=' or any whitespace
297        let allowed = |c: char| {
298            c.is_ascii() && ![':', '/', '-', '<', '>', '='].contains(&c) && !c.is_whitespace()
299        };
300
301        take_while(1.., allowed)
302            .label("character")
303            .expected_text("an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters")
304            .layer("alpm-pkgver")
305            .map(|s: &str| Self(s.to_string()))
306            .parse_next(input)
307    }
308
309    fn delimiter_error_context<'a, O, P>(
310        parser: P,
311    ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
312    where
313        P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
314    {
315        parser.label("character")
316            .expected_text("an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace character")
317            .layer("alpm-pkgver")
318    }
319}
320
321impl AsRef<str> for PackageVersion {
322    /// Returns a reference to the inner string slice.
323    fn as_ref(&self) -> &str {
324        &self.0
325    }
326}
327
328impl FromStr for PackageVersion {
329    type Err = Error;
330    /// Create a PackageVersion from a string and return it in a Result
331    fn from_str(s: &str) -> Result<Self, Self::Err> {
332        Ok(Self::parser_until_eof.parse(Input::new(s))?)
333    }
334}
335
336impl Display for PackageVersion {
337    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
338        write!(fmt, "{}", self.as_ref())
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use insta::assert_snapshot;
345    use rstest::rstest;
346    use testresult::TestResult;
347
348    use super::*;
349    use crate::configure_insta;
350
351    #[rstest]
352    #[case("0", Epoch(0))]
353    #[case("1", Epoch(1))]
354    fn epoch(#[case] version: &str, #[case] expected: Epoch) -> TestResult {
355        assert_eq!(expected, Epoch::from_str(version)?);
356
357        Ok(())
358    }
359
360    #[rstest]
361    #[case("-0", "expected non-negative decimal integer")]
362    #[case("z", "expected non-negative decimal integer")]
363    fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
364        let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
365            panic!("'{input}' erroneously parsed as Epoch")
366        };
367        assert!(
368            err_msg.contains(err_snippet),
369            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
370        );
371    }
372
373    /// Make sure that we can parse valid **pkgver** strings.
374    #[rstest]
375    #[case("foo")]
376    #[case("1.0.0")]
377    // sadly, this is valid
378    #[case(".xd")]
379    fn valid_pkgver(#[case] pkgver: &str) -> TestResult {
380        let parsed = PackageVersion::new(pkgver.to_string());
381        assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
382        assert_eq!(
383            parsed.as_ref()?.to_string(),
384            pkgver,
385            "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
386            parsed?,
387            pkgver
388        );
389
390        Ok(())
391    }
392
393    /// Ensure that invalid **pkgver**s are throwing errors.
394    #[rstest]
395    #[case("1:foo")]
396    #[case("foo-1")]
397    #[case("foo/1")]
398    // ß is not ASCII
399    #[case("ß")]
400    #[case("1.ß")]
401    #[case("")]
402    fn invalid_pkgver(#[case] pkgver: &str) {
403        let (test_name, _guard) = configure_insta();
404
405        let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
406            panic!("Expected pkgver {pkgver} to be invalid.")
407        };
408
409        assert_snapshot!(test_name, err_msg.to_string());
410    }
411
412    /// Make sure that invalid package versions don't deserialize.
413    #[cfg(feature = "serde")]
414    #[rstest]
415    #[case("1:foo")]
416    #[case("foo-1")]
417    fn package_version_deserialize_error(#[case] input: &str) {
418        let Err(serde_json::Error { .. }) =
419            serde_json::from_str::<PackageVersion>(&format!("\"{input}\""))
420        else {
421            panic!("'{input}' erroneously deserialized as a PackageVersion")
422        };
423    }
424
425    /// Make sure that we can parse valid **pkgrel** strings.
426    #[rstest]
427    #[case("0")]
428    #[case("1")]
429    #[case("10")]
430    #[case("1.0")]
431    #[case("10.5")]
432    #[case("0.1")]
433    fn valid_pkgrel(#[case] pkgrel: &str) -> TestResult {
434        let parsed = PackageRelease::from_str(pkgrel);
435        assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
436        assert_eq!(
437            parsed.as_ref()?.to_string(),
438            pkgrel,
439            "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
440            parsed?,
441            pkgrel
442        );
443
444        Ok(())
445    }
446
447    /// Ensure that invalid **pkgrel**s are throwing errors.
448    #[rstest]
449    #[case(".1")]
450    #[case("1.")]
451    #[case("1..1")]
452    #[case("-1")]
453    #[case("a")]
454    #[case("1.a")]
455    #[case("1.0.0")]
456    #[case("")]
457    fn invalid_pkgrel(#[case] pkgrel: &str) {
458        let (test_name, _guard) = configure_insta();
459
460        let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
461            panic!("'{pkgrel}' erroneously parsed as PackageRelease")
462        };
463
464        assert_snapshot!(test_name, err_msg.to_string());
465    }
466
467    /// Test that pkgrel ordering works as intended
468    #[rstest]
469    #[case("1", "1.0", Ordering::Less)]
470    #[case("1.0", "2", Ordering::Less)]
471    #[case("1", "1.1", Ordering::Less)]
472    #[case("1.0", "1.1", Ordering::Less)]
473    #[case("0", "1.1", Ordering::Less)]
474    #[case("1", "11", Ordering::Less)]
475    #[case("1", "1", Ordering::Equal)]
476    #[case("1.2", "1.2", Ordering::Equal)]
477    #[case("2.0", "2.0", Ordering::Equal)]
478    #[case("2", "1.0", Ordering::Greater)]
479    #[case("1.1", "1", Ordering::Greater)]
480    #[case("1.1", "1.0", Ordering::Greater)]
481    #[case("1.1", "0", Ordering::Greater)]
482    #[case("11", "1", Ordering::Greater)]
483    fn pkgrel_cmp(
484        #[case] first: &str,
485        #[case] second: &str,
486        #[case] order: Ordering,
487    ) -> TestResult {
488        let first = PackageRelease::from_str(first)?;
489        let second = PackageRelease::from_str(second)?;
490        assert_eq!(
491            first.cmp(&second),
492            order,
493            "{first} should be {order:?} to {second}"
494        );
495
496        Ok(())
497    }
498}