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, &'static [char]>,
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 (`.`), underscores (`_`), plus signs
116    /// (`+`), or at signs (`@`).
117    /// All separators are treated the same way because they are treated
118    /// the same by [`vercmp`](https://man.archlinux.org/man/vercmp.8.en).
119    pub fn components(&self) -> UpstreamVersionComponentIter<'a> {
120        UpstreamVersionComponentIter {
121            segments: self.as_str().split(&['.', '_', '+', '@']),
122        }
123    }
124}
125
126impl Ord for ValidUpstreamVersion<'_> {
127    /// Comparing two validated upstream versions.
128    ///
129    /// This comparison aims to emulate [`vercmp`](https://man.archlinux.org/man/vercmp.8.en)'s
130    /// algorithm on validated upstream versions.
131    ///
132    /// ```
133    /// # use arch_pkg_text::value::UpstreamVersion;
134    /// let validate = |raw| UpstreamVersion(raw).validate().unwrap();
135    ///
136    /// // Two versions are considered equal if their internal strings are equal
137    /// assert!(validate("1.2.3") == validate("1.2.3"));
138    /// assert!(validate("1.2_3") == validate("1.2_3"));
139    /// assert!(validate("1_2_3") == validate("1_2_3"));
140    ///
141    /// // Each component pair of two versions are compared until an unequal pair is found
142    /// assert!(validate("1.2.0") < validate("1.2.3"));
143    /// assert!(validate("1.3.2") > validate("1.2.3"));
144    /// assert!(validate("1.2.3.0.5.6") < validate("1.2.3.4.5.6"));
145    /// assert!(validate("1.2.3.4.5") > validate("1.2.3.2.1"));
146    /// assert!(validate("2.1.4") > validate("2.1.0.5"));
147    /// assert!(validate("1.1.0") < validate("1.2"));
148    ///
149    /// // If one version is the leading part of another, the latter is considered greater
150    /// assert!(validate("1.1.0") > validate("1.1"));
151    /// assert!(validate("1.1.0") < validate("1.1.0.0"));
152    ///
153    /// // The difference between dots, underscores, plus signs, and at signs are ignored
154    /// assert!(validate("1.2.3") == validate("1.2_3"));
155    /// assert!(validate("1.2.3") == validate("1_2_3"));
156    /// assert!(validate("1.2.0") < validate("1.2_3"));
157    /// assert!(validate("1_1.0") > validate("1.1"));
158    /// assert!(validate("1+2.3") == validate("1@2_3"));
159    /// assert!(validate("1@2@3") == validate("1+2+3"));
160    /// assert!(validate("1@2.0") < validate("1.2_3"));
161    /// assert!(validate("1_1.0") > validate("1+1"));
162    ///
163    /// // Leading zeros are ignored
164    /// assert!(validate("01.02.3") == validate("1.2.03"));
165    /// assert!(validate("1.02.0") < validate("1.2.3"));
166    /// assert!(validate("1.01.0") > validate("1.1"));
167    /// assert!(validate("1.1.0") > validate("1.001"));
168    /// ```
169    ///
170    /// **NOTE:** For licensing reason, this trait method was implemented from scratch by testing
171    /// case-by-case without looking at the source code of `vercmp` so there might be edge-cases and
172    /// subtle differences.
173    /// Contributors are welcomed to propose PRs to fix these edge-cases as long as they don't look
174    /// at the source code of `vercmp`.
175    fn cmp(&self, other: &Self) -> Ordering {
176        self.components().cmp(other.components())
177    }
178}
179
180impl PartialOrd for ValidUpstreamVersion<'_> {
181    /// Return a `Some(ordering)` with `ordering` being the result of [`ValidUpstreamVersion::cmp`].
182    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl Eq for ValidUpstreamVersion<'_> {}
188
189impl PartialEq for ValidUpstreamVersion<'_> {
190    /// Return `true` if [`ValidUpstreamVersion::cmp`] returns [`Ordering::Equal`].
191    /// Otherwise, return `false`.
192    ///
193    /// **NOTE:** Two versions being equal doesn't necessarily means that their internal
194    /// strings are equal. This is because dots (`.`), underscores (`_`), plus signs (`+`),
195    /// and at signs (`@`) were ignored during parsing.
196    fn eq(&self, other: &Self) -> bool {
197        self.cmp(other) == Ordering::Equal
198    }
199}
200
201impl Hash for ValidUpstreamVersion<'_> {
202    /// This custom hash algorithm was implemented in such a way to be consistent with [`ValidUpstreamVersion::cmp`]
203    /// and [`ValidUpstreamVersion::eq`].
204    fn hash<State: Hasher>(&self, state: &mut State) {
205        for component in self.components() {
206            component.hash(state);
207        }
208    }
209}
210
211/// Error of [`UpstreamVersion::validate`].
212#[derive(Debug, Display, Clone, Copy, Error)]
213#[display("{input:?} is not a valid version because {character:?} is not a valid character")]
214pub struct ValidateUpstreamVersionError<'a> {
215    character: char,
216    input: UpstreamVersion<'a>,
217}
218
219impl<'a> UpstreamVersion<'a> {
220    /// Validate the version, return a [`ValidUpstreamVersion`] on success.
221    ///
222    /// > Package release tags follow the same naming restrictions as version tags.
223    /// > -- from <https://wiki.archlinux.org/title/Arch_package_guidelines#Package_versioning>
224    ///
225    /// > Package names can contain only alphanumeric characters and any of `@`, `.`, `_`, `+`, `-`.
226    /// > Names are not allowed to start with hyphens or dots. All letters should be lowercase.
227    /// > -- from <https://wiki.archlinux.org/title/Arch_package_guidelines#Package_naming>
228    ///
229    /// Since a dash (`-`) signifies a `pkgrel` which is not part of upstream version, it is not
230    /// considered a valid character.
231    ///
232    /// ```
233    /// # use arch_pkg_text::value::UpstreamVersion;
234    /// # use pretty_assertions::assert_eq;
235    /// assert_eq!(
236    ///     UpstreamVersion("12.34_56a").validate().unwrap().as_str(),
237    ///     "12.34_56a",
238    /// );
239    /// assert!(UpstreamVersion("2:12.34_56a-1").validate().is_err());
240    /// ```
241    pub fn validate(&self) -> Result<ValidUpstreamVersion<'a>, ValidateUpstreamVersionError<'a>> {
242        let invalid_char = self.chars().find(
243            |char| !matches!(char, '0'..='9' | '.' | '_' | '+' | '@' | 'a'..='z' | 'A'..='Z' ),
244        );
245        if let Some(character) = invalid_char {
246            Err(ValidateUpstreamVersionError {
247                character,
248                input: *self,
249            })
250        } else {
251            self.as_str().pipe(ValidUpstreamVersion).pipe(Ok)
252        }
253    }
254}
255
256impl<'a> TryFrom<UpstreamVersion<'a>> for ValidUpstreamVersion<'a> {
257    type Error = ValidateUpstreamVersionError<'a>;
258    fn try_from(value: UpstreamVersion<'a>) -> Result<Self, Self::Error> {
259        value.validate()
260    }
261}
262
263impl<'a> From<ValidUpstreamVersion<'a>> for UpstreamVersion<'a> {
264    fn from(value: ValidUpstreamVersion<'a>) -> Self {
265        value.as_str().pipe(UpstreamVersion)
266    }
267}