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 && let Some(rev) = v.revision() {
431 write!(s, "-r{rev}")?;
432 }
433
434 if op {
435 match &v.op {
436 None => write!(f, "{s}"),
437 Some(Operator::Less) => write!(f, "<{s}"),
438 Some(Operator::LessOrEqual) => write!(f, "<={s}"),
439 Some(Operator::Equal) => write!(f, "={s}"),
440 Some(Operator::EqualGlob) => write!(f, "={s}*"),
441 Some(Operator::Approximate) => write!(f, "~{s}"),
442 Some(Operator::GreaterOrEqual) => write!(f, ">={s}"),
443 Some(Operator::Greater) => write!(f, ">{s}"),
444 }
445 } else {
446 write!(f, "{s}")
447 }
448}
449
450impl PartialEq for Version {
451 fn eq(&self, other: &Self) -> bool {
452 cmp(self, other, true, true) == Ordering::Equal
453 }
454}
455
456impl Eq for Version {}
457
458impl Hash for Version {
459 fn hash<H: Hasher>(&self, state: &mut H) {
460 self.numbers[0].hash(state);
461 for n in &self.numbers[1..] {
462 if n.as_str().starts_with('0') {
463 n.as_str().trim_end_matches('0').hash(state);
464 } else {
465 n.value.hash(state);
466 }
467 }
468 self.letter.hash(state);
469 self.suffixes.hash(state);
470 self.revision.hash(state);
471 }
472}
473
474fn cmp(v1: &Version, v2: &Version, rev: bool, op: bool) -> Ordering {
476 cmp_not_equal!(&v1.numbers[0], &v2.numbers[0]);
478
479 for numbers in v1.numbers[1..].iter().zip_longest(&v2.numbers[1..]) {
481 match numbers {
482 Both(n1, n2) => {
483 let (s1, s2) = (n1.as_str(), n2.as_str());
485 if s1.starts_with('0') || s2.starts_with('0') {
486 cmp_not_equal!(s1.trim_end_matches('0'), s2.trim_end_matches('0'));
487 } else {
488 cmp_not_equal!(n1, n2);
489 }
490 }
491 Left(_) => return Ordering::Greater,
492 Right(_) => return Ordering::Less,
493 }
494 }
495
496 cmp_not_equal!(&v1.letter, &v2.letter);
498
499 for suffixes in v1.suffixes.iter().zip_longest(&v2.suffixes) {
501 match suffixes {
502 Both(s1, s2) => cmp_not_equal!(s1, s2),
503 Left(s) if s.kind == SuffixKind::P => return Ordering::Greater,
504 Left(_) => return Ordering::Less,
505 Right(s) if s.kind == SuffixKind::P => return Ordering::Less,
506 Right(_) => return Ordering::Greater,
507 }
508 }
509
510 if rev {
512 cmp_not_equal!(&v1.revision, &v2.revision);
513 }
514
515 if op {
517 cmp_not_equal!(&v1.op, &v2.op);
518 }
519
520 Ordering::Equal
521}
522
523impl Ord for Version {
524 fn cmp(&self, other: &Self) -> Ordering {
525 cmp(self, other, true, true)
526 }
527}
528
529impl PartialOrd for Version {
530 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
531 Some(self.cmp(other))
532 }
533}
534
535impl FromStr for Version {
536 type Err = Error;
537
538 fn from_str(s: &str) -> crate::Result<Self> {
539 Self::try_new(s)
540 }
541}
542
543struct NonRevisionVersion<'a>(&'a Version);
545
546impl PartialEq for NonRevisionVersion<'_> {
547 fn eq(&self, other: &Self) -> bool {
548 cmp(self.0, other.0, false, false) == Ordering::Equal
549 }
550}
551
552impl Eq for NonRevisionVersion<'_> {}
553
554impl fmt::Display for NonRevisionVersion<'_> {
555 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
556 fmt(f, self.0, false, false)
557 }
558}
559
560struct NonOpVersion<'a>(&'a Version);
562
563impl PartialEq for NonOpVersion<'_> {
564 fn eq(&self, other: &Self) -> bool {
565 cmp(self.0, other.0, true, false) == Ordering::Equal
566 }
567}
568
569impl Eq for NonOpVersion<'_> {}
570
571impl Ord for NonOpVersion<'_> {
572 fn cmp(&self, other: &Self) -> Ordering {
573 cmp(self.0, other.0, true, false)
574 }
575}
576
577impl PartialOrd for NonOpVersion<'_> {
578 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
579 Some(self.cmp(other))
580 }
581}
582
583impl fmt::Display for NonOpVersion<'_> {
584 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
585 fmt(f, self.0, true, false)
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use std::collections::{HashMap, HashSet};
592
593 use itertools::Itertools;
594
595 use crate::test::test_data;
596 use crate::utils::hash;
597
598 use super::*;
599
600 #[test]
601 fn new_and_parse() {
602 let data = test_data();
603
604 for s in &data.version_toml.invalid {
606 let result = Version::try_new(s);
607 assert!(result.is_err(), "{s:?} didn't fail");
608 }
609
610 for s in &data.version_toml.valid {
612 let result = Version::try_new(s);
613 assert!(result.is_ok(), "{s:?} failed");
614 let ver = result.unwrap();
615 assert_eq!(ver.to_string(), s.as_str());
616 assert!(format!("{ver:?}").contains(s));
617 }
618
619 assert!(Version::try_new_with_op(">1").is_ok());
621 assert!(Version::try_new_with_op("1").is_err());
622 assert!(Version::try_new_without_op(">1").is_err());
623 assert!(Version::try_new_without_op("1").is_ok());
624 }
625
626 #[test]
627 fn op() {
628 let ver = Version::try_new("1").unwrap();
629 assert!(ver.op().is_none());
630
631 for op in Operator::iter() {
632 let ver = Version::try_new("1").unwrap().with_op(op).unwrap();
633 assert_eq!(ver.op(), Some(op));
634 }
635 }
636
637 #[test]
638 fn rev_new_and_parse() {
639 for s in ["a", "a1", "1.1", ".1"] {
641 assert!(s.parse::<Revision>().is_err());
642 assert!(Revision::try_new(s).is_err());
643 }
644
645 for s in ["", "1", "01"] {
647 let rev = Revision::try_new(s).unwrap();
648 assert_eq!(rev.to_string(), s);
649 }
650 }
651
652 #[test]
653 fn compare() {
654 let op_map: HashMap<_, _> =
655 [("<", Ordering::Less), ("==", Ordering::Equal), (">", Ordering::Greater)]
656 .into_iter()
657 .collect();
658
659 let data = test_data();
660 for (expr, (s1, op, s2)) in data.version_toml.compares() {
661 let v1 = Version::try_new(s1).unwrap();
662 let v2 = Version::try_new(s2).unwrap();
663 if op == "!=" {
664 assert_ne!(v1, v2, "failed comparing: {expr}");
665 assert_ne!(v2, v1, "failed comparing: {expr}");
666 } else {
667 let op = op_map[op];
668 assert_eq!(v1.cmp(&v2), op, "failed comparing: {expr}");
669 assert_eq!(v2.cmp(&v1), op.reverse(), "failed comparing inverted: {expr}");
670
671 if op == Ordering::Equal {
674 assert_eq!(hash(&v1), hash(&v2), "failed hash: {expr}");
675 }
676 }
677 }
678 }
679
680 #[test]
681 fn intersects() {
682 let data = test_data();
683 for d in &data.version_toml.intersects {
684 let permutations = d
686 .vals
687 .iter()
688 .map(|s| s.as_str())
689 .permutations(2)
690 .map(|val| val.into_iter().collect_tuple().unwrap());
691 for (s1, s2) in permutations {
692 let v1 = Version::try_new(s1).unwrap();
693 let v2 = Version::try_new(s2).unwrap();
694
695 assert!(v1.intersects(&v1), "{s1} doesn't intersect {s2}");
697 assert!(v2.intersects(&v2), "{s2} doesn't intersect {s2}");
698
699 if d.status {
701 assert!(v1.intersects(&v2), "{s1} doesn't intersect {s2}");
702 } else {
703 assert!(!v1.intersects(&v2), "{s1} intersects {s2}");
704 }
705 }
706 }
707 }
708
709 #[test]
710 fn sorting() {
711 let data = test_data();
712 for d in &data.version_toml.sorting {
713 let mut reversed: Vec<Version> =
714 d.sorted.iter().map(|s| s.parse().unwrap()).rev().collect();
715 reversed.sort();
716 let mut sorted: Vec<_> = reversed.iter().map(|x| x.to_string()).collect();
717 if d.equal {
718 sorted = sorted.into_iter().rev().collect();
720 }
721 assert_eq!(&sorted, &d.sorted);
722 }
723 }
724
725 #[test]
726 fn hashing() {
727 let data = test_data();
728 for d in &data.version_toml.hashing {
729 let set: HashSet<Version> =
730 d.versions.iter().map(|s| s.parse().unwrap()).collect();
731 if d.equal {
732 assert_eq!(set.len(), 1, "failed hashing versions: {set:?}");
733 } else {
734 assert_eq!(set.len(), d.versions.len(), "failed hashing versions: {set:?}");
735 }
736 }
737 }
738}