use once_cell::sync::Lazy;
use regex::Regex;
use rez_next_common::RezCoreError;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
static TOKEN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[a-zA-Z0-9_]+").unwrap());
#[derive(Debug)]
pub struct Version {
tokens: Vec<String>,
separators: Vec<String>,
pub string_repr: String,
cached_hash: Option<u64>,
}
impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.string_repr.serialize(serializer)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
impl Version {
pub fn new(version_str: Option<&str>) -> Result<Self, RezCoreError> {
let version_str = version_str.unwrap_or("");
Self::parse(version_str)
}
pub fn as_str(&self) -> &str {
&self.string_repr
}
pub fn has_prefix(&self, prefix: &Version) -> bool {
self.tokens.len() >= prefix.tokens.len()
&& self
.tokens
.iter()
.zip(&prefix.tokens)
.all(|(token, prefix_token)| {
Self::compare_single_token(token, prefix_token) == Ordering::Equal
})
}
}
impl Version {
fn parse_internal_gil_free(s: &str) -> Result<(Vec<String>, Vec<String>), RezCoreError> {
if s.starts_with('v') || s.starts_with('V') {
return Err(RezCoreError::VersionParse(format!(
"Version prefixes not supported: '{s}'"
)));
}
if s.contains("..") || s.starts_with('.') || s.ends_with('.') {
return Err(RezCoreError::VersionParse(format!(
"Invalid version syntax: '{s}'"
)));
}
let tokens: Vec<&str> = TOKEN_REGEX.find_iter(s).map(|m| m.as_str()).collect();
if tokens.is_empty() {
return Err(RezCoreError::VersionParse(format!(
"Invalid version syntax: '{s}'"
)));
}
let numeric_tokens: Vec<_> = tokens
.iter()
.filter(|t| t.chars().all(|c| c.is_ascii_digit()))
.collect();
if numeric_tokens.len() > 5 {
return Err(RezCoreError::VersionParse(format!(
"Version too complex: '{s}'"
)));
}
if tokens.len() > 10 {
return Err(RezCoreError::VersionParse(format!(
"Version too complex: '{s}'"
)));
}
let separators: Vec<&str> = TOKEN_REGEX.split(s).collect();
if !separators[0].is_empty() || !separators[separators.len() - 1].is_empty() {
return Err(RezCoreError::VersionParse(format!(
"Invalid version syntax: '{s}'"
)));
}
for sep in &separators[1..separators.len() - 1] {
if sep.len() > 1 {
return Err(RezCoreError::VersionParse(format!(
"Invalid version syntax: '{s}'"
)));
}
if !matches!(*sep, "." | "-" | "_" | "+") {
return Err(RezCoreError::VersionParse(format!(
"Invalid separator '{sep}' in version: '{s}'"
)));
}
}
for token_str in &tokens {
if !token_str.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(RezCoreError::VersionParse(format!(
"Invalid characters in token: '{token_str}'"
)));
}
if token_str.starts_with('_') || token_str.ends_with('_') {
return Err(RezCoreError::VersionParse(format!(
"Invalid token format: '{token_str}'"
)));
}
if token_str.chars().all(|c| c.is_alphabetic()) && token_str.len() > 10 {
return Err(RezCoreError::VersionParse(format!(
"Invalid version token: '{token_str}'"
)));
}
if *token_str == "not" || *token_str == "version" {
return Err(RezCoreError::VersionParse(format!(
"Invalid version token: '{token_str}'"
)));
}
}
let token_strings: Vec<String> = tokens.into_iter().map(ToString::to_string).collect();
let sep_strings: Vec<String> = separators[1..separators.len() - 1]
.iter()
.map(ToString::to_string)
.collect();
Ok((token_strings, sep_strings))
}
#[must_use]
pub fn inf() -> Self {
Self {
tokens: vec![],
separators: vec![],
string_repr: "inf".to_string(),
cached_hash: None,
}
}
#[must_use]
pub fn is_inf(&self) -> bool {
self.string_repr == "inf"
}
#[must_use]
pub fn empty() -> Self {
Self {
tokens: vec![],
separators: vec![],
string_repr: String::new(),
cached_hash: None,
}
}
#[must_use]
pub fn epsilon() -> Self {
Self::empty()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty() && self.string_repr.is_empty()
}
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn major(&self) -> Option<u64> {
self.tokens.first().and_then(|t| t.parse::<u64>().ok())
}
pub fn minor(&self) -> Option<u64> {
self.tokens.get(1).and_then(|t| t.parse::<u64>().ok())
}
pub fn patch(&self) -> Option<u64> {
self.tokens.get(2).and_then(|t| t.parse::<u64>().ok())
}
pub fn is_epsilon(&self) -> bool {
self.is_empty()
}
pub fn is_prerelease(&self) -> bool {
if self.is_empty() || self.is_inf() {
return false;
}
for token in &self.tokens {
let s_lower = token.to_lowercase();
if s_lower.contains("alpha")
|| s_lower.contains("beta")
|| s_lower.contains("rc")
|| s_lower.contains("dev")
|| s_lower.contains("pre")
|| s_lower.contains("snapshot")
{
return true;
}
}
false
}
pub fn parse(s: &str) -> Result<Self, RezCoreError> {
let s = s.trim();
if s.is_empty() {
return Ok(Self::empty());
}
if s == "inf" {
return Ok(Self::inf());
}
if s == "epsilon" {
return Ok(Self::epsilon());
}
let (tokens, separators) = Self::parse_internal_gil_free(s)?;
Ok(Self {
tokens,
separators,
string_repr: s.to_string(),
cached_hash: None,
})
}
fn compare_rez(&self, other: &Self) -> Ordering {
match (self.is_inf(), other.is_inf()) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Greater,
(false, true) => return Ordering::Less,
(false, false) => {} }
match (self.is_empty(), other.is_empty()) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
(false, false) => {} }
Self::compare_token_strings(&self.tokens, &other.tokens)
}
fn compare_single_token(t1: &str, t2: &str) -> Ordering {
if let (Ok(n1), Ok(n2)) = (t1.parse::<i64>(), t2.parse::<i64>()) {
return n1.cmp(&n2);
}
if t1 == t2 {
return Ordering::Equal;
}
let t1_all_alpha = t1.chars().all(|c| c.is_alphabetic() || c == '_');
let t2_all_alpha = t2.chars().all(|c| c.is_alphabetic() || c == '_');
let t1_all_num = t1.chars().all(|c| c.is_ascii_digit());
let t2_all_num = t2.chars().all(|c| c.is_ascii_digit());
if t1_all_alpha && t2_all_num {
return Ordering::Less;
}
if t1_all_num && t2_all_alpha {
return Ordering::Greater;
}
if t1_all_alpha && t2_all_alpha {
return t1.cmp(t2);
}
let seg1 = Self::split_token_segments(t1);
let seg2 = Self::split_token_segments(t2);
for (s1, s2) in seg1.iter().zip(seg2.iter()) {
let s1_is_num = s1.parse::<u64>().is_ok();
let s2_is_num = s2.parse::<u64>().is_ok();
let cmp = match (s1_is_num, s2_is_num) {
(true, true) => {
let n1: u64 = s1.parse().unwrap();
let n2: u64 = s2.parse().unwrap();
n1.cmp(&n2)
}
(false, false) => s1.as_str().cmp(s2.as_str()),
(false, true) => Ordering::Less, (true, false) => Ordering::Greater, };
if cmp != Ordering::Equal {
return cmp;
}
}
seg1.len().cmp(&seg2.len())
}
fn split_token_segments(s: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut in_digits = false;
for ch in s.chars() {
let is_digit = ch.is_ascii_digit();
if current.is_empty() {
in_digits = is_digit;
current.push(ch);
} else if is_digit == in_digits {
current.push(ch);
} else {
segments.push(current.clone());
current.clear();
in_digits = is_digit;
current.push(ch);
}
}
if !current.is_empty() {
segments.push(current);
}
segments
}
fn compare_token_strings(tokens1: &[String], tokens2: &[String]) -> Ordering {
for (t1, t2) in tokens1.iter().zip(tokens2.iter()) {
let cmp = Self::compare_single_token(t1, t2);
if cmp != Ordering::Equal {
return cmp;
}
}
if tokens1.len() < tokens2.len() {
let extra_tokens = &tokens2[tokens1.len()..];
if extra_tokens.iter().all(|t| t == "0") {
Ordering::Equal
} else {
let first_extra = &tokens2[tokens1.len()];
if first_extra.chars().all(|c| c.is_ascii_digit()) {
Ordering::Greater
} else {
Ordering::Greater
}
}
} else if tokens2.len() < tokens1.len() {
let extra_tokens = &tokens1[tokens2.len()..];
if extra_tokens.iter().all(|t| t == "0") {
Ordering::Equal
} else {
let first_extra = &tokens1[tokens2.len()];
if first_extra.chars().all(|c| c.is_ascii_digit()) {
Ordering::Less
} else {
Ordering::Less
}
}
} else {
Ordering::Equal
}
}
}
impl PartialEq for Version {
fn eq(&self, other: &Self) -> bool {
self.compare_rez(other) == Ordering::Equal
}
}
impl Eq for Version {}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
self.compare_rez(other)
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Version {
fn hash<H: Hasher>(&self, state: &mut H) {
self.string_repr.hash(state);
}
}
impl Clone for Version {
fn clone(&self) -> Self {
Self {
tokens: self.tokens.clone(),
separators: self.separators.clone(),
string_repr: self.string_repr.clone(),
cached_hash: self.cached_hash,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ver(s: &str) -> Version {
Version::parse(s).unwrap()
}
#[test]
fn test_version_creation() {
let version = Version::parse("1.2.3").unwrap();
assert_eq!(version.as_str(), "1.2.3");
assert_eq!(version.tokens.len(), 3);
assert!(!version.is_empty());
}
#[test]
fn test_empty_version() {
let version = Version::parse("").unwrap();
assert_eq!(version.as_str(), "");
assert_eq!(version.tokens.len(), 0);
assert!(version.is_empty());
}
#[test]
fn test_version_inf() {
let version = Version::inf();
assert_eq!(version.as_str(), "inf");
assert!(version.is_inf());
}
#[test]
fn test_version_epsilon() {
let version = Version::epsilon();
assert_eq!(version.as_str(), "");
assert!(version.is_epsilon());
assert!(version.is_empty());
}
#[test]
fn test_version_empty() {
let version = Version::empty();
assert_eq!(version.as_str(), "");
assert!(version.is_empty());
assert!(version.is_epsilon());
}
#[test]
fn test_version_parsing_special() {
let empty = Version::parse("").unwrap();
assert!(empty.is_empty());
let inf = Version::parse("inf").unwrap();
assert!(inf.is_inf());
let epsilon = Version::parse("epsilon").unwrap();
assert!(epsilon.is_epsilon());
}
#[test]
fn test_version_comparison_boundaries() {
let empty = Version::empty();
let epsilon = Version::epsilon();
let normal = Version::parse("1.0.0").unwrap();
let inf = Version::inf();
assert_eq!(empty.cmp(&epsilon), Ordering::Equal);
assert_eq!(epsilon.cmp(&normal), Ordering::Less);
assert_eq!(normal.cmp(&inf), Ordering::Less);
assert_eq!(epsilon.cmp(&inf), Ordering::Less);
assert_eq!(inf.cmp(&normal), Ordering::Greater);
assert_eq!(normal.cmp(&epsilon), Ordering::Greater);
assert_eq!(inf.cmp(&epsilon), Ordering::Greater);
}
#[test]
fn test_version_prerelease_comparison() {
let release = Version::parse("2").unwrap();
let prerelease = Version::parse("2.alpha1").unwrap();
assert_eq!(release.cmp(&prerelease), Ordering::Greater);
assert_eq!(prerelease.cmp(&release), Ordering::Less);
assert!(release >= prerelease); assert!(prerelease < release); }
#[test]
fn test_version_copy() {
let version = Version::parse("1.2.3").unwrap();
let copied = version.clone();
assert_eq!(version.as_str(), copied.as_str());
assert_eq!(version.tokens.len(), copied.tokens.len());
}
#[test]
fn test_version_ord_basic() {
let v1 = ver("1.0.0");
let v2 = ver("1.0.0");
assert!(v1 >= v2, "1.0.0 >= 1.0.0 should be true");
assert!(v1 <= v2, "1.0.0 <= 1.0.0 should be true");
}
#[test]
fn test_version_ord_greater() {
let v1 = ver("2.0.0");
let v2 = ver("1.0.0");
assert!(v1 > v2, "2.0.0 > 1.0.0 should be true");
assert!(v2 < v1, "1.0.0 < 2.0.0 should be true");
}
#[test]
fn test_range_contains_ge() {
let range = crate::VersionRange::parse(">=1.0.0").unwrap();
assert!(
range.contains(&ver("1.0.0")),
">=1.0.0 should contain 1.0.0"
);
}
#[test]
fn test_version_trim() {
let version = Version::parse("1.2.3.4").unwrap();
let mut trimmed_tokens = version.tokens.clone();
trimmed_tokens.truncate(2);
assert_eq!(trimmed_tokens.len(), 2);
}
#[test]
fn test_prerelease_alpha_beta_rc_ordering() {
let alpha = Version::parse("1.0.alpha").unwrap();
let beta = Version::parse("1.0.beta").unwrap();
let rc = Version::parse("1.0.rc").unwrap();
let release = Version::parse("1.0").unwrap();
assert!(alpha < beta, "alpha should be less than beta");
assert!(beta < rc, "beta should be less than rc");
assert!(rc < release, "rc should be less than release");
assert!(alpha < release, "alpha should be less than release");
}
#[test]
fn test_prerelease_alpha_numbered_variants() {
let a1 = Version::parse("1.0.alpha1").unwrap();
let a2 = Version::parse("1.0.alpha2").unwrap();
let a10 = Version::parse("1.0.alpha10").unwrap();
assert!(a1 < a2, "alpha1 < alpha2");
assert!(a2 < a10, "alpha2 < alpha10 (numeric comparison)");
}
#[test]
fn test_prerelease_dev_pre_snapshot_ordering() {
let dev = Version::parse("1.0.dev").unwrap();
let alpha = Version::parse("1.0.alpha").unwrap();
let pre = Version::parse("1.0.pre").unwrap();
let snapshot = Version::parse("1.0.snapshot").unwrap();
let release = Version::parse("1.0").unwrap();
assert!(dev < release, "1.0.dev < 1.0");
assert!(alpha < release, "1.0.alpha < 1.0");
assert!(pre < release, "1.0.pre < 1.0");
assert!(snapshot < release, "1.0.snapshot < 1.0");
assert!(alpha < dev, "alpha < dev (a < d)");
assert!(dev < pre, "dev < pre (d < p)");
assert!(pre < snapshot, "pre < snapshot (p < s)");
assert!(dev.is_prerelease(), "dev is detected as prerelease");
assert!(pre.is_prerelease(), "pre is detected as prerelease");
assert!(
snapshot.is_prerelease(),
"snapshot is detected as prerelease"
);
}
#[test]
fn test_prerelease_mixed_with_numeric_tokens() {
let v_alpha = Version::parse("2.0.0-alpha").unwrap();
let v_beta = Version::parse("2.0.0-beta").unwrap();
let v_stable = Version::parse("2.0.0").unwrap();
assert!(v_alpha < v_beta, "2.0.0-alpha < 2.0.0-beta");
assert!(v_beta < v_stable, "2.0.0-beta < 2.0.0");
assert!(v_alpha.is_prerelease());
assert!(v_beta.is_prerelease());
assert!(!v_stable.is_prerelease());
}
#[test]
fn test_prerelease_rc_vs_stable_same_prefix() {
let rc1 = Version::parse("3.0.rc1").unwrap();
let stable = Version::parse("3.0").unwrap();
let rc2 = Version::parse("3.0.rc2").unwrap();
assert!(rc1 < stable, "rc1 < stable 3.0");
assert!(rc2 < stable, "rc2 < stable 3.0");
assert!(rc1 < rc2, "rc1 < rc2");
}
#[test]
fn test_prerelease_is_prerelease_detection() {
assert!(Version::parse("1.alpha").unwrap().is_prerelease());
assert!(Version::parse("1.beta").unwrap().is_prerelease());
assert!(Version::parse("1.rc").unwrap().is_prerelease());
assert!(Version::parse("1.dev").unwrap().is_prerelease());
assert!(Version::parse("1.pre").unwrap().is_prerelease());
assert!(Version::parse("1.snapshot").unwrap().is_prerelease());
assert!(!Version::parse("1.0").unwrap().is_prerelease());
assert!(!Version::parse("1.0.0").unwrap().is_prerelease());
assert!(!Version::parse("2024.5").unwrap().is_prerelease());
assert!(!Version::empty().is_prerelease());
assert!(!Version::inf().is_prerelease());
}
#[test]
fn test_version_very_large_numbers() {
let v1 = Version::parse("999999999.1").unwrap();
assert_eq!(v1.as_str(), "999999999.1");
let v2 = Version::parse("1.999999999").unwrap();
assert_eq!(v2.as_str(), "1.999999999");
let v_small = Version::parse("1.0").unwrap();
assert!(v_small < v1);
}
#[test]
fn test_version_borderline_token_count() {
let v10 = Version::parse("a.b.c.d.e.f.g.h.i.j").unwrap();
assert_eq!(v10.tokens.len(), 10);
let v11_result = Version::parse("a.b.c.d.e.f.g.h.i.j.k");
assert!(v11_result.is_err(), "Version with 11 tokens should fail");
}
#[test]
fn test_version_borderline_numeric_token_count() {
let v5 = Version::parse("1.2.3.4.5").unwrap();
assert_eq!(v5.as_str(), "1.2.3.4.5");
let v6_result = Version::parse("1.2.3.4.5.6");
assert!(
v6_result.is_err(),
"Version with 6 numeric tokens should fail"
);
}
#[test]
fn test_version_underscore_in_tokens() {
let v = Version::parse("1_0.2_0").unwrap();
assert_eq!(v.as_str(), "1_0.2_0");
assert_eq!(v.tokens.len(), 2);
}
#[test]
fn test_version_single_token() {
let v = Version::parse("123").unwrap();
assert_eq!(v.as_str(), "123");
assert_eq!(v.tokens.len(), 1);
}
#[test]
fn test_version_hash_consistency() {
let v1 = Version::parse("1.2.3").unwrap();
let v2 = Version::parse("1.2.3").unwrap();
let mut hasher1 = std::collections::hash_map::DefaultHasher::new();
let mut hasher2 = std::collections::hash_map::DefaultHasher::new();
v1.hash(&mut hasher1);
v2.hash(&mut hasher2);
assert_eq!(hasher1.finish(), hasher2.finish());
}
#[test]
fn test_version_equality_different_instances() {
let v1 = Version::parse("1.0").unwrap();
let v2 = Version::parse("1.0").unwrap();
assert_eq!(v1, v2);
}
#[test]
fn test_version_ordering_transitivity() {
let a = Version::parse("1.0").unwrap();
let b = Version::parse("2.0").unwrap();
let c = Version::parse("3.0").unwrap();
assert!(a < b);
assert!(b < c);
assert!(a < c);
}
#[test]
fn test_version_invalid_prefix() {
assert!(Version::parse("v1.0").is_err());
assert!(Version::parse("V1.0").is_err());
}
#[test]
fn test_version_invalid_syntax() {
assert!(Version::parse("1..2").is_err());
assert!(Version::parse(".1.2").is_err());
assert!(Version::parse("1.2.").is_err());
}
#[test]
fn test_version_no_tokens() {
assert!(Version::parse("...").is_err());
assert!(Version::parse("---").is_err());
}
#[test]
fn test_version_alphanumeric_mixed() {
let v = Version::parse("1a.2b.3c").unwrap();
assert_eq!(v.as_str(), "1a.2b.3c");
assert_eq!(v.tokens.len(), 3);
}
}