1#![no_std]
49
50extern crate alloc;
51
52use alloc::fmt;
53use alloc::string::String;
54use core::cmp::Ordering;
55
56#[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#[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 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 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 if left.is_none() || right.is_none() {
163 return left.cmp(&right);
164 }
165
166 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 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 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 if left.is_some_and(|c| c.is_ascii_digit()) || right.is_some_and(|c| c.is_ascii_digit()) {
192 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 } 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}