1use std::cmp::Ordering;
2use std::fmt::{self, Write};
3use std::hash::{Hash, Hasher};
4use std::str;
5use std::str::FromStr;
6
7use itertools::EitherOrBoth::{Both, Left, Right};
8use itertools::Itertools;
9use strum::{AsRefStr, Display, EnumIter, EnumString, IntoEnumIterator};
10
11use crate::Error;
12use crate::macros::cmp_not_equal;
13use crate::traits::Intersects;
14
15use super::parse;
16
17pub trait WithOp {
19 type WithOp;
20 fn with_op(self, op: Operator) -> Result<Self::WithOp, &'static str>;
21}
22
23#[derive(Debug, Default, Clone)]
24pub(crate) struct Number {
25 pub(crate) raw: String,
26 pub(crate) value: u64,
27}
28
29impl Number {
30 pub(crate) fn is_empty(&self) -> bool {
32 self.raw.is_empty()
33 }
34
35 pub(crate) fn as_str(&self) -> &str {
37 &self.raw
38 }
39
40 fn starts_with(&self, other: &Number) -> bool {
42 self.raw.starts_with(&other.raw)
43 }
44}
45
46impl PartialEq for Number {
47 fn eq(&self, other: &Self) -> bool {
48 self.value == other.value
49 }
50}
51
52impl Eq for Number {}
53
54impl Hash for Number {
55 fn hash<H: Hasher>(&self, state: &mut H) {
56 self.value.hash(state);
57 }
58}
59
60impl Ord for Number {
61 fn cmp(&self, other: &Self) -> Ordering {
62 self.value.cmp(&other.value)
63 }
64}
65
66impl PartialOrd for Number {
67 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
68 Some(self.cmp(other))
69 }
70}
71
72impl fmt::Display for Number {
73 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
74 write!(f, "{}", self.raw)
75 }
76}
77
78#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
79pub struct Revision(pub(crate) Number);
80
81impl Revision {
82 pub fn try_new(s: &str) -> crate::Result<Self> {
84 if s.is_empty() {
85 Ok(Self::default())
86 } else {
87 Ok(parse::revision(s)?)
88 }
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.0.is_empty()
94 }
95
96 pub fn as_str(&self) -> &str {
98 self.0.as_str()
99 }
100
101 fn starts_with(&self, other: &Revision) -> bool {
103 self.0.starts_with(&other.0)
104 }
105}
106
107impl FromStr for Revision {
108 type Err = Error;
109
110 fn from_str(s: &str) -> crate::Result<Self> {
111 Self::try_new(s)
112 }
113}
114
115impl fmt::Display for Revision {
116 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
117 write!(f, "{}", self.0)
118 }
119}
120
121#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
122#[strum(serialize_all = "snake_case")]
123pub(crate) enum SuffixKind {
124 Alpha,
125 Beta,
126 Pre,
127 Rc,
128 P,
129}
130
131#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
132pub(crate) struct Suffix {
133 pub(crate) kind: SuffixKind,
134 pub(crate) version: Option<Number>,
135}
136
137impl fmt::Display for Suffix {
138 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139 write!(f, "{}", self.kind)?;
140 if let Some(ver) = self.version.as_ref() {
141 write!(f, "{ver}")?;
142 }
143 Ok(())
144 }
145}
146
147#[repr(C)]
148#[derive(
149 AsRefStr,
150 Display,
151 EnumString,
152 EnumIter,
153 Debug,
154 PartialEq,
155 Eq,
156 PartialOrd,
157 Ord,
158 Hash,
159 Copy,
160 Clone,
161)]
162pub enum Operator {
163 #[strum(serialize = "<")]
164 Less = 1,
165 #[strum(serialize = "<=")]
166 LessOrEqual,
167 #[strum(serialize = "=")]
168 Equal,
169 #[strum(serialize = "=*")]
170 EqualGlob,
171 #[strum(serialize = "~")]
172 Approximate,
173 #[strum(serialize = ">=")]
174 GreaterOrEqual,
175 #[strum(serialize = ">")]
176 Greater,
177}
178
179impl Operator {
180 fn intersects(&self, lhs: &Version, rhs: &Version) -> bool {
182 match self {
183 Self::Less => NonOpVersion(rhs) < NonOpVersion(lhs),
184 Self::LessOrEqual => NonOpVersion(rhs) <= NonOpVersion(lhs),
185 Self::Equal => NonOpVersion(rhs) == NonOpVersion(lhs),
186 Self::EqualGlob => rhs.starts_with(lhs),
187 Self::Approximate => NonRevisionVersion(rhs) == NonRevisionVersion(lhs),
188 Self::GreaterOrEqual => NonOpVersion(rhs) >= NonOpVersion(lhs),
189 Self::Greater => NonOpVersion(rhs) > NonOpVersion(lhs),
190 }
191 }
192}
193
194#[derive(Clone)]
195pub struct Version {
196 pub(crate) op: Option<Operator>,
197 pub(crate) numbers: Vec<Number>,
198 pub(crate) letter: Option<char>,
199 pub(crate) suffixes: Vec<Suffix>,
200 pub(crate) revision: Revision,
201}
202
203impl fmt::Debug for Version {
204 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205 write!(f, "Version {{ {self} }}")
206 }
207}
208
209impl WithOp for Version {
210 type WithOp = Version;
211
212 fn with_op(mut self, op: Operator) -> Result<Self::WithOp, &'static str> {
213 if op == Operator::Approximate && !self.revision.is_empty() {
214 Err("~ version operator can't be used with a revision")
215 } else {
216 self.op = Some(op);
217 Ok(self)
218 }
219 }
220}
221
222impl Version {
223 pub fn try_new<S: AsRef<str>>(s: S) -> crate::Result<Self> {
225 let s = s.as_ref();
226 if s.starts_with(|c| Operator::iter().any(|op| op.as_ref().starts_with(c))) {
227 parse::version_with_op(s)
228 } else {
229 parse::version(s)
230 }
231 }
232
233 pub fn try_new_with_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
235 parse::version_with_op(s.as_ref())
236 }
237
238 pub fn try_new_without_op<S: AsRef<str>>(s: S) -> crate::Result<Self> {
240 parse::version(s.as_ref())
241 }
242}
243
244impl Version {
245 pub fn op(&self) -> Option<Operator> {
247 self.op
248 }
249
250 pub fn revision(&self) -> Option<&Revision> {
252 if self.revision.is_empty() {
253 None
254 } else {
255 Some(&self.revision)
256 }
257 }
258
259 pub fn without_op(&self) -> String {
261 NonOpVersion(self).to_string()
262 }
263
264 pub fn base(&self) -> String {
266 NonRevisionVersion(self).to_string()
267 }
268
269 fn starts_with(&self, other: &Version) -> bool {
271 let mut unmatched = false;
273
274 for numbers in self.numbers.iter().zip_longest(&other.numbers) {
276 match numbers {
277 Both(n1, n2) => {
278 if !n1.starts_with(n2) {
279 return false;
280 }
281 }
282 Left(_) => {
283 unmatched = true;
284 break;
285 }
286 Right(_) => return false,
287 }
288 }
289
290 match (&self.letter, &other.letter) {
292 (Some(c1), Some(c2)) => {
293 if unmatched || c1 != c2 {
294 return false;
295 }
296 }
297 (None, Some(_)) => return false,
298 (Some(_), None) => unmatched = true,
299 (None, None) => (),
300 }
301
302 for suffixes in self.suffixes.iter().zip_longest(&other.suffixes) {
304 match suffixes {
305 Both(s1, s2) => {
306 if unmatched || s1.kind != s2.kind {
307 return false;
308 }
309
310 match (&s1.version, &s2.version) {
312 (Some(v1), Some(v2)) => {
313 if !v1.starts_with(v2) {
314 return false;
315 }
316 }
317 (None, Some(_)) => return false,
318 (Some(_), None) => unmatched = true,
319 (None, None) => (),
320 }
321 }
322 Left(_) => {
323 unmatched = true;
324 break;
325 }
326 Right(_) => return false,
327 }
328 }
329
330 match (self.revision(), other.revision()) {
332 (Some(r1), Some(r2)) if unmatched || !r1.starts_with(r2) => false,
333 (None, Some(_)) => false,
334 _ => true,
335 }
336 }
337}
338
339macro_rules! unbounded {
341 () => {
342 Operator::Less | Operator::LessOrEqual | Operator::Greater | Operator::GreaterOrEqual
343 };
344}
345
346macro_rules! ranged {
348 ($ranged:expr, $ranged_op:expr, $other:expr, $other_op:expr) => {
349 match ($ranged_op, $other_op) {
350 (op, Approximate | EqualGlob) if op.intersects($ranged, $other) => true,
352
353 (Greater | GreaterOrEqual, Approximate) => $other_op.intersects($other, $ranged),
356 (_, Approximate) => false,
357
358 (Less | LessOrEqual, EqualGlob) => match $other.revision().map(|r| r.as_str()) {
361 None | Some("0") => $ranged.starts_with($other),
362 _ => false,
363 },
364
365 (_, EqualGlob) => $ranged.starts_with($other),
367
368 (_, op) => unreachable!("operator should be previously handled: {op:?}"),
370 }
371 };
372}
373
374impl Intersects<Self> for Version {
375 fn intersects(&self, other: &Self) -> bool {
376 use Operator::*;
377 match (self.op, other.op) {
378 (Some(Less | LessOrEqual), Some(Less | LessOrEqual)) => true,
380 (Some(Greater | GreaterOrEqual), Some(Greater | GreaterOrEqual)) => true,
381
382 (Some(lhs @ unbounded!()), Some(rhs @ unbounded!())) => {
384 lhs.intersects(self, other) && rhs.intersects(other, self)
385 }
386
387 (None, None) | (Some(Approximate), Some(Approximate)) => self == other,
389
390 (Some(op), None | Some(Equal)) => op.intersects(self, other),
392 (None | Some(Equal), Some(op)) => op.intersects(other, self),
393
394 (Some(op @ EqualGlob), Some(EqualGlob)) => {
396 op.intersects(self, other) || op.intersects(other, self)
397 }
398
399 (Some(EqualGlob), Some(Approximate)) => other.starts_with(self),
401 (Some(Approximate), Some(EqualGlob)) => self.starts_with(other),
402
403 (Some(lhs @ unbounded!()), Some(rhs)) => ranged!(self, lhs, other, rhs),
405 (Some(lhs), Some(rhs @ unbounded!())) => ranged!(other, rhs, self, lhs),
406 }
407 }
408}
409
410impl fmt::Display for Version {
411 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
412 fmt(f, self, true, true)
413 }
414}
415
416fn fmt(f: &mut fmt::Formatter, v: &Version, rev: bool, op: bool) -> fmt::Result {
418 let mut s = String::new();
419
420 write!(s, "{}", v.numbers.iter().join("."))?;
421
422 if let Some(c) = &v.letter {
423 write!(s, "{c}")?;
424 }
425
426 for suffix in &v.suffixes {
427 write!(s, "_{suffix}")?;
428 }
429
430 if rev {
431 if let Some(rev) = v.revision() {
432 write!(s, "-r{rev}")?;
433 }
434 }
435
436 if op {
437 match &v.op {
438 None => write!(f, "{s}"),
439 Some(Operator::Less) => write!(f, "<{s}"),
440 Some(Operator::LessOrEqual) => write!(f, "<={s}"),
441 Some(Operator::Equal) => write!(f, "={s}"),
442 Some(Operator::EqualGlob) => write!(f, "={s}*"),
443 Some(Operator::Approximate) => write!(f, "~{s}"),
444 Some(Operator::GreaterOrEqual) => write!(f, ">={s}"),
445 Some(Operator::Greater) => write!(f, ">{s}"),
446 }
447 } else {
448 write!(f, "{s}")
449 }
450}
451
452impl PartialEq for Version {
453 fn eq(&self, other: &Self) -> bool {
454 cmp(self, other, true, true) == Ordering::Equal
455 }
456}
457
458impl Eq for Version {}
459
460impl Hash for Version {
461 fn hash<H: Hasher>(&self, state: &mut H) {
462 self.numbers[0].hash(state);
463 for n in &self.numbers[1..] {
464 if n.as_str().starts_with('0') {
465 n.as_str().trim_end_matches('0').hash(state);
466 } else {
467 n.value.hash(state);
468 }
469 }
470 self.letter.hash(state);
471 self.suffixes.hash(state);
472 self.revision.hash(state);
473 }
474}
475
476fn cmp(v1: &Version, v2: &Version, rev: bool, op: bool) -> Ordering {
478 cmp_not_equal!(&v1.numbers[0], &v2.numbers[0]);
480
481 for numbers in v1.numbers[1..].iter().zip_longest(&v2.numbers[1..]) {
483 match numbers {
484 Both(n1, n2) => {
485 let (s1, s2) = (n1.as_str(), n2.as_str());
487 if s1.starts_with('0') || s2.starts_with('0') {
488 cmp_not_equal!(s1.trim_end_matches('0'), s2.trim_end_matches('0'));
489 } else {
490 cmp_not_equal!(n1, n2);
491 }
492 }
493 Left(_) => return Ordering::Greater,
494 Right(_) => return Ordering::Less,
495 }
496 }
497
498 cmp_not_equal!(&v1.letter, &v2.letter);
500
501 for suffixes in v1.suffixes.iter().zip_longest(&v2.suffixes) {
503 match suffixes {
504 Both(s1, s2) => cmp_not_equal!(s1, s2),
505 Left(s) if s.kind == SuffixKind::P => return Ordering::Greater,
506 Left(_) => return Ordering::Less,
507 Right(s) if s.kind == SuffixKind::P => return Ordering::Less,
508 Right(_) => return Ordering::Greater,
509 }
510 }
511
512 if rev {
514 cmp_not_equal!(&v1.revision, &v2.revision);
515 }
516
517 if op {
519 cmp_not_equal!(&v1.op, &v2.op);
520 }
521
522 Ordering::Equal
523}
524
525impl Ord for Version {
526 fn cmp(&self, other: &Self) -> Ordering {
527 cmp(self, other, true, true)
528 }
529}
530
531impl PartialOrd for Version {
532 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
533 Some(self.cmp(other))
534 }
535}
536
537impl FromStr for Version {
538 type Err = Error;
539
540 fn from_str(s: &str) -> crate::Result<Self> {
541 Self::try_new(s)
542 }
543}
544
545struct NonRevisionVersion<'a>(&'a Version);
547
548impl PartialEq for NonRevisionVersion<'_> {
549 fn eq(&self, other: &Self) -> bool {
550 cmp(self.0, other.0, false, false) == Ordering::Equal
551 }
552}
553
554impl Eq for NonRevisionVersion<'_> {}
555
556impl fmt::Display for NonRevisionVersion<'_> {
557 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
558 fmt(f, self.0, false, false)
559 }
560}
561
562struct NonOpVersion<'a>(&'a Version);
564
565impl PartialEq for NonOpVersion<'_> {
566 fn eq(&self, other: &Self) -> bool {
567 cmp(self.0, other.0, true, false) == Ordering::Equal
568 }
569}
570
571impl Eq for NonOpVersion<'_> {}
572
573impl Ord for NonOpVersion<'_> {
574 fn cmp(&self, other: &Self) -> Ordering {
575 cmp(self.0, other.0, true, false)
576 }
577}
578
579impl PartialOrd for NonOpVersion<'_> {
580 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
581 Some(self.cmp(other))
582 }
583}
584
585impl fmt::Display for NonOpVersion<'_> {
586 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
587 fmt(f, self.0, true, false)
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use std::collections::{HashMap, HashSet};
594
595 use itertools::Itertools;
596
597 use crate::test::test_data;
598 use crate::utils::hash;
599
600 use super::*;
601
602 #[test]
603 fn new_and_parse() {
604 let data = test_data();
605
606 for s in &data.version_toml.invalid {
608 let result = Version::try_new(s);
609 assert!(result.is_err(), "{s:?} didn't fail");
610 }
611
612 for s in &data.version_toml.valid {
614 let result = Version::try_new(s);
615 assert!(result.is_ok(), "{s:?} failed");
616 let ver = result.unwrap();
617 assert_eq!(ver.to_string(), s.as_str());
618 assert!(format!("{ver:?}").contains(s));
619 }
620
621 assert!(Version::try_new_with_op(">1").is_ok());
623 assert!(Version::try_new_with_op("1").is_err());
624 assert!(Version::try_new_without_op(">1").is_err());
625 assert!(Version::try_new_without_op("1").is_ok());
626 }
627
628 #[test]
629 fn op() {
630 let ver = Version::try_new("1").unwrap();
631 assert!(ver.op().is_none());
632
633 for op in Operator::iter() {
634 let ver = Version::try_new("1").unwrap().with_op(op).unwrap();
635 assert_eq!(ver.op(), Some(op));
636 }
637 }
638
639 #[test]
640 fn rev_new_and_parse() {
641 for s in ["a", "a1", "1.1", ".1"] {
643 assert!(s.parse::<Revision>().is_err());
644 assert!(Revision::try_new(s).is_err());
645 }
646
647 for s in ["", "1", "01"] {
649 let rev = Revision::try_new(s).unwrap();
650 assert_eq!(rev.to_string(), s);
651 }
652 }
653
654 #[test]
655 fn compare() {
656 let op_map: HashMap<_, _> =
657 [("<", Ordering::Less), ("==", Ordering::Equal), (">", Ordering::Greater)]
658 .into_iter()
659 .collect();
660
661 let data = test_data();
662 for (expr, (s1, op, s2)) in data.version_toml.compares() {
663 let v1 = Version::try_new(s1).unwrap();
664 let v2 = Version::try_new(s2).unwrap();
665 if op == "!=" {
666 assert_ne!(v1, v2, "failed comparing: {expr}");
667 assert_ne!(v2, v1, "failed comparing: {expr}");
668 } else {
669 let op = op_map[op];
670 assert_eq!(v1.cmp(&v2), op, "failed comparing: {expr}");
671 assert_eq!(v2.cmp(&v1), op.reverse(), "failed comparing inverted: {expr}");
672
673 if op == Ordering::Equal {
676 assert_eq!(hash(&v1), hash(&v2), "failed hash: {expr}");
677 }
678 }
679 }
680 }
681
682 #[test]
683 fn intersects() {
684 let data = test_data();
685 for d in &data.version_toml.intersects {
686 let permutations = d
688 .vals
689 .iter()
690 .map(|s| s.as_str())
691 .permutations(2)
692 .map(|val| val.into_iter().collect_tuple().unwrap());
693 for (s1, s2) in permutations {
694 let v1 = Version::try_new(s1).unwrap();
695 let v2 = Version::try_new(s2).unwrap();
696
697 assert!(v1.intersects(&v1), "{s1} doesn't intersect {s2}");
699 assert!(v2.intersects(&v2), "{s2} doesn't intersect {s2}");
700
701 if d.status {
703 assert!(v1.intersects(&v2), "{s1} doesn't intersect {s2}");
704 } else {
705 assert!(!v1.intersects(&v2), "{s1} intersects {s2}");
706 }
707 }
708 }
709 }
710
711 #[test]
712 fn sorting() {
713 let data = test_data();
714 for d in &data.version_toml.sorting {
715 let mut reversed: Vec<Version> =
716 d.sorted.iter().map(|s| s.parse().unwrap()).rev().collect();
717 reversed.sort();
718 let mut sorted: Vec<_> = reversed.iter().map(|x| x.to_string()).collect();
719 if d.equal {
720 sorted = sorted.into_iter().rev().collect();
722 }
723 assert_eq!(&sorted, &d.sorted);
724 }
725 }
726
727 #[test]
728 fn hashing() {
729 let data = test_data();
730 for d in &data.version_toml.hashing {
731 let set: HashSet<Version> =
732 d.versions.iter().map(|s| s.parse().unwrap()).collect();
733 if d.equal {
734 assert_eq!(set.len(), 1, "failed hashing versions: {set:?}");
735 } else {
736 assert_eq!(set.len(), d.versions.len(), "failed hashing versions: {set:?}");
737 }
738 }
739 }
740}