//! Version parsing/comparison (port of `shared/version.js`): 2–4 numeric components, //! optional prerelease and build metadata. use crate::core::error::{CoreError, CoreResult}; use once_cell::sync::Lazy; use regex::Regex; use std::cmp::Ordering; static VERSION: Lazy = Lazy::new(|| { Regex::new(r"^v?(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$").unwrap() }); #[derive(Debug, Clone, PartialEq)] pub struct Version { pub numbers: [u64; 4], pub prerelease: Vec, } pub fn parse_version(input: &str) -> CoreResult { let raw = input.trim(); let caps = VERSION.captures(raw).ok_or_else(|| CoreError::plain(format!("Invalid version: {}", if raw.is_empty() { "" } else { raw })))?; let n = |i: usize| caps.get(i).map(|m| m.as_str().parse::().unwrap_or(u64::MAX)).unwrap_or(0); Ok(Version { numbers: [n(1), n(2), n(3), n(4)], prerelease: caps.get(5).map(|m| m.as_str().split('.').map(str::to_string).collect()).unwrap_or_default(), }) } fn compare_prerelease(left: &[String], right: &[String]) -> Ordering { match (left.is_empty(), right.is_empty()) { (true, true) => return Ordering::Equal, (true, false) => return Ordering::Greater, (false, true) => return Ordering::Less, _ => {} } for i in 0..left.len().max(right.len()) { let (Some(l), Some(r)) = (left.get(i), right.get(i)) else { return if left.get(i).is_none() { Ordering::Less } else { Ordering::Greater }; }; if l == r { continue; } let ln = l.chars().all(|c| c.is_ascii_digit()); let rn = r.chars().all(|c| c.is_ascii_digit()); return match (ln, rn) { (true, true) => l.parse::().unwrap_or(0).cmp(&r.parse::().unwrap_or(0)), (true, false) => Ordering::Less, (false, true) => Ordering::Greater, _ => l.cmp(r), }; } Ordering::Equal } pub fn compare_versions(left: &str, right: &str) -> CoreResult { let (l, r) = (parse_version(left)?, parse_version(right)?); Ok(l.numbers.cmp(&r.numbers).then_with(|| compare_prerelease(&l.prerelease, &r.prerelease))) } pub fn cmp_or_equal(left: &str, right: &str) -> Ordering { compare_versions(left, right).unwrap_or(Ordering::Equal) } #[cfg(test)] mod tests { use super::*; #[test] fn ordering() { assert_eq!(compare_versions("0.1.1", "0.1.0").unwrap(), Ordering::Greater); assert_eq!(compare_versions("1.0.0-alpha", "1.0.0").unwrap(), Ordering::Less); assert_eq!(compare_versions("1.0.0-alpha.2", "1.0.0-alpha.10").unwrap(), Ordering::Less); assert_eq!(compare_versions("1.0", "1.0.0.0").unwrap(), Ordering::Equal); assert_eq!(compare_versions("v2.0.0+build", "2.0.0").unwrap(), Ordering::Equal); assert!(parse_version("1").is_err()); } }