1use crate::phpver::{version_compare, version_compare_op};
9use crate::version::{
10 self, group, normalize, regex, VersionError, MODIFIER_REGEX, STABILITIES_REGEX,
11};
12use pcre2::bytes::Regex;
13use std::cmp::Ordering;
14use std::fmt;
15use std::sync::OnceLock;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum Op {
19 Eq,
20 Lt,
21 Le,
22 Gt,
23 Ge,
24 Ne,
25}
26
27impl Op {
28 pub fn parse(s: &str) -> Option<Op> {
29 Some(match s {
30 "=" | "==" => Op::Eq,
31 "<" => Op::Lt,
32 "<=" => Op::Le,
33 ">" => Op::Gt,
34 ">=" => Op::Ge,
35 "<>" | "!=" => Op::Ne,
36 _ => return None,
37 })
38 }
39
40 pub fn as_str(self) -> &'static str {
42 match self {
43 Op::Eq => "==",
44 Op::Lt => "<",
45 Op::Le => "<=",
46 Op::Gt => ">",
47 Op::Ge => ">=",
48 Op::Ne => "!=",
49 }
50 }
51
52 fn no_equal(self) -> &'static str {
54 match self {
55 Op::Eq => "",
56 Op::Lt | Op::Le => "<",
57 Op::Gt | Op::Ge => ">",
58 Op::Ne => "!",
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Hash)]
65pub struct Bound {
66 pub version: String,
67 pub inclusive: bool,
68}
69
70impl Bound {
71 pub fn zero() -> Bound {
72 Bound {
73 version: "0.0.0.0-dev".into(),
74 inclusive: true,
75 }
76 }
77 pub fn positive_infinity() -> Bound {
78 Bound {
79 version: format!("{}.0.0.0", i64::MAX),
80 inclusive: false,
81 }
82 }
83 pub fn is_zero(&self) -> bool {
84 self.version == "0.0.0.0-dev" && self.inclusive
85 }
86 pub fn is_positive_infinity(&self) -> bool {
87 self.version == format!("{}.0.0.0", i64::MAX) && !self.inclusive
88 }
89 pub fn compare_to(&self, other: &Bound, op: &str) -> bool {
91 if self == other {
92 return false;
93 }
94 let c = version_compare(&self.version, &other.version);
95 if c != Ordering::Equal {
96 return match op {
97 ">" => c == Ordering::Greater,
98 _ => c == Ordering::Less,
99 };
100 }
101 if op == ">" {
102 other.inclusive
103 } else {
104 !other.inclusive
105 }
106 }
107}
108
109impl fmt::Display for Bound {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(
112 f,
113 "{} [{}]",
114 self.version,
115 if self.inclusive {
116 "inclusive"
117 } else {
118 "exclusive"
119 }
120 )
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub enum Constraint {
126 Single {
128 op: Op,
129 version: String,
130 },
131 Multi {
133 constraints: Vec<Constraint>,
134 conjunctive: bool,
135 },
136 MatchAll,
137 MatchNone,
138}
139
140impl Constraint {
141 pub fn new(op: Op, version: impl Into<String>) -> Constraint {
142 Constraint::Single {
143 op,
144 version: version.into(),
145 }
146 }
147
148 pub fn is_single(&self) -> bool {
149 matches!(self, Constraint::Single { .. })
150 }
151
152 fn version_compare_branches(a: &str, b: &str, op: Op, compare_branches: bool) -> bool {
154 let a_branch = a.starts_with("dev-");
155 let b_branch = b.starts_with("dev-");
156 if op == Op::Ne && (a_branch || b_branch) {
157 return a != b;
158 }
159 if a_branch && b_branch {
160 return op == Op::Eq && a == b;
161 }
162 if !compare_branches && (a_branch || b_branch) {
163 return false;
164 }
165 version_compare_op(a, b, op.as_str())
166 }
167
168 pub fn match_specific(&self, provider: &Constraint, compare_branches: bool) -> bool {
172 let (
173 Constraint::Single { op, version },
174 Constraint::Single {
175 op: pop,
176 version: pversion,
177 },
178 ) = (self, provider)
179 else {
180 return false;
181 };
182 let no_equal_op = op.no_equal();
183 let provider_no_equal_op = pop.no_equal();
184 let is_equal_op = *op == Op::Eq;
185 let is_non_equal_op = *op == Op::Ne;
186 let is_provider_equal_op = *pop == Op::Eq;
187 let is_provider_non_equal_op = *pop == Op::Ne;
188
189 if is_non_equal_op || is_provider_non_equal_op {
190 if is_non_equal_op
191 && !is_provider_non_equal_op
192 && !is_provider_equal_op
193 && pversion.starts_with("dev-")
194 {
195 return false;
196 }
197 if is_provider_non_equal_op
198 && !is_non_equal_op
199 && !is_equal_op
200 && version.starts_with("dev-")
201 {
202 return false;
203 }
204 if !is_equal_op && !is_provider_equal_op {
205 return true;
206 }
207 return Self::version_compare_branches(pversion, version, Op::Ne, compare_branches);
208 }
209 if *op != Op::Eq && no_equal_op == provider_no_equal_op {
210 return !(version.starts_with("dev-") || pversion.starts_with("dev-"));
211 }
212 let (version1, version2, operator) = if is_equal_op {
213 (version.as_str(), pversion.as_str(), *pop)
214 } else {
215 (pversion.as_str(), version.as_str(), *op)
216 };
217 if Self::version_compare_branches(version1, version2, operator, compare_branches) {
218 return !(pop.as_str() == provider_no_equal_op
219 && op.as_str() != no_equal_op
220 && version_compare_op(pversion, version, "=="));
221 }
222 false
223 }
224
225 pub fn matches(&self, provider: &Constraint) -> bool {
227 match self {
228 Constraint::MatchAll => true,
229 Constraint::MatchNone => false,
230 Constraint::Single { .. } => match provider {
231 Constraint::Single { .. } => self.match_specific(provider, false),
232 other => other.matches(self),
233 },
234 Constraint::Multi {
235 constraints,
236 conjunctive,
237 } => {
238 if !conjunctive {
239 return constraints.iter().any(|c| provider.matches(c));
240 }
241 if let Constraint::Multi {
242 conjunctive: false, ..
243 } = provider
244 {
245 return provider.matches(self);
246 }
247 constraints.iter().all(|c| provider.matches(c))
248 }
249 }
250 }
251
252 pub fn matches_version(&self, version: &str) -> bool {
255 self.matches(&Constraint::new(Op::Eq, version))
256 }
257
258 pub fn create(constraints: Vec<Constraint>, conjunctive: bool) -> Constraint {
260 if constraints.is_empty() {
261 return Constraint::MatchAll;
262 }
263 if constraints.len() == 1 {
264 return constraints
265 .into_iter()
266 .next()
267 .unwrap_or(Constraint::MatchAll);
268 }
269 if let Some((optimized, conj)) = Self::optimize_constraints(&constraints, conjunctive) {
270 if optimized.len() == 1 {
271 return optimized.into_iter().next().unwrap_or(Constraint::MatchAll);
272 }
273 return Constraint::Multi {
274 constraints: optimized,
275 conjunctive: conj,
276 };
277 }
278 Constraint::Multi {
279 constraints,
280 conjunctive,
281 }
282 }
283
284 fn optimize_constraints(
286 constraints: &[Constraint],
287 conjunctive: bool,
288 ) -> Option<(Vec<Constraint>, bool)> {
289 if conjunctive {
290 return None;
291 }
292 let two_bounds = |c: &Constraint| -> Option<(String, String, Constraint, Constraint)> {
293 let Constraint::Multi {
294 constraints: parts,
295 conjunctive: true,
296 } = c
297 else {
298 return None;
299 };
300 if parts.len() != 2 {
301 return None;
302 }
303 let s0 = parts[0].to_string();
304 let s1 = parts[1].to_string();
305 if s0.starts_with(">=") && s1.starts_with('<') && !s1.starts_with("<=") {
306 Some((s0, s1, parts[0].clone(), parts[1].clone()))
307 } else {
308 None
309 }
310 };
311 let mut left = constraints[0].clone();
312 let mut merged: Vec<Constraint> = Vec::new();
313 let mut optimized = false;
314 for right in &constraints[1..] {
315 let fused = match (two_bounds(&left), two_bounds(right)) {
316 (Some((_, l1, l_lo, _)), Some((r0, _, _, r_hi))) if l1[2..] == r0[3..] => {
317 Some(Constraint::Multi {
318 constraints: vec![l_lo, r_hi],
319 conjunctive: true,
320 })
321 }
322 _ => None,
323 };
324 if let Some(f) = fused {
325 optimized = true;
326 left = f;
327 } else {
328 merged.push(left);
329 left = right.clone();
330 }
331 }
332 if optimized {
333 merged.push(left);
334 return Some((merged, false));
335 }
336 None
337 }
338
339 pub fn lower_bound(&self) -> Bound {
340 self.bounds().0
341 }
342
343 pub fn upper_bound(&self) -> Bound {
344 self.bounds().1
345 }
346
347 fn bounds(&self) -> (Bound, Bound) {
349 match self {
350 Constraint::MatchAll => (Bound::zero(), Bound::positive_infinity()),
351 Constraint::MatchNone => {
352 let b = Bound {
353 version: "0.0.0.0-dev".into(),
354 inclusive: false,
355 };
356 (b.clone(), b)
357 }
358 Constraint::Single { op, version } => {
359 if version.starts_with("dev-") {
360 return (Bound::zero(), Bound::positive_infinity());
361 }
362 let b = |inc: bool| Bound {
363 version: version.clone(),
364 inclusive: inc,
365 };
366 match op {
367 Op::Eq => (b(true), b(true)),
368 Op::Lt => (Bound::zero(), b(false)),
369 Op::Le => (Bound::zero(), b(true)),
370 Op::Gt => (b(false), Bound::positive_infinity()),
371 Op::Ge => (b(true), Bound::positive_infinity()),
372 Op::Ne => (Bound::zero(), Bound::positive_infinity()),
373 }
374 }
375 Constraint::Multi {
376 constraints,
377 conjunctive,
378 } => {
379 let mut lower: Option<Bound> = None;
380 let mut upper: Option<Bound> = None;
381 for c in constraints {
382 let (l, u) = c.bounds();
383 match (&lower, &upper) {
384 (Some(cl), Some(cu)) => {
385 if l.compare_to(cl, if *conjunctive { ">" } else { "<" }) {
386 lower = Some(l);
387 }
388 if u.compare_to(cu, if *conjunctive { "<" } else { ">" }) {
389 upper = Some(u);
390 }
391 }
392 _ => {
393 lower = Some(l);
394 upper = Some(u);
395 }
396 }
397 }
398 (
399 lower.unwrap_or_else(Bound::zero),
400 upper.unwrap_or_else(Bound::positive_infinity),
401 )
402 }
403 }
404 }
405}
406
407impl fmt::Display for Constraint {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410 match self {
411 Constraint::Single { op, version } => write!(f, "{} {}", op.as_str(), version),
412 Constraint::Multi {
413 constraints,
414 conjunctive,
415 } => {
416 let parts: Vec<String> = constraints.iter().map(|c| c.to_string()).collect();
417 write!(
418 f,
419 "[{}]",
420 parts.join(if *conjunctive { " " } else { " || " })
421 )
422 }
423 Constraint::MatchAll => write!(f, "*"),
424 Constraint::MatchNone => write!(f, "[]"),
425 }
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq)]
431pub struct ParsedConstraint {
432 pub constraint: Constraint,
433 pub pretty: String,
434}
435
436pub fn parse_constraints(input: &str) -> Result<ParsedConstraint, VersionError> {
440 thread_local! {
441 static CACHE: std::cell::RefCell<std::collections::HashMap<String, ParsedConstraint>> =
442 std::cell::RefCell::new(std::collections::HashMap::new());
443 }
444 if let Some(hit) = CACHE.with(|c| c.borrow().get(input).cloned()) {
445 return Ok(hit);
446 }
447 let parsed = parse_constraints_uncached(input)?;
448 CACHE.with(|c| {
449 c.borrow_mut().insert(input.to_owned(), parsed.clone());
450 });
451 Ok(parsed)
452}
453
454fn parse_constraints_uncached(input: &str) -> Result<ParsedConstraint, VersionError> {
455 static OR: OnceLock<Regex> = OnceLock::new();
456 static AND: OnceLock<Regex> = OnceLock::new();
457 let pretty = input.to_owned();
458 let trimmed = input.trim();
459 let or_parts = preg_split(regex(&OR, r"\s*\|\|?\s*", false), trimmed);
460 let mut or_groups: Vec<Constraint> = Vec::new();
461 for or_part in or_parts {
462 let and_parts = preg_split(
463 regex(
464 &AND,
465 r"(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)",
466 false,
467 ),
468 &or_part,
469 );
470 let objects: Vec<Constraint> = if and_parts.len() > 1 {
471 let mut out = Vec::new();
472 for p in &and_parts {
473 out.extend(parse_constraint(p)?);
474 }
475 out
476 } else {
477 parse_constraint(&and_parts[0])?
478 };
479 or_groups.push(if objects.len() == 1 {
480 objects.into_iter().next().unwrap_or(Constraint::MatchAll)
481 } else {
482 Constraint::Multi {
483 constraints: objects,
484 conjunctive: true,
485 }
486 });
487 }
488 Ok(ParsedConstraint {
489 constraint: Constraint::create(or_groups, false),
490 pretty,
491 })
492}
493
494fn preg_split(re: &Regex, subject: &str) -> Vec<String> {
497 let bytes = subject.as_bytes();
498 let mut out = Vec::new();
499 let mut last = 0;
500 for m in re.find_iter(bytes).flatten() {
501 if m.start() == m.end() && m.start() == last && last == bytes.len() {
502 break;
503 }
504 out.push(subject[last..m.start()].to_owned());
505 last = m.end();
506 }
507 out.push(subject[last..].to_owned());
508 out
509}
510
511fn manipulate_version_string(
513 matches: &[String],
514 position: usize,
515 increment: i64,
516) -> Option<String> {
517 let mut m: Vec<String> = matches.to_vec();
518 let mut position = position;
519 let mut i = 4;
520 while i > 0 {
521 if i > position {
522 m[i] = "0".to_owned();
523 } else if i == position && increment != 0 {
524 let v: i64 = m[i].parse().unwrap_or(0) + increment;
525 if v < 0 {
526 m[i] = "0".to_owned();
527 position -= 1;
528 if i == 1 {
529 return None;
530 }
531 } else {
532 m[i] = v.to_string();
533 }
534 }
535 i -= 1;
536 }
537 Some(format!("{}.{}.{}.{}", m[1], m[2], m[3], m[4]))
538}
539
540fn empty(s: &str) -> bool {
541 s.is_empty() || s == "0"
542}
543
544fn parse_constraint(input: &str) -> Result<Vec<Constraint>, VersionError> {
547 static AS: OnceLock<Regex> = OnceLock::new();
548 static STAB: OnceLock<Regex> = OnceLock::new();
549 static REF: OnceLock<Regex> = OnceLock::new();
550 static ANY: OnceLock<Regex> = OnceLock::new();
551 static TILDE: OnceLock<Regex> = OnceLock::new();
552 static CARET: OnceLock<Regex> = OnceLock::new();
553 static XRANGE: OnceLock<Regex> = OnceLock::new();
554 static HYPHEN: OnceLock<Regex> = OnceLock::new();
555 static BASIC: OnceLock<Regex> = OnceLock::new();
556 static MODIFIER_END: OnceLock<Regex> = OnceLock::new();
557
558 let mut constraint = input.to_owned();
559 let mut stability_modifier: Option<String> = None;
560
561 if let Ok(Some(caps)) =
562 regex(&AS, r"^([^,\s]++) ++as ++([^,\s]++)$", false).captures(constraint.as_bytes())
563 {
564 constraint = group(&caps, 1).to_owned();
565 }
566 if let Ok(Some(caps)) = regex(&STAB, &format!(r"^([^,\s]*?)@({STABILITIES_REGEX})$"), true)
567 .captures(constraint.as_bytes())
568 {
569 let head = group(&caps, 1).to_owned();
570 let stab = group(&caps, 2).to_owned();
571 constraint = if head.is_empty() {
572 "*".to_owned()
573 } else {
574 head
575 };
576 if stab != "stable" {
577 stability_modifier = Some(stab);
578 }
579 }
580 if let Ok(Some(caps)) =
581 regex(&REF, r"^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$", true).captures(constraint.as_bytes())
582 {
583 constraint = group(&caps, 1).to_owned();
584 }
585 if let Ok(Some(caps)) =
586 regex(&ANY, r"^(v)?[xX*](\.[xX*])*$", true).captures(constraint.as_bytes())
587 {
588 if !group(&caps, 1).is_empty() || !group(&caps, 2).is_empty() {
589 return Ok(vec![Constraint::new(Op::Ge, "0.0.0.0-dev")]);
590 }
591 return Ok(vec![Constraint::MatchAll]);
592 }
593
594 let version_regex = format!(
595 r"v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:{MODIFIER_REGEX}|\.([xX*][.-]?dev))(?:\+[^\s]+)?"
596 );
597 let groups_of = |re: &Regex, s: &str| -> Option<Vec<String>> {
598 let caps = re.captures(s.as_bytes()).ok()??;
599 Some(
600 (0..caps.len())
601 .map(|i| group(&caps, i).to_owned())
602 .collect(),
603 )
604 };
605
606 if let Some(m) = groups_of(
608 regex(&TILDE, &format!("^~>?{version_regex}$"), true),
609 &constraint,
610 ) {
611 if constraint.starts_with("~>") {
612 return Err(VersionError(format!(
613 "Could not parse version constraint {constraint}: Invalid operator \"~>\", you probably meant to use the \"~\" operator"
614 )));
615 }
616 let at = |i: usize| m.get(i).map(String::as_str).unwrap_or("");
617 let mut position = if !at(4).is_empty() {
618 4
619 } else if !at(3).is_empty() {
620 3
621 } else if !at(2).is_empty() {
622 2
623 } else {
624 1
625 };
626 if !empty(at(8)) {
627 position += 1;
628 }
629 let mut stability_suffix = String::new();
630 if empty(at(5)) && empty(at(7)) && empty(at(8)) {
631 stability_suffix.push_str("-dev");
632 }
633 let low = normalize(&format!("{constraint}{stability_suffix}")[1..], None)?;
634 let high_position = std::cmp::max(1, position - 1);
635 let high = manipulate_version_string(&m, high_position, 1)
636 .map(|v| format!("{v}-dev"))
637 .ok_or_else(|| {
638 VersionError(format!("Could not parse version constraint {constraint}"))
639 })?;
640 return Ok(vec![
641 Constraint::new(Op::Ge, low),
642 Constraint::new(Op::Lt, high),
643 ]);
644 }
645 if let Some(m) = groups_of(
647 regex(&CARET, &format!(r"^\^{version_regex}($)"), true),
648 &constraint,
649 ) {
650 let at = |i: usize| m.get(i).map(String::as_str).unwrap_or("");
651 let position = if at(1) != "0" || at(2).is_empty() {
652 1
653 } else if at(2) != "0" || at(3).is_empty() {
654 2
655 } else {
656 3
657 };
658 let mut stability_suffix = String::new();
659 if empty(at(5)) && empty(at(7)) && empty(at(8)) {
660 stability_suffix.push_str("-dev");
661 }
662 let low = normalize(&format!("{constraint}{stability_suffix}")[1..], None)?;
663 let high = manipulate_version_string(&m, position, 1)
664 .map(|v| format!("{v}-dev"))
665 .ok_or_else(|| {
666 VersionError(format!("Could not parse version constraint {constraint}"))
667 })?;
668 return Ok(vec![
669 Constraint::new(Op::Ge, low),
670 Constraint::new(Op::Lt, high),
671 ]);
672 }
673 if let Some(m) = groups_of(
675 regex(
676 &XRANGE,
677 r"^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$",
678 false,
679 ),
680 &constraint,
681 ) {
682 let at = |i: usize| m.get(i).map(String::as_str).unwrap_or("");
683 let position = if !at(3).is_empty() {
684 3
685 } else if !at(2).is_empty() {
686 2
687 } else {
688 1
689 };
690 let mut mm = m.clone();
691 while mm.len() < 5 {
692 mm.push(String::new());
693 }
694 let low = manipulate_version_string(&mm, position, 0)
695 .map(|v| format!("{v}-dev"))
696 .ok_or_else(|| {
697 VersionError(format!("Could not parse version constraint {constraint}"))
698 })?;
699 let high = manipulate_version_string(&mm, position, 1)
700 .map(|v| format!("{v}-dev"))
701 .ok_or_else(|| {
702 VersionError(format!("Could not parse version constraint {constraint}"))
703 })?;
704 if low == "0.0.0.0-dev" {
705 return Ok(vec![Constraint::new(Op::Lt, high)]);
706 }
707 return Ok(vec![
708 Constraint::new(Op::Ge, low),
709 Constraint::new(Op::Lt, high),
710 ]);
711 }
712 let hyphen = regex(
714 &HYPHEN,
715 &format!(r"^(?P<from>{version_regex}) +- +(?P<to>{version_regex})($)"),
716 true,
717 );
718 if let Some(m) = groups_of(hyphen, &constraint) {
719 let at = |i: usize| m.get(i).map(String::as_str).unwrap_or("");
721 let mut low_suffix = String::new();
722 if empty(at(6)) && empty(at(8)) && empty(at(9)) {
723 low_suffix.push_str("-dev");
724 }
725 let low = normalize(at(1), None)?;
726 let lower = Constraint::new(Op::Ge, format!("{low}{low_suffix}"));
727 let php_empty = |s: &str| s.is_empty();
729 let upper = if (!php_empty(at(12)) && !php_empty(at(13)))
730 || !empty(at(15))
731 || !empty(at(17))
732 || !empty(at(18))
733 {
734 Constraint::new(Op::Le, normalize(at(10), None)?)
735 } else {
736 let high_match = vec![
737 String::new(),
738 at(11).to_owned(),
739 at(12).to_owned(),
740 at(13).to_owned(),
741 at(14).to_owned(),
742 ];
743 normalize(at(10), None)?;
744 let pos = if php_empty(at(12)) { 1 } else { 2 };
745 let high = manipulate_version_string(&high_match, pos, 1)
746 .map(|v| format!("{v}-dev"))
747 .ok_or_else(|| {
748 VersionError(format!("Could not parse version constraint {constraint}"))
749 })?;
750 Constraint::new(Op::Lt, high)
751 };
752 return Ok(vec![lower, upper]);
753 }
754 if let Some(m) = groups_of(
756 regex(&BASIC, r"^(<>|!=|>=?|<=?|==?)?\s*(.*)", false),
757 &constraint,
758 ) {
759 let at = |i: usize| m.get(i).map(String::as_str).unwrap_or("");
760 let op_str = at(1);
761 let raw = at(2);
762 let version = match normalize(raw, None) {
763 Ok(v) => v,
764 Err(e) => {
765 if raw.ends_with("-dev")
766 && raw
767 .bytes()
768 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'/'))
769 {
770 normalize(&format!("dev-{}", &raw[..raw.len() - 4]), None)?
771 } else {
772 return Err(VersionError(format!(
773 "Could not parse version constraint {constraint}: {}",
774 e.0
775 )));
776 }
777 }
778 };
779 let op = if op_str.is_empty() { "=" } else { op_str };
780 let mut version = version;
781 if op != "=="
782 && op != "="
783 && stability_modifier.is_some()
784 && version::parse_stability(&version) == "stable"
785 {
786 if let Some(s) = &stability_modifier {
787 version = format!("{version}-{s}");
788 }
789 } else if op == "<" || op == ">=" {
790 let modifier_end = regex(&MODIFIER_END, &format!("-{MODIFIER_REGEX}$"), false);
791 let lower = raw.to_lowercase();
792 if !modifier_end.is_match(lower.as_bytes()).unwrap_or(false) && !raw.starts_with("dev-")
793 {
794 version.push_str("-dev");
795 }
796 }
797 let op = Op::parse(op).ok_or_else(|| {
798 VersionError(format!("Could not parse version constraint {constraint}"))
799 })?;
800 return Ok(vec![Constraint::new(op, version)]);
801 }
802 Err(VersionError(format!(
803 "Could not parse version constraint {constraint}"
804 )))
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 fn p(s: &str) -> String {
812 parse_constraints(s).unwrap().constraint.to_string()
813 }
814
815 #[test]
816 fn parses_common_forms() {
817 assert_eq!(p("^1.2"), "[>= 1.2.0.0-dev < 2.0.0.0-dev]");
818 assert_eq!(p("~1.2"), "[>= 1.2.0.0-dev < 2.0.0.0-dev]");
819 assert_eq!(p("~1.2.3"), "[>= 1.2.3.0-dev < 1.3.0.0-dev]");
820 assert_eq!(p("^0.3"), "[>= 0.3.0.0-dev < 0.4.0.0-dev]");
821 assert_eq!(p("1.0.*"), "[>= 1.0.0.0-dev < 1.1.0.0-dev]");
822 assert_eq!(p("*"), "*");
823 assert_eq!(p(">=1.0"), ">= 1.0.0.0-dev");
824 assert_eq!(p("<2"), "< 2.0.0.0-dev");
825 assert_eq!(p("1.0.0"), "== 1.0.0.0");
826 assert_eq!(p("^1.0 || ^2.0"), "[>= 1.0.0.0-dev < 3.0.0.0-dev]");
827 assert_eq!(
828 p("~1.2 || ~2.0 || ~4.0"),
829 "[[>= 1.2.0.0-dev < 3.0.0.0-dev] || [>= 4.0.0.0-dev < 5.0.0.0-dev]]"
830 );
831 assert_eq!(p(">=1.0 <2.0"), "[>= 1.0.0.0-dev < 2.0.0.0-dev]");
832 assert_eq!(p("1.0 - 2.0"), "[>= 1.0.0.0-dev < 2.1.0.0-dev]");
833 assert_eq!(p("dev-main"), "== dev-main");
834 assert_eq!(p("1.x-dev"), "== 1.9999999.9999999.9999999-dev");
835 assert_eq!(p("^8.1@dev"), "[>= 8.1.0.0-dev < 9.0.0.0-dev]");
836 assert_eq!(p("!=1.0"), "!= 1.0.0.0");
837 assert_eq!(p("dev-main#abc"), "== dev-main");
838 assert_eq!(p("^1.0@beta"), "[>= 1.0.0.0-dev < 2.0.0.0-dev]");
839 assert_eq!(p("2.0.x-dev"), "== 2.0.9999999.9999999-dev");
840 }
841
842 #[test]
843 fn matching_and_bounds() {
844 let c = parse_constraints("^1.2").unwrap().constraint;
845 assert!(c.matches_version("1.5.0.0"));
846 assert!(!c.matches_version("2.0.0.0"));
847 assert!(c.matches_version("1.2.0.0-beta1"));
848 assert!(!c.matches_version("dev-main"));
849 let d = parse_constraints("dev-main").unwrap().constraint;
850 assert!(d.matches_version("dev-main"));
851 assert!(!d.matches_version("1.0.0.0"));
852 let ne = parse_constraints("!=1.0").unwrap().constraint;
853 assert!(ne.matches_version("dev-main"));
854 assert_eq!(c.lower_bound().to_string(), "1.2.0.0-dev [inclusive]");
855 assert_eq!(c.upper_bound().to_string(), "2.0.0.0-dev [exclusive]");
856 }
857}