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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
167pub enum Bump {
168 Patch,
169 Minor,
170 Major,
171}
172
173pub fn bump_between(prev: &str, new: &str) -> Option<Bump> {
177 let p = parse_exact(prev)?;
178 let n = parse_exact(new)?;
179 if n <= p {
180 return None;
181 }
182 if n.0 > p.0 {
183 Some(Bump::Major)
184 } else if n.1 > p.1 {
185 Some(Bump::Minor)
186 } else {
187 Some(Bump::Patch)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn bumps() {
197 assert_eq!(bump_between("1.0.0", "2.0.0"), Some(Bump::Major));
198 assert_eq!(bump_between("1.2.0", "1.3.0"), Some(Bump::Minor));
199 assert_eq!(bump_between("1.2.3", "1.2.4"), Some(Bump::Patch));
200 assert_eq!(bump_between("1.2.3", "2.0.0"), Some(Bump::Major));
202 assert_eq!(bump_between("1.2.3", "1.2.3"), None);
204 assert_eq!(bump_between("1.2.3", "1.2.0"), None);
205 assert_eq!(bump_between("1.2.3", "nope"), None);
206 assert!(Bump::Major > Bump::Minor && Bump::Minor > Bump::Patch);
208 }
209
210 #[test]
211 fn exact() {
212 assert!(satisfies("1.2.3", "1.2.3"));
213 assert!(satisfies("=1.2.3", "1.2.3"));
214 assert!(!satisfies("1.2.3", "1.2.4"));
215 }
216
217 #[test]
218 fn caret_major() {
219 assert!(satisfies("^1.2.3", "1.2.3"));
220 assert!(satisfies("^1.2.3", "1.9.0"));
221 assert!(!satisfies("^1.2.3", "2.0.0"));
222 assert!(!satisfies("^1.2.3", "1.2.2"));
223 assert!(satisfies("^1.2", "1.2.0"));
225 assert!(satisfies("^1.2", "1.5.9"));
226 assert!(!satisfies("^1.2", "2.0.0"));
227 assert!(!satisfies("^1.2", "1.1.9"));
228 assert!(satisfies("^1", "1.0.0") && satisfies("^1", "1.9.9") && !satisfies("^1", "2.0.0"));
230 assert!(satisfies("1.2", "1.4.0") && !satisfies("1.2", "2.0.0"));
232 }
233
234 #[test]
235 fn caret_zero() {
236 assert!(satisfies("^0.2.3", "0.2.3") && satisfies("^0.2.3", "0.2.9"));
238 assert!(!satisfies("^0.2.3", "0.3.0") && !satisfies("^0.2.3", "0.2.2"));
239 assert!(satisfies("^0.0.3", "0.0.3") && !satisfies("^0.0.3", "0.0.4"));
241 assert!(satisfies("^0.2", "0.2.5") && !satisfies("^0.2", "0.3.0"));
243 assert!(satisfies("^0", "0.9.9") && !satisfies("^0", "1.0.0"));
245 }
246
247 #[test]
248 fn tilde() {
249 assert!(satisfies("~1.2.3", "1.2.9") && !satisfies("~1.2.3", "1.3.0"));
251 assert!(satisfies("~1.2", "1.2.9") && !satisfies("~1.2", "1.3.0"));
253 assert!(satisfies("~1", "1.9.9") && !satisfies("~1", "2.0.0"));
255 }
256
257 #[test]
258 fn comparators_and_any() {
259 assert!(satisfies(">=1.0", "1.0.0") && satisfies(">=1.0", "9.9.9") && !satisfies(">=1.0", "0.9.9"));
260 assert!(satisfies(">1.0", "1.0.1") && !satisfies(">1.0", "1.0.0"));
261 assert!(satisfies("<2.0", "1.9.9") && !satisfies("<2.0", "2.0.0"));
262 assert!(satisfies("<=1.5", "1.5.0") && !satisfies("<=1.5", "1.5.1"));
263 for c in ["*", "latest", ""] {
264 assert!(satisfies(c, "3.1.4"), "{c} should match anything");
265 }
266 }
267
268 #[test]
269 fn best_match_picks_highest() {
270 let vs: Vec<String> = ["1.0.0", "1.2.0", "1.4.9", "2.0.0", "1.4.2"]
271 .iter().map(|s| s.to_string()).collect();
272 assert_eq!(best_match("^1.2", &vs), Some("1.4.9"));
273 assert_eq!(best_match("~1.4", &vs), Some("1.4.9"));
274 assert_eq!(best_match(">=1.0", &vs), Some("2.0.0"));
275 assert_eq!(best_match("^3", &vs), None);
276 assert_eq!(best_match("1.4.2", &vs), Some("1.4.2"));
277 }
278
279 #[test]
280 fn unparseable_never_matches() {
281 assert!(!satisfies("^1.2.3", "not-a-version"));
282 assert!(!satisfies("garbage", "1.2.3"));
283 }
284}