Skip to main content

rez_next_version/
version.rs

1//! Version implementation
2
3use regex::Regex;
4use rez_next_common::RezCoreError;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::hash::{Hash, Hasher};
8
9/// High-performance version representation compatible with rez
10#[derive(Debug)]
11pub struct Version {
12    /// Version tokens
13    tokens: Vec<String>,
14    /// Separators between tokens
15    separators: Vec<String>,
16    /// Cached string representation
17    pub string_repr: String,
18    /// Cached hash value
19    cached_hash: Option<u64>,
20}
21
22impl Serialize for Version {
23    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
24    where
25        S: serde::Serializer,
26    {
27        // Serialize as string representation for simplicity
28        self.string_repr.serialize(serializer)
29    }
30}
31
32impl<'de> Deserialize<'de> for Version {
33    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34    where
35        D: serde::Deserializer<'de>,
36    {
37        let s = String::deserialize(deserializer)?;
38        Self::parse(&s).map_err(serde::de::Error::custom)
39    }
40}
41
42impl Version {
43    pub fn new(version_str: Option<&str>) -> Result<Self, RezCoreError> {
44        let version_str = version_str.unwrap_or("");
45        Self::parse(version_str)
46    }
47
48    pub fn as_str(&self) -> &str {
49        &self.string_repr
50    }
51}
52
53impl Version {
54    /// Internal parsing function that runs without GIL
55    /// Returns (tokens, separators) as pure Rust data
56    fn parse_internal_gil_free(s: &str) -> Result<(Vec<String>, Vec<String>), RezCoreError> {
57        // Validate version format - reject obvious invalid patterns
58        if s.starts_with('v') || s.starts_with('V') {
59            return Err(RezCoreError::VersionParse(format!(
60                "Version prefixes not supported: '{}'",
61                s
62            )));
63        }
64
65        // Check for invalid characters or patterns
66        if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
67            return Err(RezCoreError::VersionParse(format!(
68                "Invalid version syntax: '{}'",
69                s
70            )));
71        }
72
73        // Use regex to find tokens (alphanumeric + underscore)
74        let token_regex = Regex::new(r"[a-zA-Z0-9_]+").unwrap();
75        let tokens: Vec<&str> = token_regex.find_iter(s).map(|m| m.as_str()).collect();
76
77        if tokens.is_empty() {
78            return Err(RezCoreError::VersionParse(format!(
79                "Invalid version syntax: '{}'",
80                s
81            )));
82        }
83
84        // Check for too many numeric-only tokens (reject versions like 1.2.3.4.5.6)
85        let numeric_tokens: Vec<_> = tokens
86            .iter()
87            .filter(|t| t.chars().all(|c| c.is_ascii_digit()))
88            .collect();
89        if numeric_tokens.len() > 5 {
90            return Err(RezCoreError::VersionParse(format!(
91                "Version too complex: '{}'",
92                s
93            )));
94        }
95
96        // Check for too many tokens overall
97        if tokens.len() > 10 {
98            return Err(RezCoreError::VersionParse(format!(
99                "Version too complex: '{}'",
100                s
101            )));
102        }
103
104        // Extract separators
105        let separators: Vec<&str> = token_regex.split(s).collect();
106
107        // Validate separators (should be empty at start/end, single char in middle)
108        if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
109            return Err(RezCoreError::VersionParse(format!(
110                "Invalid version syntax: '{}'",
111                s
112            )));
113        }
114
115        for sep in &separators[1..separators.len() - 1] {
116            if sep.len() > 1 {
117                return Err(RezCoreError::VersionParse(format!(
118                    "Invalid version syntax: '{}'",
119                    s
120                )));
121            }
122            // Only allow specific separators
123            if !matches!(*sep, "." | "-" | "_" | "+") {
124                return Err(RezCoreError::VersionParse(format!(
125                    "Invalid separator '{}' in version: '{}'",
126                    sep, s
127                )));
128            }
129        }
130
131        // Validate tokens before creating them
132        for token_str in &tokens {
133            // Check if token contains only valid characters
134            if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
135                return Err(RezCoreError::VersionParse(format!(
136                    "Invalid characters in token: '{}'",
137                    token_str
138                )));
139            }
140
141            // Check for invalid patterns
142            if token_str.starts_with('_') || token_str.ends_with('_') {
143                return Err(RezCoreError::VersionParse(format!(
144                    "Invalid token format: '{}'",
145                    token_str
146                )));
147            }
148
149            // Reject tokens that are purely alphabetic and don't look like version components
150            if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
151                return Err(RezCoreError::VersionParse(format!(
152                    "Invalid version token: '{}'",
153                    token_str
154                )));
155            }
156
157            // Reject common invalid patterns
158            if *token_str == "not" || *token_str == "version" {
159                return Err(RezCoreError::VersionParse(format!(
160                    "Invalid version token: '{}'",
161                    token_str
162                )));
163            }
164        }
165
166        // Convert to owned strings
167        let token_strings: Vec<String> = tokens.into_iter().map(|s| s.to_string()).collect();
168        let sep_strings: Vec<String> = separators[1..separators.len() - 1]
169            .iter()
170            .map(|s| s.to_string())
171            .collect();
172
173        Ok((token_strings, sep_strings))
174    }
175
176    /// Create the infinite version (largest possible version)
177    pub fn inf() -> Self {
178        Self {
179            tokens: vec![],
180            separators: vec![],
181            string_repr: "inf".to_string(),
182            cached_hash: None,
183        }
184    }
185
186    /// Check if this is the infinite version
187    pub fn is_inf(&self) -> bool {
188        self.string_repr == "inf"
189    }
190
191    /// Create an empty version (smallest possible version)
192    pub fn empty() -> Self {
193        Self {
194            tokens: vec![],
195            separators: vec![],
196            string_repr: "".to_string(),
197            cached_hash: None,
198        }
199    }
200
201    /// Create the epsilon version (alias for empty, smallest possible version)
202    pub fn epsilon() -> Self {
203        Self::empty()
204    }
205
206    /// Check if this is an empty version
207    pub fn is_empty(&self) -> bool {
208        self.tokens.is_empty() && self.string_repr.is_empty()
209    }
210
211    /// Number of version tokens (components)
212    pub fn len(&self) -> usize {
213        self.tokens.len()
214    }
215
216    /// Get the major version component (first token as u64), if available
217    pub fn major(&self) -> Option<u64> {
218        self.tokens.first().and_then(|t| t.parse::<u64>().ok())
219    }
220
221    /// Get the minor version component (second token as u64), if available
222    pub fn minor(&self) -> Option<u64> {
223        self.tokens.get(1).and_then(|t| t.parse::<u64>().ok())
224    }
225
226    /// Get the patch version component (third token as u64), if available
227    pub fn patch(&self) -> Option<u64> {
228        self.tokens.get(2).and_then(|t| t.parse::<u64>().ok())
229    }
230
231    /// Check if this is the epsilon version (alias for is_empty)
232    pub fn is_epsilon(&self) -> bool {
233        self.is_empty()
234    }
235
236    /// Check if this version is a prerelease version
237    pub fn is_prerelease(&self) -> bool {
238        if self.is_empty() || self.is_inf() {
239            return false;
240        }
241
242        // Check if any token contains alphabetic characters that indicate prerelease
243        for token in &self.tokens {
244            let s_lower = token.to_lowercase();
245            // Common prerelease indicators
246            if s_lower.contains("alpha")
247                || s_lower.contains("beta")
248                || s_lower.contains("rc")
249                || s_lower.contains("dev")
250                || s_lower.contains("pre")
251                || s_lower.contains("snapshot")
252            {
253                return true;
254            }
255        }
256        false
257    }
258
259    /// Parse a version string into a Version object
260    pub fn parse(s: &str) -> Result<Self, RezCoreError> {
261        let s = s.trim();
262
263        // Handle empty version (epsilon version)
264        if s.is_empty() {
265            return Ok(Self::empty());
266        }
267
268        // Handle infinite version
269        if s == "inf" {
270            return Ok(Self::inf());
271        }
272
273        // Handle epsilon version explicitly
274        if s == "epsilon" {
275            return Ok(Self::epsilon());
276        }
277
278        // Parse using the GIL-free method
279        let (tokens, separators) = Self::parse_internal_gil_free(s)?;
280
281        Ok(Self {
282            tokens,
283            separators,
284            string_repr: s.to_string(),
285            cached_hash: None,
286        })
287    }
288
289    /// Compare two versions using rez-compatible rules
290    fn compare_rez(&self, other: &Self) -> Ordering {
291        // Handle infinite versions (inf is largest)
292        match (self.is_inf(), other.is_inf()) {
293            (true, true) => return Ordering::Equal,
294            (true, false) => return Ordering::Greater,
295            (false, true) => return Ordering::Less,
296            (false, false) => {} // Continue with normal comparison
297        }
298
299        // Handle empty/epsilon versions (epsilon version is smallest)
300        match (self.is_empty(), other.is_empty()) {
301            (true, true) => return Ordering::Equal,
302            (true, false) => return Ordering::Less,
303            (false, true) => return Ordering::Greater,
304            (false, false) => {} // Continue with normal comparison
305        }
306
307        // Compare tokens using string comparison for now
308        Self::compare_token_strings(&self.tokens, &other.tokens)
309    }
310
311    /// Compare a single token, handling mixed alphanumeric strings.
312    ///
313    /// Rez token comparison rules:
314    /// 1. Both numeric: numeric integer comparison.
315    /// 2. Both alpha: lexicographic comparison.
316    /// 3. Mixed (alpha vs numeric segment): **alpha < numeric** — alphabetic segments
317    ///    sort *before* numeric ones, matching rez semantics where `1.0.alpha < 1.0.0`.
318    /// 4. Mixed alphanumeric tokens (e.g. "alpha10"): split into alternating segments and
319    ///    apply rules 1–3 per segment pair.
320    fn compare_single_token(t1: &str, t2: &str) -> Ordering {
321        // Fast path: both purely numeric
322        if let (Ok(n1), Ok(n2)) = (t1.parse::<i64>(), t2.parse::<i64>()) {
323            return n1.cmp(&n2);
324        }
325        // Fast path: equal strings
326        if t1 == t2 {
327            return Ordering::Equal;
328        }
329
330        // Fast path: purely alpha vs purely numeric → alpha is Less (rez spec)
331        let t1_all_alpha = t1.chars().all(|c| c.is_alphabetic() || c == '_');
332        let t2_all_alpha = t2.chars().all(|c| c.is_alphabetic() || c == '_');
333        let t1_all_num = t1.chars().all(|c| c.is_ascii_digit());
334        let t2_all_num = t2.chars().all(|c| c.is_ascii_digit());
335        if t1_all_alpha && t2_all_num {
336            return Ordering::Less;
337        }
338        if t1_all_num && t2_all_alpha {
339            return Ordering::Greater;
340        }
341        // Both purely alpha: lexicographic
342        if t1_all_alpha && t2_all_alpha {
343            return t1.cmp(t2);
344        }
345
346        // Mixed alphanumeric tokens: split into segments and compare segment-by-segment.
347        // Within a segment pair: alpha segment < numeric segment (rez spec).
348        let seg1 = Self::split_token_segments(t1);
349        let seg2 = Self::split_token_segments(t2);
350
351        for (s1, s2) in seg1.iter().zip(seg2.iter()) {
352            let s1_is_num = s1.parse::<u64>().is_ok();
353            let s2_is_num = s2.parse::<u64>().is_ok();
354            let cmp = match (s1_is_num, s2_is_num) {
355                (true, true) => {
356                    let n1: u64 = s1.parse().unwrap();
357                    let n2: u64 = s2.parse().unwrap();
358                    n1.cmp(&n2)
359                }
360                (false, false) => s1.as_str().cmp(s2.as_str()),
361                (false, true) => Ordering::Less, // alpha segment < numeric segment
362                (true, false) => Ordering::Greater, // numeric segment > alpha segment
363            };
364            if cmp != Ordering::Equal {
365                return cmp;
366            }
367        }
368        seg1.len().cmp(&seg2.len())
369    }
370
371    /// Split a token into alternating alpha/numeric segments.
372    /// E.g. "alpha10" → ["alpha", "10"], "rc2" → ["rc", "2"]
373    fn split_token_segments(s: &str) -> Vec<String> {
374        let mut segments = Vec::new();
375        let mut current = String::new();
376        let mut in_digits = false;
377
378        for ch in s.chars() {
379            let is_digit = ch.is_ascii_digit();
380            if current.is_empty() {
381                in_digits = is_digit;
382                current.push(ch);
383            } else if is_digit == in_digits {
384                current.push(ch);
385            } else {
386                segments.push(current.clone());
387                current.clear();
388                in_digits = is_digit;
389                current.push(ch);
390            }
391        }
392        if !current.is_empty() {
393            segments.push(current);
394        }
395        segments
396    }
397
398    /// Compare token arrays using rez-compatible rules.
399    fn compare_token_strings(tokens1: &[String], tokens2: &[String]) -> Ordering {
400        for (t1, t2) in tokens1.iter().zip(tokens2.iter()) {
401            let cmp = Self::compare_single_token(t1, t2);
402            if cmp != Ordering::Equal {
403                return cmp;
404            }
405        }
406
407        // If all compared tokens are equal, shorter version is considered greater.
408        // This follows rez semantics where "2" > "2.alpha1".
409        tokens2.len().cmp(&tokens1.len())
410    }
411}
412
413impl PartialEq for Version {
414    fn eq(&self, other: &Self) -> bool {
415        self.compare_rez(other) == Ordering::Equal
416    }
417}
418
419impl Eq for Version {}
420
421impl Ord for Version {
422    fn cmp(&self, other: &Self) -> Ordering {
423        self.compare_rez(other)
424    }
425}
426
427impl PartialOrd for Version {
428    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
429        Some(self.cmp(other))
430    }
431}
432
433impl Hash for Version {
434    fn hash<H: Hasher>(&self, state: &mut H) {
435        self.string_repr.hash(state);
436    }
437}
438
439impl Clone for Version {
440    fn clone(&self) -> Self {
441        Self {
442            tokens: self.tokens.clone(),
443            separators: self.separators.clone(),
444            string_repr: self.string_repr.clone(),
445            cached_hash: self.cached_hash,
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_version_creation() {
456        let version = Version::parse("1.2.3").unwrap();
457        assert_eq!(version.as_str(), "1.2.3");
458        assert_eq!(version.tokens.len(), 3);
459        assert!(!version.is_empty());
460    }
461
462    #[test]
463    fn test_empty_version() {
464        let version = Version::parse("").unwrap();
465        assert_eq!(version.as_str(), "");
466        assert_eq!(version.tokens.len(), 0);
467        assert!(version.is_empty());
468    }
469
470    #[test]
471    fn test_version_inf() {
472        let version = Version::inf();
473        assert_eq!(version.as_str(), "inf");
474        assert!(version.is_inf());
475    }
476
477    #[test]
478    fn test_version_epsilon() {
479        let version = Version::epsilon();
480        assert_eq!(version.as_str(), "");
481        assert!(version.is_epsilon());
482        assert!(version.is_empty());
483    }
484
485    #[test]
486    fn test_version_empty() {
487        let version = Version::empty();
488        assert_eq!(version.as_str(), "");
489        assert!(version.is_empty());
490        assert!(version.is_epsilon());
491    }
492
493    #[test]
494    fn test_version_parsing_special() {
495        // Test parsing empty version
496        let empty = Version::parse("").unwrap();
497        assert!(empty.is_empty());
498
499        // Test parsing inf version
500        let inf = Version::parse("inf").unwrap();
501        assert!(inf.is_inf());
502
503        // Test parsing epsilon version
504        let epsilon = Version::parse("epsilon").unwrap();
505        assert!(epsilon.is_epsilon());
506    }
507
508    #[test]
509    fn test_version_comparison_boundaries() {
510        let empty = Version::empty();
511        let epsilon = Version::epsilon();
512        let normal = Version::parse("1.0.0").unwrap();
513        let inf = Version::inf();
514
515        // Test epsilon/empty equivalence
516        assert_eq!(empty.cmp(&epsilon), Ordering::Equal);
517
518        // Test ordering: epsilon < normal < inf
519        assert_eq!(epsilon.cmp(&normal), Ordering::Less);
520        assert_eq!(normal.cmp(&inf), Ordering::Less);
521        assert_eq!(epsilon.cmp(&inf), Ordering::Less);
522
523        // Test reverse ordering
524        assert_eq!(inf.cmp(&normal), Ordering::Greater);
525        assert_eq!(normal.cmp(&epsilon), Ordering::Greater);
526        assert_eq!(inf.cmp(&epsilon), Ordering::Greater);
527    }
528
529    #[test]
530    fn test_version_prerelease_comparison() {
531        // Test that release versions are greater than pre-release versions
532        let release = Version::parse("2").unwrap();
533        let prerelease = Version::parse("2.alpha1").unwrap();
534
535        // "2" should be greater than "2.alpha1"
536        assert_eq!(release.cmp(&prerelease), Ordering::Greater);
537        assert_eq!(prerelease.cmp(&release), Ordering::Less);
538
539        // Test with comparison operators
540        assert!(release >= prerelease); // "2" < "2.alpha1" should be false
541        assert!(prerelease < release); // "2.alpha1" < "2" should be true
542    }
543
544    #[test]
545    fn test_version_copy() {
546        let version = Version::parse("1.2.3").unwrap();
547        let copied = version.clone();
548        assert_eq!(version.as_str(), copied.as_str());
549        assert_eq!(version.tokens.len(), copied.tokens.len());
550    }
551
552    #[test]
553    fn test_version_trim() {
554        let version = Version::parse("1.2.3.4").unwrap();
555        // Create a trimmed version by taking only first 2 tokens
556        let mut trimmed_tokens = version.tokens.clone();
557        trimmed_tokens.truncate(2);
558        assert_eq!(trimmed_tokens.len(), 2);
559    }
560
561    // ─── Pre-release token ordering chain tests (Cycle 29) ──────────────
562
563    #[test]
564    fn test_prerelease_alpha_beta_rc_ordering() {
565        // Standard prerelease ordering: alpha < beta < rc < release
566        let alpha = Version::parse("1.0.alpha").unwrap();
567        let beta = Version::parse("1.0.beta").unwrap();
568        let rc = Version::parse("1.0.rc").unwrap();
569        let release = Version::parse("1.0").unwrap();
570
571        assert!(alpha < beta, "alpha should be less than beta");
572        assert!(beta < rc, "beta should be less than rc");
573        assert!(rc < release, "rc should be less than release");
574        assert!(alpha < release, "alpha should be less than release");
575    }
576
577    #[test]
578    fn test_prerelease_alpha_numbered_variants() {
579        // Numbered alpha variants: alpha1 < alpha2 < alpha10
580        let a1 = Version::parse("1.0.alpha1").unwrap();
581        let a2 = Version::parse("1.0.alpha2").unwrap();
582        let a10 = Version::parse("1.0.alpha10").unwrap();
583
584        assert!(a1 < a2, "alpha1 < alpha2");
585        assert!(a2 < a10, "alpha2 < alpha10 (numeric comparison)");
586    }
587
588    #[test]
589    fn test_prerelease_dev_pre_snapshot_ordering() {
590        // In rez, token comparison is lexicographic for alphabetic tokens.
591        // "dev" (d...) > "alpha" (a...) by dictionary order.
592        // The key property is that all these sort BELOW the base release (shorter token list).
593        let dev = Version::parse("1.0.dev").unwrap();
594        let alpha = Version::parse("1.0.alpha").unwrap();
595        let pre = Version::parse("1.0.pre").unwrap();
596        let snapshot = Version::parse("1.0.snapshot").unwrap();
597        let release = Version::parse("1.0").unwrap();
598
599        // All prerelease variants are less than the base release (shorter = greater in rez)
600        assert!(dev < release, "1.0.dev < 1.0");
601        assert!(alpha < release, "1.0.alpha < 1.0");
602        assert!(pre < release, "1.0.pre < 1.0");
603        assert!(snapshot < release, "1.0.snapshot < 1.0");
604
605        // Lexicographic order among prerelease labels
606        assert!(alpha < dev, "alpha < dev (a < d)");
607        assert!(dev < pre, "dev < pre (d < p)");
608        assert!(pre < snapshot, "pre < snapshot (p < s)");
609
610        // is_prerelease detection
611        assert!(dev.is_prerelease(), "dev is detected as prerelease");
612        assert!(pre.is_prerelease(), "pre is detected as prerelease");
613        assert!(
614            snapshot.is_prerelease(),
615            "snapshot is detected as prerelease"
616        );
617    }
618
619    #[test]
620    fn test_prerelease_mixed_with_numeric_tokens() {
621        // Versions like 2.0.0-alpha vs 2.0.0-beta
622        let v_alpha = Version::parse("2.0.0-alpha").unwrap();
623        let v_beta = Version::parse("2.0.0-beta").unwrap();
624        let v_stable = Version::parse("2.0.0").unwrap();
625
626        assert!(v_alpha < v_beta, "2.0.0-alpha < 2.0.0-beta");
627        assert!(v_beta < v_stable, "2.0.0-beta < 2.0.0");
628        assert!(v_alpha.is_prerelease());
629        assert!(v_beta.is_prerelease());
630        assert!(!v_stable.is_prerelease());
631    }
632
633    #[test]
634    fn test_prerelease_rc_vs_stable_same_prefix() {
635        // RC versions sort below their corresponding release
636        let rc1 = Version::parse("3.0.rc1").unwrap();
637        let stable = Version::parse("3.0").unwrap();
638        let rc2 = Version::parse("3.0.rc2").unwrap();
639
640        assert!(rc1 < stable, "rc1 < stable 3.0");
641        assert!(rc2 < stable, "rc2 < stable 3.0");
642        assert!(rc1 < rc2, "rc1 < rc2");
643    }
644
645    #[test]
646    fn test_prerelease_is_prerelease_detection() {
647        // Verify all known prerelease markers are detected
648        assert!(Version::parse("1.alpha").unwrap().is_prerelease());
649        assert!(Version::parse("1.beta").unwrap().is_prerelease());
650        assert!(Version::parse("1.rc").unwrap().is_prerelease());
651        assert!(Version::parse("1.dev").unwrap().is_prerelease());
652        assert!(Version::parse("1.pre").unwrap().is_prerelease());
653        assert!(Version::parse("1.snapshot").unwrap().is_prerelease());
654
655        // Non-prerelease versions
656        assert!(!Version::parse("1.0").unwrap().is_prerelease());
657        assert!(!Version::parse("1.0.0").unwrap().is_prerelease());
658        assert!(!Version::parse("2024.5").unwrap().is_prerelease());
659
660        // Edge cases: empty/inf are not prerelease
661        assert!(!Version::empty().is_prerelease());
662        assert!(!Version::inf().is_prerelease());
663    }
664}