rez_next_version/
version.rs

1//! Version implementation
2
3use super::parser::{StateMachineParser, TokenType};
4#[cfg(feature = "python-bindings")]
5use super::version_token::AlphanumericVersionToken;
6use once_cell::sync::Lazy;
7#[cfg(feature = "python-bindings")]
8use pyo3::prelude::*;
9#[cfg(feature = "python-bindings")]
10use pyo3::types::PyTuple;
11use regex::Regex;
12use rez_next_common::RezCoreError;
13use serde::{Deserialize, Serialize};
14use std::cmp::Ordering;
15use std::hash::{Hash, Hasher};
16
17/// Global state machine parser instance for optimal performance
18static OPTIMIZED_PARSER: Lazy<StateMachineParser> = Lazy::new(|| StateMachineParser::new());
19
20/// High-performance version representation compatible with rez
21#[cfg_attr(feature = "python-bindings", pyclass)]
22#[derive(Debug)]
23pub struct Version {
24    /// Version tokens (rez-compatible)
25    #[cfg(feature = "python-bindings")]
26    tokens: Vec<PyObject>,
27    /// Version tokens (non-Python version)
28    #[cfg(not(feature = "python-bindings"))]
29    tokens: Vec<String>,
30    /// Separators between tokens
31    separators: Vec<String>,
32    /// Cached string representation
33    #[cfg(feature = "python-bindings")]
34    #[pyo3(get)]
35    pub string_repr: String,
36    /// Cached string representation (non-Python version)
37    #[cfg(not(feature = "python-bindings"))]
38    pub string_repr: String,
39    /// Cached hash value
40    cached_hash: Option<u64>,
41}
42
43impl Serialize for Version {
44    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
45    where
46        S: serde::Serializer,
47    {
48        // Serialize as string representation for simplicity
49        self.string_repr.serialize(serializer)
50    }
51}
52
53impl<'de> Deserialize<'de> for Version {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: serde::Deserializer<'de>,
57    {
58        let s = String::deserialize(deserializer)?;
59        Self::parse(&s).map_err(serde::de::Error::custom)
60    }
61}
62
63#[cfg(feature = "python-bindings")]
64#[pymethods]
65impl Version {
66    #[new]
67    pub fn new(version_str: Option<&str>) -> PyResult<Self> {
68        let version_str = version_str.unwrap_or("");
69        Self::parse(version_str)
70            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
71    }
72
73    /// Create a copy of the version
74    pub fn copy(&self) -> Self {
75        Python::with_gil(|py| {
76            let cloned_tokens: Vec<PyObject> = self
77                .tokens
78                .iter()
79                .map(|token| token.clone_ref(py))
80                .collect();
81
82            Self {
83                tokens: cloned_tokens,
84                separators: self.separators.clone(),
85                string_repr: self.string_repr.clone(),
86                cached_hash: self.cached_hash,
87            }
88        })
89    }
90
91    /// Return a copy of the version, possibly with fewer tokens
92    pub fn trim(&self, len_: usize) -> Self {
93        Python::with_gil(|py| {
94            let new_tokens: Vec<PyObject> = if len_ >= self.tokens.len() {
95                self.tokens
96                    .iter()
97                    .map(|token| token.clone_ref(py))
98                    .collect()
99            } else {
100                self.tokens[..len_]
101                    .iter()
102                    .map(|token| token.clone_ref(py))
103                    .collect()
104            };
105
106            let new_separators = if len_ <= 1 {
107                vec![]
108            } else {
109                let sep_len = (len_ - 1).min(self.separators.len());
110                self.separators[..sep_len].to_vec()
111            };
112
113            // Reconstruct string representation
114            let string_repr = Self::reconstruct_string(&new_tokens, &new_separators);
115
116            Self {
117                tokens: new_tokens,
118                separators: new_separators,
119                string_repr,
120                cached_hash: None,
121            }
122        })
123    }
124
125    /// Return the next version (increment last token)
126    pub fn next(&self) -> PyResult<Self> {
127        if self.tokens.is_empty() {
128            // Return Version.inf for empty version
129            return Ok(Self::inf());
130        }
131
132        Python::with_gil(|py| {
133            let mut new_tokens: Vec<PyObject> = self
134                .tokens
135                .iter()
136                .map(|token| token.clone_ref(py))
137                .collect();
138            let last_token = new_tokens.pop().unwrap();
139
140            // Call next() method on the last token
141            let next_token = last_token.call_method0(py, "next")?;
142            new_tokens.push(next_token);
143
144            let string_repr = Self::reconstruct_string(&new_tokens, &self.separators);
145
146            Ok(Self {
147                tokens: new_tokens,
148                separators: self.separators.clone(),
149                string_repr,
150                cached_hash: None,
151            })
152        })
153    }
154
155    pub fn as_str(&self) -> &str {
156        &self.string_repr
157    }
158
159    /// Convert to a tuple of strings
160    pub fn as_tuple(&self) -> PyResult<PyObject> {
161        Python::with_gil(|py| {
162            let string_tokens: Result<Vec<String>, PyErr> = self
163                .tokens
164                .iter()
165                .map(|token| {
166                    let token_str = token.call_method0(py, "__str__")?;
167                    token_str.extract::<String>(py)
168                })
169                .collect();
170
171            let tuple = PyTuple::new(py, string_tokens?)?;
172            Ok(tuple.into())
173        })
174    }
175
176    /// Semantic versioning major version
177    #[getter]
178    pub fn major(&self) -> PyResult<PyObject> {
179        self.get_token(0)
180    }
181
182    /// Semantic versioning minor version
183    #[getter]
184    pub fn minor(&self) -> PyResult<PyObject> {
185        self.get_token(1)
186    }
187
188    /// Semantic versioning patch version
189    #[getter]
190    pub fn patch(&self) -> PyResult<PyObject> {
191        self.get_token(2)
192    }
193
194    /// Get token at index (like __getitem__)
195    pub fn get_token(&self, index: usize) -> PyResult<PyObject> {
196        if index < self.tokens.len() {
197            Python::with_gil(|py| Ok(self.tokens[index].clone_ref(py)))
198        } else {
199            Python::with_gil(|py| Ok(py.None()))
200        }
201    }
202
203    /// Length of version (number of tokens)
204    fn __len__(&self) -> usize {
205        self.tokens.len()
206    }
207
208    /// Get item by index
209    fn __getitem__(&self, index: isize) -> PyResult<PyObject> {
210        if index < 0 {
211            return Python::with_gil(|py| Ok(py.None()));
212        }
213        self.get_token(index as usize)
214    }
215
216    /// Boolean conversion (true if version has tokens)
217    fn __bool__(&self) -> bool {
218        !self.tokens.is_empty()
219    }
220
221    fn __str__(&self) -> String {
222        self.string_repr.clone()
223    }
224
225    fn __repr__(&self) -> String {
226        format!("Version('{}')", self.string_repr)
227    }
228
229    /// Create the infinite version (class method)
230    #[classmethod]
231    pub fn inf_class(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
232        Self::inf()
233    }
234
235    /// Create the epsilon version (smallest possible version, class method)
236    #[classmethod]
237    pub fn epsilon_class(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
238        Self::epsilon()
239    }
240
241    /// Create an empty version (smallest possible version, class method)
242    #[classmethod]
243    pub fn empty_class(_cls: &Bound<'_, pyo3::types::PyType>) -> Self {
244        Self::empty()
245    }
246
247    /// Parse a version string (static method)
248    #[staticmethod]
249    pub fn parse_static(s: &str) -> PyResult<Self> {
250        Self::parse(s).map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
251    }
252
253    fn __lt__(&self, other: &Self) -> bool {
254        self.cmp(other) == Ordering::Less
255    }
256
257    fn __le__(&self, other: &Self) -> bool {
258        matches!(self.cmp(other), Ordering::Less | Ordering::Equal)
259    }
260
261    fn __eq__(&self, other: &Self) -> bool {
262        self.cmp(other) == Ordering::Equal
263    }
264
265    fn __ne__(&self, other: &Self) -> bool {
266        self.cmp(other) != Ordering::Equal
267    }
268
269    fn __gt__(&self, other: &Self) -> bool {
270        self.cmp(other) == Ordering::Greater
271    }
272
273    fn __ge__(&self, other: &Self) -> bool {
274        matches!(self.cmp(other), Ordering::Greater | Ordering::Equal)
275    }
276
277    fn __hash__(&self) -> u64 {
278        if let Some(cached) = self.cached_hash {
279            return cached;
280        }
281
282        use std::collections::hash_map::DefaultHasher;
283        let mut hasher = DefaultHasher::new();
284        self.string_repr.hash(&mut hasher);
285        hasher.finish()
286    }
287}
288
289#[cfg(not(feature = "python-bindings"))]
290impl Version {
291    pub fn new(version_str: Option<&str>) -> Result<Self, RezCoreError> {
292        let version_str = version_str.unwrap_or("");
293        Self::parse(version_str)
294    }
295
296    pub fn as_str(&self) -> &str {
297        &self.string_repr
298    }
299}
300
301impl Version {
302    /// Internal parsing function that runs without GIL
303    /// Returns (tokens, separators) as pure Rust data
304    fn parse_internal_gil_free(s: &str) -> Result<(Vec<String>, Vec<String>), RezCoreError> {
305        // Validate version format - reject obvious invalid patterns
306        if s.starts_with('v') || s.starts_with('V') {
307            return Err(RezCoreError::VersionParse(format!(
308                "Version prefixes not supported: '{}'",
309                s
310            )));
311        }
312
313        // Check for invalid characters or patterns
314        if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
315            return Err(RezCoreError::VersionParse(format!(
316                "Invalid version syntax: '{}'",
317                s
318            )));
319        }
320
321        // Use regex to find tokens (alphanumeric + underscore)
322        let token_regex = Regex::new(r"[a-zA-Z0-9_]+").unwrap();
323        let tokens: Vec<&str> = token_regex.find_iter(s).map(|m| m.as_str()).collect();
324
325        if tokens.is_empty() {
326            return Err(RezCoreError::VersionParse(format!(
327                "Invalid version syntax: '{}'",
328                s
329            )));
330        }
331
332        // Check for too many numeric-only tokens (reject versions like 1.2.3.4.5.6)
333        let numeric_tokens: Vec<_> = tokens
334            .iter()
335            .filter(|t| t.chars().all(|c| c.is_ascii_digit()))
336            .collect();
337        if numeric_tokens.len() > 5 {
338            return Err(RezCoreError::VersionParse(format!(
339                "Version too complex: '{}'",
340                s
341            )));
342        }
343
344        // Check for too many tokens overall
345        if tokens.len() > 10 {
346            return Err(RezCoreError::VersionParse(format!(
347                "Version too complex: '{}'",
348                s
349            )));
350        }
351
352        // Extract separators
353        let separators: Vec<&str> = token_regex.split(s).collect();
354
355        // Validate separators (should be empty at start/end, single char in middle)
356        if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
357            return Err(RezCoreError::VersionParse(format!(
358                "Invalid version syntax: '{}'",
359                s
360            )));
361        }
362
363        for sep in &separators[1..separators.len() - 1] {
364            if sep.len() > 1 {
365                return Err(RezCoreError::VersionParse(format!(
366                    "Invalid version syntax: '{}'",
367                    s
368                )));
369            }
370            // Only allow specific separators
371            if !matches!(*sep, "." | "-" | "_" | "+") {
372                return Err(RezCoreError::VersionParse(format!(
373                    "Invalid separator '{}' in version: '{}'",
374                    sep, s
375                )));
376            }
377        }
378
379        // Validate tokens before creating them
380        for token_str in &tokens {
381            // Check if token contains only valid characters
382            if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
383                return Err(RezCoreError::VersionParse(format!(
384                    "Invalid characters in token: '{}'",
385                    token_str
386                )));
387            }
388
389            // Check for invalid patterns
390            if token_str.starts_with('_') || token_str.ends_with('_') {
391                return Err(RezCoreError::VersionParse(format!(
392                    "Invalid token format: '{}'",
393                    token_str
394                )));
395            }
396
397            // Reject tokens that are purely alphabetic and don't look like version components
398            if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
399                return Err(RezCoreError::VersionParse(format!(
400                    "Invalid version token: '{}'",
401                    token_str
402                )));
403            }
404
405            // Reject common invalid patterns
406            if *token_str == "not" || *token_str == "version" {
407                return Err(RezCoreError::VersionParse(format!(
408                    "Invalid version token: '{}'",
409                    token_str
410                )));
411            }
412        }
413
414        // Convert to owned strings
415        let token_strings: Vec<String> = tokens.into_iter().map(|s| s.to_string()).collect();
416        let sep_strings: Vec<String> = separators[1..separators.len() - 1]
417            .iter()
418            .map(|s| s.to_string())
419            .collect();
420
421        Ok((token_strings, sep_strings))
422    }
423
424    /// Create Version with Python tokens (requires GIL)
425    #[cfg(feature = "python-bindings")]
426    fn create_version_with_python_tokens(
427        py: Python<'_>,
428        tokens: Vec<String>,
429        separators: Vec<String>,
430        original_str: &str,
431    ) -> Result<Self, RezCoreError> {
432        // Create rez-compatible tokens
433        let mut py_tokens = Vec::new();
434        for token_str in tokens {
435            // For now, create all tokens as AlphanumericVersionToken
436            // TODO: Implement proper NumericToken vs AlphanumericVersionToken distinction
437            let alpha_class = py.get_type::<AlphanumericVersionToken>();
438            let py_token = alpha_class
439                .call1((token_str,))
440                .map_err(|e| RezCoreError::PyO3(e))?
441                .into();
442            py_tokens.push(py_token);
443        }
444
445        Ok(Self {
446            tokens: py_tokens,
447            separators,
448            string_repr: original_str.to_string(),
449            cached_hash: None,
450        })
451    }
452
453    /// Extract token strings without GIL (cached from string representation)
454    #[cfg(feature = "python-bindings")]
455    fn extract_token_strings_gil_free(&self) -> Vec<String> {
456        // For now, parse from string representation
457        // TODO: Cache token strings to avoid re-parsing
458        if self.is_inf() || self.is_empty() {
459            return vec![];
460        }
461
462        let token_regex = Regex::new(r"[a-zA-Z0-9_]+").unwrap();
463        token_regex
464            .find_iter(&self.string_repr)
465            .map(|m| m.as_str().to_string())
466            .collect()
467    }
468
469    /// Compare token strings without GIL
470    #[cfg(feature = "python-bindings")]
471    fn compare_token_strings(self_tokens: &[String], other_tokens: &[String]) -> Ordering {
472        let max_len = self_tokens.len().max(other_tokens.len());
473
474        for i in 0..max_len {
475            let self_token = self_tokens.get(i);
476            let other_token = other_tokens.get(i);
477
478            match (self_token, other_token) {
479                (Some(self_tok), Some(other_tok)) => {
480                    // Compare tokens using string comparison for now
481                    // TODO: Implement proper rez token comparison logic
482                    match self_tok.cmp(other_tok) {
483                        Ordering::Equal => continue,
484                        other => return other,
485                    }
486                }
487                (Some(_), None) => {
488                    // self has more tokens than other
489                    // Check if the extra token indicates a pre-release
490                    if let Some(extra_token) = self_tokens.get(i) {
491                        // Check if it's a pre-release indicator (starts with alpha)
492                        if extra_token
493                            .chars()
494                            .next()
495                            .map_or(false, |c| c.is_alphabetic())
496                        {
497                            return Ordering::Less; // Pre-release is less than release
498                        }
499                    }
500                    return Ordering::Greater; // More tokens = greater (default)
501                }
502                (None, Some(_)) => {
503                    // other has more tokens than self
504                    // Check if the extra token indicates a pre-release
505                    if let Some(extra_token) = other_tokens.get(i) {
506                        // Check if it's a pre-release indicator (starts with alpha)
507                        if extra_token
508                            .chars()
509                            .next()
510                            .map_or(false, |c| c.is_alphabetic())
511                        {
512                            return Ordering::Greater; // Release is greater than pre-release
513                        }
514                    }
515                    return Ordering::Less; // Fewer tokens = less (default)
516                }
517                (None, None) => break, // Both exhausted
518            }
519        }
520
521        Ordering::Equal
522    }
523
524    /// Create the infinite version (largest possible version)
525    pub fn inf() -> Self {
526        Self {
527            tokens: vec![],
528            separators: vec![],
529            string_repr: "inf".to_string(),
530            cached_hash: None,
531        }
532    }
533
534    /// Check if this is the infinite version
535    pub fn is_inf(&self) -> bool {
536        self.string_repr == "inf"
537    }
538
539    /// Create an empty version (smallest possible version)
540    pub fn empty() -> Self {
541        Self {
542            tokens: vec![],
543            separators: vec![],
544            string_repr: "".to_string(),
545            cached_hash: None,
546        }
547    }
548
549    /// Create the epsilon version (alias for empty, smallest possible version)
550    pub fn epsilon() -> Self {
551        Self::empty()
552    }
553
554    /// Check if this is an empty version
555    pub fn is_empty(&self) -> bool {
556        self.tokens.is_empty() && self.string_repr.is_empty()
557    }
558
559    /// Check if this is the epsilon version (alias for is_empty)
560    pub fn is_epsilon(&self) -> bool {
561        self.is_empty()
562    }
563
564    /// Check if this version is a prerelease version
565    #[cfg(feature = "python-bindings")]
566    pub fn is_prerelease(&self) -> bool {
567        if self.is_empty() || self.is_inf() {
568            return false;
569        }
570
571        Python::with_gil(|py| {
572            // Check if any token contains alphabetic characters that indicate prerelease
573            for token in &self.tokens {
574                if let Ok(token_str) = token.call_method0(py, "__str__") {
575                    if let Ok(s) = token_str.extract::<String>(py) {
576                        let s_lower = s.to_lowercase();
577                        // Common prerelease indicators
578                        if s_lower.contains("alpha")
579                            || s_lower.contains("beta")
580                            || s_lower.contains("rc")
581                            || s_lower.contains("dev")
582                            || s_lower.contains("pre")
583                            || s_lower.contains("snapshot")
584                        {
585                            return true;
586                        }
587                    }
588                }
589            }
590            false
591        })
592    }
593
594    /// Check if this version is a prerelease version (non-Python version)
595    #[cfg(not(feature = "python-bindings"))]
596    pub fn is_prerelease(&self) -> bool {
597        if self.is_empty() || self.is_inf() {
598            return false;
599        }
600
601        // Check if any token contains alphabetic characters that indicate prerelease
602        for token in &self.tokens {
603            let s_lower = token.to_lowercase();
604            // Common prerelease indicators
605            if s_lower.contains("alpha")
606                || s_lower.contains("beta")
607                || s_lower.contains("rc")
608                || s_lower.contains("dev")
609                || s_lower.contains("pre")
610                || s_lower.contains("snapshot")
611            {
612                return true;
613            }
614        }
615        false
616    }
617
618    /// Parse a version string using optimized state machine parser (experimental)
619    #[cfg(feature = "python-bindings")]
620    pub fn parse_optimized(s: &str) -> Result<Self, RezCoreError> {
621        let s = s.trim();
622
623        // Handle special cases first
624        if s.is_empty() {
625            return Ok(Self::empty());
626        }
627        if s == "inf" {
628            return Ok(Self::inf());
629        }
630        if s == "epsilon" {
631            return Ok(Self::epsilon());
632        }
633
634        // Validate version format - reject obvious invalid patterns
635        if s.starts_with('v') || s.starts_with('V') {
636            return Err(RezCoreError::VersionParse(format!(
637                "Version prefixes not supported: '{}'",
638                s
639            )));
640        }
641
642        // Check for invalid characters or patterns
643        if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
644            return Err(RezCoreError::VersionParse(format!(
645                "Invalid version syntax: '{}'",
646                s
647            )));
648        }
649
650        // Use the optimized state machine parser
651        let (tokens, separators) = OPTIMIZED_PARSER.parse_tokens(s)?;
652
653        // Convert to Python tokens for compatibility
654        Python::with_gil(|py| {
655            let mut py_tokens = Vec::new();
656
657            for token in tokens {
658                match token {
659                    TokenType::Numeric(n) => {
660                        // Create numeric token as string for now (rez compatibility)
661                        let alpha_class = py.get_type::<AlphanumericVersionToken>();
662                        let py_token = alpha_class
663                            .call1((n.to_string(),))
664                            .map_err(|e| RezCoreError::PyO3(e))?
665                            .into();
666                        py_tokens.push(py_token);
667                    }
668                    TokenType::Alphanumeric(s) => {
669                        let alpha_class = py.get_type::<AlphanumericVersionToken>();
670                        let py_token = alpha_class
671                            .call1((s,))
672                            .map_err(|e| RezCoreError::PyO3(e))?
673                            .into();
674                        py_tokens.push(py_token);
675                    }
676                    TokenType::Separator(_) => {
677                        // Separators are handled separately
678                    }
679                }
680            }
681
682            let sep_strings: Vec<String> = separators.into_iter().map(|c| c.to_string()).collect();
683
684            Ok(Self {
685                tokens: py_tokens,
686                separators: sep_strings,
687                string_repr: s.to_string(),
688                cached_hash: None,
689            })
690        })
691    }
692
693    /// Parse a version string using legacy simulation (for benchmarking)
694    /// This method intentionally includes overhead to simulate legacy parsing performance
695    #[cfg(feature = "python-bindings")]
696    pub fn parse_legacy_simulation(s: &str) -> Result<Self, RezCoreError> {
697        let s = s.trim();
698
699        // Simulate legacy parsing overhead with intentional computational overhead
700        // This represents the performance characteristics of older parsing methods
701
702        // Simulate regex compilation overhead (legacy parsers often recompile regexes)
703        let _regex_overhead = regex::Regex::new(r"[0-9]+").unwrap();
704
705        // Simulate multiple string allocations (legacy parsers often create many temporary strings)
706        let _temp_strings: Vec<String> = s.chars().map(|c| c.to_string()).collect();
707
708        // Simulate inefficient character-by-character processing with computational overhead
709        let mut _dummy_work = 0u64;
710        for c in s.chars() {
711            // Simulate inefficient processing with computational work instead of sleep
712            for i in 0..100 {
713                _dummy_work = _dummy_work.wrapping_add(c as u64 * i);
714            }
715        }
716
717        // Simulate multiple validation passes (legacy parsers often validate multiple times)
718        for _pass in 0..10 {
719            let _validation = s.contains('.') || s.contains('-') || s.contains('+');
720            // Additional computational overhead
721            _dummy_work = _dummy_work.wrapping_add(_pass);
722        }
723
724        // Finally, use the optimized parser but with the overhead above
725        Self::parse(s)
726    }
727
728    /// Parse a version string with GIL release optimization
729    #[cfg(feature = "python-bindings")]
730    pub fn parse_with_gil_release(s: &str) -> Result<Self, RezCoreError> {
731        let s = s.trim();
732
733        // Handle special cases first (no GIL needed)
734        if s.is_empty() {
735            return Ok(Self::empty());
736        }
737        if s == "inf" {
738            return Ok(Self::inf());
739        }
740        if s == "epsilon" {
741            return Ok(Self::epsilon());
742        }
743
744        // Perform validation and parsing with GIL release
745        Python::with_gil(|py| {
746            py.allow_threads(|| {
747                // All validation and token extraction in GIL-free zone
748                Self::parse_internal_gil_free(s)
749            })
750            .and_then(|(tokens, separators)| {
751                // Convert to Python objects with GIL
752                Self::create_version_with_python_tokens(py, tokens, separators, s)
753            })
754        })
755    }
756
757    /// Parse a version string into a Version object using rez-compatible logic
758    #[cfg(feature = "python-bindings")]
759    pub fn parse(s: &str) -> Result<Self, RezCoreError> {
760        let s = s.trim();
761
762        // Handle empty version (epsilon version)
763        if s.is_empty() {
764            return Ok(Self::empty());
765        }
766
767        // Handle infinite version
768        if s == "inf" {
769            return Ok(Self::inf());
770        }
771
772        // Handle epsilon version explicitly
773        if s == "epsilon" {
774            return Ok(Self::epsilon());
775        }
776
777        // Validate version format - reject obvious invalid patterns
778        if s.starts_with('v') || s.starts_with('V') {
779            return Err(RezCoreError::VersionParse(format!(
780                "Version prefixes not supported: '{}'",
781                s
782            )));
783        }
784
785        // Check for invalid characters or patterns
786        if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
787            return Err(RezCoreError::VersionParse(format!(
788                "Invalid version syntax: '{}'",
789                s
790            )));
791        }
792
793        Python::with_gil(|py| {
794            // Use regex to find tokens (alphanumeric + underscore)
795            let token_regex = Regex::new(r"[a-zA-Z0-9_]+").unwrap();
796            let tokens: Vec<&str> = token_regex.find_iter(s).map(|m| m.as_str()).collect();
797
798            if tokens.is_empty() {
799                return Err(RezCoreError::VersionParse(format!(
800                    "Invalid version syntax: '{}'",
801                    s
802                )));
803            }
804
805            // Check for too many numeric-only tokens (reject versions like 1.2.3.4.5.6)
806            let numeric_tokens: Vec<_> = tokens
807                .iter()
808                .filter(|t| t.chars().all(|c| c.is_ascii_digit()))
809                .collect();
810            if numeric_tokens.len() > 5 {
811                return Err(RezCoreError::VersionParse(format!(
812                    "Version too complex: '{}'",
813                    s
814                )));
815            }
816
817            // Check for too many tokens overall
818            if tokens.len() > 10 {
819                return Err(RezCoreError::VersionParse(format!(
820                    "Version too complex: '{}'",
821                    s
822                )));
823            }
824
825            // Extract separators
826            let separators: Vec<&str> = token_regex.split(s).collect();
827
828            // Validate separators (should be empty at start/end, single char in middle)
829            if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
830                return Err(RezCoreError::VersionParse(format!(
831                    "Invalid version syntax: '{}'",
832                    s
833                )));
834            }
835
836            for sep in &separators[1..separators.len() - 1] {
837                if sep.len() > 1 {
838                    return Err(RezCoreError::VersionParse(format!(
839                        "Invalid version syntax: '{}'",
840                        s
841                    )));
842                }
843                // Only allow specific separators
844                if !matches!(*sep, "." | "-" | "_" | "+") {
845                    return Err(RezCoreError::VersionParse(format!(
846                        "Invalid separator '{}' in version: '{}'",
847                        sep, s
848                    )));
849                }
850            }
851
852            // Validate tokens before creating them
853            for token_str in &tokens {
854                // Check if token contains only valid characters
855                if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
856                    return Err(RezCoreError::VersionParse(format!(
857                        "Invalid characters in token: '{}'",
858                        token_str
859                    )));
860                }
861
862                // Check for invalid patterns
863                if token_str.starts_with('_') || token_str.ends_with('_') {
864                    return Err(RezCoreError::VersionParse(format!(
865                        "Invalid token format: '{}'",
866                        token_str
867                    )));
868                }
869
870                // Reject tokens that are purely alphabetic and don't look like version components
871                if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
872                    return Err(RezCoreError::VersionParse(format!(
873                        "Invalid version token: '{}'",
874                        token_str
875                    )));
876                }
877
878                // Reject common invalid patterns
879                if *token_str == "not" || *token_str == "version" {
880                    return Err(RezCoreError::VersionParse(format!(
881                        "Invalid version token: '{}'",
882                        token_str
883                    )));
884                }
885            }
886
887            // Create rez-compatible tokens
888            let mut py_tokens = Vec::new();
889            for token_str in tokens {
890                // For now, create all tokens as AlphanumericVersionToken
891                // TODO: Implement proper NumericToken vs AlphanumericVersionToken distinction
892                let alpha_class = py.get_type::<AlphanumericVersionToken>();
893                let py_token = alpha_class
894                    .call1((token_str,))
895                    .map_err(|e| RezCoreError::PyO3(e))?
896                    .into();
897                py_tokens.push(py_token);
898            }
899
900            let sep_strings: Vec<String> = separators[1..separators.len() - 1]
901                .iter()
902                .map(|s| s.to_string())
903                .collect();
904
905            Ok(Self {
906                tokens: py_tokens,
907                separators: sep_strings,
908                string_repr: s.to_string(),
909                cached_hash: None,
910            })
911        })
912    }
913
914    /// Parse a version string into a Version object (non-Python version)
915    #[cfg(not(feature = "python-bindings"))]
916    pub fn parse(s: &str) -> Result<Self, RezCoreError> {
917        let s = s.trim();
918
919        // Handle empty version (epsilon version)
920        if s.is_empty() {
921            return Ok(Self::empty());
922        }
923
924        // Handle infinite version
925        if s == "inf" {
926            return Ok(Self::inf());
927        }
928
929        // Handle epsilon version explicitly
930        if s == "epsilon" {
931            return Ok(Self::epsilon());
932        }
933
934        // Parse using the GIL-free method
935        let (tokens, separators) = Self::parse_internal_gil_free(s)?;
936
937        Ok(Self {
938            tokens,
939            separators,
940            string_repr: s.to_string(),
941            cached_hash: None,
942        })
943    }
944
945    /// Reconstruct string representation from tokens and separators
946    #[cfg(feature = "python-bindings")]
947    fn reconstruct_string(tokens: &[PyObject], separators: &[String]) -> String {
948        if tokens.is_empty() {
949            return "".to_string();
950        }
951
952        Python::with_gil(|py| {
953            let mut result = String::new();
954
955            for (i, token) in tokens.iter().enumerate() {
956                if i > 0 && i - 1 < separators.len() {
957                    result.push_str(&separators[i - 1]);
958                } else if i > 0 {
959                    result.push('.'); // Default separator
960                }
961
962                if let Ok(token_str) = token.call_method0(py, "__str__") {
963                    if let Ok(s) = token_str.extract::<String>(py) {
964                        result.push_str(&s);
965                    }
966                }
967            }
968
969            result
970        })
971    }
972
973    /// Reconstruct string representation from tokens and separators (non-Python version)
974    #[cfg(not(feature = "python-bindings"))]
975    fn reconstruct_string(tokens: &[String], separators: &[String]) -> String {
976        if tokens.is_empty() {
977            return "".to_string();
978        }
979
980        let mut result = String::new();
981        for (i, token) in tokens.iter().enumerate() {
982            if i > 0 && i - 1 < separators.len() {
983                result.push_str(&separators[i - 1]);
984            } else if i > 0 {
985                result.push('.'); // Default separator
986            }
987            result.push_str(token);
988        }
989        result
990    }
991
992    /// Compare two versions with GIL release optimization
993    #[cfg(feature = "python-bindings")]
994    pub fn cmp_with_gil_release(&self, other: &Self) -> Ordering {
995        // Handle infinite versions (inf is largest) - no GIL needed
996        match (self.is_inf(), other.is_inf()) {
997            (true, true) => return Ordering::Equal,
998            (true, false) => return Ordering::Greater,
999            (false, true) => return Ordering::Less,
1000            (false, false) => {} // Continue with normal comparison
1001        }
1002
1003        // Handle empty/epsilon versions (epsilon version is smallest) - no GIL needed
1004        match (self.is_empty(), other.is_empty()) {
1005            (true, true) => return Ordering::Equal,
1006            (true, false) => return Ordering::Less,
1007            (false, true) => return Ordering::Greater,
1008            (false, false) => {} // Continue with normal comparison
1009        }
1010
1011        // Compare tokens using rez logic with GIL release
1012        Python::with_gil(|py| {
1013            py.allow_threads(|| {
1014                // Extract string representations without GIL
1015                let self_strings = self.extract_token_strings_gil_free();
1016                let other_strings = other.extract_token_strings_gil_free();
1017
1018                // Perform comparison in GIL-free zone
1019                Self::compare_token_strings(&self_strings, &other_strings)
1020            })
1021        })
1022    }
1023
1024    /// Compare two versions using rez-compatible rules
1025    #[cfg(feature = "python-bindings")]
1026    pub fn cmp(&self, other: &Self) -> Ordering {
1027        // Handle infinite versions (inf is largest)
1028        match (self.is_inf(), other.is_inf()) {
1029            (true, true) => return Ordering::Equal,
1030            (true, false) => return Ordering::Greater,
1031            (false, true) => return Ordering::Less,
1032            (false, false) => {} // Continue with normal comparison
1033        }
1034
1035        // Handle empty/epsilon versions (epsilon version is smallest)
1036        match (self.is_empty(), other.is_empty()) {
1037            (true, true) => return Ordering::Equal,
1038            (true, false) => return Ordering::Less,
1039            (false, true) => return Ordering::Greater,
1040            (false, false) => {} // Continue with normal comparison
1041        }
1042
1043        // Compare tokens using rez logic
1044        Python::with_gil(|py| {
1045            let max_len = self.tokens.len().max(other.tokens.len());
1046
1047            for i in 0..max_len {
1048                let self_token = self.tokens.get(i);
1049                let other_token = other.tokens.get(i);
1050
1051                match (self_token, other_token) {
1052                    (Some(self_tok), Some(other_tok)) => {
1053                        // Compare tokens using their less_than method
1054                        if let (Ok(self_lt_other), Ok(other_lt_self)) = (
1055                            self_tok.call_method1(py, "less_than", (other_tok,)),
1056                            other_tok.call_method1(py, "less_than", (self_tok,)),
1057                        ) {
1058                            if let (Ok(self_lt), Ok(other_lt)) = (
1059                                self_lt_other.extract::<bool>(py),
1060                                other_lt_self.extract::<bool>(py),
1061                            ) {
1062                                if self_lt {
1063                                    return Ordering::Less;
1064                                } else if other_lt {
1065                                    return Ordering::Greater;
1066                                }
1067                                // Equal, continue to next token
1068                            }
1069                        }
1070                    }
1071                    (Some(_), None) => {
1072                        // self has more tokens than other
1073                        // Check if the extra token indicates a pre-release
1074                        if let Some(extra_token) = self.tokens.get(i) {
1075                            if let Ok(token_str) = extra_token.call_method0(py, "__str__") {
1076                                if let Ok(s) = token_str.extract::<String>(py) {
1077                                    // Check if it's a pre-release indicator (starts with alpha)
1078                                    if s.chars().next().map_or(false, |c| c.is_alphabetic()) {
1079                                        return Ordering::Less; // Pre-release is less than release
1080                                    }
1081                                }
1082                            }
1083                        }
1084                        return Ordering::Greater; // More tokens = greater (default)
1085                    }
1086                    (None, Some(_)) => {
1087                        // other has more tokens than self
1088                        // Check if the extra token indicates a pre-release
1089                        if let Some(extra_token) = other.tokens.get(i) {
1090                            if let Ok(token_str) = extra_token.call_method0(py, "__str__") {
1091                                if let Ok(s) = token_str.extract::<String>(py) {
1092                                    // Check if it's a pre-release indicator (starts with alpha)
1093                                    if s.chars().next().map_or(false, |c| c.is_alphabetic()) {
1094                                        return Ordering::Greater; // Release is greater than pre-release
1095                                    }
1096                                }
1097                            }
1098                        }
1099                        return Ordering::Less; // Fewer tokens = less (default)
1100                    }
1101                    (None, None) => break, // Both exhausted
1102                }
1103            }
1104
1105            Ordering::Equal
1106        })
1107    }
1108
1109    /// Compare two versions using rez-compatible rules (non-Python version)
1110    #[cfg(not(feature = "python-bindings"))]
1111    pub fn cmp(&self, other: &Self) -> Ordering {
1112        // Handle infinite versions (inf is largest)
1113        match (self.is_inf(), other.is_inf()) {
1114            (true, true) => return Ordering::Equal,
1115            (true, false) => return Ordering::Greater,
1116            (false, true) => return Ordering::Less,
1117            (false, false) => {} // Continue with normal comparison
1118        }
1119
1120        // Handle empty/epsilon versions (epsilon version is smallest)
1121        match (self.is_empty(), other.is_empty()) {
1122            (true, true) => return Ordering::Equal,
1123            (true, false) => return Ordering::Less,
1124            (false, true) => return Ordering::Greater,
1125            (false, false) => {} // Continue with normal comparison
1126        }
1127
1128        // Compare tokens using string comparison for now
1129        Self::compare_token_strings(&self.tokens, &other.tokens)
1130    }
1131
1132    /// Simple string-based token comparison for non-Python version
1133    #[cfg(not(feature = "python-bindings"))]
1134    fn compare_token_strings(tokens1: &[String], tokens2: &[String]) -> Ordering {
1135        for (t1, t2) in tokens1.iter().zip(tokens2.iter()) {
1136            // Try to parse as numbers first
1137            match (t1.parse::<i64>(), t2.parse::<i64>()) {
1138                (Ok(n1), Ok(n2)) => {
1139                    let cmp = n1.cmp(&n2);
1140                    if cmp != Ordering::Equal {
1141                        return cmp;
1142                    }
1143                }
1144                _ => {
1145                    // Fall back to string comparison
1146                    let cmp = t1.cmp(t2);
1147                    if cmp != Ordering::Equal {
1148                        return cmp;
1149                    }
1150                }
1151            }
1152        }
1153
1154        // If all compared tokens are equal, shorter version is considered greater
1155        // This follows semantic versioning where "2" > "2.alpha1"
1156        tokens2.len().cmp(&tokens1.len())
1157    }
1158}
1159
1160impl PartialEq for Version {
1161    fn eq(&self, other: &Self) -> bool {
1162        self.cmp(other) == Ordering::Equal
1163    }
1164}
1165
1166impl Eq for Version {}
1167
1168impl PartialOrd for Version {
1169    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1170        Some(self.cmp(other))
1171    }
1172}
1173
1174impl Ord for Version {
1175    fn cmp(&self, other: &Self) -> Ordering {
1176        // Call the Version::cmp method, not the trait method
1177        Version::cmp(self, other)
1178    }
1179}
1180
1181impl Hash for Version {
1182    fn hash<H: Hasher>(&self, state: &mut H) {
1183        self.string_repr.hash(state);
1184    }
1185}
1186
1187#[cfg(feature = "python-bindings")]
1188impl Clone for Version {
1189    fn clone(&self) -> Self {
1190        Python::with_gil(|py| {
1191            let cloned_tokens: Vec<PyObject> = self
1192                .tokens
1193                .iter()
1194                .map(|token| token.clone_ref(py))
1195                .collect();
1196
1197            Self {
1198                tokens: cloned_tokens,
1199                separators: self.separators.clone(),
1200                string_repr: self.string_repr.clone(),
1201                cached_hash: self.cached_hash,
1202            }
1203        })
1204    }
1205}
1206
1207#[cfg(not(feature = "python-bindings"))]
1208impl Clone for Version {
1209    fn clone(&self) -> Self {
1210        Self {
1211            tokens: self.tokens.clone(),
1212            separators: self.separators.clone(),
1213            string_repr: self.string_repr.clone(),
1214            cached_hash: self.cached_hash,
1215        }
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222
1223    #[test]
1224    fn test_version_creation() {
1225        let version = Version::parse("1.2.3").unwrap();
1226        assert_eq!(version.as_str(), "1.2.3");
1227        assert_eq!(version.tokens.len(), 3);
1228        assert!(!version.is_empty());
1229    }
1230
1231    #[test]
1232    fn test_empty_version() {
1233        let version = Version::parse("").unwrap();
1234        assert_eq!(version.as_str(), "");
1235        assert_eq!(version.tokens.len(), 0);
1236        assert!(version.is_empty());
1237    }
1238
1239    #[test]
1240    fn test_version_inf() {
1241        let version = Version::inf();
1242        assert_eq!(version.as_str(), "inf");
1243        assert!(version.is_inf());
1244    }
1245
1246    #[test]
1247    fn test_version_epsilon() {
1248        let version = Version::epsilon();
1249        assert_eq!(version.as_str(), "");
1250        assert!(version.is_epsilon());
1251        assert!(version.is_empty());
1252    }
1253
1254    #[test]
1255    fn test_version_empty() {
1256        let version = Version::empty();
1257        assert_eq!(version.as_str(), "");
1258        assert!(version.is_empty());
1259        assert!(version.is_epsilon());
1260    }
1261
1262    #[test]
1263    fn test_version_parsing_special() {
1264        // Test parsing empty version
1265        let empty = Version::parse("").unwrap();
1266        assert!(empty.is_empty());
1267
1268        // Test parsing inf version
1269        let inf = Version::parse("inf").unwrap();
1270        assert!(inf.is_inf());
1271
1272        // Test parsing epsilon version
1273        let epsilon = Version::parse("epsilon").unwrap();
1274        assert!(epsilon.is_epsilon());
1275    }
1276
1277    #[test]
1278    fn test_version_comparison_boundaries() {
1279        let empty = Version::empty();
1280        let epsilon = Version::epsilon();
1281        let normal = Version::parse("1.0.0").unwrap();
1282        let inf = Version::inf();
1283
1284        // Test epsilon/empty equivalence
1285        assert_eq!(empty.cmp(&epsilon), Ordering::Equal);
1286
1287        // Test ordering: epsilon < normal < inf
1288        assert_eq!(epsilon.cmp(&normal), Ordering::Less);
1289        assert_eq!(normal.cmp(&inf), Ordering::Less);
1290        assert_eq!(epsilon.cmp(&inf), Ordering::Less);
1291
1292        // Test reverse ordering
1293        assert_eq!(inf.cmp(&normal), Ordering::Greater);
1294        assert_eq!(normal.cmp(&epsilon), Ordering::Greater);
1295        assert_eq!(inf.cmp(&epsilon), Ordering::Greater);
1296    }
1297
1298    #[test]
1299    fn test_version_prerelease_comparison() {
1300        // Test that release versions are greater than pre-release versions
1301        let release = Version::parse("2").unwrap();
1302        let prerelease = Version::parse("2.alpha1").unwrap();
1303
1304        // "2" should be greater than "2.alpha1"
1305        assert_eq!(release.cmp(&prerelease), Ordering::Greater);
1306        assert_eq!(prerelease.cmp(&release), Ordering::Less);
1307
1308        // Test with comparison operators
1309        assert!(!(release < prerelease)); // "2" < "2.alpha1" should be false
1310        assert!(prerelease < release); // "2.alpha1" < "2" should be true
1311    }
1312
1313    #[test]
1314    fn test_version_copy() {
1315        let version = Version::parse("1.2.3").unwrap();
1316        let copied = version.clone();
1317        assert_eq!(version.as_str(), copied.as_str());
1318        assert_eq!(version.tokens.len(), copied.tokens.len());
1319    }
1320
1321    #[test]
1322    fn test_version_trim() {
1323        let version = Version::parse("1.2.3.4").unwrap();
1324        // Create a trimmed version by taking only first 2 tokens
1325        let mut trimmed_tokens = version.tokens.clone();
1326        trimmed_tokens.truncate(2);
1327        assert_eq!(trimmed_tokens.len(), 2);
1328    }
1329}