1use std::cmp::Ordering;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Version {
13 release: Vec<u64>,
14 pre: Option<(u8, u64)>,
17}
18
19impl Version {
20 pub fn parse(s: &str) -> Option<Version> {
22 let s = s.trim();
23 let s = s.strip_prefix('v').unwrap_or(s);
24 let s = s.split('+').next().unwrap_or(s);
26 let end = s
28 .find(|c: char| !c.is_ascii_digit() && c != '.')
29 .unwrap_or(s.len());
30 let (rel_str, rest) = s.split_at(end);
31 let release: Vec<u64> = rel_str
32 .split('.')
33 .filter(|p| !p.is_empty())
34 .map(|p| p.parse::<u64>().ok())
35 .collect::<Option<Vec<_>>>()?;
36 if release.is_empty() {
37 return None;
38 }
39 let pre = parse_pre(rest);
40 Some(Version { release, pre })
41 }
42
43 pub fn cmp_to(&self, other: &Version) -> Ordering {
45 let max = self.release.len().max(other.release.len());
46 for i in 0..max {
47 let a = self.release.get(i).copied().unwrap_or(0);
48 let b = other.release.get(i).copied().unwrap_or(0);
49 match a.cmp(&b) {
50 Ordering::Equal => continue,
51 ord => return ord,
52 }
53 }
54 match (self.pre, other.pre) {
56 (None, None) => Ordering::Equal,
57 (Some(_), None) => Ordering::Less,
58 (None, Some(_)) => Ordering::Greater,
59 (Some(a), Some(b)) => a.cmp(&b),
60 }
61 }
62}
63
64fn parse_pre(rest: &str) -> Option<(u8, u64)> {
66 let r = rest
67 .trim_start_matches(['.', '-', '_'])
68 .to_ascii_lowercase();
69 let (rank, tail) = if let Some(t) = r.strip_prefix("alpha") {
70 (0u8, t)
71 } else if let Some(t) = r.strip_prefix('a') {
72 (0, t)
73 } else if let Some(t) = r.strip_prefix("beta") {
74 (1, t)
75 } else if let Some(t) = r.strip_prefix('b') {
76 (1, t)
77 } else if let Some(t) = r.strip_prefix("rc") {
78 (2, t)
79 } else if let Some(t) = r.strip_prefix("c") {
80 (2, t)
81 } else {
82 return None;
83 };
84 let n: u64 = tail
85 .trim_start_matches(['.', '-', '_'])
86 .chars()
87 .take_while(|c| c.is_ascii_digit())
88 .collect::<String>()
89 .parse()
90 .unwrap_or(0);
91 Some((rank, n))
92}
93
94fn satisfies_one(version: &Version, constraint: &str) -> bool {
97 let c = constraint.trim();
98 if c.is_empty() {
99 return true;
100 }
101 let (op, rhs) = split_op(c);
102 if (op == "==" || op == "!=") && rhs.ends_with(".*") {
104 let prefix = rhs.trim_end_matches(".*");
105 let Some(pv) = Version::parse(prefix) else {
106 return false;
107 };
108 let matches_prefix = version.release.len() >= pv.release.len()
109 && version.release[..pv.release.len()] == pv.release[..];
110 return if op == "==" {
111 matches_prefix
112 } else {
113 !matches_prefix
114 };
115 }
116 let Some(bound) = Version::parse(rhs) else {
117 return false;
118 };
119 let ord = version.cmp_to(&bound);
120 match op {
121 "==" => ord == Ordering::Equal,
122 "!=" => ord != Ordering::Equal,
123 "<" => ord == Ordering::Less,
124 "<=" => ord != Ordering::Greater,
125 ">" => ord == Ordering::Greater,
126 ">=" => ord != Ordering::Less,
127 "~=" => compatible_release(version, &bound),
128 _ => false,
129 }
130}
131
132fn compatible_release(version: &Version, bound: &Version) -> bool {
134 if version.cmp_to(bound) == Ordering::Less {
135 return false;
136 }
137 if bound.release.len() < 2 {
138 return true; }
140 let keep = bound.release.len() - 1;
141 version.release.len() >= keep && version.release[..keep] == bound.release[..keep]
142}
143
144fn split_op(c: &str) -> (&str, &str) {
145 for op in ["==", "!=", "<=", ">=", "~=", "<", ">"] {
146 if let Some(rest) = c.strip_prefix(op) {
147 return (op, rest.trim());
148 }
149 }
150 ("==", c)
152}
153
154pub fn specs_intersect(a: &str, b: &str) -> bool {
164 let mut bounds: Vec<String> = Vec::new();
165 for spec in [a, b] {
166 for part in spec.split(',').map(str::trim).filter(|p| !p.is_empty()) {
167 let (_, rhs) = split_op(part);
168 let rhs = rhs.trim_end_matches(".*").trim();
169 if Version::parse(rhs).is_some() {
170 bounds.push(rhs.to_string());
171 }
172 }
173 }
174 let mut candidates: Vec<String> = vec!["0".to_string()];
175 for bnd in &bounds {
176 candidates.push(bnd.clone());
177 candidates.push(format!("{bnd}.1")); if let Some(inc) = incr_last(bnd) {
179 candidates.push(inc);
180 }
181 }
182 candidates
183 .iter()
184 .any(|c| matches_spec(c, a) && matches_spec(c, b))
185}
186
187fn incr_last(v: &str) -> Option<String> {
189 let parsed = Version::parse(v)?;
190 let mut rel = parsed.release;
191 let last = rel.last_mut()?;
192 *last += 1;
193 Some(
194 rel.iter()
195 .map(|n| n.to_string())
196 .collect::<Vec<_>>()
197 .join("."),
198 )
199}
200
201pub fn matches_spec(version: &str, spec: &str) -> bool {
204 let Some(v) = Version::parse(version) else {
205 return false;
206 };
207 spec.split(',')
208 .map(str::trim)
209 .filter(|p| !p.is_empty())
210 .all(|p| satisfies_one(&v, p))
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn orders_releases_and_prereleases() {
219 assert_eq!(
220 Version::parse("1.2.0")
221 .unwrap()
222 .cmp_to(&Version::parse("1.10.0").unwrap()),
223 Ordering::Less
224 );
225 assert_eq!(
227 Version::parse("1.0rc1")
228 .unwrap()
229 .cmp_to(&Version::parse("1.0").unwrap()),
230 Ordering::Less
231 );
232 assert_eq!(
233 Version::parse("1.0a1")
234 .unwrap()
235 .cmp_to(&Version::parse("1.0b1").unwrap()),
236 Ordering::Less
237 );
238 }
239
240 #[test]
241 fn matches_ranges() {
242 assert!(matches_spec("2.4.1", "<2.11.3"));
243 assert!(!matches_spec("2.11.3", "<2.11.3"));
244 assert!(matches_spec("1.5", ">=1.0,<2.0"));
245 assert!(!matches_spec("2.0", ">=1.0,<2.0"));
246 assert!(matches_spec("1.4.7", "==1.4.*"));
247 assert!(!matches_spec("1.5.0", "==1.4.*"));
248 assert!(matches_spec("1.4.9", "~=1.4.2"));
249 assert!(!matches_spec("1.5.0", "~=1.4.2"));
250 assert!(matches_spec("3.1.2", ">=3.1.0"));
251 }
252
253 #[test]
254 fn unparseable_is_no_match() {
255 assert!(!matches_spec("not-a-version", "<2.0"));
256 assert!(!matches_spec("1.0", "≤2.0")); }
258
259 #[test]
260 fn specifier_set_intersection() {
261 assert!(specs_intersect(">=2.0", "<2.11.3"));
263 assert!(specs_intersect(">=1.0,<3.0", ">=2.0,<2.5"));
264 assert!(specs_intersect("", "<2.0")); assert!(!specs_intersect(">=2.11.3", "<2.11.3"));
267 assert!(!specs_intersect(">=3.0", "<2.0"));
268 assert!(!specs_intersect(">=1.0,<2.0", ">=2.0"));
269 assert!(specs_intersect("~=1.4", "==1.4.7"));
271 assert!(!specs_intersect("~=1.4", "==2.0.0"));
272 assert!(specs_intersect(">=1.0", "==1.5.*"));
273 }
274}