1#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Version {
14 Semver(semver::Version),
17 Numeric(Vec<u64>),
20 Opaque(String),
22}
23
24impl Version {
25 pub fn parse_lenient(s: &str) -> Self {
42 let stripped = s
43 .strip_prefix('v')
44 .or_else(|| s.strip_prefix('V'))
45 .unwrap_or(s);
46
47 if let Ok(v) = semver::Version::parse(stripped) {
48 return Version::Semver(v);
49 }
50
51 let parts: Vec<&str> = stripped.splitn(3, '.').collect();
53 let padded = match parts.len() {
54 1 => Some(format!("{}.0.0", parts[0])),
55 2 => Some(format!("{}.{}.0", parts[0], parts[1])),
56 _ => None,
57 };
58 if let Some(ref padded) = padded {
59 if let Ok(v) = semver::Version::parse(padded) {
60 return Version::Semver(v);
61 }
62 }
63
64 let raw_parts: Vec<&str> = s.split('.').collect();
66 let mut segments = Vec::new();
67 for part in &raw_parts {
68 match part.parse::<u64>() {
69 Ok(n) => segments.push(n),
70 Err(_) => return Version::Opaque(s.to_string()),
71 }
72 }
73 if segments.is_empty() {
74 return Version::Opaque(s.to_string());
75 }
76 Version::Numeric(segments)
77 }
78
79 pub fn is_downgrade(&self, new: &Self) -> bool {
102 match (self, new) {
103 (Version::Semver(old), Version::Semver(new)) => new < old,
104 (Version::Numeric(old), Version::Numeric(new)) => numeric_downgrade(old, new),
105 (Version::Semver(old), Version::Numeric(new_segs)) => {
106 let old_segs = [old.major, old.minor, old.patch];
107 numeric_downgrade(&old_segs, new_segs)
108 }
109 (Version::Numeric(old_segs), Version::Semver(new)) => {
110 let new_segs = [new.major, new.minor, new.patch];
111 numeric_downgrade(old_segs, &new_segs)
112 }
113 _ => false,
114 }
115 }
116}
117
118fn numeric_downgrade(old: &[u64], new: &[u64]) -> bool {
120 let max_len = old.len().max(new.len());
121 for i in 0..max_len {
122 let o = old.get(i).copied().unwrap_or(0);
123 let n = new.get(i).copied().unwrap_or(0);
124 if n < o {
125 return true;
126 }
127 if n > o {
128 return false;
129 }
130 }
131 false
132}
133
134pub fn is_version_downgrade(old_ver: &str, new_ver: &str) -> bool {
139 Version::parse_lenient(old_ver).is_downgrade(&Version::parse_lenient(new_ver))
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
151 fn parse_standard_semver() {
152 let v = Version::parse_lenient("1.2.3");
153 assert_eq!(v, Version::Semver(semver::Version::new(1, 2, 3)));
154 }
155
156 #[test]
157 fn parse_v_prefix() {
158 assert_eq!(
159 Version::parse_lenient("v1.2.3"),
160 Version::Semver(semver::Version::new(1, 2, 3))
161 );
162 assert_eq!(
163 Version::parse_lenient("V1.2.3"),
164 Version::Semver(semver::Version::new(1, 2, 3))
165 );
166 }
167
168 #[test]
169 fn parse_two_parts() {
170 assert_eq!(
171 Version::parse_lenient("1.2"),
172 Version::Semver(semver::Version::new(1, 2, 0))
173 );
174 }
175
176 #[test]
177 fn parse_single_part() {
178 assert_eq!(
179 Version::parse_lenient("42"),
180 Version::Semver(semver::Version::new(42, 0, 0))
181 );
182 }
183
184 #[test]
185 fn parse_prerelease() {
186 let v = Version::parse_lenient("1.2.3-beta.1");
187 match v {
188 Version::Semver(sv) => {
189 assert_eq!(sv.major, 1);
190 assert_eq!(sv.minor, 2);
191 assert_eq!(sv.patch, 3);
192 assert!(!sv.pre.is_empty());
193 }
194 other => panic!("expected Semver, got {:?}", other),
195 }
196 }
197
198 #[test]
199 fn parse_build_metadata() {
200 let v = Version::parse_lenient("1.2.3+build.456");
201 match v {
202 Version::Semver(sv) => {
203 assert_eq!((sv.major, sv.minor, sv.patch), (1, 2, 3));
204 assert!(!sv.build.is_empty());
205 }
206 other => panic!("expected Semver, got {:?}", other),
207 }
208 }
209
210 #[test]
211 fn parse_prerelease_and_build() {
212 let v = Version::parse_lenient("1.0.0-alpha.1+build.789");
213 match v {
214 Version::Semver(sv) => {
215 assert_eq!(sv.major, 1);
216 assert!(!sv.pre.is_empty());
217 assert!(!sv.build.is_empty());
218 }
219 other => panic!("expected Semver, got {:?}", other),
220 }
221 }
222
223 #[test]
224 fn parse_v_prefix_two_parts() {
225 assert_eq!(
226 Version::parse_lenient("v1.2"),
227 Version::Semver(semver::Version::new(1, 2, 0))
228 );
229 }
230
231 #[test]
232 fn parse_v_prefix_single_part() {
233 assert_eq!(
234 Version::parse_lenient("v5"),
235 Version::Semver(semver::Version::new(5, 0, 0))
236 );
237 }
238
239 #[test]
240 fn parse_v_prefix_prerelease() {
241 let v = Version::parse_lenient("v2.0.0-rc.1");
242 match v {
243 Version::Semver(sv) => {
244 assert_eq!(sv.major, 2);
245 assert!(!sv.pre.is_empty());
246 }
247 other => panic!("expected Semver, got {:?}", other),
248 }
249 }
250
251 #[test]
252 fn parse_zero_version() {
253 assert_eq!(
254 Version::parse_lenient("0.0.0"),
255 Version::Semver(semver::Version::new(0, 0, 0))
256 );
257 }
258
259 #[test]
260 fn parse_large_numbers() {
261 assert_eq!(
262 Version::parse_lenient("999.888.777"),
263 Version::Semver(semver::Version::new(999, 888, 777))
264 );
265 }
266
267 #[test]
268 fn parse_single_zero() {
269 assert_eq!(
270 Version::parse_lenient("0"),
271 Version::Semver(semver::Version::new(0, 0, 0))
272 );
273 }
274
275 #[test]
276 fn parse_four_part_is_numeric() {
277 assert_eq!(
278 Version::parse_lenient("1.2.3.4"),
279 Version::Numeric(vec![1, 2, 3, 4])
280 );
281 }
282
283 #[test]
284 fn parse_date_based_is_numeric() {
285 assert_eq!(
287 Version::parse_lenient("2024.01.15"),
288 Version::Numeric(vec![2024, 1, 15])
289 );
290 }
291
292 #[test]
293 fn parse_leading_zeros_is_numeric() {
294 assert_eq!(
295 Version::parse_lenient("01.02.03"),
296 Version::Numeric(vec![1, 2, 3])
297 );
298 }
299
300 #[test]
301 fn parse_non_numeric_is_opaque() {
302 assert_eq!(Version::parse_lenient("abc"), Version::Opaque("abc".into()));
303 assert_eq!(
304 Version::parse_lenient("foo.bar.baz"),
305 Version::Opaque("foo.bar.baz".into())
306 );
307 }
308
309 #[test]
310 fn parse_whitespace_is_opaque() {
311 assert!(matches!(
312 Version::parse_lenient(" 1.2.3"),
313 Version::Opaque(_)
314 ));
315 assert!(matches!(
316 Version::parse_lenient("1.2.3 "),
317 Version::Opaque(_)
318 ));
319 }
320
321 #[test]
322 fn parse_empty_string_is_opaque() {
323 assert!(matches!(Version::parse_lenient(""), Version::Opaque(_)));
324 }
325
326 #[test]
331 fn downgrade_semver() {
332 assert!(is_version_downgrade("2.0.0", "1.5.0"));
333 assert!(is_version_downgrade("1.1.0", "1.0.0"));
334 assert!(is_version_downgrade("1.0.1", "1.0.0"));
335 }
336
337 #[test]
338 fn upgrade_semver_not_flagged() {
339 assert!(!is_version_downgrade("1.0.0", "1.1.0"));
340 assert!(!is_version_downgrade("1.0.0", "2.0.0"));
341 assert!(!is_version_downgrade("1.0.0", "1.0.1"));
342 }
343
344 #[test]
345 fn equal_semver_not_flagged() {
346 assert!(!is_version_downgrade("1.0.0", "1.0.0"));
347 }
348
349 #[test]
350 fn downgrade_v_prefix() {
351 assert!(is_version_downgrade("v2.0.0", "v1.0.0"));
352 assert!(!is_version_downgrade("v1.0.0", "v2.0.0"));
353 }
354
355 #[test]
356 fn downgrade_prerelease() {
357 assert!(is_version_downgrade("1.0.0", "1.0.0-rc1"));
358 assert!(!is_version_downgrade("1.0.0-rc1", "1.0.0"));
359 }
360
361 #[test]
362 fn downgrade_build_metadata() {
363 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.2"));
364 assert!(is_version_downgrade("1.0.0+build.2", "1.0.0+build.1"));
365 assert!(!is_version_downgrade("1.0.0+build.1", "1.0.0+build.1"));
366 }
367
368 #[test]
369 fn downgrade_mixed_v_prefix() {
370 assert!(is_version_downgrade("v2.0.0", "1.0.0"));
371 assert!(is_version_downgrade("2.0.0", "v1.0.0"));
372 assert!(!is_version_downgrade("v1.0.0", "2.0.0"));
373 assert!(!is_version_downgrade("1.0.0", "v2.0.0"));
374 }
375
376 #[test]
377 fn downgrade_prerelease_ordering() {
378 assert!(is_version_downgrade("1.0.0-beta.1", "1.0.0-alpha.1"));
379 assert!(is_version_downgrade("1.0.0-rc.1", "1.0.0-beta.1"));
380 assert!(!is_version_downgrade("1.0.0-alpha.1", "1.0.0-beta.1"));
381 assert!(!is_version_downgrade("1.0.0-beta.1", "1.0.0-rc.1"));
382 }
383
384 #[test]
385 fn downgrade_prerelease_numeric_ordering() {
386 assert!(is_version_downgrade("1.0.0-rc.2", "1.0.0-rc.1"));
387 assert!(!is_version_downgrade("1.0.0-rc.1", "1.0.0-rc.2"));
388 }
389
390 #[test]
391 fn downgrade_equal_with_v_prefix() {
392 assert!(!is_version_downgrade("v1.0.0", "v1.0.0"));
393 }
394
395 #[test]
396 fn downgrade_padded_two_part() {
397 assert!(is_version_downgrade("1.2", "1.1"));
398 assert!(!is_version_downgrade("1.1", "1.2"));
399 assert!(!is_version_downgrade("1.2", "1.2"));
400 }
401
402 #[test]
403 fn downgrade_padded_single_part() {
404 assert!(is_version_downgrade("2", "1"));
405 assert!(!is_version_downgrade("1", "2"));
406 assert!(!is_version_downgrade("5", "5"));
407 }
408
409 #[test]
410 fn downgrade_mixed_part_counts_semver() {
411 assert!(is_version_downgrade("2.0", "1.9.9"));
412 assert!(!is_version_downgrade("1.9.9", "2.0"));
413 }
414
415 #[test]
416 fn downgrade_v_prefix_two_part() {
417 assert!(is_version_downgrade("v2.0", "v1.0"));
418 assert!(!is_version_downgrade("v1.0", "v2.0"));
419 }
420
421 #[test]
426 fn downgrade_four_part() {
427 assert!(is_version_downgrade("1.2.3.4", "1.2.3.3"));
428 assert!(!is_version_downgrade("1.2.3.3", "1.2.3.4"));
429 assert!(!is_version_downgrade("1.2.3.4", "1.2.3.4"));
430 }
431
432 #[test]
433 fn downgrade_date_based() {
434 assert!(is_version_downgrade("2024.01.15", "2023.12.01"));
435 assert!(!is_version_downgrade("2023.12.01", "2024.01.15"));
436 }
437
438 #[test]
439 fn downgrade_non_numeric_not_flagged() {
440 assert!(!is_version_downgrade("abc", "def"));
441 assert!(!is_version_downgrade("foo.bar", "foo.baz"));
442 }
443
444 #[test]
445 fn downgrade_numeric_unequal_length() {
446 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
447 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
448 }
449
450 #[test]
451 fn downgrade_large_major_numeric_equal() {
452 assert!(!is_version_downgrade("2024.1.15", "2024.1.15"));
454 }
455
456 #[test]
461 fn downgrade_semver_vs_four_part() {
462 assert!(!is_version_downgrade("1.2.3", "1.2.3.4"));
465 assert!(is_version_downgrade("1.2.3.4", "1.2.3"));
466 }
467
468 #[test]
469 fn downgrade_v_prefix_vs_four_part() {
470 assert!(!is_version_downgrade("v1.2.3", "1.2.3.4"));
474 assert!(is_version_downgrade("1.2.3.4", "v1.2.3"));
475 }
476
477 #[test]
482 fn downgrade_empty_strings() {
483 assert!(!is_version_downgrade("", "1.0.0"));
484 assert!(!is_version_downgrade("1.0.0", ""));
485 assert!(!is_version_downgrade("", ""));
486 }
487}