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