diff --git a/Windows/Rust/CubeLib/src/lib.rs b/Windows/Rust/CubeLib/src/lib.rs index a10c581..44e6561 100644 --- a/Windows/Rust/CubeLib/src/lib.rs +++ b/Windows/Rust/CubeLib/src/lib.rs @@ -5,6 +5,7 @@ //! ## Modules //! //! - [`websocket`] - WebSocket client and server implementations +//! - [`version`] - Version string comparison utilities //! //! ## Quick Start //! @@ -27,7 +28,19 @@ //! client.connect().await; //! } //! ``` +//! +//! ## Version Comparison +//! +//! ```rust +//! use cube_lib::version::{version_less_than, needs_update}; +//! +//! // Check if update is needed +//! assert!(version_less_than("1.0.0", "1.0.1")); +//! assert!(needs_update("1.0.0", "1.0.1")); +//! ``` pub mod websocket; +pub mod version; pub use websocket::{WebSocketClient, WebSocketConfig, WebSocketMessage, OutgoingMessage, ConnectionStatus, ReconnectedCallback, MessageSender}; +pub use version::{version_less_than, compare_versions, needs_update, parse_version}; diff --git a/Windows/Rust/CubeLib/src/version.rs b/Windows/Rust/CubeLib/src/version.rs new file mode 100644 index 0000000..cf54fb7 --- /dev/null +++ b/Windows/Rust/CubeLib/src/version.rs @@ -0,0 +1,139 @@ +//! # Version utilities +//! +//! Provides version string comparison utilities for semver-like version strings. + +use std::cmp::Ordering; + +/// Parse a version string into a vector of unsigned integers. +/// +/// "1.2.3" → [1, 2, 3] +/// "1.2" → [1, 2] +/// "1.2.3.4.5" → [1, 2, 3, 4, 5] +/// "" or invalid parts are skipped. +pub fn parse_version(version: &str) -> Vec { + version + .split('.') + .filter(|s| !s.is_empty()) + .filter_map(|s| s.parse().ok()) + .collect() +} + +/// Compare two version strings. +/// +/// Returns `true` if `current < required` (i.e. current needs to be updated). +/// +/// Comparison is done part-by-part (split by `.`). +/// Missing parts are treated as `0`. +/// Examples: +/// - `"1.0.0"` < `"1.0.1"` → true +/// - `"1.0.0"` < `"1.1.0"` → true +/// - `"1.0.0"` < `"2.0.0"` → true +/// - `"1.0.0"` < `"1.0.0"` → false (equal) +/// - `"1.0.1"` < `"1.0.0"` → false +/// +/// # Arguments +/// * `current` - The current/local version string +/// * `required` - The required/server version string +pub fn version_less_than(current: &str, required: &str) -> bool { + compare_versions(current, required) == Ordering::Less +} + +/// Compare two version strings, returning the standard `Ordering` result. +/// +/// This is the canonical comparison function. Use [`version_less_than`] if you +/// only need a boolean upgrade check. +pub fn compare_versions(a: &str, b: &str) -> Ordering { + let a_parts = parse_version(a); + let b_parts = parse_version(b); + + let max_len = a_parts.len().max(b_parts.len()); + + for i in 0..max_len { + let av = a_parts.get(i).copied().unwrap_or(0); + let bv = b_parts.get(i).copied().unwrap_or(0); + + match av.cmp(&bv) { + Ordering::Less => return Ordering::Less, + Ordering::Greater => return Ordering::Greater, + Ordering::Equal => continue, + } + } + + Ordering::Equal +} + +/// Check if a local version is outdated compared to a server version. +/// +/// Returns `true` if: +/// - local is "0.0.0" / empty (file does not exist) +/// - local version < server version +pub fn needs_update(local_version: &str, server_version: &str) -> bool { + local_version == "0.0.0" + || local_version.is_empty() + || version_less_than(local_version, server_version) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_version() { + assert_eq!(parse_version("1.2.3"), vec![1, 2, 3]); + assert_eq!(parse_version("1.2"), vec![1, 2]); + assert_eq!(parse_version("1.2.3.4.5"), vec![1, 2, 3, 4, 5]); + assert_eq!(parse_version(""), Vec::::new()); + assert_eq!(parse_version("1..3"), vec![1, 3]); + assert_eq!(parse_version("abc"), Vec::::new()); + } + + #[test] + fn test_version_less_than() { + // Basic comparisons + assert!(version_less_than("1.0.0", "1.0.1")); + assert!(version_less_than("1.0.0", "1.1.0")); + assert!(version_less_than("1.0.0", "2.0.0")); + assert!(version_less_than("0.0.1", "0.0.2")); + + // Equal versions + assert!(!version_less_than("1.0.0", "1.0.0")); + assert!(!version_less_than("1.2.3", "1.2.3")); + + // Newer local version + assert!(!version_less_than("1.0.1", "1.0.0")); + assert!(!version_less_than("2.0.0", "1.0.0")); + + // Different part counts + assert!(version_less_than("1.0", "1.0.1")); + assert!(version_less_than("1", "1.0.1")); + assert!(version_less_than("1.0.0", "1.0.0.1")); + assert!(version_less_than("1", "2")); + + // Treats missing parts as 0 + assert!(version_less_than("1", "1.0.1")); + assert!(!version_less_than("1.0.0", "1")); + } + + #[test] + fn test_compare_versions() { + use std::cmp::Ordering; + assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less); + assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater); + assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal); + } + + #[test] + fn test_needs_update() { + // Missing local + assert!(needs_update("0.0.0", "1.0.0")); + assert!(needs_update("", "1.0.0")); + + // Needs update + assert!(needs_update("1.0.0", "1.0.1")); + assert!(needs_update("1.0.0", "2.0.0")); + + // Already up to date + assert!(!needs_update("1.0.0", "1.0.0")); + assert!(!needs_update("1.0.1", "1.0.0")); + } +}