Skip to main content

deps_bundler/
version.rs

1//! Version comparison utilities for Ruby gems.
2//!
3//! Provides version comparison and requirement matching for Bundler ecosystem.
4
5use std::cmp::Ordering;
6
7/// Compares two version strings numerically.
8pub fn compare_versions(a: &str, b: &str) -> Ordering {
9    let a_parts: Vec<u64> = a
10        .split('.')
11        .filter_map(|p| p.split(|c: char| !c.is_ascii_digit()).next())
12        .filter_map(|p| p.parse().ok())
13        .collect();
14    let b_parts: Vec<u64> = b
15        .split('.')
16        .filter_map(|p| p.split(|c: char| !c.is_ascii_digit()).next())
17        .filter_map(|p| p.parse().ok())
18        .collect();
19
20    let max_len = a_parts.len().max(b_parts.len());
21    for i in 0..max_len {
22        let ap = a_parts.get(i).copied().unwrap_or(0);
23        let bp = b_parts.get(i).copied().unwrap_or(0);
24        match ap.cmp(&bp) {
25            Ordering::Equal => {}
26            other => return other,
27        }
28    }
29    Ordering::Equal
30}
31
32/// Checks if a version matches the given requirement.
33pub fn version_matches_requirement(version: &str, requirement: &str) -> bool {
34    let req = requirement.trim();
35
36    if req == "*" {
37        return true;
38    }
39
40    // Pessimistic operator (~>)
41    if req.starts_with("~>") {
42        let req_ver = req.trim_start_matches("~>").trim();
43        return matches_pessimistic(version, req_ver);
44    }
45
46    // Greater than or equal
47    if req.starts_with(">=") {
48        let req_ver = req.trim_start_matches(">=").trim();
49        return compare_versions(version, req_ver) != Ordering::Less;
50    }
51
52    // Greater than
53    if req.starts_with('>') && !req.starts_with(">=") {
54        let req_ver = req.trim_start_matches('>').trim();
55        return compare_versions(version, req_ver) == Ordering::Greater;
56    }
57
58    // Less than or equal
59    if req.starts_with("<=") {
60        let req_ver = req.trim_start_matches("<=").trim();
61        return compare_versions(version, req_ver) != Ordering::Greater;
62    }
63
64    // Less than
65    if req.starts_with('<') && !req.starts_with("<=") {
66        let req_ver = req.trim_start_matches('<').trim();
67        return compare_versions(version, req_ver) == Ordering::Less;
68    }
69
70    // Not equal
71    if req.starts_with("!=") {
72        let req_ver = req.trim_start_matches("!=").trim();
73        return version != req_ver;
74    }
75
76    // Exact match
77    if let Some(req_ver) = req.strip_prefix('=') {
78        return version == req_ver.trim();
79    }
80
81    // Default: exact match or prefix match
82    version == req || version.starts_with(&format!("{req}."))
83}
84
85/// Checks if a version matches a pessimistic requirement (~>).
86fn matches_pessimistic(version: &str, requirement: &str) -> bool {
87    let req_parts: Vec<&str> = requirement.split('.').collect();
88    let ver_parts: Vec<&str> = version.split('.').collect();
89
90    if ver_parts.len() < req_parts.len() {
91        return false;
92    }
93
94    // All parts except the last must match exactly
95    for i in 0..(req_parts.len().saturating_sub(1)) {
96        let req_part = req_parts
97            .get(i)
98            .and_then(|p| p.split(|c: char| !c.is_ascii_digit()).next());
99        let ver_part = ver_parts
100            .get(i)
101            .and_then(|p| p.split(|c: char| !c.is_ascii_digit()).next());
102        if req_part != ver_part {
103            return false;
104        }
105    }
106
107    // Last part of version must be >= last part of requirement
108    let last_idx = req_parts.len() - 1;
109    let req_last: u64 = req_parts[last_idx]
110        .split(|c: char| !c.is_ascii_digit())
111        .next()
112        .and_then(|p| p.parse().ok())
113        .unwrap_or(0);
114    let ver_last: u64 = ver_parts
115        .get(last_idx)
116        .and_then(|v| v.split(|c: char| !c.is_ascii_digit()).next())
117        .and_then(|p| p.parse().ok())
118        .unwrap_or(0);
119
120    ver_last >= req_last
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_compare_versions() {
129        assert_eq!(compare_versions("1.0.0", "1.0.0"), Ordering::Equal);
130        assert_eq!(compare_versions("1.0.1", "1.0.0"), Ordering::Greater);
131        assert_eq!(compare_versions("1.0.0", "1.0.1"), Ordering::Less);
132        assert_eq!(compare_versions("2.0.0", "1.9.9"), Ordering::Greater);
133        assert_eq!(compare_versions("1.0.0", "1.0"), Ordering::Equal);
134        assert_eq!(compare_versions("1.0", "1.0.0"), Ordering::Equal);
135    }
136
137    #[test]
138    fn test_matches_pessimistic() {
139        // ~> 1.0 means >= 1.0, < 2.0
140        assert!(matches_pessimistic("1.0.5", "1.0"));
141        assert!(matches_pessimistic("1.0.0", "1.0"));
142        assert!(matches_pessimistic("1.9.9", "1.0"));
143        assert!(!matches_pessimistic("2.0.0", "1.0"));
144
145        // ~> 1.0.5 means >= 1.0.5, < 1.1.0
146        assert!(matches_pessimistic("1.0.5", "1.0.5"));
147        assert!(matches_pessimistic("1.0.9", "1.0.5"));
148        assert!(!matches_pessimistic("1.1.0", "1.0.5"));
149        assert!(!matches_pessimistic("1.0.4", "1.0.5"));
150    }
151
152    #[test]
153    fn test_version_matches_requirement() {
154        // Pessimistic operator
155        assert!(version_matches_requirement("7.0.8", "~> 7.0"));
156        assert!(version_matches_requirement("7.0.0", "~> 7.0"));
157        assert!(!version_matches_requirement("8.0.0", "~> 7.0"));
158
159        // Greater than or equal
160        assert!(version_matches_requirement("1.5.0", ">= 1.1"));
161        assert!(version_matches_requirement("1.1.0", ">= 1.1"));
162        assert!(!version_matches_requirement("1.0.0", ">= 1.1"));
163
164        // Greater than
165        assert!(version_matches_requirement("2.0.0", "> 1.0"));
166        assert!(!version_matches_requirement("1.0.0", "> 1.0"));
167
168        // Less than or equal
169        assert!(version_matches_requirement("1.0.0", "<= 1.0"));
170        assert!(!version_matches_requirement("1.1.0", "<= 1.0"));
171
172        // Less than
173        assert!(version_matches_requirement("0.9.0", "< 1.0"));
174        assert!(!version_matches_requirement("1.0.0", "< 1.0"));
175
176        // Exact match
177        assert!(version_matches_requirement("1.0.0", "= 1.0.0"));
178        assert!(!version_matches_requirement("1.0.1", "= 1.0.0"));
179
180        // Not equal
181        assert!(version_matches_requirement("1.0.1", "!= 1.0.0"));
182        assert!(!version_matches_requirement("1.0.0", "!= 1.0.0"));
183
184        // Wildcard
185        assert!(version_matches_requirement("1.0.0", "*"));
186        assert!(version_matches_requirement("0.0.1", "*"));
187        assert!(version_matches_requirement("99.99.99", "*"));
188    }
189}