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