uapi_version/
lib.rs

1//! Compare versions according to the [UAPI Version Format
2//! Specification](https://uapi-group.org/specifications/specs/version_format_specification/).
3//!
4//! This implementation is written purely in Rust and does not rely on any third party
5//! dependencies. Most notably, it doesn't link to `libsystemd`. It is `#![no_std]` and thus can,
6//! for example, also be used for UEFI development.
7//!
8//! # Examples
9//!
10//! You can compare two versions:
11//!
12//! ```
13//! use std::cmp::Ordering;
14//!
15//! use uapi_version::Version;
16//!
17//! let a = Version::from("225.1");
18//! let b = Version::from("2");
19//!
20//! assert_eq!(a.cmp(&b), Ordering::Greater)
21//! ```
22//!
23//! [`Version`] implements [`std::cmp::Ord`] and thus can be used to order a list of versions.
24//!
25//! ```
26//! use uapi_version::Version;
27//!
28//! let mut versions = [
29//!     "5.2",
30//!     "abc-5",
31//!     "1.0.0~rc1",
32//! ].map(Version::from);
33//!
34//! versions.sort();
35//!
36//! assert_eq!(versions, [ "abc-5", "1.0.0~rc1", "5.2" ].map(Version::from))
37//! ```
38//!
39//! You can also use [`strverscmp`] to compare two strings directly:
40//!
41//! ```
42//! use std::cmp::Ordering;
43//!
44//! use uapi_version::strverscmp;
45//!
46//! assert_eq!(strverscmp("124", "123"), Ordering::Greater)
47//! ```
48#![no_std]
49
50extern crate alloc;
51
52use alloc::fmt;
53use alloc::string::String;
54use core::cmp::Ordering;
55
56/// The `Version` type.
57///
58/// Can be built from any string that is a sequence of zero or more characters.
59///
60/// # Examples
61///
62/// ```
63/// use std::cmp::Ordering;
64///
65/// use uapi_version::Version;
66///
67/// let a = Version::from("1.0.0");
68/// let b = Version::from("2.0.0");
69///
70/// // `a` is smaller (i.e. older) than `b`.
71/// assert_eq!(a.cmp(&b), Ordering::Less)
72/// ```
73#[derive(PartialEq, Eq, Debug, Clone)]
74pub struct Version(String);
75
76impl Version {
77    #[must_use]
78    pub fn as_str(&self) -> &str {
79        &self.0
80    }
81
82    #[must_use]
83    pub fn into_string(self) -> String {
84        self.0
85    }
86}
87
88impl From<&str> for Version {
89    fn from(s: &str) -> Self {
90        Self(s.into())
91    }
92}
93
94impl From<String> for Version {
95    fn from(s: String) -> Self {
96        Self(s)
97    }
98}
99
100impl From<&String> for Version {
101    fn from(s: &String) -> Self {
102        Self(s.into())
103    }
104}
105
106impl fmt::Display for Version {
107    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112impl PartialOrd for Version {
113    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114        Some(self.cmp(other))
115    }
116}
117
118impl Ord for Version {
119    fn cmp(&self, other: &Self) -> Ordering {
120        strverscmp(&self.0, &other.0)
121    }
122}
123
124/// Compare two version strings.
125///
126/// # Examples
127///
128/// ```
129/// use std::cmp::Ordering;
130///
131/// use uapi_version::strverscmp;
132///
133/// assert_eq!(strverscmp("1.0.0", "2.0.0"), Ordering::Less)
134/// ```
135#[must_use]
136#[allow(clippy::too_many_lines)]
137pub fn strverscmp(a: &str, b: &str) -> Ordering {
138    let mut left_iter = a.chars().peekable();
139    let mut right_iter = b.chars().peekable();
140
141    loop {
142        let mut left = left_iter.next();
143        let mut right = right_iter.next();
144
145        // Step 1: Skip invalid chars
146        while left.is_some() && !left.is_some_and(is_valid_version_char) {
147            left = left_iter.next();
148        }
149        while right.is_some() && !right.is_some_and(is_valid_version_char) {
150            right = right_iter.next();
151        }
152
153        // Step 2: Handle '~'
154        if left.is_some_and(|c| c == '~') || right.is_some_and(|c| c == '~') {
155            let ordering = compare_special_char('~', left, right);
156            if ordering != Ordering::Equal {
157                return ordering;
158            }
159        }
160
161        // Step 3: Handle empty
162        if left.is_none() || right.is_none() {
163            return left.cmp(&right);
164        }
165
166        // Step 4: Handle '-'
167        if left.is_some_and(|c| c == '-') || right.is_some_and(|c| c == '-') {
168            let ordering = compare_special_char('-', left, right);
169            if ordering != Ordering::Equal {
170                return ordering;
171            }
172        }
173
174        // Step 5: Handle '^'
175        if left.is_some_and(|c| c == '^') || right.is_some_and(|c| c == '^') {
176            let ordering = compare_special_char('^', left, right);
177            if ordering != Ordering::Equal {
178                return ordering;
179            }
180        }
181
182        // Step 6: Handle '.'
183        if left.is_some_and(|c| c == '.') || right.is_some_and(|c| c == '.') {
184            let ordering = compare_special_char('.', left, right);
185            if ordering != Ordering::Equal {
186                return ordering;
187            }
188        }
189
190        // Step 7: Handle numerical prefix
191        if left.is_some_and(|c| c.is_ascii_digit()) || right.is_some_and(|c| c.is_ascii_digit()) {
192            // Skip leading '0's
193            while left.is_some_and(|c| c == '0') {
194                if !left_iter.peek().is_some_and(|c| c == &'0') {
195                    break;
196                }
197                left = left_iter.next();
198            }
199            while right.is_some_and(|c| c == '0') {
200                if !right_iter.peek().is_some_and(|c| c == &'0') {
201                    break;
202                }
203                right = right_iter.next();
204            }
205
206            let mut left_digit_prefix = String::new();
207            while left.is_some_and(|c| c.is_ascii_digit()) {
208                if let Some(char) = left {
209                    left_digit_prefix.push(char);
210                }
211                if !left_iter.peek().is_some_and(char::is_ascii_digit) {
212                    break;
213                }
214                left = left_iter.next();
215            }
216
217            let mut right_digit_prefix = String::new();
218            while right.is_some_and(|c| c.is_ascii_digit()) {
219                if let Some(char) = right {
220                    right_digit_prefix.push(char);
221                }
222                if !right_iter.peek().is_some_and(char::is_ascii_digit) {
223                    break;
224                }
225                right = right_iter.next();
226            }
227
228            if left_digit_prefix.len() != right_digit_prefix.len() {
229                return left_digit_prefix.len().cmp(&right_digit_prefix.len());
230            }
231
232            let ordering = left_digit_prefix.cmp(&right_digit_prefix);
233            if ordering != Ordering::Equal {
234                return ordering;
235            }
236        // Step 8: Handle alphabetical prefix
237        } else {
238            let mut left_alpha_prefix = String::new();
239            while left.is_some_and(|c| c.is_ascii_alphabetic()) {
240                if let Some(char) = left {
241                    left_alpha_prefix.push(char);
242                }
243                if !left_iter.peek().is_some_and(char::is_ascii_alphabetic) {
244                    break;
245                }
246                left = left_iter.next();
247            }
248
249            let mut right_alpha_prefix = String::new();
250            while right.is_some_and(|c| c.is_ascii_alphabetic()) {
251                if let Some(char) = right {
252                    right_alpha_prefix.push(char);
253                }
254                if !right_iter.peek().is_some_and(char::is_ascii_alphabetic) {
255                    break;
256                }
257                right = right_iter.next();
258            }
259
260            let ordering = left_alpha_prefix.cmp(&right_alpha_prefix);
261            if ordering != Ordering::Equal {
262                return ordering;
263            }
264        }
265    }
266}
267
268fn compare_special_char(char: char, left: Option<char>, right: Option<char>) -> Ordering {
269    let left_bool = !left.is_some_and(|c| c == char);
270    let right_bool = !right.is_some_and(|c| c == char);
271    left_bool.cmp(&right_bool)
272}
273
274fn is_valid_version_char(c: char) -> bool {
275    c.is_ascii_alphanumeric() || matches!(c, '~' | '-' | '^' | '.')
276}