1pub type Version = (u64, u64, u64);
16
17pub fn parse_exact(v: &str) -> Option<Version> {
20 let (ver, given) = parse_partial(v)?;
21 if given == 3 {
22 Some(ver)
23 } else {
24 None
25 }
26}
27
28fn parse_partial(s: &str) -> Option<(Version, u8)> {
32 let s = s.trim().trim_start_matches('v').trim();
33 if s.is_empty() {
34 return None;
35 }
36 let mut parts = s.split('.');
37 let major: u64 = parts.next()?.trim().parse().ok()?;
38 let (minor, has_minor) = match parts.next() {
39 Some(m) => (m.trim().parse().ok()?, true),
40 None => (0, false),
41 };
42 let (patch, has_patch) = match parts.next() {
43 Some(p) => (p.trim().parse().ok()?, true),
44 None => (0, false),
45 };
46 if parts.next().is_some() {
47 return None; }
49 Some(((major, minor, patch), 1 + has_minor as u8 + has_patch as u8))
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum Cmp {
54 Ge,
55 Gt,
56 Le,
57 Lt,
58}
59
60#[derive(Debug, Clone)]
61enum Constraint {
62 Any,
64 Exact(Version),
66 Caret(Version, Version),
68 Tilde(Version, Version),
70 Compare(Cmp, Version),
72}
73
74fn caret_upper((maj, min, pat): Version, given: u8) -> Version {
78 if maj > 0 {
79 (maj + 1, 0, 0)
80 } else if min > 0 {
81 (0, min + 1, 0)
82 } else if pat > 0 {
83 (0, 0, pat + 1)
84 } else {
85 match given {
86 1 => (1, 0, 0),
87 2 => (0, 1, 0),
88 _ => (0, 0, 1),
89 }
90 }
91}
92
93fn tilde_upper((maj, min, _pat): Version, given: u8) -> Version {
96 if given >= 2 {
97 (maj, min + 1, 0)
98 } else {
99 (maj + 1, 0, 0)
100 }
101}
102
103fn parse_constraint(s: &str) -> Option<Constraint> {
104 let s = s.trim();
105 if s.is_empty() || s == "*" || s.eq_ignore_ascii_case("latest") {
106 return Some(Constraint::Any);
107 }
108 if let Some(rest) = s.strip_prefix('^') {
109 let (v, given) = parse_partial(rest)?;
110 return Some(Constraint::Caret(v, caret_upper(v, given)));
111 }
112 if let Some(rest) = s.strip_prefix('~') {
113 let (v, given) = parse_partial(rest)?;
114 return Some(Constraint::Tilde(v, tilde_upper(v, given)));
115 }
116 for (pfx, cmp) in [(">=", Cmp::Ge), ("<=", Cmp::Le), (">", Cmp::Gt), ("<", Cmp::Lt)] {
117 if let Some(rest) = s.strip_prefix(pfx) {
118 return Some(Constraint::Compare(cmp, parse_partial(rest)?.0));
119 }
120 }
121 if let Some(rest) = s.strip_prefix('=') {
124 return Some(Constraint::Exact(parse_partial(rest)?.0));
125 }
126 let (v, given) = parse_partial(s)?;
127 if given == 3 {
128 Some(Constraint::Exact(v))
129 } else {
130 Some(Constraint::Caret(v, caret_upper(v, given)))
131 }
132}
133
134pub fn satisfies(constraint: &str, version: &str) -> bool {
139 let (Some(c), Some(v)) = (parse_constraint(constraint), parse_exact(version)) else {
140 return false;
141 };
142 match c {
143 Constraint::Any => true,
144 Constraint::Exact(e) => v == e,
145 Constraint::Caret(lo, hi) | Constraint::Tilde(lo, hi) => v >= lo && v < hi,
146 Constraint::Compare(Cmp::Ge, r) => v >= r,
147 Constraint::Compare(Cmp::Gt, r) => v > r,
148 Constraint::Compare(Cmp::Le, r) => v <= r,
149 Constraint::Compare(Cmp::Lt, r) => v < r,
150 }
151}
152
153pub fn best_match<'a>(constraint: &str, versions: &'a [String]) -> Option<&'a str> {
157 versions
158 .iter()
159 .filter(|v| satisfies(constraint, v))
160 .max_by_key(|v| parse_exact(v).unwrap_or((0, 0, 0)))
161 .map(|s| s.as_str())
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn exact() {
170 assert!(satisfies("1.2.3", "1.2.3"));
171 assert!(satisfies("=1.2.3", "1.2.3"));
172 assert!(!satisfies("1.2.3", "1.2.4"));
173 }
174
175 #[test]
176 fn caret_major() {
177 assert!(satisfies("^1.2.3", "1.2.3"));
178 assert!(satisfies("^1.2.3", "1.9.0"));
179 assert!(!satisfies("^1.2.3", "2.0.0"));
180 assert!(!satisfies("^1.2.3", "1.2.2"));
181 assert!(satisfies("^1.2", "1.2.0"));
183 assert!(satisfies("^1.2", "1.5.9"));
184 assert!(!satisfies("^1.2", "2.0.0"));
185 assert!(!satisfies("^1.2", "1.1.9"));
186 assert!(satisfies("^1", "1.0.0") && satisfies("^1", "1.9.9") && !satisfies("^1", "2.0.0"));
188 assert!(satisfies("1.2", "1.4.0") && !satisfies("1.2", "2.0.0"));
190 }
191
192 #[test]
193 fn caret_zero() {
194 assert!(satisfies("^0.2.3", "0.2.3") && satisfies("^0.2.3", "0.2.9"));
196 assert!(!satisfies("^0.2.3", "0.3.0") && !satisfies("^0.2.3", "0.2.2"));
197 assert!(satisfies("^0.0.3", "0.0.3") && !satisfies("^0.0.3", "0.0.4"));
199 assert!(satisfies("^0.2", "0.2.5") && !satisfies("^0.2", "0.3.0"));
201 assert!(satisfies("^0", "0.9.9") && !satisfies("^0", "1.0.0"));
203 }
204
205 #[test]
206 fn tilde() {
207 assert!(satisfies("~1.2.3", "1.2.9") && !satisfies("~1.2.3", "1.3.0"));
209 assert!(satisfies("~1.2", "1.2.9") && !satisfies("~1.2", "1.3.0"));
211 assert!(satisfies("~1", "1.9.9") && !satisfies("~1", "2.0.0"));
213 }
214
215 #[test]
216 fn comparators_and_any() {
217 assert!(satisfies(">=1.0", "1.0.0") && satisfies(">=1.0", "9.9.9") && !satisfies(">=1.0", "0.9.9"));
218 assert!(satisfies(">1.0", "1.0.1") && !satisfies(">1.0", "1.0.0"));
219 assert!(satisfies("<2.0", "1.9.9") && !satisfies("<2.0", "2.0.0"));
220 assert!(satisfies("<=1.5", "1.5.0") && !satisfies("<=1.5", "1.5.1"));
221 for c in ["*", "latest", ""] {
222 assert!(satisfies(c, "3.1.4"), "{c} should match anything");
223 }
224 }
225
226 #[test]
227 fn best_match_picks_highest() {
228 let vs: Vec<String> = ["1.0.0", "1.2.0", "1.4.9", "2.0.0", "1.4.2"]
229 .iter().map(|s| s.to_string()).collect();
230 assert_eq!(best_match("^1.2", &vs), Some("1.4.9"));
231 assert_eq!(best_match("~1.4", &vs), Some("1.4.9"));
232 assert_eq!(best_match(">=1.0", &vs), Some("2.0.0"));
233 assert_eq!(best_match("^3", &vs), None);
234 assert_eq!(best_match("1.4.2", &vs), Some("1.4.2"));
235 }
236
237 #[test]
238 fn unparseable_never_matches() {
239 assert!(!satisfies("^1.2.3", "not-a-version"));
240 assert!(!satisfies("garbage", "1.2.3"));
241 }
242}