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}