1use once_cell::sync::Lazy;
4use regex::Regex;
5use rez_next_common::RezCoreError;
6use serde::{Deserialize, Serialize};
7use std::cmp::Ordering;
8use std::hash::{Hash, Hasher};
9
10static TOKEN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[a-zA-Z0-9_]+").unwrap());
12
13#[derive(Debug)]
15pub struct Version {
16 tokens: Vec<String>,
18 separators: Vec<String>,
20 pub string_repr: String,
22 cached_hash: Option<u64>,
24}
25
26impl Serialize for Version {
27 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28 where
29 S: serde::Serializer,
30 {
31 self.string_repr.serialize(serializer)
33 }
34}
35
36impl std::fmt::Display for Version {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(f, "{}", self.as_str())
39 }
40}
41
42impl<'de> Deserialize<'de> for Version {
43 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44 where
45 D: serde::Deserializer<'de>,
46 {
47 let s = String::deserialize(deserializer)?;
48 Self::parse(&s).map_err(serde::de::Error::custom)
49 }
50}
51
52impl Version {
53 pub fn new(version_str: Option<&str>) -> Result<Self, RezCoreError> {
54 let version_str = version_str.unwrap_or("");
55 Self::parse(version_str)
56 }
57
58 pub fn as_str(&self) -> &str {
59 &self.string_repr
60 }
61
62 pub fn has_prefix(&self, prefix: &Version) -> bool {
64 self.tokens.len() >= prefix.tokens.len()
65 && self
66 .tokens
67 .iter()
68 .zip(&prefix.tokens)
69 .all(|(token, prefix_token)| {
70 Self::compare_single_token(token, prefix_token) == Ordering::Equal
71 })
72 }
73}
74
75impl Version {
76 fn parse_internal_gil_free(s: &str) -> Result<(Vec<String>, Vec<String>), RezCoreError> {
79 if s.starts_with('v') || s.starts_with('V') {
81 return Err(RezCoreError::VersionParse(format!(
82 "Version prefixes not supported: '{s}'"
83 )));
84 }
85
86 if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
88 return Err(RezCoreError::VersionParse(format!(
89 "Invalid version syntax: '{s}'"
90 )));
91 }
92
93 let tokens: Vec<&str> = TOKEN_REGEX.find_iter(s).map(|m| m.as_str()).collect();
95
96 if tokens.is_empty() {
97 return Err(RezCoreError::VersionParse(format!(
98 "Invalid version syntax: '{s}'"
99 )));
100 }
101
102 let numeric_tokens: Vec<_> = tokens
104 .iter()
105 .filter(|t| t.chars().all(|c| c.is_ascii_digit()))
106 .collect();
107 if numeric_tokens.len() > 5 {
108 return Err(RezCoreError::VersionParse(format!(
109 "Version too complex: '{s}'"
110 )));
111 }
112
113 if tokens.len() > 10 {
115 return Err(RezCoreError::VersionParse(format!(
116 "Version too complex: '{s}'"
117 )));
118 }
119
120 let separators: Vec<&str> = TOKEN_REGEX.split(s).collect();
122
123 if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
125 return Err(RezCoreError::VersionParse(format!(
126 "Invalid version syntax: '{s}'"
127 )));
128 }
129
130 for sep in &separators[1..separators.len() - 1] {
131 if sep.len() > 1 {
132 return Err(RezCoreError::VersionParse(format!(
133 "Invalid version syntax: '{s}'"
134 )));
135 }
136 if !matches!(*sep, "." | "-" | "_" | "+") {
138 return Err(RezCoreError::VersionParse(format!(
139 "Invalid separator '{sep}' in version: '{s}'"
140 )));
141 }
142 }
143
144 for token_str in &tokens {
146 if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
148 return Err(RezCoreError::VersionParse(format!(
149 "Invalid characters in token: '{token_str}'"
150 )));
151 }
152
153 if token_str.starts_with('_') || token_str.ends_with('_') {
155 return Err(RezCoreError::VersionParse(format!(
156 "Invalid token format: '{token_str}'"
157 )));
158 }
159
160 if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
162 return Err(RezCoreError::VersionParse(format!(
163 "Invalid version token: '{token_str}'"
164 )));
165 }
166
167 if *token_str == "not" || *token_str == "version" {
169 return Err(RezCoreError::VersionParse(format!(
170 "Invalid version token: '{token_str}'"
171 )));
172 }
173 }
174
175 let token_strings: Vec<String> = tokens.into_iter().map(ToString::to_string).collect();
177 let sep_strings: Vec<String> = separators[1..separators.len() - 1]
178 .iter()
179 .map(ToString::to_string)
180 .collect();
181
182 Ok((token_strings, sep_strings))
183 }
184
185 #[must_use]
187 pub fn inf() -> Self {
188 Self {
189 tokens: vec![],
190 separators: vec![],
191 string_repr: "inf".to_string(),
192 cached_hash: None,
193 }
194 }
195
196 #[must_use]
198 pub fn is_inf(&self) -> bool {
199 self.string_repr == "inf"
200 }
201
202 #[must_use]
204 pub fn empty() -> Self {
205 Self {
206 tokens: vec![],
207 separators: vec![],
208 string_repr: String::new(),
209 cached_hash: None,
210 }
211 }
212
213 #[must_use]
215 pub fn epsilon() -> Self {
216 Self::empty()
217 }
218
219 pub fn is_empty(&self) -> bool {
221 self.tokens.is_empty() && self.string_repr.is_empty()
222 }
223
224 pub fn len(&self) -> usize {
226 self.tokens.len()
227 }
228
229 pub fn major(&self) -> Option<u64> {
231 self.tokens.first().and_then(|t| t.parse::<u64>().ok())
232 }
233
234 pub fn minor(&self) -> Option<u64> {
236 self.tokens.get(1).and_then(|t| t.parse::<u64>().ok())
237 }
238
239 pub fn patch(&self) -> Option<u64> {
241 self.tokens.get(2).and_then(|t| t.parse::<u64>().ok())
242 }
243
244 pub fn is_epsilon(&self) -> bool {
246 self.is_empty()
247 }
248
249 pub fn is_prerelease(&self) -> bool {
251 if self.is_empty() || self.is_inf() {
252 return false;
253 }
254
255 for token in &self.tokens {
257 let s_lower = token.to_lowercase();
258 if s_lower.contains("alpha")
260 || s_lower.contains("beta")
261 || s_lower.contains("rc")
262 || s_lower.contains("dev")
263 || s_lower.contains("pre")
264 || s_lower.contains("snapshot")
265 {
266 return true;
267 }
268 }
269 false
270 }
271
272 pub fn parse(s: &str) -> Result<Self, RezCoreError> {
274 let s = s.trim();
275
276 if s.is_empty() {
278 return Ok(Self::empty());
279 }
280
281 if s == "inf" {
283 return Ok(Self::inf());
284 }
285
286 if s == "epsilon" {
288 return Ok(Self::epsilon());
289 }
290
291 let (tokens, separators) = Self::parse_internal_gil_free(s)?;
293
294 Ok(Self {
295 tokens,
296 separators,
297 string_repr: s.to_string(),
298 cached_hash: None,
299 })
300 }
301
302 fn compare_rez(&self, other: &Self) -> Ordering {
304 match (self.is_inf(), other.is_inf()) {
306 (true, true) => return Ordering::Equal,
307 (true, false) => return Ordering::Greater,
308 (false, true) => return Ordering::Less,
309 (false, false) => {} }
311
312 match (self.is_empty(), other.is_empty()) {
314 (true, true) => return Ordering::Equal,
315 (true, false) => return Ordering::Less,
316 (false, true) => return Ordering::Greater,
317 (false, false) => {} }
319
320 Self::compare_token_strings(&self.tokens, &other.tokens)
322 }
323
324 fn compare_single_token(t1: &str, t2: &str) -> Ordering {
334 if let (Ok(n1), Ok(n2)) = (t1.parse::<i64>(), t2.parse::<i64>()) {
336 return n1.cmp(&n2);
337 }
338 if t1 == t2 {
340 return Ordering::Equal;
341 }
342
343 let t1_all_alpha = t1.chars().all(|c| c.is_alphabetic() || c == '_');
345 let t2_all_alpha = t2.chars().all(|c| c.is_alphabetic() || c == '_');
346 let t1_all_num = t1.chars().all(|c| c.is_ascii_digit());
347 let t2_all_num = t2.chars().all(|c| c.is_ascii_digit());
348 if t1_all_alpha && t2_all_num {
349 return Ordering::Less;
350 }
351 if t1_all_num && t2_all_alpha {
352 return Ordering::Greater;
353 }
354 if t1_all_alpha && t2_all_alpha {
356 return t1.cmp(t2);
357 }
358
359 let seg1 = Self::split_token_segments(t1);
362 let seg2 = Self::split_token_segments(t2);
363
364 for (s1, s2) in seg1.iter().zip(seg2.iter()) {
365 let s1_is_num = s1.parse::<u64>().is_ok();
366 let s2_is_num = s2.parse::<u64>().is_ok();
367 let cmp = match (s1_is_num, s2_is_num) {
368 (true, true) => {
369 let n1: u64 = s1.parse().unwrap();
370 let n2: u64 = s2.parse().unwrap();
371 n1.cmp(&n2)
372 }
373 (false, false) => s1.as_str().cmp(s2.as_str()),
374 (false, true) => Ordering::Less, (true, false) => Ordering::Greater, };
377 if cmp != Ordering::Equal {
378 return cmp;
379 }
380 }
381 seg1.len().cmp(&seg2.len())
382 }
383
384 fn split_token_segments(s: &str) -> Vec<String> {
387 let mut segments = Vec::new();
388 let mut current = String::new();
389 let mut in_digits = false;
390
391 for ch in s.chars() {
392 let is_digit = ch.is_ascii_digit();
393 if current.is_empty() {
394 in_digits = is_digit;
395 current.push(ch);
396 } else if is_digit == in_digits {
397 current.push(ch);
398 } else {
399 segments.push(current.clone());
400 current.clear();
401 in_digits = is_digit;
402 current.push(ch);
403 }
404 }
405 if !current.is_empty() {
406 segments.push(current);
407 }
408 segments
409 }
410
411 fn compare_token_strings(tokens1: &[String], tokens2: &[String]) -> Ordering {
418 for (t1, t2) in tokens1.iter().zip(tokens2.iter()) {
419 let cmp = Self::compare_single_token(t1, t2);
420 if cmp != Ordering::Equal {
421 return cmp;
422 }
423 }
424
425 if tokens1.len() < tokens2.len() {
431 let extra_tokens = &tokens2[tokens1.len()..];
433 if extra_tokens.iter().all(|t| t == "0") {
434 Ordering::Equal
436 } else {
437 let first_extra = &tokens2[tokens1.len()];
439 if first_extra.chars().all(|c| c.is_ascii_digit()) {
440 Ordering::Greater
443 } else {
444 Ordering::Greater
447 }
448 }
449 } else if tokens2.len() < tokens1.len() {
450 let extra_tokens = &tokens1[tokens2.len()..];
452 if extra_tokens.iter().all(|t| t == "0") {
453 Ordering::Equal
455 } else {
456 let first_extra = &tokens1[tokens2.len()];
458 if first_extra.chars().all(|c| c.is_ascii_digit()) {
459 Ordering::Less
463 } else {
464 Ordering::Less
468 }
469 }
470 } else {
471 Ordering::Equal
472 }
473 }
474}
475
476impl PartialEq for Version {
477 fn eq(&self, other: &Self) -> bool {
478 self.compare_rez(other) == Ordering::Equal
479 }
480}
481
482impl Eq for Version {}
483
484impl Ord for Version {
485 fn cmp(&self, other: &Self) -> Ordering {
486 self.compare_rez(other)
487 }
488}
489
490impl PartialOrd for Version {
491 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
492 Some(self.cmp(other))
493 }
494}
495
496impl Hash for Version {
497 fn hash<H: Hasher>(&self, state: &mut H) {
498 self.string_repr.hash(state);
499 }
500}
501
502impl Clone for Version {
503 fn clone(&self) -> Self {
504 Self {
505 tokens: self.tokens.clone(),
506 separators: self.separators.clone(),
507 string_repr: self.string_repr.clone(),
508 cached_hash: self.cached_hash,
509 }
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516
517 fn ver(s: &str) -> Version {
518 Version::parse(s).unwrap()
519 }
520
521 #[test]
522 fn test_version_creation() {
523 let version = Version::parse("1.2.3").unwrap();
524 assert_eq!(version.as_str(), "1.2.3");
525 assert_eq!(version.tokens.len(), 3);
526 assert!(!version.is_empty());
527 }
528
529 #[test]
530 fn test_empty_version() {
531 let version = Version::parse("").unwrap();
532 assert_eq!(version.as_str(), "");
533 assert_eq!(version.tokens.len(), 0);
534 assert!(version.is_empty());
535 }
536
537 #[test]
538 fn test_version_inf() {
539 let version = Version::inf();
540 assert_eq!(version.as_str(), "inf");
541 assert!(version.is_inf());
542 }
543
544 #[test]
545 fn test_version_epsilon() {
546 let version = Version::epsilon();
547 assert_eq!(version.as_str(), "");
548 assert!(version.is_epsilon());
549 assert!(version.is_empty());
550 }
551
552 #[test]
553 fn test_version_empty() {
554 let version = Version::empty();
555 assert_eq!(version.as_str(), "");
556 assert!(version.is_empty());
557 assert!(version.is_epsilon());
558 }
559
560 #[test]
561 fn test_version_parsing_special() {
562 let empty = Version::parse("").unwrap();
564 assert!(empty.is_empty());
565
566 let inf = Version::parse("inf").unwrap();
568 assert!(inf.is_inf());
569
570 let epsilon = Version::parse("epsilon").unwrap();
572 assert!(epsilon.is_epsilon());
573 }
574
575 #[test]
576 fn test_version_comparison_boundaries() {
577 let empty = Version::empty();
578 let epsilon = Version::epsilon();
579 let normal = Version::parse("1.0.0").unwrap();
580 let inf = Version::inf();
581
582 assert_eq!(empty.cmp(&epsilon), Ordering::Equal);
584
585 assert_eq!(epsilon.cmp(&normal), Ordering::Less);
587 assert_eq!(normal.cmp(&inf), Ordering::Less);
588 assert_eq!(epsilon.cmp(&inf), Ordering::Less);
589
590 assert_eq!(inf.cmp(&normal), Ordering::Greater);
592 assert_eq!(normal.cmp(&epsilon), Ordering::Greater);
593 assert_eq!(inf.cmp(&epsilon), Ordering::Greater);
594 }
595
596 #[test]
597 fn test_version_prerelease_comparison() {
598 let release = Version::parse("2").unwrap();
600 let prerelease = Version::parse("2.alpha1").unwrap();
601
602 assert_eq!(release.cmp(&prerelease), Ordering::Greater);
604 assert_eq!(prerelease.cmp(&release), Ordering::Less);
605
606 assert!(release >= prerelease); assert!(prerelease < release); }
610
611 #[test]
612 fn test_version_copy() {
613 let version = Version::parse("1.2.3").unwrap();
614 let copied = version.clone();
615 assert_eq!(version.as_str(), copied.as_str());
616 assert_eq!(version.tokens.len(), copied.tokens.len());
617 }
618
619 #[test]
620 fn test_version_ord_basic() {
621 let v1 = ver("1.0.0");
622 let v2 = ver("1.0.0");
623 assert!(v1 >= v2, "1.0.0 >= 1.0.0 should be true");
624 assert!(v1 <= v2, "1.0.0 <= 1.0.0 should be true");
625 }
626
627 #[test]
628 fn test_version_ord_greater() {
629 let v1 = ver("2.0.0");
630 let v2 = ver("1.0.0");
631 assert!(v1 > v2, "2.0.0 > 1.0.0 should be true");
632 assert!(v2 < v1, "1.0.0 < 2.0.0 should be true");
633 }
634
635 #[test]
636 fn test_range_contains_ge() {
637 let range = crate::VersionRange::parse(">=1.0.0").unwrap();
639 assert!(
640 range.contains(&ver("1.0.0")),
641 ">=1.0.0 should contain 1.0.0"
642 );
643 }
644
645 #[test]
646 fn test_version_trim() {
647 let version = Version::parse("1.2.3.4").unwrap();
648 let mut trimmed_tokens = version.tokens.clone();
650 trimmed_tokens.truncate(2);
651 assert_eq!(trimmed_tokens.len(), 2);
652 }
653
654 #[test]
657 fn test_prerelease_alpha_beta_rc_ordering() {
658 let alpha = Version::parse("1.0.alpha").unwrap();
660 let beta = Version::parse("1.0.beta").unwrap();
661 let rc = Version::parse("1.0.rc").unwrap();
662 let release = Version::parse("1.0").unwrap();
663
664 assert!(alpha < beta, "alpha should be less than beta");
665 assert!(beta < rc, "beta should be less than rc");
666 assert!(rc < release, "rc should be less than release");
667 assert!(alpha < release, "alpha should be less than release");
668 }
669
670 #[test]
671 fn test_prerelease_alpha_numbered_variants() {
672 let a1 = Version::parse("1.0.alpha1").unwrap();
674 let a2 = Version::parse("1.0.alpha2").unwrap();
675 let a10 = Version::parse("1.0.alpha10").unwrap();
676
677 assert!(a1 < a2, "alpha1 < alpha2");
678 assert!(a2 < a10, "alpha2 < alpha10 (numeric comparison)");
679 }
680
681 #[test]
682 fn test_prerelease_dev_pre_snapshot_ordering() {
683 let dev = Version::parse("1.0.dev").unwrap();
687 let alpha = Version::parse("1.0.alpha").unwrap();
688 let pre = Version::parse("1.0.pre").unwrap();
689 let snapshot = Version::parse("1.0.snapshot").unwrap();
690 let release = Version::parse("1.0").unwrap();
691
692 assert!(dev < release, "1.0.dev < 1.0");
694 assert!(alpha < release, "1.0.alpha < 1.0");
695 assert!(pre < release, "1.0.pre < 1.0");
696 assert!(snapshot < release, "1.0.snapshot < 1.0");
697
698 assert!(alpha < dev, "alpha < dev (a < d)");
700 assert!(dev < pre, "dev < pre (d < p)");
701 assert!(pre < snapshot, "pre < snapshot (p < s)");
702
703 assert!(dev.is_prerelease(), "dev is detected as prerelease");
705 assert!(pre.is_prerelease(), "pre is detected as prerelease");
706 assert!(
707 snapshot.is_prerelease(),
708 "snapshot is detected as prerelease"
709 );
710 }
711
712 #[test]
713 fn test_prerelease_mixed_with_numeric_tokens() {
714 let v_alpha = Version::parse("2.0.0-alpha").unwrap();
716 let v_beta = Version::parse("2.0.0-beta").unwrap();
717 let v_stable = Version::parse("2.0.0").unwrap();
718
719 assert!(v_alpha < v_beta, "2.0.0-alpha < 2.0.0-beta");
720 assert!(v_beta < v_stable, "2.0.0-beta < 2.0.0");
721 assert!(v_alpha.is_prerelease());
722 assert!(v_beta.is_prerelease());
723 assert!(!v_stable.is_prerelease());
724 }
725
726 #[test]
727 fn test_prerelease_rc_vs_stable_same_prefix() {
728 let rc1 = Version::parse("3.0.rc1").unwrap();
730 let stable = Version::parse("3.0").unwrap();
731 let rc2 = Version::parse("3.0.rc2").unwrap();
732
733 assert!(rc1 < stable, "rc1 < stable 3.0");
734 assert!(rc2 < stable, "rc2 < stable 3.0");
735 assert!(rc1 < rc2, "rc1 < rc2");
736 }
737
738 #[test]
739 fn test_prerelease_is_prerelease_detection() {
740 assert!(Version::parse("1.alpha").unwrap().is_prerelease());
742 assert!(Version::parse("1.beta").unwrap().is_prerelease());
743 assert!(Version::parse("1.rc").unwrap().is_prerelease());
744 assert!(Version::parse("1.dev").unwrap().is_prerelease());
745 assert!(Version::parse("1.pre").unwrap().is_prerelease());
746 assert!(Version::parse("1.snapshot").unwrap().is_prerelease());
747
748 assert!(!Version::parse("1.0").unwrap().is_prerelease());
750 assert!(!Version::parse("1.0.0").unwrap().is_prerelease());
751 assert!(!Version::parse("2024.5").unwrap().is_prerelease());
752
753 assert!(!Version::empty().is_prerelease());
755 assert!(!Version::inf().is_prerelease());
756 }
757
758 #[test]
761 fn test_version_very_large_numbers() {
762 let v1 = Version::parse("999999999.1").unwrap();
764 assert_eq!(v1.as_str(), "999999999.1");
765
766 let v2 = Version::parse("1.999999999").unwrap();
767 assert_eq!(v2.as_str(), "1.999999999");
768
769 let v_small = Version::parse("1.0").unwrap();
771 assert!(v_small < v1);
772 }
773
774 #[test]
775 fn test_version_borderline_token_count() {
776 let v10 = Version::parse("a.b.c.d.e.f.g.h.i.j").unwrap();
778 assert_eq!(v10.tokens.len(), 10);
779
780 let v11_result = Version::parse("a.b.c.d.e.f.g.h.i.j.k");
782 assert!(v11_result.is_err(), "Version with 11 tokens should fail");
783 }
784
785 #[test]
786 fn test_version_borderline_numeric_token_count() {
787 let v5 = Version::parse("1.2.3.4.5").unwrap();
789 assert_eq!(v5.as_str(), "1.2.3.4.5");
790
791 let v6_result = Version::parse("1.2.3.4.5.6");
793 assert!(
794 v6_result.is_err(),
795 "Version with 6 numeric tokens should fail"
796 );
797 }
798
799 #[test]
800 fn test_version_underscore_in_tokens() {
801 let v = Version::parse("1_0.2_0").unwrap();
803 assert_eq!(v.as_str(), "1_0.2_0");
804 assert_eq!(v.tokens.len(), 2);
805 }
806
807 #[test]
808 fn test_version_single_token() {
809 let v = Version::parse("123").unwrap();
811 assert_eq!(v.as_str(), "123");
812 assert_eq!(v.tokens.len(), 1);
813 }
814
815 #[test]
816 fn test_version_hash_consistency() {
817 let v1 = Version::parse("1.2.3").unwrap();
819 let v2 = Version::parse("1.2.3").unwrap();
820
821 let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
822 let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
823 v1.hash(&mut hasher1);
824 v2.hash(&mut hasher2);
825
826 assert_eq!(hasher1.finish(), hasher2.finish());
827 }
828
829 #[test]
830 fn test_version_equality_different_instances() {
831 let v1 = Version::parse("1.0").unwrap();
833 let v2 = Version::parse("1.0").unwrap();
834 assert_eq!(v1, v2);
835 }
836
837 #[test]
838 fn test_version_ordering_transitivity() {
839 let a = Version::parse("1.0").unwrap();
841 let b = Version::parse("2.0").unwrap();
842 let c = Version::parse("3.0").unwrap();
843
844 assert!(a < b);
845 assert!(b < c);
846 assert!(a < c);
847 }
848
849 #[test]
850 fn test_version_invalid_prefix() {
851 assert!(Version::parse("v1.0").is_err());
853 assert!(Version::parse("V1.0").is_err());
854 }
855
856 #[test]
857 fn test_version_invalid_syntax() {
858 assert!(Version::parse("1..2").is_err());
860
861 assert!(Version::parse(".1.2").is_err());
863 assert!(Version::parse("1.2.").is_err());
864 }
865
866 #[test]
867 fn test_version_no_tokens() {
868 assert!(Version::parse("...").is_err());
870 assert!(Version::parse("---").is_err());
871 }
872
873 #[test]
874 fn test_version_alphanumeric_mixed() {
875 let v = Version::parse("1a.2b.3c").unwrap();
877 assert_eq!(v.as_str(), "1a.2b.3c");
878 assert_eq!(v.tokens.len(), 3);
879 }
880}