1use crate::version::{Stability, Version};
19
20#[derive(Debug, thiserror::Error, PartialEq, Eq)]
21#[error("constraint outside the supported subset: {0:?}")]
22pub struct UnsupportedConstraint(pub String);
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25enum Simple {
26 Any,
27 Cmp(Op, Version),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31enum Op {
32 Eq,
33 Ne,
34 Lt,
35 Le,
36 Gt,
37 Ge,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct Constraint {
43 groups: Vec<Vec<Simple>>,
44}
45
46impl Constraint {
47 pub fn parse(input: &str) -> Result<Self, UnsupportedConstraint> {
48 let mut groups = Vec::new();
49 for or_part in split_or(input) {
50 let or_part = or_part.trim();
51 if or_part.is_empty() {
52 return Err(UnsupportedConstraint(input.to_owned()));
53 }
54 groups.push(parse_and_group(or_part, input)?);
55 }
56 if groups.is_empty() {
57 return Err(UnsupportedConstraint(input.to_owned()));
58 }
59 Ok(Constraint { groups })
60 }
61
62 pub fn matches(&self, v: &Version) -> bool {
63 self.groups.iter().any(|group| {
64 group.iter().all(|c| match c {
65 Simple::Any => true,
66 Simple::Cmp(op, bound) => match op {
67 Op::Eq => v == bound,
68 Op::Ne => v != bound,
69 Op::Lt => v < bound,
70 Op::Le => v <= bound,
71 Op::Gt => v > bound,
72 Op::Ge => v >= bound,
73 },
74 })
75 })
76 }
77}
78
79fn split_or(s: &str) -> Vec<&str> {
80 if s.contains("||") {
82 s.split("||").collect()
83 } else if s.contains('|') {
84 s.split('|').collect()
85 } else {
86 vec![s]
87 }
88}
89
90fn parse_and_group(part: &str, original: &str) -> Result<Vec<Simple>, UnsupportedConstraint> {
91 let raw: Vec<&str> = part
93 .split([',', ' ', '\t'])
94 .filter(|t| !t.is_empty())
95 .collect();
96 let mut tokens: Vec<String> = Vec::new();
97 let mut i = 0;
98 while i < raw.len() {
99 let t = raw[i];
100 if matches!(t, ">=" | ">" | "<=" | "<" | "==" | "=" | "!=" | "<>") && i + 1 < raw.len() {
101 tokens.push(format!("{}{}", t, raw[i + 1]));
102 i += 2;
103 } else if t == "-" && !tokens.is_empty() && i + 1 < raw.len() {
104 let from = tokens.pop().unwrap_or_default();
105 tokens.push(format!("{from} - {}", raw[i + 1]));
106 i += 2;
107 } else {
108 tokens.push(t.to_owned());
109 i += 1;
110 }
111 }
112
113 let mut out = Vec::new();
114 for token in tokens {
115 parse_simple(&token, original, &mut out)?;
116 }
117 if out.is_empty() {
118 return Err(UnsupportedConstraint(original.to_owned()));
119 }
120 Ok(out)
121}
122
123fn has_stability_suffix(s: &str) -> bool {
125 s.chars()
126 .any(|c| !(c.is_ascii_digit() || c == '.' || c == 'v' || c == 'V'))
127}
128
129fn parse_version(s: &str, original: &str) -> Result<Version, UnsupportedConstraint> {
130 Version::parse(s).map_err(|_| UnsupportedConstraint(original.to_owned()))
131}
132
133fn bump(v: &Version, position: usize) -> Version {
136 let mut parts = v.parts;
137 parts[position - 1] += 1;
138 for p in parts.iter_mut().skip(position) {
139 *p = 0;
140 }
141 Version {
142 parts,
143 stability: Stability::Dev,
144 pre_number: 0,
145 }
146}
147
148fn as_dev_floor(mut v: Version) -> Version {
149 v.stability = Stability::Dev;
150 v.pre_number = 0;
151 v
152}
153
154fn parse_simple(
155 token: &str,
156 original: &str,
157 out: &mut Vec<Simple>,
158) -> Result<(), UnsupportedConstraint> {
159 let t = token.trim();
160
161 if let Some((from, to)) = t.split_once(" - ") {
163 let low = parse_version(from, original)?;
164 let low = if has_stability_suffix(from) {
165 low
166 } else {
167 as_dev_floor(low)
168 };
169 out.push(Simple::Cmp(Op::Ge, low));
170
171 let to_trim = to.trim();
172 let dotted = to_trim.trim_start_matches(['v', 'V']);
173 let numeric_parts = dotted.split('.').count();
174 let high = parse_version(to_trim, original)?;
175 if has_stability_suffix(to_trim) || numeric_parts >= 3 {
176 out.push(Simple::Cmp(Op::Le, high));
177 } else {
178 out.push(Simple::Cmp(Op::Lt, bump(&high, numeric_parts)));
179 }
180 return Ok(());
181 }
182
183 if t.chars().all(|c| matches!(c, '*' | 'x' | 'X' | '.' | 'v')) && t.contains(['*', 'x', 'X']) {
185 out.push(Simple::Any);
186 return Ok(());
187 }
188
189 if let Some(stem) = t.strip_suffix(".*").or_else(|| t.strip_suffix(".x")) {
191 let base = parse_version(stem, original)?;
192 if has_stability_suffix(stem) {
193 return Err(UnsupportedConstraint(original.to_owned()));
194 }
195 let position = stem.trim_start_matches(['v', 'V']).split('.').count();
196 if base.parts == [0, 0, 0, 0] {
197 out.push(Simple::Cmp(Op::Lt, bump(&base, position)));
198 } else {
199 out.push(Simple::Cmp(Op::Ge, as_dev_floor(base.clone())));
200 out.push(Simple::Cmp(Op::Lt, bump(&base, position)));
201 }
202 return Ok(());
203 }
204
205 if let Some(rest) = t.strip_prefix('^') {
207 let v = parse_version(rest, original)?;
208 let low = if has_stability_suffix(rest) {
209 v.clone()
210 } else {
211 as_dev_floor(v.clone())
212 };
213 let stem = rest.split(['-', '+']).next().unwrap_or(rest);
215 let given = stem.trim_start_matches(['v', 'V']).split('.').count();
216 let position = if v.parts[0] != 0 || given < 2 {
217 1
218 } else if v.parts[1] != 0 || given < 3 {
219 2
220 } else {
221 3
222 };
223 out.push(Simple::Cmp(Op::Ge, low));
224 out.push(Simple::Cmp(Op::Lt, bump(&v, position)));
225 return Ok(());
226 }
227 if let Some(rest) = t.strip_prefix('~') {
228 if rest.starts_with('>') {
229 return Err(UnsupportedConstraint(original.to_owned()));
230 }
231 let v = parse_version(rest, original)?;
232 let low = if has_stability_suffix(rest) {
233 v.clone()
234 } else {
235 as_dev_floor(v.clone())
236 };
237 let stem = rest.split(['-', '+']).next().unwrap_or(rest);
238 let given = stem.trim_start_matches(['v', 'V']).split('.').count();
239 let position = given.clamp(1, 4).max(2) - 1; out.push(Simple::Cmp(Op::Ge, low));
241 out.push(Simple::Cmp(Op::Lt, bump(&v, position)));
242 return Ok(());
243 }
244
245 let (op, rest) = if let Some(r) = t.strip_prefix(">=") {
247 (Op::Ge, r)
248 } else if let Some(r) = t.strip_prefix("<=") {
249 (Op::Le, r)
250 } else if let Some(r) = t.strip_prefix("<>").or_else(|| t.strip_prefix("!=")) {
251 (Op::Ne, r)
252 } else if let Some(r) = t.strip_prefix('>') {
253 (Op::Gt, r)
254 } else if let Some(r) = t.strip_prefix('<') {
255 (Op::Lt, r)
256 } else if let Some(r) = t.strip_prefix("==").or_else(|| t.strip_prefix('=')) {
257 (Op::Eq, r)
258 } else {
259 (Op::Eq, t)
260 };
261 let rest = rest.trim();
262 let v = parse_version(rest, original)?;
263 let v = if matches!(op, Op::Lt | Op::Ge) && !has_stability_suffix(rest) {
265 as_dev_floor(v)
266 } else {
267 v
268 };
269 out.push(Simple::Cmp(op, v));
270 Ok(())
271}
272
273pub fn satisfies(version: &str, constraint: &str) -> Result<bool, UnsupportedConstraint> {
275 let v =
276 Version::parse(version).map_err(|_| UnsupportedConstraint(format!("version {version}")))?;
277 Ok(Constraint::parse(constraint)?.matches(&v))
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn sat(v: &str, c: &str) -> bool {
285 satisfies(v, c).unwrap_or_else(|e| panic!("{e}"))
286 }
287
288 #[test]
289 fn operators_and_dev_floors() {
290 assert!(sat("8.5.10", ">=8.1"));
291 assert!(sat("8.1.0-beta1", ">=8.1")); assert!(!sat("2.0.0-beta1", "<2.0")); assert!(sat("1.9.9", "<2.0"));
294 assert!(!sat("8.1.0-beta1", ">8.1")); assert!(sat("8.1.1", ">8.1"));
296 assert!(sat("2.0.0", "<=2.0"));
297 assert!(!sat("2.0.1", "<=2.0"));
298 assert!(sat("1.2.3", "1.2.3"));
299 assert!(!sat("1.2.3", "!=1.2.3"));
300 }
301
302 #[test]
303 fn caret_tilde_wildcards() {
304 assert!(sat("8.5.10", "^8.1"));
305 assert!(!sat("9.0.0-alpha1", "^8.1"));
306 assert!(sat("8.1.0-RC1", "^8.1")); assert!(sat("0.3.7", "^0.3"));
308 assert!(!sat("0.4.0", "^0.3"));
309 assert!(!sat("0.0.4", "^0.0.3"));
310 assert!(sat("1.2.9", "~1.2.3"));
311 assert!(!sat("1.3.0", "~1.2.3"));
312 assert!(sat("1.9.0", "~1.2"));
313 assert!(!sat("2.0.0-alpha1", "~1.2"));
314 assert!(sat("123.4.5", "*"));
315 assert!(sat("1.2.9-beta1", "1.2.*"));
316 assert!(!sat("1.3.0-dev", "1.2.*"));
317 }
318
319 #[test]
320 fn and_or_and_hyphen() {
321 assert!(sat("7.4.33", "^7.4|^8.0"));
322 assert!(sat("8.0.2", "^7.4||^8.0"));
323 assert!(!sat("8.0.2", "^8.1|^7.4"));
324 assert!(sat("1.5.0", ">=1.0 <2.0"));
325 assert!(sat("1.5.0", ">=1.0,<2.0"));
326 assert!(sat("1.5.0", ">= 1.0 , < 2.0"));
327 assert!(sat("2.0.0", "1.0 - 2.0")); assert!(sat("2.0.9", "1.0 - 2.0"));
329 assert!(!sat("2.1.0", "1.0 - 2.0"));
330 assert!(sat("2.0.0", "1.0.0 - 2.0.0")); assert!(!sat("2.0.1", "1.0.0 - 2.0.0"));
332 }
333
334 #[test]
335 fn unsupported_forms_error_out() {
336 for c in ["dev-master", "1.0 as 2.0", "@dev", "~>1.2"] {
337 assert!(
338 satisfies("1.0.0", c).is_err(),
339 "{c} should have been rejected"
340 );
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
349pub struct LowerBound {
350 pub version: Version,
351 pub inclusive: bool,
352}
353
354impl LowerBound {
355 fn is_higher_than(&self, other: &LowerBound) -> bool {
358 match self.version.cmp(&other.version) {
359 std::cmp::Ordering::Greater => true,
360 std::cmp::Ordering::Less => false,
361 std::cmp::Ordering::Equal => !self.inclusive && other.inclusive,
362 }
363 }
364}
365
366impl Constraint {
367 pub fn lower_bound(&self) -> Option<LowerBound> {
368 let mut result: Option<LowerBound> = None;
369 for group in &self.groups {
370 let mut group_bound: Option<LowerBound> = None;
372 for c in group {
373 let candidate = match c {
374 Simple::Cmp(Op::Ge, v) | Simple::Cmp(Op::Eq, v) => LowerBound {
375 version: v.clone(),
376 inclusive: true,
377 },
378 Simple::Cmp(Op::Gt, v) => LowerBound {
379 version: v.clone(),
380 inclusive: false,
381 },
382 _ => continue,
383 };
384 if group_bound
385 .as_ref()
386 .is_none_or(|g| candidate.is_higher_than(g))
387 {
388 group_bound = Some(candidate);
389 }
390 }
391 let gb = group_bound?;
393 if result.as_ref().is_none_or(|r| r.is_higher_than(&gb)) {
394 result = Some(gb);
395 }
396 }
397 result
398 }
399}
400
401#[cfg(test)]
402mod lower_bound_tests {
403 use super::*;
404
405 fn lb(c: &str) -> Option<(String, bool)> {
406 Constraint::parse(c).expect(c).lower_bound().map(|b| {
407 (
408 format!(
409 "{}.{}.{}.{}",
410 b.version.parts[0], b.version.parts[1], b.version.parts[2], b.version.parts[3]
411 ),
412 b.inclusive,
413 )
414 })
415 }
416
417 #[test]
418 fn bounds() {
419 assert_eq!(lb("^8.2"), Some(("8.2.0.0".into(), true)));
420 assert_eq!(lb(">=8.1 <8.4"), Some(("8.1.0.0".into(), true)));
421 assert_eq!(lb(">8.1"), Some(("8.1.0.0".into(), false)));
422 assert_eq!(lb("^7.4 || ^8.0"), Some(("7.4.0.0".into(), true)));
423 assert_eq!(lb("*"), None);
424 assert_eq!(lb("<8.0"), None);
425 assert_eq!(lb("8.2.1"), Some(("8.2.1.0".into(), true)));
426 }
427}