1use crate::{
2 constraint::{Constraint, MatchAllConstraint, MultiConstraint, Operator, SingleConstraint},
3 error::{Result, SemverError},
4 version::{expand_stability, Stability},
5};
6use regex::Regex;
7use std::{cmp, sync::LazyLock};
8
9const MODIFIER_PATTERN: &str =
12 r"[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?([.-]?dev)?";
13
14static STABILITY_EXTRACT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
16 Regex::new(&format!(r"(?i){MODIFIER_PATTERN}(?:\+.*)?$"))
17 .expect("Invalid stability extract regex")
18});
19
20static CLASSICAL_VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
22 Regex::new(&format!(
23 r"(?i)^v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?{MODIFIER_PATTERN}$"
24 ))
25 .expect("Invalid classical version regex")
26});
27
28static DATE_VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
30 Regex::new(&format!(
31 r"(?i)^v?(\d{{4}}(?:[.:-]?\d{{2}}){{1,6}}(?:[.:-]?\d{{1,3}}){{0,2}}){MODIFIER_PATTERN}$"
32 ))
33 .expect("Invalid date version regex")
34});
35
36static NUMERIC_BRANCH_REGEX: LazyLock<Regex> = LazyLock::new(|| {
38 Regex::new(r"(?i)^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$")
39 .expect("Invalid numeric branch regex")
40});
41
42static ALIAS_REGEX: LazyLock<Regex> =
44 LazyLock::new(|| Regex::new(r"^([^,\s]+)\s+as\s+([^,\s]+)$").expect("Invalid alias regex"));
45
46static STABILITY_FLAG_REGEX: LazyLock<Regex> = LazyLock::new(|| {
48 Regex::new(r"(?i)@(stable|RC|beta|alpha|dev)$").expect("Invalid stability flag regex")
49});
50
51static BUILD_METADATA_REGEX: LazyLock<Regex> =
53 LazyLock::new(|| Regex::new(r"^([^,\s+]+)\+[^\s]+$").expect("Invalid build metadata regex"));
54
55static OR_SPLITTER: LazyLock<Regex> =
57 LazyLock::new(|| Regex::new(r"\s*\|\|?\s*").expect("Invalid OR splitter regex"));
58
59static MATCH_ALL_REGEX: LazyLock<Regex> =
61 LazyLock::new(|| Regex::new(r"(?i)^(v)?[xX*](\.[xX*])*$").expect("Invalid match all regex"));
62
63const VERSION_REGEX_PATTERN: &str = r"v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?([.-]?dev)?|\.([xX*][.-]?dev))(?:\+[^\s]+)?";
65
66static TILDE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
68 Regex::new(&format!(r"(?i)^~>?{VERSION_REGEX_PATTERN}$")).expect("Invalid tilde regex")
69});
70
71static CARET_REGEX: LazyLock<Regex> = LazyLock::new(|| {
73 Regex::new(&format!(r"(?i)^\^{VERSION_REGEX_PATTERN}($)")).expect("Invalid caret regex")
74});
75
76static XRANGE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
78 Regex::new(r"(?i)^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$")
79 .expect("Invalid x-range regex")
80});
81
82static HYPHEN_REGEX: LazyLock<Regex> = LazyLock::new(|| {
84 Regex::new(&format!(
85 r"(?i)^(?P<from>{VERSION_REGEX_PATTERN}) +- +(?P<to>{VERSION_REGEX_PATTERN})($)"
86 ))
87 .expect("Invalid hyphen regex")
88});
89
90static COMPARATOR_REGEX: LazyLock<Regex> = LazyLock::new(|| {
92 Regex::new(r"^(<>|!=|>=?|<=?|==?)?\s*(.*)").expect("Invalid comparator regex")
93});
94
95static STABILITY_CONSTRAINT_REGEX: LazyLock<Regex> = LazyLock::new(|| {
97 Regex::new(r"(?i)^([^,\s]*?)@(stable|RC|beta|alpha|dev)$")
98 .expect("Invalid stability constraint regex")
99});
100
101static REF_STRIP_REGEX: LazyLock<Regex> = LazyLock::new(|| {
103 Regex::new(r"(?i)^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$").expect("Invalid ref strip regex")
104});
105
106#[derive(Debug, Clone, Default)]
108pub struct VersionParser;
109
110impl VersionParser {
111 pub fn parse_stability(version: &str) -> Stability {
113 let version = version.split('#').next().unwrap_or(version);
115
116 if version.to_lowercase().starts_with("dev-") || version.ends_with("-dev") {
118 return Stability::Dev;
119 }
120
121 let lower = version.to_lowercase();
123 if let Some(caps) = STABILITY_EXTRACT_REGEX.captures(&lower) {
124 if caps.get(3).is_some() {
126 return Stability::Dev;
127 }
128
129 if let Some(stability_match) = caps.get(1) {
131 return match stability_match.as_str() {
132 "rc" => Stability::RC,
133 "beta" | "b" => Stability::Beta,
134 "alpha" | "a" => Stability::Alpha,
135 _ => Stability::Stable,
136 };
137 }
138 }
139
140 Stability::Stable
141 }
142
143 pub fn normalize_stability(stability: &str) -> Result<String> {
148 let lower = stability.to_lowercase();
149 match lower.as_str() {
150 "rc" => Ok("RC".to_string()),
151 "dev" => Ok("dev".to_string()),
152 "beta" => Ok("beta".to_string()),
153 "alpha" => Ok("alpha".to_string()),
154 "stable" => Ok("stable".to_string()),
155 _ => Err(SemverError::InvalidStability(stability.to_string())),
156 }
157 }
158
159 #[must_use]
161 pub fn is_valid(version: &str) -> bool {
162 Self::normalize(version).is_ok()
163 }
164
165 pub fn normalize(version: &str) -> Result<String> {
170 Self::normalize_full(version, None)
171 }
172
173 #[allow(
178 clippy::missing_panics_doc,
179 reason = "All unwraps are safe due to regex match checks"
180 )]
181 pub fn normalize_full(version: &str, full_version: Option<&str>) -> Result<String> {
182 let version = version.trim();
183 let orig_version = version;
184 let full_version = full_version.unwrap_or(version);
185
186 let version = ALIAS_REGEX
188 .captures(version)
189 .map_or(version, |caps| caps.get(1).map_or(version, |m| m.as_str()));
190
191 let version = STABILITY_FLAG_REGEX
193 .captures(version)
194 .map_or(version, |caps| {
195 &version[..version.len() - caps.get(0).unwrap().as_str().len()]
196 });
197
198 let version = match version.to_lowercase().as_str() {
200 "master" | "trunk" | "default" => {
201 return Ok(format!("dev-{}", version.to_lowercase()));
202 },
203 _ => version,
204 };
205
206 if version.to_lowercase().starts_with("dev-") {
208 return Ok(format!("dev-{}", &version[4..]));
209 }
210
211 let version = BUILD_METADATA_REGEX
213 .captures(version)
214 .map_or(version, |caps| caps.get(1).map_or(version, |m| m.as_str()));
215
216 if let Some(normalized) = Self::try_classical_version(version) {
218 return Ok(normalized);
219 }
220
221 if let Some(normalized) = Self::try_date_version(version) {
223 return Ok(normalized);
224 }
225
226 if let Some(normalized) = Self::try_dev_branch(version) {
228 return Ok(normalized);
229 }
230
231 Err(Self::make_error(orig_version, full_version))
232 }
233
234 fn try_classical_version(version: &str) -> Option<String> {
236 let caps = CLASSICAL_VERSION_REGEX.captures(version)?;
237
238 let major = caps.get(1)?.as_str();
239 let minor = caps
240 .get(2)
241 .map_or(".0", |m| m.as_str())
242 .trim_start_matches('.');
243 let patch = caps
244 .get(3)
245 .map_or(".0", |m| m.as_str())
246 .trim_start_matches('.');
247 let extra = caps
248 .get(4)
249 .map_or(".0", |m| m.as_str())
250 .trim_start_matches('.');
251
252 let mut result = format!(
253 "{}.{}.{}.{}",
254 major,
255 if minor.is_empty() { "0" } else { minor },
256 if patch.is_empty() { "0" } else { patch },
257 if extra.is_empty() { "0" } else { extra }
258 );
259
260 if let Some(stability_match) = caps.get(5) {
262 let stability = stability_match.as_str();
263 if stability.to_lowercase() != "stable" {
264 let expanded = expand_stability(stability);
265 result.push('-');
266 result.push_str(expanded);
267
268 if let Some(stability_num) = caps.get(6) {
270 let num = stability_num.as_str().trim_start_matches(['.', '-']);
271 if !num.is_empty() {
272 result.push_str(num);
273 }
274 }
275 }
276 }
277
278 if caps.get(7).is_some() {
280 result.push_str("-dev");
281 }
282
283 Some(result)
284 }
285
286 fn try_date_version(version: &str) -> Option<String> {
288 let caps = DATE_VERSION_REGEX.captures(version)?;
289
290 let date_part = caps.get(1)?.as_str();
291 let normalized_date: String = date_part
293 .chars()
294 .map(|c| if c.is_ascii_digit() { c } else { '.' })
295 .collect();
296
297 let mut result = String::new();
299 let mut last_was_dot = false;
300 for c in normalized_date.chars() {
301 if c == '.' {
302 if !last_was_dot {
303 result.push(c);
304 last_was_dot = true;
305 }
306 } else {
307 result.push(c);
308 last_was_dot = false;
309 }
310 }
311
312 while result.ends_with('.') {
314 result.pop();
315 }
316
317 if let Some(stability_match) = caps.get(2) {
319 let stability = stability_match.as_str();
320 if stability.to_lowercase() != "stable" {
321 let expanded = expand_stability(stability);
322 result.push('-');
323 result.push_str(expanded);
324
325 if let Some(stability_num) = caps.get(3) {
326 let num = stability_num.as_str().trim_start_matches(['.', '-']);
327 if !num.is_empty() {
328 result.push_str(num);
329 }
330 }
331 }
332 }
333
334 if caps.get(4).is_some() {
336 result.push_str("-dev");
337 }
338
339 Some(result)
340 }
341
342 #[allow(clippy::case_sensitive_file_extension_comparisons)]
344 fn try_dev_branch(version: &str) -> Option<String> {
345 let lower = version.to_lowercase();
346 if !lower.ends_with("-dev") && !lower.ends_with(".dev") && !lower.ends_with("_dev") {
347 return None;
348 }
349
350 let base = &version[..version.len() - 4];
352
353 let normalized = Self::normalize_branch(base);
355
356 if normalized.starts_with("dev-") {
358 None
359 } else {
360 Some(normalized)
361 }
362 }
363
364 pub fn normalize_branch(name: &str) -> String {
366 let name = name.trim();
367
368 if let Some(caps) = NUMERIC_BRANCH_REGEX.captures(name) {
370 let mut parts = Vec::with_capacity(4);
371
372 for i in 1..=4 {
373 let part = caps
374 .get(i)
375 .map_or("x", |m| m.as_str().trim_start_matches('.'));
376
377 let lower = part.to_lowercase();
378 let normalized = match lower.as_str() {
379 "*" | "x" => "9999999".to_string(),
380 _ => part.to_string(),
381 };
382 parts.push(normalized);
383 }
384
385 while parts.len() < 4 {
387 parts.push("9999999".to_string());
388 }
389
390 let version = parts.join(".");
392 let version = version.replace(['x', 'X', '*'], "9999999");
393
394 return format!("{version}-dev");
395 }
396
397 format!("dev-{name}")
399 }
400
401 #[deprecated(
405 note = "No longer needed in Composer 2, which doesn't normalize branch names to 9999999-dev"
406 )]
407 #[must_use]
408 pub fn normalize_default_branch(name: &str) -> String {
409 if name == "dev-master" || name == "dev-default" || name == "dev-trunk" {
410 "9999999-dev".to_string()
411 } else {
412 name.to_string()
413 }
414 }
415
416 #[must_use]
418 #[allow(clippy::missing_panics_doc)]
419 pub fn parse_numeric_alias_prefix(branch: &str) -> Option<String> {
420 let re = Regex::new(r"(?i)^(?P<version>(\d+\.)*\d+)(?:\.x)?-dev$").expect("Invalid regex");
421 if let Some(caps) = re.captures(branch) {
422 if let Some(version) = caps.name("version") {
423 return Some(format!("{}.", version.as_str()));
424 }
425 }
426 None
427 }
428
429 fn make_error(orig_version: &str, full_version: &str) -> SemverError {
431 let mut extra_message = String::new();
432
433 if full_version.contains(" as ") {
435 if full_version.ends_with(&format!(" as {orig_version}")) {
436 extra_message =
437 format!(" in \"{full_version}\", the alias must be an exact version");
438 } else if full_version.starts_with(&format!("{orig_version} as ")) {
439 extra_message = format!(
440 " in \"{full_version}\", the alias source must be an exact version, if it is a branch name you should prefix it with dev-"
441 );
442 }
443 }
444
445 SemverError::InvalidVersionWithContext {
446 version: orig_version.to_string(),
447 context: extra_message,
448 }
449 }
450
451 #[allow(clippy::missing_panics_doc)]
456 pub fn parse_constraints(constraints: &str) -> Result<Box<dyn Constraint>> {
457 let constraints = constraints.trim();
458 if constraints.is_empty() {
459 return Err(SemverError::EmptyConstraint);
460 }
461
462 let pretty_constraint = constraints.to_string();
463
464 if constraints.contains("|||") {
466 return Err(SemverError::ConstraintParseFailed(
467 constraints.to_string(),
468 "Consecutive OR operators".to_string(),
469 ));
470 }
471
472 let or_parts: Vec<&str> = OR_SPLITTER.split(constraints).collect();
474
475 if or_parts.first().is_some_and(|s| s.is_empty()) {
477 return Err(SemverError::ConstraintParseFailed(
478 constraints.to_string(),
479 "Leading || or | operator".to_string(),
480 ));
481 }
482 if or_parts.last().is_some_and(|s| s.is_empty()) {
483 return Err(SemverError::ConstraintParseFailed(
484 constraints.to_string(),
485 "Trailing || or | operator".to_string(),
486 ));
487 }
488
489 let mut or_groups: Vec<Box<dyn Constraint>> = Vec::new();
490
491 for or_constraint in or_parts {
492 let or_constraint = or_constraint.trim();
493 if or_constraint.is_empty() {
494 return Err(SemverError::ConstraintParseFailed(
495 constraints.to_string(),
496 "Empty constraint in OR group".to_string(),
497 ));
498 }
499
500 if or_constraint.contains(",,")
502 || or_constraint.contains(", ,")
503 || or_constraint.contains(" ,,")
504 {
505 return Err(SemverError::ConstraintParseFailed(
506 constraints.to_string(),
507 "Consecutive comma operators".to_string(),
508 ));
509 }
510
511 if or_constraint.trim_end().ends_with(',') {
513 return Err(SemverError::ConstraintParseFailed(
514 constraints.to_string(),
515 "Trailing comma operator".to_string(),
516 ));
517 }
518
519 if or_constraint.trim_start().starts_with(',') {
521 return Err(SemverError::ConstraintParseFailed(
522 constraints.to_string(),
523 "Leading comma operator".to_string(),
524 ));
525 }
526
527 let and_parts: Vec<&str> = Self::split_and_constraints(or_constraint);
529
530 if and_parts.first().is_some_and(|s| s.is_empty()) {
532 return Err(SemverError::ConstraintParseFailed(
533 constraints.to_string(),
534 "Leading comma operator".to_string(),
535 ));
536 }
537 if and_parts.last().is_some_and(|s| s.is_empty()) {
538 return Err(SemverError::ConstraintParseFailed(
539 constraints.to_string(),
540 "Trailing comma operator".to_string(),
541 ));
542 }
543
544 let mut constraint_objects: Vec<Box<dyn Constraint>> = Vec::new();
545
546 for and_constraint in and_parts {
547 let and_constraint = and_constraint.trim();
548 if and_constraint.is_empty() {
549 return Err(SemverError::ConstraintParseFailed(
550 constraints.to_string(),
551 "Empty constraint in AND group (possibly double comma)".to_string(),
552 ));
553 }
554 let parsed = Self::parse_constraint(and_constraint)?;
555 for c in parsed {
556 constraint_objects.push(c);
557 }
558 }
559
560 let constraint: Box<dyn Constraint> = if constraint_objects.len() == 1 {
561 constraint_objects.pop().unwrap()
562 } else {
563 Box::new(MultiConstraint::new(constraint_objects, true))
564 };
565
566 or_groups.push(constraint);
567 }
568
569 let mut parsed_constraint = MultiConstraint::create(or_groups, false);
570 parsed_constraint.set_pretty_string(pretty_constraint);
571
572 Ok(parsed_constraint)
573 }
574
575 fn split_and_constraints(constraint: &str) -> Vec<&str> {
577 let mut parts = Vec::new();
579 let mut current_start = 0;
580 let mut in_hyphen_range = false;
581 let chars: Vec<char> = constraint.chars().collect();
582 let len = chars.len();
583 let mut i = 0;
584
585 while i < len {
586 let c = chars[i];
587
588 if c == ' ' && i + 2 < len && chars[i + 1] == '-' && chars[i + 2] == ' ' {
590 in_hyphen_range = true;
591 i += 3;
592 continue;
593 }
594
595 if c == ' ' && i + 4 < len {
597 let next_chars: String = chars[i..i + 4].iter().collect();
598 if next_chars == " as " {
599 i += 4;
600 continue;
601 }
602 }
603
604 if (c == ',' || c == ' ') && !in_hyphen_range {
606 let before = &constraint[current_start..i].trim_end();
608 let after_start = i + 1;
609 let after = if after_start < len {
610 constraint[after_start..].trim_start()
611 } else {
612 ""
613 };
614
615 if after.starts_with("as ") {
617 i += 1;
618 continue;
619 }
620
621 if !before.is_empty()
622 && !before.ends_with('>')
623 && !before.ends_with('<')
624 && !before.ends_with('=')
625 && !before.ends_with("as")
626 {
627 let part = constraint[current_start..i].trim();
628 if !part.is_empty() {
629 parts.push(part);
630 }
631
632 while i < len && (chars[i] == ',' || chars[i] == ' ') {
634 i += 1;
635 }
636 current_start = i;
637 in_hyphen_range = false;
638 continue;
639 }
640 }
641
642 i += 1;
643 }
644
645 let remaining = constraint[current_start..].trim();
647 if !remaining.is_empty() {
648 parts.push(remaining);
649 }
650
651 if parts.is_empty() {
652 parts.push(constraint.trim());
653 }
654
655 parts
656 }
657
658 fn parse_constraint(constraint: &str) -> Result<Vec<Box<dyn Constraint>>> {
660 let constraint = constraint.trim();
661
662 let constraint = ALIAS_REGEX.captures(constraint).map_or(constraint, |caps| {
664 caps.get(1).map_or(constraint, |m| m.as_str())
665 });
666
667 let (constraint, stability_modifier) = STABILITY_CONSTRAINT_REGEX
669 .captures(constraint)
670 .map_or((constraint, None), |caps| {
671 let base = caps.get(1).map_or("", |m| m.as_str());
672 let stability = caps.get(2).map_or("", |m| m.as_str());
673 let base = if base.is_empty() { "*" } else { base };
674 let modifier = if stability.to_lowercase() == "stable" {
675 None
676 } else {
677 Some(stability.to_string())
678 };
679 (base, modifier)
680 });
681
682 let constraint = REF_STRIP_REGEX
684 .captures(constraint)
685 .map_or(constraint, |caps| {
686 caps.get(1).map_or(constraint, |m| m.as_str())
687 });
688
689 if let Some(caps) = MATCH_ALL_REGEX.captures(constraint) {
691 if caps.get(1).is_some() || caps.get(2).is_some() {
692 return Ok(vec![Box::new(SingleConstraint::new(
694 Operator::Ge,
695 "0.0.0.0-dev",
696 ))]);
697 }
698 return Ok(vec![Box::new(MatchAllConstraint::new())]);
700 }
701
702 if let Some(caps) = TILDE_REGEX.captures(constraint) {
704 return Self::parse_tilde_constraint(constraint, &caps, stability_modifier.as_ref());
705 }
706
707 if let Some(caps) = CARET_REGEX.captures(constraint) {
709 return Self::parse_caret_constraint(constraint, &caps, stability_modifier.as_ref());
710 }
711
712 if let Some(caps) = XRANGE_REGEX.captures(constraint) {
714 return Ok(Self::parse_xrange_constraint(&caps));
715 }
716
717 if let Some(caps) = HYPHEN_REGEX.captures(constraint) {
719 return Self::parse_hyphen_constraint(constraint, &caps);
720 }
721
722 if let Some(caps) = COMPARATOR_REGEX.captures(constraint) {
724 return Self::parse_comparator_constraint(&caps, stability_modifier.as_ref());
725 }
726
727 Err(SemverError::ConstraintParseFailed(
728 constraint.to_string(),
729 "Unknown constraint format".to_string(),
730 ))
731 }
732
733 fn parse_tilde_constraint(
735 constraint: &str,
736 caps: ®ex::Captures,
737 _stability_modifier: Option<&String>,
738 ) -> Result<Vec<Box<dyn Constraint>>> {
739 if constraint.starts_with("~>") {
741 return Err(SemverError::ConstraintParseFailed(
742 constraint.to_string(),
743 "Invalid operator \"~>\", you probably meant to use the \"~\" operator".to_string(),
744 ));
745 }
746
747 let position = if caps.get(4).is_some_and(|m| !m.as_str().is_empty()) {
749 4
750 } else if caps.get(3).is_some_and(|m| !m.as_str().is_empty()) {
751 3
752 } else if caps.get(2).is_some_and(|m| !m.as_str().is_empty()) {
753 2
754 } else {
755 1
756 };
757
758 let position = if caps.get(8).is_some() {
760 position + 1
761 } else {
762 position
763 };
764
765 let stability_suffix =
767 if caps.get(5).is_none() && caps.get(7).is_none() && caps.get(8).is_none() {
768 "-dev"
769 } else {
770 ""
771 };
772
773 let low_version = Self::normalize(&format!(
775 "{}{}",
776 &constraint[1..], stability_suffix
778 ))?;
779 let lower_bound = SingleConstraint::new(Operator::Ge, low_version);
780
781 let high_position = cmp::max(1, position - 1);
783 let high_version = format!(
784 "{}-dev",
785 Self::manipulate_version_string(caps, high_position, 1)
786 );
787 let upper_bound = SingleConstraint::new(Operator::Lt, high_version);
788
789 Ok(vec![Box::new(lower_bound), Box::new(upper_bound)])
790 }
791
792 fn parse_caret_constraint(
794 constraint: &str,
795 caps: ®ex::Captures,
796 _stability_modifier: Option<&String>,
797 ) -> Result<Vec<Box<dyn Constraint>>> {
798 let major = caps.get(1).map_or("0", |m| m.as_str());
799 let minor = caps.get(2).map(|m| m.as_str());
800 let patch = caps.get(3).map(|m| m.as_str());
801
802 let position = if major != "0" || minor.is_none() || minor == Some("") {
804 1
805 } else if minor != Some("0") || patch.is_none() || patch == Some("") {
806 2
807 } else {
808 3
809 };
810
811 let stability_suffix =
813 if caps.get(5).is_none() && caps.get(7).is_none() && caps.get(8).is_none() {
814 "-dev"
815 } else {
816 ""
817 };
818
819 let low_version = Self::normalize(&format!(
821 "{}{}",
822 &constraint[1..], stability_suffix
824 ))?;
825 let lower_bound = SingleConstraint::new(Operator::Ge, low_version);
826
827 let high_version = format!("{}-dev", Self::manipulate_version_string(caps, position, 1));
829 let upper_bound = SingleConstraint::new(Operator::Lt, high_version);
830
831 Ok(vec![Box::new(lower_bound), Box::new(upper_bound)])
832 }
833
834 fn parse_xrange_constraint(caps: ®ex::Captures) -> Vec<Box<dyn Constraint>> {
836 let position = if caps.get(3).is_some_and(|m| !m.as_str().is_empty()) {
837 3
838 } else if caps.get(2).is_some_and(|m| !m.as_str().is_empty()) {
839 2
840 } else {
841 1
842 };
843
844 let low_version = format!("{}-dev", Self::manipulate_version_string(caps, position, 0));
845 let high_version = format!("{}-dev", Self::manipulate_version_string(caps, position, 1));
846
847 if low_version == "0.0.0.0-dev" {
848 return vec![Box::new(SingleConstraint::new(Operator::Lt, high_version))];
850 }
851
852 vec![
853 Box::new(SingleConstraint::new(Operator::Ge, low_version)),
854 Box::new(SingleConstraint::new(Operator::Lt, high_version)),
855 ]
856 }
857
858 fn parse_hyphen_constraint(
860 constraint: &str,
861 caps: ®ex::Captures,
862 ) -> Result<Vec<Box<dyn Constraint>>> {
863 if constraint.contains('*') || constraint.to_lowercase().contains('x') {
865 let from_part = caps.name("from").map_or("", |m| m.as_str());
867 let to_part = caps.name("to").map_or("", |m| m.as_str());
868
869 let from_has_invalid_wildcard = (from_part.contains('*')
870 || from_part.to_lowercase().contains('x'))
871 && !from_part.to_lowercase().ends_with("-dev")
872 && !from_part.to_lowercase().ends_with(".dev");
873
874 let to_has_invalid_wildcard = (to_part.contains('*')
875 || to_part.to_lowercase().contains('x'))
876 && !to_part.to_lowercase().ends_with("-dev")
877 && !to_part.to_lowercase().ends_with(".dev");
878
879 if from_has_invalid_wildcard || to_has_invalid_wildcard {
880 return Err(SemverError::ConstraintParseFailed(
881 constraint.to_string(),
882 "Wildcards in hyphen ranges require -dev suffix".to_string(),
883 ));
884 }
885 }
886
887 let from_str = caps.name("from").map_or("", |m| m.as_str());
888 let to_str = caps.name("to").map_or("", |m| m.as_str());
889
890 let low_stability_suffix =
892 if caps.get(6).is_none() && caps.get(8).is_none() && caps.get(9).is_none() {
893 "-dev"
894 } else {
895 ""
896 };
897
898 let low_version = format!("{}{low_stability_suffix}", Self::normalize(from_str)?);
899 let lower_bound = SingleConstraint::new(Operator::Ge, low_version);
900
901 let is_complete_version = {
906 let has_patch = caps.get(13).is_some_and(|m| !m.as_str().is_empty());
908 let has_fourth = caps.get(14).is_some_and(|m| !m.as_str().is_empty());
909 let has_stability = caps.get(16).is_some_and(|m| !m.as_str().is_empty())
911 || caps.get(17).is_some_and(|m| !m.as_str().is_empty())
912 || caps.get(18).is_some_and(|m| !m.as_str().is_empty());
913
914 let to_lower = to_str.to_lowercase();
916 let to_has_stability = to_lower.contains("-dev")
917 || to_lower.contains(".dev")
918 || to_lower.contains("-rc")
919 || to_lower.contains("-beta")
920 || to_lower.contains("-alpha")
921 || to_lower.contains("-b")
922 || to_lower.contains("-a");
923
924 has_patch || has_fourth || has_stability || to_has_stability
926 };
927
928 let upper_bound = if is_complete_version {
929 let high_version = Self::normalize(to_str)?;
931 SingleConstraint::new(Operator::Le, high_version)
932 } else {
933 let minor_present = caps.get(12).is_some_and(|m| !m.as_str().is_empty());
936 let position = if minor_present { 2 } else { 1 };
937
938 let to_matches = [
940 "",
941 caps.get(11).map_or("0", |m| m.as_str()),
942 caps.get(12).map_or("", |m| m.as_str()),
943 caps.get(13).map_or("", |m| m.as_str()),
944 caps.get(14).map_or("", |m| m.as_str()),
945 ];
946 let high_version = format!(
947 "{}-dev",
948 Self::manipulate_version_array(&to_matches, position, 1)
949 );
950 SingleConstraint::new(Operator::Lt, high_version)
951 };
952
953 Ok(vec![Box::new(lower_bound), Box::new(upper_bound)])
954 }
955
956 fn parse_comparator_constraint(
958 caps: ®ex::Captures,
959 stability_modifier: Option<&String>,
960 ) -> Result<Vec<Box<dyn Constraint>>> {
961 let op_str = caps.get(1).map_or("=", |m| m.as_str());
962 let version_str = caps.get(2).map_or("", |m| m.as_str()).trim();
963
964 if version_str.is_empty() {
965 return Err(SemverError::ConstraintParseFailed(
966 format!("{op_str}{version_str}"),
967 "Missing version after operator".to_string(),
968 ));
969 }
970
971 let original_lower = version_str.to_lowercase();
973 let has_explicit_stability = original_lower.contains("-stable")
974 || original_lower.contains("-dev")
975 || original_lower.contains("-alpha")
976 || original_lower.contains("-beta")
977 || original_lower.contains("-rc")
978 || original_lower.contains(".dev")
979 || original_lower.contains(".alpha")
980 || original_lower.contains(".beta")
981 || original_lower.contains(".rc")
982 || version_str.starts_with("dev-");
983
984 let version = match Self::normalize(version_str) {
986 Ok(v) => v,
987 Err(_) => {
988 if version_str.ends_with("-dev")
990 && version_str
991 .chars()
992 .all(|c| c.is_alphanumeric() || c == '-' || c == '.' || c == '/')
993 {
994 Self::normalize(&format!("dev-{}", &version_str[..version_str.len() - 4]))?
996 } else {
997 return Err(SemverError::ConstraintParseFailed(
998 version_str.to_string(),
999 "Invalid version".to_string(),
1000 ));
1001 }
1002 },
1003 };
1004
1005 let op = match op_str {
1006 "<>" | "!=" => Operator::Ne,
1007 ">=" => Operator::Ge,
1008 "<=" => Operator::Le,
1009 ">" => Operator::Gt,
1010 "<" => Operator::Lt,
1011 "==" | "=" | "" => Operator::Eq,
1012 _ => {
1013 return Err(SemverError::InvalidOperator(op_str.to_string()));
1014 },
1015 };
1016
1017 let version = if op == Operator::Eq {
1020 version
1021 } else if let Some(modifier) = stability_modifier {
1022 if Self::parse_stability(&version) == Stability::Stable {
1023 format!("{version}-{modifier}")
1024 } else {
1025 version
1026 }
1027 } else if (op == Operator::Lt || op == Operator::Ge) && !has_explicit_stability {
1028 format!("{version}-dev")
1030 } else {
1031 version
1032 };
1033
1034 Ok(vec![Box::new(SingleConstraint::new(op, version))])
1035 }
1036
1037 fn manipulate_version_string(
1039 caps: ®ex::Captures,
1040 position: usize,
1041 increment: i32,
1042 ) -> String {
1043 let mut parts: [String; 4] = [
1044 caps.get(1).map_or("0", |m| m.as_str()).to_string(),
1045 caps.get(2).map_or("0", |m| m.as_str()).to_string(),
1046 caps.get(3).map_or("0", |m| m.as_str()).to_string(),
1047 caps.get(4).map_or("0", |m| m.as_str()).to_string(),
1048 ];
1049
1050 for part in &mut parts {
1052 if part.is_empty() {
1053 *part = "0".to_string();
1054 }
1055 }
1056
1057 for i in (0..4).rev() {
1059 if i + 1 > position {
1060 parts[i] = "0".to_string();
1061 } else if i + 1 == position && increment != 0 {
1062 let val: i64 = parts[i].parse().unwrap_or(0);
1063 parts[i] = (val + i64::from(increment)).to_string();
1064 }
1065 }
1066
1067 format!("{}.{}.{}.{}", parts[0], parts[1], parts[2], parts[3])
1068 }
1069
1070 fn manipulate_version_array(parts: &[&str; 5], position: usize, increment: i32) -> String {
1072 let mut result: [String; 4] = [
1073 parts[1].to_string(),
1074 if parts[2].is_empty() {
1075 "0".to_string()
1076 } else {
1077 parts[2].to_string()
1078 },
1079 if parts[3].is_empty() {
1080 "0".to_string()
1081 } else {
1082 parts[3].to_string()
1083 },
1084 if parts[4].is_empty() {
1085 "0".to_string()
1086 } else {
1087 parts[4].to_string()
1088 },
1089 ];
1090
1091 for i in (0..4).rev() {
1093 if i + 1 > position {
1094 result[i] = "0".to_string();
1095 } else if i + 1 == position && increment != 0 {
1096 let val: i64 = result[i].parse().unwrap_or(0);
1097 result[i] = (val + i64::from(increment)).to_string();
1098 }
1099 }
1100
1101 format!("{}.{}.{}.{}", result[0], result[1], result[2], result[3])
1102 }
1103}
1104
1105#[cfg(test)]
1106mod tests {
1107 use super::*;
1108
1109 #[test]
1110 fn test_parse_stability() {
1111 assert_eq!(VersionParser::parse_stability("1.0.0"), Stability::Stable);
1112 assert_eq!(VersionParser::parse_stability("dev-master"), Stability::Dev);
1113 assert_eq!(VersionParser::parse_stability("1.0.0-dev"), Stability::Dev);
1114 assert_eq!(
1115 VersionParser::parse_stability("1.0.0-alpha"),
1116 Stability::Alpha
1117 );
1118 assert_eq!(
1119 VersionParser::parse_stability("1.0.0-beta"),
1120 Stability::Beta
1121 );
1122 assert_eq!(VersionParser::parse_stability("1.0.0-RC"), Stability::RC);
1123 }
1124
1125 #[test]
1126 fn test_normalize_branch() {
1127 assert_eq!(
1128 VersionParser::normalize_branch("v1.x"),
1129 "1.9999999.9999999.9999999-dev"
1130 );
1131 assert_eq!(
1132 VersionParser::normalize_branch("v1.*"),
1133 "1.9999999.9999999.9999999-dev"
1134 );
1135 assert_eq!(
1136 VersionParser::normalize_branch("v1.0"),
1137 "1.0.9999999.9999999-dev"
1138 );
1139 assert_eq!(
1140 VersionParser::normalize_branch("2.0"),
1141 "2.0.9999999.9999999-dev"
1142 );
1143 assert_eq!(VersionParser::normalize_branch("master"), "dev-master");
1144 assert_eq!(
1145 VersionParser::normalize_branch("feature-a"),
1146 "dev-feature-a"
1147 );
1148 }
1149
1150 #[test]
1151 fn test_basic_normalization() {
1152 assert_eq!(VersionParser::normalize("1.0.0").unwrap(), "1.0.0.0");
1153 assert_eq!(VersionParser::normalize("1.2.3.4").unwrap(), "1.2.3.4");
1154 assert_eq!(VersionParser::normalize("v1.0.0").unwrap(), "1.0.0.0");
1155 assert_eq!(
1156 VersionParser::normalize("dev-master").unwrap(),
1157 "dev-master"
1158 );
1159 }
1160}