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    num::NonZeroUsize,
15    str::FromStr,
16};
17
18use alpm_parsers::iter_char_context;
19use serde::{Deserialize, Serialize};
20use winnow::{
21    ModalResult,
22    Parser,
23    ascii::{dec_uint, digit1},
24    combinator::{Repeat, cut_err, eof, opt, preceded, repeat, seq, terminated},
25    error::{StrContext, StrContextValue},
26    token::one_of,
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 that is guaranteed to be greater than `0`.
41///
42/// ## Examples
43/// ```
44/// use std::str::FromStr;
45///
46/// use alpm_types::Epoch;
47///
48/// assert!(Epoch::from_str("1").is_ok());
49/// assert!(Epoch::from_str("0").is_err());
50/// ```
51///
52/// [alpm-epoch]: https://alpm.archlinux.page/specifications/alpm-epoch.7.html
53#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
54pub struct Epoch(pub NonZeroUsize);
55
56impl Epoch {
57    /// Create a new Epoch
58    pub fn new(epoch: NonZeroUsize) -> Self {
59        Epoch(epoch)
60    }
61
62    /// Recognizes an [`Epoch`] in a string slice.
63    ///
64    /// Consumes all of its input.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if `input` is not a valid _alpm_epoch_.
69    pub fn parser(input: &mut &str) -> ModalResult<Self> {
70        terminated(dec_uint, eof)
71            .verify_map(NonZeroUsize::new)
72            .context(StrContext::Label("package epoch"))
73            .context(StrContext::Expected(StrContextValue::Description(
74                "positive non-zero decimal integer",
75            )))
76            .map(Self)
77            .parse_next(input)
78    }
79}
80
81impl FromStr for Epoch {
82    type Err = Error;
83    /// Create an Epoch from a string and return it in a Result
84    fn from_str(s: &str) -> Result<Self, Self::Err> {
85        Ok(Self::parser.parse(s)?)
86    }
87}
88
89impl Display for Epoch {
90    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
91        write!(fmt, "{}", self.0)
92    }
93}
94
95/// The release version of a package.
96///
97/// A [`PackageRelease`] wraps a [`usize`] for its `major` version and an optional [`usize`] for its
98/// `minor` version.
99///
100/// [`PackageRelease`] is used to indicate the build version of a package.
101/// It is mostly useful in conjunction with a [`PackageVersion`] (see [`Version`]).
102/// Refer to [alpm-pkgrel] for more details on the format.
103///
104/// ## Examples
105/// ```
106/// use std::str::FromStr;
107///
108/// use alpm_types::PackageRelease;
109///
110/// assert!(PackageRelease::from_str("1").is_ok());
111/// assert!(PackageRelease::from_str("1.1").is_ok());
112/// assert!(PackageRelease::from_str("0").is_ok());
113/// assert!(PackageRelease::from_str("a").is_err());
114/// assert!(PackageRelease::from_str("1.a").is_err());
115/// ```
116///
117/// [alpm-pkgrel]: https://alpm.archlinux.page/specifications/alpm-pkgrel.7.html
118#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub struct PackageRelease {
120    /// The major version of this package release.
121    pub major: usize,
122    /// The optional minor version of this package release.
123    pub minor: Option<usize>,
124}
125
126impl PackageRelease {
127    /// Creates a new [`PackageRelease`] from a `major` and optional `minor` integer version.
128    ///
129    /// ## Examples
130    /// ```
131    /// use alpm_types::PackageRelease;
132    ///
133    /// # fn main() {
134    /// let release = PackageRelease::new(1, Some(2));
135    /// assert_eq!(format!("{release}"), "1.2");
136    /// # }
137    /// ```
138    pub fn new(major: usize, minor: Option<usize>) -> Self {
139        PackageRelease { major, minor }
140    }
141
142    /// Recognizes a [`PackageRelease`] in a string slice.
143    ///
144    /// Consumes all of its input.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if `input` does not contain a valid [`PackageRelease`].
149    pub fn parser(input: &mut &str) -> ModalResult<Self> {
150        seq!(Self {
151            major: digit1.try_map(FromStr::from_str)
152                .context(StrContext::Label("package release"))
153                .context(StrContext::Expected(StrContextValue::Description(
154                    "positive decimal integer",
155                ))),
156            minor: opt(preceded('.', cut_err(digit1.try_map(FromStr::from_str))))
157                .context(StrContext::Label("package release"))
158                .context(StrContext::Expected(StrContextValue::Description(
159                    "single '.' followed by positive decimal integer",
160                ))),
161            _: eof.context(StrContext::Expected(StrContextValue::Description(
162                "end of package release value",
163            ))),
164        })
165        .parse_next(input)
166    }
167}
168
169impl FromStr for PackageRelease {
170    type Err = Error;
171    /// Creates a [`PackageRelease`] from a string slice.
172    ///
173    /// Delegates to [`PackageRelease::parser`].
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if [`PackageRelease::parser`] fails.
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        Ok(Self::parser.parse(s)?)
180    }
181}
182
183impl Display for PackageRelease {
184    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
185        write!(fmt, "{}", self.major)?;
186        if let Some(minor) = self.minor {
187            write!(fmt, ".{minor}")?;
188        }
189        Ok(())
190    }
191}
192
193impl PartialOrd for PackageRelease {
194    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195        Some(self.cmp(other))
196    }
197}
198
199impl Ord for PackageRelease {
200    fn cmp(&self, other: &Self) -> Ordering {
201        let major_order = self.major.cmp(&other.major);
202        if major_order != Ordering::Equal {
203            return major_order;
204        }
205
206        match (self.minor, other.minor) {
207            (None, None) => Ordering::Equal,
208            (None, Some(_)) => Ordering::Less,
209            (Some(_), None) => Ordering::Greater,
210            (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
211        }
212    }
213}
214
215/// A pkgver of a package
216///
217/// PackageVersion is used to denote the upstream version of a package.
218///
219/// A PackageVersion wraps a `String`, which is guaranteed to only contain alphanumeric characters,
220/// `"_"`, `"+"` or `"."`, but to not start with a `"_"`, a `"+"` or a `"."` character and to be at
221/// least one char long.
222///
223/// NOTE: This implementation of PackageVersion is stricter than that of libalpm/pacman. It does not
224/// allow empty strings `""`, or chars that are not in the allowed set, or `"."` as the first
225/// character.
226///
227/// ## Examples
228/// ```
229/// use std::str::FromStr;
230///
231/// use alpm_types::PackageVersion;
232///
233/// assert!(PackageVersion::new("1".to_string()).is_ok());
234/// assert!(PackageVersion::new("1.1".to_string()).is_ok());
235/// assert!(PackageVersion::new("foo".to_string()).is_ok());
236/// assert!(PackageVersion::new("0".to_string()).is_ok());
237/// assert!(PackageVersion::new(".0.1".to_string()).is_err());
238/// assert!(PackageVersion::new("_1.0".to_string()).is_err());
239/// assert!(PackageVersion::new("+1.0".to_string()).is_err());
240/// ```
241#[derive(Clone, Debug, Deserialize, Eq, Serialize)]
242pub struct PackageVersion(pub(crate) String);
243
244impl PackageVersion {
245    /// Create a new PackageVersion from a string and return it in a Result
246    pub fn new(pkgver: String) -> Result<Self, Error> {
247        PackageVersion::from_str(pkgver.as_str())
248    }
249
250    /// Return a reference to the inner type
251    pub fn inner(&self) -> &str {
252        &self.0
253    }
254
255    /// Return an iterator over all segments of this version.
256    pub fn segments(&self) -> VersionSegments {
257        VersionSegments::new(&self.0)
258    }
259
260    /// Recognizes a [`PackageVersion`] in a string slice.
261    ///
262    /// Consumes all of its input.
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if `input` is not a valid _alpm-pkgrel_.
267    pub fn parser(input: &mut &str) -> ModalResult<Self> {
268        let alnum = |c: char| c.is_ascii_alphanumeric();
269
270        let first_character = one_of(alnum)
271            .context(StrContext::Label("first pkgver character"))
272            .context(StrContext::Expected(StrContextValue::Description(
273                "ASCII alphanumeric character",
274            )));
275        let special_tail_character = ['_', '+', '.'];
276        let tail_character = one_of((alnum, special_tail_character));
277
278        // no error context because this is infallible due to `0..`
279        // note the empty tuple collection to avoid allocation
280        let tail: Repeat<_, _, _, (), _> = repeat(0.., tail_character);
281
282        (
283            first_character,
284            tail,
285            eof.context(StrContext::Label("pkgver character"))
286                .context(StrContext::Expected(StrContextValue::Description(
287                    "ASCII alphanumeric character",
288                )))
289                .context_with(iter_char_context!(special_tail_character)),
290        )
291            .take()
292            .map(|s: &str| Self(s.to_string()))
293            .parse_next(input)
294    }
295}
296
297impl FromStr for PackageVersion {
298    type Err = Error;
299    /// Create a PackageVersion from a string and return it in a Result
300    fn from_str(s: &str) -> Result<Self, Self::Err> {
301        Ok(Self::parser.parse(s)?)
302    }
303}
304
305impl Display for PackageVersion {
306    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
307        write!(fmt, "{}", self.inner())
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use rstest::rstest;
314
315    use super::*;
316
317    #[rstest]
318    #[case("1", Ok(Epoch(NonZeroUsize::new(1).unwrap())))]
319    fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
320        assert_eq!(result, Epoch::from_str(version));
321    }
322
323    #[rstest]
324    #[case("0", "expected positive non-zero decimal integer")]
325    #[case("-0", "expected positive non-zero decimal integer")]
326    #[case("z", "expected positive non-zero decimal integer")]
327    fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
328        let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
329            panic!("'{input}' erroneously parsed as Epoch")
330        };
331        assert!(
332            err_msg.contains(err_snippet),
333            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
334        );
335    }
336
337    /// Make sure that we can parse valid **pkgver** strings.
338    #[rstest]
339    #[case("foo")]
340    #[case("1.0.0")]
341    fn valid_pkgver(#[case] pkgver: &str) {
342        let parsed = PackageVersion::new(pkgver.to_string());
343        assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
344        assert_eq!(
345            parsed.as_ref().unwrap().to_string(),
346            pkgver,
347            "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
348            parsed.unwrap(),
349            pkgver
350        );
351    }
352
353    /// Ensure that invalid **pkgver**s are throwing errors.
354    #[rstest]
355    #[case("1:foo", "invalid pkgver character")]
356    #[case("foo-1", "invalid pkgver character")]
357    #[case("foo,1", "invalid pkgver character")]
358    #[case(".foo", "invalid first pkgver character")]
359    #[case("_foo", "invalid first pkgver character")]
360    // ß is not in [:alnum:]
361    #[case("ß", "invalid first pkgver character")]
362    #[case("1.ß", "invalid pkgver character")]
363    fn invalid_pkgver(#[case] pkgver: &str, #[case] err_snippet: &str) {
364        let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
365            panic!("Expected pkgver {pkgver} to be invalid.")
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 **pkgrel** strings.
374    #[rstest]
375    #[case("0")]
376    #[case("1")]
377    #[case("10")]
378    #[case("1.0")]
379    #[case("10.5")]
380    #[case("0.1")]
381    fn valid_pkgrel(#[case] pkgrel: &str) {
382        let parsed = PackageRelease::from_str(pkgrel);
383        assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
384        assert_eq!(
385            parsed.as_ref().unwrap().to_string(),
386            pkgrel,
387            "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
388            parsed.unwrap(),
389            pkgrel
390        );
391    }
392
393    /// Ensure that invalid **pkgrel**s are throwing errors.
394    #[rstest]
395    #[case(".1", "expected positive decimal integer")]
396    #[case("1.", "expected single '.' followed by positive decimal integer")]
397    #[case("1..1", "expected single '.' followed by positive decimal integer")]
398    #[case("-1", "expected positive decimal integer")]
399    #[case("a", "expected positive decimal integer")]
400    #[case("1.a", "expected single '.' followed by positive decimal integer")]
401    #[case("1.0.0", "expected end of package release")]
402    #[case("", "expected positive decimal integer")]
403    fn invalid_pkgrel(#[case] pkgrel: &str, #[case] err_snippet: &str) {
404        let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
405            panic!("'{pkgrel}' erroneously parsed as PackageRelease")
406        };
407        assert!(
408            err_msg.contains(err_snippet),
409            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
410        );
411    }
412
413    /// Test that pkgrel ordering works as intended
414    #[rstest]
415    #[case("1", "1.0", Ordering::Less)]
416    #[case("1.0", "2", Ordering::Less)]
417    #[case("1", "1.1", Ordering::Less)]
418    #[case("1.0", "1.1", Ordering::Less)]
419    #[case("0", "1.1", Ordering::Less)]
420    #[case("1", "11", Ordering::Less)]
421    #[case("1", "1", Ordering::Equal)]
422    #[case("1.2", "1.2", Ordering::Equal)]
423    #[case("2.0", "2.0", Ordering::Equal)]
424    #[case("2", "1.0", Ordering::Greater)]
425    #[case("1.1", "1", Ordering::Greater)]
426    #[case("1.1", "1.0", Ordering::Greater)]
427    #[case("1.1", "0", Ordering::Greater)]
428    #[case("11", "1", Ordering::Greater)]
429    fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
430        let first = PackageRelease::from_str(first).unwrap();
431        let second = PackageRelease::from_str(second).unwrap();
432        assert_eq!(
433            first.cmp(&second),
434            order,
435            "{first} should be {order:?} to {second}"
436        );
437    }
438}