arch_pkg_text/value/
upstream_version.rs

1use super::UpstreamVersion;
2use core::{
3    cmp::Ordering,
4    hash::{Hash, Hasher},
5    iter::FusedIterator,
6    str::Split,
7};
8use derive_more::{AsRef, Display, Error};
9use pipe_trait::Pipe;
10
11/// Component of [`UpstreamVersion`], it includes a numeric prefix and a non-numeric suffix.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct UpstreamVersionComponent<'a> {
14    prefix: Option<u64>,
15    suffix: &'a str,
16}
17
18impl<'a> UpstreamVersionComponent<'a> {
19    /// Construct a new component.
20    pub fn new(prefix: Option<u64>, suffix: &'a str) -> Self {
21        UpstreamVersionComponent { prefix, suffix }
22    }
23
24    /// Parse a component from segment text.
25    ///
26    /// ```
27    /// # use arch_pkg_text::value::UpstreamVersionComponent;
28    /// # use pretty_assertions::assert_eq;
29    /// assert_eq!(UpstreamVersionComponent::parse("").components(), (None, ""));
30    /// assert_eq!(
31    ///     UpstreamVersionComponent::parse("alpha").components(),
32    ///     (None, "alpha"),
33    /// );
34    /// assert_eq!(
35    ///     UpstreamVersionComponent::parse("123").components(),
36    ///     (Some(123), ""),
37    /// );
38    /// assert_eq!(
39    ///     UpstreamVersionComponent::parse("123alpha").components(),
40    ///     (Some(123), "alpha"),
41    /// );
42    /// assert_eq!(
43    ///     UpstreamVersionComponent::parse("0alpha").components(),
44    ///     (Some(0), "alpha"),
45    /// );
46    /// assert_eq!(
47    ///     UpstreamVersionComponent::parse("00alpha").components(),
48    ///     (Some(0), "alpha"),
49    /// );
50    /// ```
51    pub fn parse(segment: &'a str) -> Self {
52        if segment.is_empty() {
53            return UpstreamVersionComponent::new(None, "");
54        }
55        let mut prefix = 0;
56        let mut boundary = segment.len();
57        for (idx, char) in segment.char_indices() {
58            if char.is_ascii_digit() {
59                prefix *= 10;
60                prefix += char as u64 - b'0' as u64;
61            } else {
62                boundary = idx;
63                break;
64            }
65        }
66        let prefix = (boundary != 0).then_some(prefix);
67        let suffix = &segment[boundary..];
68        UpstreamVersionComponent::new(prefix, suffix)
69    }
70
71    /// Exact the numeric prefix and non-numeric suffix.
72    pub fn components(&self) -> (Option<u64>, &'a str) {
73        let UpstreamVersionComponent { prefix, suffix } = *self;
74        (prefix, suffix)
75    }
76}
77
78/// Iterator over [`UpstreamVersionComponent`].
79///
80/// This struct is created by calling [`ValidUpstreamVersion::components`].
81#[derive(Debug, Clone)]
82pub struct UpstreamVersionComponentIter<'a> {
83    segments: Split<'a, [char; 2]>,
84}
85
86impl<'a> Iterator for UpstreamVersionComponentIter<'a> {
87    type Item = UpstreamVersionComponent<'a>;
88    fn next(&mut self) -> Option<Self::Item> {
89        self.segments.next().map(UpstreamVersionComponent::parse)
90    }
91}
92
93impl DoubleEndedIterator for UpstreamVersionComponentIter<'_> {
94    fn next_back(&mut self) -> Option<Self::Item> {
95        self.segments
96            .next_back()
97            .map(UpstreamVersionComponent::parse)
98    }
99}
100
101impl FusedIterator for UpstreamVersionComponentIter<'_> {}
102
103/// Upstream version which has been [validated](UpstreamVersion::validate).
104#[derive(Debug, Display, Clone, Copy, AsRef)]
105pub struct ValidUpstreamVersion<'a>(&'a str);
106
107impl<'a> ValidUpstreamVersion<'a> {
108    /// Get an immutable reference to the raw string underneath.
109    pub fn as_str(&self) -> &'a str {
110        self.0
111    }
112
113    /// Iterate over all components of the version.
114    ///
115    /// Components are separated by dots (`.`) or underscores (`_`).
116    /// Either separator are treated the same way because they are treated
117    /// the same by [`vercmp`](https://man.archlinux.org/man/vercmp.8.en).
118    pub fn components(&self) -> UpstreamVersionComponentIter<'a> {
119        UpstreamVersionComponentIter {
120            segments: self.as_str().split(['.', '_']),
121        }
122    }
123}
124
125impl Ord for ValidUpstreamVersion<'_> {
126    /// Comparing two validated upstream versions.
127    ///
128    /// This comparison aims to emulate [`vercmp`](https://man.archlinux.org/man/vercmp.8.en)'s
129    /// algorithm on validated upstream versions.
130    ///
131    /// ```
132    /// # use arch_pkg_text::value::UpstreamVersion;
133    /// let validate = |raw| UpstreamVersion(raw).validate().unwrap();
134    ///
135    /// // Two versions are considered equal if their internal strings are equal
136    /// assert!(validate("1.2.3") == validate("1.2.3"));
137    /// assert!(validate("1.2_3") == validate("1.2_3"));
138    /// assert!(validate("1_2_3") == validate("1_2_3"));
139    ///
140    /// // Each component pair of two versions are compared until an unequal pair is found
141    /// assert!(validate("1.2.0") < validate("1.2.3"));
142    /// assert!(validate("1.3.2") > validate("1.2.3"));
143    /// assert!(validate("1.2.3.0.5.6") < validate("1.2.3.4.5.6"));
144    /// assert!(validate("1.2.3.4.5") > validate("1.2.3.2.1"));
145    /// assert!(validate("2.1.4") > validate("2.1.0.5"));
146    /// assert!(validate("1.1.0") < validate("1.2"));
147    ///
148    /// // If one version is the leading part of another, the latter is considered greater
149    /// assert!(validate("1.1.0") > validate("1.1"));
150    /// assert!(validate("1.1.0") < validate("1.1.0.0"));
151    ///
152    /// // The difference between dots and underscores are ignored
153    /// assert!(validate("1.2.3") == validate("1.2_3"));
154    /// assert!(validate("1.2.3") == validate("1_2_3"));
155    /// assert!(validate("1.2.0") < validate("1.2_3"));
156    /// assert!(validate("1_1.0") > validate("1.1"));
157    ///
158    /// // Leading zeros are ignored
159    /// assert!(validate("01.02.3") == validate("1.2.03"));
160    /// assert!(validate("1.02.0") < validate("1.2.3"));
161    /// assert!(validate("1.01.0") > validate("1.1"));
162    /// assert!(validate("1.1.0") > validate("1.001"));
163    /// ```
164    ///
165    /// **NOTE:** For licensing reason, this trait method was implemented from scratch by testing
166    /// case-by-case without looking at the source code of `vercmp` so there might be edge-cases and
167    /// subtle differences.
168    /// Contributors are welcomed to propose PRs to fix these edge-cases as long as they don't look
169    /// at the source code of `vercmp`.
170    fn cmp(&self, other: &Self) -> Ordering {
171        self.components().cmp(other.components())
172    }
173}
174
175impl PartialOrd for ValidUpstreamVersion<'_> {
176    /// Return a `Some(ordering)` with `ordering` being the result of [`ValidUpstreamVersion::cmp`].
177    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
178        Some(self.cmp(other))
179    }
180}
181
182impl Eq for ValidUpstreamVersion<'_> {}
183
184impl PartialEq for ValidUpstreamVersion<'_> {
185    /// Return `true` if [`ValidUpstreamVersion::cmp`] returns [`Ordering::Equal`].
186    /// Otherwise, return `false`.
187    ///
188    /// **NOTE:** Two versions being equal doesn't necessary means that their internal
189    /// strings are equal. This is because dots (`.`) and underscores (`_`) are ignored.
190    fn eq(&self, other: &Self) -> bool {
191        self.cmp(other) == Ordering::Equal
192    }
193}
194
195impl Hash for ValidUpstreamVersion<'_> {
196    /// This custom hash algorithm was implemented in such a way to be consistent with [`ValidUpstreamVersion::cmp`]
197    /// and [`ValidUpstreamVersion::eq`].
198    fn hash<State: Hasher>(&self, state: &mut State) {
199        for component in self.components() {
200            component.hash(state);
201        }
202    }
203}
204
205/// Error of [`UpstreamVersion::validate`].
206#[derive(Debug, Display, Clone, Copy, Error)]
207#[display("{input:?} is not a valid version because {character:?} is not a valid character")]
208pub struct ValidateUpstreamVersionError<'a> {
209    character: char,
210    input: UpstreamVersion<'a>,
211}
212
213impl<'a> UpstreamVersion<'a> {
214    /// Validate the version, return a [`ValidUpstreamVersion`] on success.
215    ///
216    /// A valid version is a string that contains only alphanumerics, dots (`.`), and underscores (`_`).
217    ///
218    /// ```
219    /// # use arch_pkg_text::value::UpstreamVersion;
220    /// # use pretty_assertions::assert_eq;
221    /// assert_eq!(
222    ///     UpstreamVersion("12.34_56a").validate().unwrap().as_str(),
223    ///     "12.34_56a",
224    /// );
225    /// assert!(UpstreamVersion("2:12.34_56a-1").validate().is_err());
226    /// ```
227    pub fn validate(&self) -> Result<ValidUpstreamVersion<'a>, ValidateUpstreamVersionError<'a>> {
228        let invalid_char = self
229            .chars()
230            .find(|char| !matches!(char, '0'..='9' | '.' | '_' | 'a'..='z' | 'A'..='Z' ));
231        if let Some(character) = invalid_char {
232            Err(ValidateUpstreamVersionError {
233                character,
234                input: *self,
235            })
236        } else {
237            self.as_str().pipe(ValidUpstreamVersion).pipe(Ok)
238        }
239    }
240}
241
242impl<'a> TryFrom<UpstreamVersion<'a>> for ValidUpstreamVersion<'a> {
243    type Error = ValidateUpstreamVersionError<'a>;
244    fn try_from(value: UpstreamVersion<'a>) -> Result<Self, Self::Error> {
245        value.validate()
246    }
247}
248
249impl<'a> From<ValidUpstreamVersion<'a>> for UpstreamVersion<'a> {
250    fn from(value: ValidUpstreamVersion<'a>) -> Self {
251        value.as_str().pipe(UpstreamVersion)
252    }
253}