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}