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