1use serde::{Deserialize, Serialize};
2use std::cmp::Ordering;
3use std::fmt;
4use std::str::FromStr;
5use thiserror::Error;
6
7#[derive(Error, Debug)]
8pub enum VersionError {
9 #[error("Invalid version string: {0}")]
10 InvalidVersion(String),
11 #[error("Invalid version specifier: {0}")]
12 InvalidSpecifier(String),
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Version {
18 pub major: u64,
19 pub minor: u64,
20 pub patch: u64,
21 pub pre_release: Option<String>,
22 pub local: Option<String>,
24 pub original: String,
26}
27
28impl PartialEq for Version {
29 fn eq(&self, other: &Self) -> bool {
30 self.major == other.major
31 && self.minor == other.minor
32 && self.patch == other.patch
33 && self.pre_release == other.pre_release
34 }
35}
36
37impl Eq for Version {}
38
39impl Version {
40 pub fn new(major: u64, minor: u64, patch: u64) -> Self {
41 Self {
42 major,
43 minor,
44 patch,
45 pre_release: None,
46 local: None,
47 original: format!("{major}.{minor}.{patch}"),
48 }
49 }
50
51 pub fn is_prerelease(&self) -> bool {
53 self.pre_release.is_some()
54 }
55
56 pub fn same_major(&self, other: &Version) -> bool {
58 self.major == other.major
59 }
60
61 pub fn same_minor(&self, other: &Version) -> bool {
63 self.major == other.major && self.minor == other.minor
64 }
65}
66
67impl FromStr for Version {
68 type Err = VersionError;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 let s = s.trim();
72
73 let (version_part, local) = if let Some(idx) = s.find('+') {
75 (&s[..idx], Some(s[idx + 1..].to_string()))
76 } else {
77 (s, None)
78 };
79
80 let (base_part, pre_release) = parse_prerelease(version_part);
82
83 let parts: Vec<&str> = base_part.split('.').collect();
85
86 let major = parts
87 .first()
88 .and_then(|s| s.parse().ok())
89 .ok_or_else(|| VersionError::InvalidVersion(s.to_string()))?;
90
91 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
92
93 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
94
95 Ok(Version {
96 major,
97 minor,
98 patch,
99 pre_release,
100 local,
101 original: s.to_string(),
102 })
103 }
104}
105
106fn parse_prerelease(s: &str) -> (&str, Option<String>) {
107 let patterns = [
109 "dev", "post", "alpha", "beta", "rc", "a", "b", "c", "-",
110 ];
111
112 for pattern in patterns {
113 if let Some(idx) = s.to_lowercase().find(pattern)
114 && idx > 0 {
115 return (&s[..idx], Some(s[idx..].to_string()));
116 }
117 }
118
119 (s, None)
120}
121
122impl Ord for Version {
123 fn cmp(&self, other: &Self) -> Ordering {
124 match self.major.cmp(&other.major) {
125 Ordering::Equal => {}
126 ord => return ord,
127 }
128 match self.minor.cmp(&other.minor) {
129 Ordering::Equal => {}
130 ord => return ord,
131 }
132 match self.patch.cmp(&other.patch) {
133 Ordering::Equal => {}
134 ord => return ord,
135 }
136
137 match (&self.pre_release, &other.pre_release) {
139 (None, Some(_)) => Ordering::Greater,
140 (Some(_), None) => Ordering::Less,
141 (Some(a), Some(b)) => a.cmp(b),
142 (None, None) => Ordering::Equal,
143 }
144 }
145}
146
147impl PartialOrd for Version {
148 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
149 Some(self.cmp(other))
150 }
151}
152
153impl fmt::Display for Version {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "{}", self.original)
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub enum VersionSpec {
162 Pinned(Version),
164 Minimum(Version),
166 Maximum(Version),
168 GreaterThan(Version),
170 LessThan(Version),
172 Range { min: Version, max: Version },
174 Caret(Version),
176 Tilde(Version),
178 Compatible(Version),
180 Wildcard { prefix: String, pattern: String },
182 NotEqual(Version),
184 Complex(String),
186 Any,
188}
189
190impl VersionSpec {
191 pub fn parse(s: &str) -> Result<Self, VersionError> {
193 let s = s.trim();
194
195 if s.is_empty() || s == "*" {
196 return Ok(VersionSpec::Any);
197 }
198
199 if let Some(version_str) = s.strip_prefix('^') {
201 let version = Version::from_str(version_str)?;
202 return Ok(VersionSpec::Caret(version));
203 }
204
205 if let Some(version_str) = s.strip_prefix("~=") {
207 let version = Version::from_str(version_str)?;
208 return Ok(VersionSpec::Compatible(version));
209 }
210 if let Some(version_str) = s.strip_prefix('~') {
211 let version = Version::from_str(version_str)?;
212 return Ok(VersionSpec::Tilde(version));
213 }
214
215 if s.contains('*') {
217 if let Some(prefix) = s.strip_prefix("==") {
218 return Ok(VersionSpec::Wildcard {
219 prefix: prefix.replace(".*", "").replace("*", ""),
220 pattern: s.to_string(),
221 });
222 }
223 return Ok(VersionSpec::Wildcard {
224 prefix: s.replace(".*", "").replace("*", ""),
225 pattern: s.to_string(),
226 });
227 }
228
229 if s.contains(',') {
231 let parts: Vec<&str> = s.split(',').collect();
232 if parts.len() == 2 {
233 let min_part = parts[0].trim();
234 let max_part = parts[1].trim();
235
236 if let (Some(min_str), Some(max_str)) = (
237 min_part.strip_prefix(">="),
238 max_part.strip_prefix('<'),
239 ) {
240 let min = Version::from_str(min_str)?;
241 let max = Version::from_str(max_str)?;
242 return Ok(VersionSpec::Range { min, max });
243 }
244 }
245 return Ok(VersionSpec::Complex(s.to_string()));
247 }
248
249 if let Some(version_str) = s.strip_prefix("==") {
251 let version = Version::from_str(version_str)?;
252 return Ok(VersionSpec::Pinned(version));
253 }
254 if let Some(version_str) = s.strip_prefix(">=") {
255 let version = Version::from_str(version_str)?;
256 return Ok(VersionSpec::Minimum(version));
257 }
258 if let Some(version_str) = s.strip_prefix("<=") {
259 let version = Version::from_str(version_str)?;
260 return Ok(VersionSpec::Maximum(version));
261 }
262 if let Some(version_str) = s.strip_prefix("!=") {
263 let version = Version::from_str(version_str)?;
264 return Ok(VersionSpec::NotEqual(version));
265 }
266 if let Some(version_str) = s.strip_prefix('>') {
267 let version = Version::from_str(version_str)?;
268 return Ok(VersionSpec::GreaterThan(version));
269 }
270 if let Some(version_str) = s.strip_prefix('<') {
271 let version = Version::from_str(version_str)?;
272 return Ok(VersionSpec::LessThan(version));
273 }
274
275 if let Ok(version) = Version::from_str(s) {
277 return Ok(VersionSpec::Pinned(version));
278 }
279
280 Ok(VersionSpec::Complex(s.to_string()))
281 }
282
283 pub fn satisfies(&self, version: &Version) -> bool {
285 match self {
286 VersionSpec::Any => true,
287 VersionSpec::Pinned(v) => version == v,
288 VersionSpec::Minimum(v) => version >= v,
289 VersionSpec::Maximum(v) => version <= v,
290 VersionSpec::GreaterThan(v) => version > v,
291 VersionSpec::LessThan(v) => version < v,
292 VersionSpec::Range { min, max } => version >= min && version < max,
293 VersionSpec::Caret(v) => {
294 if version < v {
298 return false;
299 }
300 if v.major == 0 {
301 if v.minor == 0 {
302 version.major == 0 && version.minor == 0 && version.patch == v.patch
304 } else {
305 version.major == 0 && version.minor == v.minor
307 }
308 } else {
309 version.major == v.major
311 }
312 }
313 VersionSpec::Tilde(v) => {
314 version >= v && version.major == v.major && version.minor == v.minor
315 }
316 VersionSpec::Compatible(v) => {
317 let dot_count = v.original.chars().filter(|c| *c == '.').count();
320 if dot_count < 2 {
321 version >= v && version.major == v.major
323 } else {
324 version >= v && version.major == v.major && version.minor == v.minor
326 }
327 }
328 VersionSpec::Wildcard { prefix, .. } => {
329 version.original.starts_with(&format!("{prefix}."))
331 || version.original == *prefix
332 }
333 VersionSpec::NotEqual(v) => version != v,
334 VersionSpec::Complex(_) => false, }
336 }
337
338 pub fn base_version(&self) -> Option<&Version> {
340 match self {
341 VersionSpec::Pinned(v)
342 | VersionSpec::Minimum(v)
343 | VersionSpec::Maximum(v)
344 | VersionSpec::GreaterThan(v)
345 | VersionSpec::LessThan(v)
346 | VersionSpec::Caret(v)
347 | VersionSpec::Tilde(v)
348 | VersionSpec::Compatible(v)
349 | VersionSpec::NotEqual(v) => Some(v),
350 VersionSpec::Range { min, .. } => Some(min),
351 VersionSpec::Wildcard { .. } | VersionSpec::Complex(_) | VersionSpec::Any => None,
352 }
353 }
354
355 pub fn max_major(&self) -> Option<u64> {
357 match self {
358 VersionSpec::Range { max, .. } => Some(max.major),
359 VersionSpec::Caret(v) => Some(v.major),
360 VersionSpec::LessThan(v) | VersionSpec::Maximum(v) => Some(v.major),
361 VersionSpec::Minimum(v)
363 | VersionSpec::GreaterThan(v)
364 | VersionSpec::Pinned(v)
365 | VersionSpec::Compatible(v)
366 | VersionSpec::Tilde(v) => Some(v.major),
367 VersionSpec::NotEqual(v) => Some(v.major),
368 VersionSpec::Wildcard { prefix, .. } => {
369 prefix.split('.').next().and_then(|s| s.parse().ok())
370 }
371 VersionSpec::Complex(_) | VersionSpec::Any => None,
372 }
373 }
374
375 pub fn version_string(&self) -> Option<String> {
378 match self {
379 VersionSpec::Pinned(v)
380 | VersionSpec::Minimum(v)
381 | VersionSpec::Maximum(v)
382 | VersionSpec::GreaterThan(v)
383 | VersionSpec::LessThan(v)
384 | VersionSpec::Caret(v)
385 | VersionSpec::Tilde(v)
386 | VersionSpec::Compatible(v)
387 | VersionSpec::NotEqual(v) => Some(v.to_string()),
388 VersionSpec::Range { min, .. } => Some(min.to_string()),
389 VersionSpec::Wildcard { prefix, .. } => Some(format!("{prefix}.*")),
390 VersionSpec::Complex(s) => Some(s.clone()),
391 VersionSpec::Any => None,
392 }
393 }
394
395 pub fn to_cargo_string(&self) -> Option<String> {
398 match self {
399 VersionSpec::Caret(v) => Some(v.to_string()), VersionSpec::Tilde(v) => Some(format!("~{v}")),
401 VersionSpec::Pinned(v) => Some(format!("={v}")), VersionSpec::Minimum(v) => Some(format!(">={v}")),
403 VersionSpec::Maximum(v) => Some(format!("<={v}")),
404 VersionSpec::GreaterThan(v) => Some(format!(">{v}")),
405 VersionSpec::LessThan(v) => Some(format!("<{v}")),
406 VersionSpec::Range { min, max } => Some(format!(">={min}, <{max}")),
407 VersionSpec::Wildcard { prefix, .. } => Some(format!("{prefix}.*")),
408 VersionSpec::NotEqual(v) => Some(format!("!={v}")),
409 VersionSpec::Compatible(v) => Some(v.to_string()), VersionSpec::Complex(s) => Some(s.clone()),
411 VersionSpec::Any => Some("*".to_string()),
412 }
413 }
414
415 pub fn is_rewritable(&self) -> bool {
417 !matches!(self, VersionSpec::Complex(_) | VersionSpec::Any)
418 }
419
420 pub fn with_version(&self, new_version: &Version) -> VersionSpec {
422 match self {
423 VersionSpec::Pinned(_) => VersionSpec::Pinned(new_version.clone()),
424 VersionSpec::Minimum(_) => VersionSpec::Minimum(new_version.clone()),
425 VersionSpec::Maximum(_) => VersionSpec::Maximum(new_version.clone()),
426 VersionSpec::GreaterThan(_) => VersionSpec::GreaterThan(new_version.clone()),
427 VersionSpec::LessThan(_) => VersionSpec::LessThan(new_version.clone()),
428 VersionSpec::Range { max, .. } => {
429 if new_version >= max {
431 VersionSpec::Range {
432 min: new_version.clone(),
433 max: Version::new(new_version.major + 1, 0, 0),
434 }
435 } else {
436 VersionSpec::Range {
437 min: new_version.clone(),
438 max: max.clone(),
439 }
440 }
441 }
442 VersionSpec::Caret(_) => VersionSpec::Caret(new_version.clone()),
443 VersionSpec::Tilde(_) => VersionSpec::Tilde(new_version.clone()),
444 VersionSpec::Compatible(_) => VersionSpec::Compatible(new_version.clone()),
445 VersionSpec::Wildcard { prefix, pattern } => {
446 let segments = prefix.split('.').count();
449 let new_prefix = match segments {
450 0 | 1 => format!("{}", new_version.major),
451 _ => format!("{}.{}", new_version.major, new_version.minor),
452 };
453 VersionSpec::Wildcard {
454 prefix: new_prefix,
455 pattern: pattern.clone(),
456 }
457 }
458 VersionSpec::NotEqual(_) => VersionSpec::NotEqual(new_version.clone()),
459 VersionSpec::Complex(s) => VersionSpec::Complex(s.clone()),
460 VersionSpec::Any => VersionSpec::Any,
461 }
462 }
463}
464
465impl fmt::Display for VersionSpec {
466 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
467 match self {
468 VersionSpec::Any => write!(f, "*"),
469 VersionSpec::Pinned(v) => write!(f, "=={v}"),
470 VersionSpec::Minimum(v) => write!(f, ">={v}"),
471 VersionSpec::Maximum(v) => write!(f, "<={v}"),
472 VersionSpec::GreaterThan(v) => write!(f, ">{v}"),
473 VersionSpec::LessThan(v) => write!(f, "<{v}"),
474 VersionSpec::Range { min, max } => write!(f, ">={min},<{max}"),
475 VersionSpec::Caret(v) => write!(f, "^{v}"),
476 VersionSpec::Tilde(v) => write!(f, "~{v}"),
477 VersionSpec::Compatible(v) => write!(f, "~={v}"),
478 VersionSpec::Wildcard { prefix, .. } => write!(f, "=={prefix}.*"),
479 VersionSpec::NotEqual(v) => write!(f, "!={v}"),
480 VersionSpec::Complex(s) => write!(f, "{s}"),
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_parse_version() {
491 let v = Version::from_str("1.2.3").unwrap();
492 assert_eq!(v.major, 1);
493 assert_eq!(v.minor, 2);
494 assert_eq!(v.patch, 3);
495
496 let v = Version::from_str("2.0").unwrap();
497 assert_eq!(v.major, 2);
498 assert_eq!(v.minor, 0);
499 assert_eq!(v.patch, 0);
500 }
501
502 #[test]
503 fn test_version_comparison() {
504 let v1 = Version::from_str("1.2.3").unwrap();
505 let v2 = Version::from_str("1.2.4").unwrap();
506 let v3 = Version::from_str("2.0.0").unwrap();
507
508 assert!(v1 < v2);
509 assert!(v2 < v3);
510 assert!(v1 < v3);
511 }
512
513 #[test]
514 fn test_parse_version_spec() {
515 assert!(matches!(
516 VersionSpec::parse("==1.2.3").unwrap(),
517 VersionSpec::Pinned(_)
518 ));
519 assert!(matches!(
520 VersionSpec::parse(">=1.2.3").unwrap(),
521 VersionSpec::Minimum(_)
522 ));
523 assert!(matches!(
524 VersionSpec::parse("^1.2.3").unwrap(),
525 VersionSpec::Caret(_)
526 ));
527 assert!(matches!(
528 VersionSpec::parse(">=1.0.0,<2.0.0").unwrap(),
529 VersionSpec::Range { .. }
530 ));
531 }
532
533 #[test]
534 fn test_satisfies() {
535 let spec = VersionSpec::parse(">=1.0.0,<2.0.0").unwrap();
536 assert!(spec.satisfies(&Version::from_str("1.5.0").unwrap()));
537 assert!(!spec.satisfies(&Version::from_str("2.0.0").unwrap()));
538 assert!(!spec.satisfies(&Version::from_str("0.9.0").unwrap()));
539 }
540}