1use std::cmp::Ordering;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
20enum PrereleaseSegment {
21 Numeric(u64),
22 Alphanumeric(String),
23}
24
25impl PrereleaseSegment {
26 fn parse(s: &str) -> Self {
27 if !s.is_empty()
28 && s.bytes().all(|b| b.is_ascii_digit())
29 && let Ok(n) = s.parse::<u64>()
30 {
31 return Self::Numeric(n);
32 }
33 Self::Alphanumeric(s.to_lowercase())
34 }
35}
36
37impl Ord for PrereleaseSegment {
38 fn cmp(&self, other: &Self) -> Ordering {
39 match (self, other) {
40 (Self::Numeric(a), Self::Numeric(b)) => a.cmp(b),
41 (Self::Alphanumeric(a), Self::Alphanumeric(b)) => a.cmp(b),
42 (Self::Numeric(_), Self::Alphanumeric(_)) => Ordering::Less,
43 (Self::Alphanumeric(_), Self::Numeric(_)) => Ordering::Greater,
44 }
45 }
46}
47
48impl PartialOrd for PrereleaseSegment {
49 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
50 Some(self.cmp(other))
51 }
52}
53
54#[derive(Debug, Clone)]
62pub(crate) struct ParsedVersion {
63 major: u64,
64 minor: u64,
65 patch: u64,
66 revision: u64,
67 pre: Vec<PrereleaseSegment>,
68}
69
70impl ParsedVersion {
71 pub(crate) fn parse(version: &str) -> Self {
72 let version = version.trim();
73 let core_and_pre = version.split('+').next().unwrap_or(version);
75 let (core, pre) = match core_and_pre.split_once('-') {
76 Some((a, b)) => (a, Some(b)),
77 None => (core_and_pre, None),
78 };
79
80 let mut parts = core.split('.').map(|s| s.parse::<u64>().unwrap_or(0));
81 let major = parts.next().unwrap_or(0);
82 let minor = parts.next().unwrap_or(0);
83 let patch = parts.next().unwrap_or(0);
84 let revision = parts.next().unwrap_or(0);
85
86 let pre = pre
87 .map(|p| p.split('.').map(PrereleaseSegment::parse).collect())
88 .unwrap_or_default();
89
90 Self {
91 major,
92 minor,
93 patch,
94 revision,
95 pre,
96 }
97 }
98}
99
100fn compare_pre(a: &[PrereleaseSegment], b: &[PrereleaseSegment]) -> Ordering {
101 for (x, y) in a.iter().zip(b.iter()) {
102 let ord = x.cmp(y);
103 if ord != Ordering::Equal {
104 return ord;
105 }
106 }
107 a.len().cmp(&b.len())
110}
111
112fn compare_parsed(a: &ParsedVersion, b: &ParsedVersion) -> Ordering {
113 a.major
114 .cmp(&b.major)
115 .then_with(|| a.minor.cmp(&b.minor))
116 .then_with(|| a.patch.cmp(&b.patch))
117 .then_with(|| a.revision.cmp(&b.revision))
118 .then_with(|| match (a.pre.is_empty(), b.pre.is_empty()) {
119 (true, true) => Ordering::Equal,
120 (true, false) => Ordering::Greater,
122 (false, true) => Ordering::Less,
123 (false, false) => compare_pre(&a.pre, &b.pre),
124 })
125}
126
127pub fn compare_versions(a: &str, b: &str) -> Ordering {
132 compare_parsed(&ParsedVersion::parse(a), &ParsedVersion::parse(b))
133}
134
135pub fn is_prerelease(version: &str) -> bool {
145 let without_build = version.split('+').next().unwrap_or(version);
146 without_build.contains('-')
147}
148
149pub(crate) enum VersionRange {
153 Exact(ParsedVersion),
154 Minimum {
155 version: ParsedVersion,
156 inclusive: bool,
157 },
158 Maximum {
159 version: ParsedVersion,
160 inclusive: bool,
161 },
162 Bounded {
163 min: ParsedVersion,
164 min_inclusive: bool,
165 max: ParsedVersion,
166 max_inclusive: bool,
167 },
168}
169
170pub(crate) fn parse_range(range: &str) -> Option<VersionRange> {
171 let range = range.trim();
172 if range.is_empty() {
173 return None;
174 }
175
176 let first = range.chars().next()?;
177 if first != '[' && first != '(' {
178 return Some(VersionRange::Minimum {
180 version: ParsedVersion::parse(range),
181 inclusive: true,
182 });
183 }
184
185 let last = range.chars().next_back()?;
186 if last != ']' && last != ')' {
187 return None;
188 }
189
190 let min_inclusive = first == '[';
191 let max_inclusive = last == ']';
192 let inner = &range[first.len_utf8()..range.len() - last.len_utf8()];
193
194 if let Some((lo, hi)) = inner.split_once(',') {
195 let lo = lo.trim();
196 let hi = hi.trim();
197 let min = (!lo.is_empty()).then(|| ParsedVersion::parse(lo));
198 let max = (!hi.is_empty()).then(|| ParsedVersion::parse(hi));
199 match (min, max) {
200 (Some(min), Some(max)) => Some(VersionRange::Bounded {
201 min,
202 min_inclusive,
203 max,
204 max_inclusive,
205 }),
206 (Some(version), None) => Some(VersionRange::Minimum {
207 version,
208 inclusive: min_inclusive,
209 }),
210 (None, Some(version)) => Some(VersionRange::Maximum {
211 version,
212 inclusive: max_inclusive,
213 }),
214 (None, None) => None,
215 }
216 } else {
217 Some(VersionRange::Exact(ParsedVersion::parse(inner.trim())))
219 }
220}
221
222fn satisfies_min(v: &ParsedVersion, min: &ParsedVersion, inclusive: bool) -> bool {
223 let ord = compare_parsed(v, min);
224 if inclusive {
225 ord != Ordering::Less
226 } else {
227 ord == Ordering::Greater
228 }
229}
230
231fn satisfies_max(v: &ParsedVersion, max: &ParsedVersion, inclusive: bool) -> bool {
232 let ord = compare_parsed(v, max);
233 if inclusive {
234 ord != Ordering::Greater
235 } else {
236 ord == Ordering::Less
237 }
238}
239
240pub(crate) fn compare_minimum_floor(range: &str, other: &str) -> Option<Ordering> {
252 match parse_range(range)? {
253 VersionRange::Minimum { version, .. } => {
254 Some(compare_parsed(&version, &ParsedVersion::parse(other)))
255 }
256 VersionRange::Exact(_) | VersionRange::Maximum { .. } | VersionRange::Bounded { .. } => {
257 None
258 }
259 }
260}
261
262pub(crate) fn range_contains(version: &str, range: &VersionRange) -> bool {
264 let v = ParsedVersion::parse(version);
265
266 match range {
267 VersionRange::Exact(target) => compare_parsed(&v, target) == Ordering::Equal,
268 VersionRange::Minimum { version, inclusive } => satisfies_min(&v, version, *inclusive),
269 VersionRange::Maximum { version, inclusive } => satisfies_max(&v, version, *inclusive),
270 VersionRange::Bounded {
271 min,
272 min_inclusive,
273 max,
274 max_inclusive,
275 } => satisfies_min(&v, min, *min_inclusive) && satisfies_max(&v, max, *max_inclusive),
276 }
277}
278
279pub fn satisfies(version: &str, range: &str) -> bool {
287 match parse_range(range) {
288 Some(parsed_range) => range_contains(version, &parsed_range),
289 None => false,
290 }
291}
292
293pub(crate) enum FloatPattern {
297 Any { include_prerelease: bool },
299 NumericPrefix {
302 prefix: String,
303 include_prerelease: bool,
304 },
305 PrereleaseLabelPrefix {
308 stable_prefix: String,
309 label_prefix: String,
310 },
311}
312
313pub(crate) fn parse_float(pattern: &str) -> Option<FloatPattern> {
314 let pattern = pattern.trim();
315 if pattern == "*" {
316 return Some(FloatPattern::Any {
317 include_prerelease: false,
318 });
319 }
320 if pattern == "*-*" {
321 return Some(FloatPattern::Any {
322 include_prerelease: true,
323 });
324 }
325
326 if let Some(prefix) = pattern.strip_suffix(".*-*") {
327 return Some(FloatPattern::NumericPrefix {
328 prefix: prefix.to_string(),
329 include_prerelease: true,
330 });
331 }
332
333 if let Some(prefix) = pattern.strip_suffix(".*") {
334 if let Some((stable, label)) = prefix.split_once('-') {
335 return Some(FloatPattern::PrereleaseLabelPrefix {
336 stable_prefix: stable.to_string(),
337 label_prefix: label.to_string(),
338 });
339 }
340 return Some(FloatPattern::NumericPrefix {
341 prefix: prefix.to_string(),
342 include_prerelease: false,
343 });
344 }
345
346 None
347}
348
349fn numeric_prefix_matches(version: &str, prefix: &str) -> bool {
352 let core = version.split(['-', '+']).next().unwrap_or(version);
353 let core_parts: Vec<&str> = core.split('.').collect();
354 let prefix_parts: Vec<&str> = prefix.split('.').collect();
355 if prefix_parts.len() > core_parts.len() {
356 return false;
357 }
358 core_parts
359 .iter()
360 .zip(prefix_parts.iter())
361 .all(|(c, p)| c == p)
362}
363
364fn stable_core_matches(version: &str, stable_prefix: &str) -> bool {
366 let v = ParsedVersion::parse(version);
367 let s = ParsedVersion::parse(stable_prefix);
368 v.major == s.major && v.minor == s.minor && v.patch == s.patch && v.revision == s.revision
369}
370
371fn prerelease_label_starts_with(version: &str, label_prefix: &str) -> bool {
373 let Some((_, pre)) = version.split_once('-') else {
374 return false;
375 };
376 let pre = pre.split('+').next().unwrap_or(pre);
377 pre.to_lowercase().starts_with(&label_prefix.to_lowercase())
378}
379
380pub(crate) fn float_matches(version: &str, parsed: &ParsedVersion, pattern: &FloatPattern) -> bool {
383 match pattern {
384 FloatPattern::Any { include_prerelease } => *include_prerelease || parsed.pre.is_empty(),
385 FloatPattern::NumericPrefix {
386 prefix,
387 include_prerelease,
388 } => {
389 (*include_prerelease || parsed.pre.is_empty())
390 && numeric_prefix_matches(version, prefix)
391 }
392 FloatPattern::PrereleaseLabelPrefix {
393 stable_prefix,
394 label_prefix,
395 } => {
396 stable_core_matches(version, stable_prefix)
397 && (parsed.pre.is_empty() || prerelease_label_starts_with(version, label_prefix))
398 }
399 }
400}
401
402pub fn resolve_float<'a>(versions: &'a [String], pattern: &str) -> Option<&'a str> {
409 let float = parse_float(pattern)?;
410 let mut best: Option<(&'a str, ParsedVersion)> = None;
411
412 for v in versions {
413 let parsed = ParsedVersion::parse(v);
414 if !float_matches(v, &parsed, &float) {
415 continue;
416 }
417
418 let is_better = best.as_ref().is_none_or(|(_, best_parsed)| {
419 compare_parsed(&parsed, best_parsed) == Ordering::Greater
420 });
421 if is_better {
422 best = Some((v.as_str(), parsed));
423 }
424 }
425
426 best.map(|(v, _)| v)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_parse_range_accepts_well_formed_ranges() {
435 assert!(parse_range("[1.0.0]").is_some());
436 assert!(parse_range("[1.0,2.0)").is_some());
437 assert!(parse_range("(1.0,2.0]").is_some());
438 assert!(parse_range("1.0.0").is_some());
439 }
440
441 #[test]
442 fn test_parse_float_accepts_well_formed_floats() {
443 assert!(parse_float("*").is_some());
444 assert!(parse_float("*-*").is_some());
445 assert!(parse_float("1.1.*").is_some());
446 assert!(parse_float("1.1.*-*").is_some());
447 }
448
449 #[test]
450 fn test_parse_range_rejects_malformed_range() {
451 assert!(parse_range("[1.0,2.0").is_none());
452 }
453
454 #[test]
455 fn test_parse_float_rejects_malformed_float() {
456 assert!(parse_float("1.*.0").is_none());
458 assert!(parse_float("*.1").is_none());
459 }
460
461 #[test]
462 fn test_compare_versions_basic() {
463 assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
464 assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
465 assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
466 assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
467 assert_eq!(compare_versions("10.0.0", "9.0.0"), Ordering::Greater);
468 }
469
470 #[test]
471 fn test_compare_versions_missing_components_are_zero() {
472 assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Equal);
473 assert_eq!(compare_versions("1", "1.0.0.0"), Ordering::Equal);
474 assert_eq!(compare_versions("1.2", "1.2.0.1"), Ordering::Less);
475 }
476
477 #[test]
478 fn test_compare_versions_four_component() {
479 assert_eq!(compare_versions("1.0.0.1", "1.0.0.0"), Ordering::Greater);
480 assert_eq!(compare_versions("1.0.0.10", "1.0.0.9"), Ordering::Greater);
481 }
482
483 #[test]
484 fn test_compare_versions_stable_beats_prerelease() {
485 assert_eq!(compare_versions("1.0.0", "1.0.0-rc"), Ordering::Greater);
486 assert_eq!(compare_versions("1.0.0-rc", "1.0.0"), Ordering::Less);
487 }
488
489 #[test]
490 fn test_compare_versions_prerelease_case_insensitive() {
491 assert_eq!(
492 compare_versions("1.0.0-Alpha", "1.0.0-alpha"),
493 Ordering::Equal
494 );
495 assert_eq!(
496 compare_versions("1.0.0-RC.1", "1.0.0-rc.1"),
497 Ordering::Equal
498 );
499 }
500
501 #[test]
502 fn test_compare_versions_prerelease_numeric_vs_alphanumeric() {
503 assert_eq!(compare_versions("1.0.0-1", "1.0.0-alpha"), Ordering::Less);
505 }
506
507 #[test]
508 fn test_compare_versions_prerelease_more_fields_higher_precedence() {
509 assert_eq!(
510 compare_versions("1.0.0-alpha.1", "1.0.0-alpha"),
511 Ordering::Greater
512 );
513 }
514
515 #[test]
516 fn test_compare_versions_build_metadata_ignored() {
517 assert_eq!(
518 compare_versions("1.0.0+build1", "1.0.0+build2"),
519 Ordering::Equal
520 );
521 assert_eq!(
522 compare_versions("1.0.0-rc+build1", "1.0.0-rc+build2"),
523 Ordering::Equal
524 );
525 }
526
527 #[test]
528 fn test_is_prerelease_dotnet_labels() {
529 assert!(is_prerelease("13.0.0-rtm"));
533 assert!(is_prerelease("8.0.0-servicing.23"));
534 assert!(is_prerelease("1.0.0-CI-20240101"));
535 assert!(is_prerelease("6.0.0-final"));
536 assert!(is_prerelease("7.0.0-preview.1"));
537 }
538
539 #[test]
540 fn test_is_prerelease_stable() {
541 assert!(!is_prerelease("1.0.0"));
542 assert!(!is_prerelease("13.0.0"));
543 assert!(!is_prerelease("1.0.0.5"));
544 }
545
546 #[test]
547 fn test_is_prerelease_ignores_build_metadata() {
548 assert!(!is_prerelease("1.0.0+build-with-dash"));
549 assert!(is_prerelease("1.0.0-rc+build"));
550 }
551
552 #[test]
553 fn test_satisfies_bare_is_minimum_inclusive() {
554 assert!(satisfies("1.0.0", "1.0"));
555 assert!(satisfies("2.0.0", "1.0"));
556 assert!(!satisfies("0.9.0", "1.0"));
557 }
558
559 #[test]
560 fn test_satisfies_exact_pin() {
561 assert!(satisfies("1.0.0", "[1.0]"));
562 assert!(satisfies("1.0", "[1.0.0]"));
563 assert!(!satisfies("1.0.1", "[1.0]"));
564 }
565
566 #[test]
567 fn test_satisfies_open_minimum() {
568 assert!(satisfies("1.0.0", "[1.0,)"));
569 assert!(satisfies("1.5.0", "[1.0,)"));
570 assert!(!satisfies("0.9.0", "[1.0,)"));
571
572 assert!(satisfies("1.0.1", "(1.0,)"));
573 assert!(!satisfies("1.0.0", "(1.0,)"));
574 }
575
576 #[test]
577 fn test_satisfies_open_maximum() {
578 assert!(satisfies("1.0.0", "(,1.0]"));
579 assert!(!satisfies("1.0.1", "(,1.0]"));
580
581 assert!(satisfies("0.9.0", "(,1.0)"));
582 assert!(!satisfies("1.0.0", "(,1.0)"));
583 }
584
585 #[test]
586 fn test_satisfies_bounded() {
587 assert!(satisfies("1.5.0", "[1.0,2.0]"));
588 assert!(satisfies("2.0.0", "[1.0,2.0]"));
589 assert!(!satisfies("2.0.0", "[1.0,2.0)"));
590 assert!(!satisfies("1.0.0", "(1.0,2.0)"));
591 assert!(satisfies("1.0.1", "(1.0,2.0)"));
592 assert!(!satisfies("1.0.0", "(1.0,2.0]"));
593 assert!(satisfies("1.0.1", "(1.0,2.0]"));
594 }
595
596 #[test]
597 fn test_satisfies_malformed_range_returns_false() {
598 assert!(!satisfies("1.0.0", ""));
599 assert!(!satisfies("1.0.0", "[1.0"));
600 assert!(!satisfies("1.0.0", "(,)"));
601 }
602
603 #[test]
604 fn test_resolve_float_any_stable_only() {
605 let versions: Vec<String> = ["1.0.0", "2.0.0-rc", "1.5.0"]
606 .into_iter()
607 .map(String::from)
608 .collect();
609 assert_eq!(resolve_float(&versions, "*"), Some("1.5.0"));
610 }
611
612 #[test]
613 fn test_resolve_float_any_including_prerelease() {
614 let versions: Vec<String> = ["1.0.0", "2.0.0-rc", "1.5.0"]
615 .into_iter()
616 .map(String::from)
617 .collect();
618 assert_eq!(resolve_float(&versions, "*-*"), Some("2.0.0-rc"));
619 }
620
621 #[test]
622 fn test_resolve_float_numeric_prefix_stable() {
623 let versions: Vec<String> = ["1.1.0", "1.1.5", "1.2.0", "1.1.9-beta"]
624 .into_iter()
625 .map(String::from)
626 .collect();
627 assert_eq!(resolve_float(&versions, "1.1.*"), Some("1.1.5"));
628 }
629
630 #[test]
631 fn test_resolve_float_numeric_prefix_including_prerelease() {
632 let versions: Vec<String> = ["1.1.0", "1.1.5", "1.1.9-beta"]
633 .into_iter()
634 .map(String::from)
635 .collect();
636 assert_eq!(resolve_float(&versions, "1.1.*-*"), Some("1.1.9-beta"));
637 }
638
639 #[test]
640 fn test_resolve_float_prerelease_label_prefix_stable_wins() {
641 let versions: Vec<String> = ["1.2.0-rc.1", "1.2.0-rc.2", "1.2.0"]
642 .into_iter()
643 .map(String::from)
644 .collect();
645 assert_eq!(resolve_float(&versions, "1.2.0-rc.*"), Some("1.2.0"));
646 }
647
648 #[test]
649 fn test_resolve_float_prerelease_label_prefix_no_stable() {
650 let versions: Vec<String> = ["1.2.0-rc.1", "1.2.0-rc.2", "1.2.0-beta"]
651 .into_iter()
652 .map(String::from)
653 .collect();
654 assert_eq!(resolve_float(&versions, "1.2.0-rc.*"), Some("1.2.0-rc.2"));
655 }
656
657 #[test]
658 fn test_resolve_float_no_match_returns_none() {
659 let versions: Vec<String> = vec!["2.0.0".to_string()];
660 assert_eq!(resolve_float(&versions, "1.1.*"), None);
661 }
662
663 #[test]
664 fn test_resolve_float_invalid_pattern_returns_none() {
665 let versions: Vec<String> = vec!["1.0.0".to_string()];
666 assert_eq!(resolve_float(&versions, "not-a-pattern"), None);
667 }
668}