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