1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::{String, ToString};
3use core::{fmt, str::FromStr};
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub struct SemVer {
12 major: u64,
13 minor: u64,
14 patch: u64,
15 pre: Option<String>,
16 build: Option<String>,
17}
18
19impl SemVer {
20 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
22 Self {
23 major,
24 minor,
25 patch,
26 pre: None,
27 build: None,
28 }
29 }
30
31 pub fn parse(s: &str) -> PrimitiveResult<Self> {
33 if s.is_empty() {
34 return Err(PrimitiveError::Empty);
35 }
36
37 let (s, build) = if let Some(idx) = s.find('+') {
38 let b = s[idx + 1..].to_string();
39 if b.contains('+') {
40 return Err(PrimitiveError::Invalid {
41 message: "build metadata must not contain '+'",
42 });
43 }
44 validate_identifier_set(&b, IdentifierKind::Build)?;
45 (&s[..idx], Some(b))
46 } else {
47 (s, None)
48 };
49
50 let (s, pre) = if let Some(idx) = s.find('-') {
51 let p = s[idx + 1..].to_string();
52 validate_identifier_set(&p, IdentifierKind::PreRelease)?;
53 (&s[..idx], Some(p))
54 } else {
55 (s, None)
56 };
57
58 let mut parts = s.splitn(4, '.');
59 let major = parse_version_component(parts.next().unwrap_or(""))?;
60 let minor = parse_version_component(parts.next().unwrap_or(""))?;
61 let patch = parse_version_component(parts.next().unwrap_or(""))?;
62
63 if parts.next().is_some() {
64 return Err(PrimitiveError::Invalid {
65 message: "semver must have exactly three dot-separated components",
66 });
67 }
68
69 Ok(Self {
70 major,
71 minor,
72 patch,
73 pre,
74 build,
75 })
76 }
77
78 pub fn major(&self) -> u64 {
80 self.major
81 }
82 pub fn minor(&self) -> u64 {
84 self.minor
85 }
86 pub fn patch(&self) -> u64 {
88 self.patch
89 }
90
91 pub fn pre(&self) -> Option<&str> {
93 self.pre.as_deref()
94 }
95
96 pub fn build(&self) -> Option<&str> {
98 self.build.as_deref()
99 }
100
101 pub fn is_pre_release(&self) -> bool {
103 self.pre.is_some()
104 }
105
106 pub fn cmp_precedence(&self, other: &Self) -> core::cmp::Ordering {
112 let core =
113 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
114 if core != core::cmp::Ordering::Equal {
115 return core;
116 }
117
118 match (&self.pre, &other.pre) {
120 (None, None) => core::cmp::Ordering::Equal,
121 (Some(_), None) => core::cmp::Ordering::Less,
122 (None, Some(_)) => core::cmp::Ordering::Greater,
123 (Some(a), Some(b)) => compare_pre_release(a, b),
124 }
125 }
126}
127
128fn parse_version_component(s: &str) -> PrimitiveResult<u64> {
129 if s.is_empty() {
130 return Err(PrimitiveError::Invalid {
131 message: "semver component must not be empty",
132 });
133 }
134 if s.len() > 1 && s.starts_with('0') {
135 return Err(PrimitiveError::Invalid {
136 message: "semver component must not have leading zeros",
137 });
138 }
139 parse_u64(s).ok_or(PrimitiveError::Invalid {
140 message: "semver component must be a non-negative integer",
141 })
142}
143
144fn parse_u64(s: &str) -> Option<u64> {
145 if s.is_empty() {
146 return None;
147 }
148 let mut result: u64 = 0;
149 for b in s.bytes() {
150 if !b.is_ascii_digit() {
151 return None;
152 }
153 let digit = (b - b'0') as u64;
154 result = result.checked_mul(10)?.checked_add(digit)?;
155 }
156 Some(result)
157}
158
159#[derive(Copy, Clone)]
160enum IdentifierKind {
161 PreRelease,
162 Build,
163}
164
165fn validate_identifier_set(s: &str, kind: IdentifierKind) -> PrimitiveResult<()> {
166 if s.is_empty() {
167 return Err(PrimitiveError::Invalid {
168 message: match kind {
169 IdentifierKind::PreRelease => "pre-release identifier must not be empty after '-'",
170 IdentifierKind::Build => "build metadata must not be empty after '+'",
171 },
172 });
173 }
174
175 for identifier in s.split('.') {
176 if identifier.is_empty() {
177 return Err(PrimitiveError::Invalid {
178 message: "semver identifiers must not be empty",
179 });
180 }
181
182 if !identifier
183 .bytes()
184 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
185 {
186 return Err(PrimitiveError::Invalid {
187 message: "semver identifiers must contain only ASCII alphanumerics and hyphens",
188 });
189 }
190
191 if matches!(kind, IdentifierKind::PreRelease)
192 && is_numeric_identifier(identifier)
193 && identifier.len() > 1
194 && identifier.starts_with('0')
195 {
196 return Err(PrimitiveError::Invalid {
197 message: "numeric pre-release identifiers must not have leading zeros",
198 });
199 }
200 }
201
202 Ok(())
203}
204
205fn is_numeric_identifier(s: &str) -> bool {
206 s.bytes().all(|b| b.is_ascii_digit())
207}
208
209fn compare_numeric_identifier(a: &str, b: &str) -> core::cmp::Ordering {
210 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
211}
212
213fn compare_pre_release(a: &str, b: &str) -> core::cmp::Ordering {
214 for (left, right) in a.split('.').zip(b.split('.')) {
215 let left_numeric = is_numeric_identifier(left);
216 let right_numeric = is_numeric_identifier(right);
217
218 let ordering = match (left_numeric, right_numeric) {
219 (true, true) => compare_numeric_identifier(left, right),
220 (true, false) => core::cmp::Ordering::Less,
221 (false, true) => core::cmp::Ordering::Greater,
222 (false, false) => left.cmp(right),
223 };
224
225 if ordering != core::cmp::Ordering::Equal {
226 return ordering;
227 }
228 }
229
230 a.split('.').count().cmp(&b.split('.').count())
231}
232
233impl fmt::Display for SemVer {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
236 if let Some(pre) = &self.pre {
237 write!(f, "-{pre}")?;
238 }
239 if let Some(build) = &self.build {
240 write!(f, "+{build}")?;
241 }
242 Ok(())
243 }
244}
245
246impl FromStr for SemVer {
247 type Err = PrimitiveError;
248
249 fn from_str(s: &str) -> Result<Self, Self::Err> {
250 Self::parse(s)
251 }
252}
253
254impl PartialEq<str> for SemVer {
255 fn eq(&self, other: &str) -> bool {
256 Self::parse(other).is_ok_and(|other| self == &other)
257 }
258}
259
260impl PartialEq<&str> for SemVer {
261 fn eq(&self, other: &&str) -> bool {
262 self.eq(*other)
263 }
264}
265
266impl PartialEq<String> for SemVer {
267 fn eq(&self, other: &String) -> bool {
268 self.eq(other.as_str())
269 }
270}
271
272impl PartialEq<&String> for SemVer {
273 fn eq(&self, other: &&String) -> bool {
274 self.eq(other.as_str())
275 }
276}
277
278impl PartialOrd for SemVer {
279 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
280 Some(self.cmp(other))
281 }
282}
283
284impl Ord for SemVer {
285 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
286 self.cmp_precedence(other)
287 .then_with(|| self.build.cmp(&other.build))
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::SemVer;
294 use crate::PrimitiveError;
295 use alloc::collections::BTreeSet;
296 use alloc::string::ToString;
297
298 #[test]
299 fn parses_simple() {
300 let v = SemVer::parse("1.2.3").unwrap();
301 assert_eq!(v.major(), 1);
302 assert_eq!(v.minor(), 2);
303 assert_eq!(v.patch(), 3);
304 assert!(v.pre().is_none());
305 assert!(v.build().is_none());
306 }
307
308 #[test]
309 fn parses_with_pre_release() {
310 let v = SemVer::parse("2.0.0-beta.1").unwrap();
311 assert_eq!(v.pre(), Some("beta.1"));
312 assert!(v.is_pre_release());
313 }
314
315 #[test]
316 fn parses_with_build() {
317 let v = SemVer::parse("1.0.0+build.456").unwrap();
318 assert_eq!(v.build(), Some("build.456"));
319 }
320
321 #[test]
322 fn parses_pre_and_build() {
323 let v = SemVer::parse("1.0.0-alpha.1+build.001").unwrap();
324 assert_eq!(v.pre(), Some("alpha.1"));
325 assert_eq!(v.build(), Some("build.001"));
326 }
327
328 #[test]
329 fn rejects_empty() {
330 assert_eq!(SemVer::parse("").unwrap_err(), PrimitiveError::Empty);
331 }
332
333 #[test]
334 fn rejects_missing_components() {
335 assert!(SemVer::parse("1.2").is_err());
336 }
337
338 #[test]
339 fn rejects_too_many_components() {
340 assert!(SemVer::parse("1.2.3.4").is_err());
341 }
342
343 #[test]
344 fn rejects_leading_zeros() {
345 assert!(SemVer::parse("1.02.3").is_err());
346 }
347
348 #[test]
349 fn rejects_non_numeric() {
350 assert!(SemVer::parse("a.b.c").is_err());
351 }
352
353 #[test]
354 fn rejects_empty_pre_release() {
355 assert!(SemVer::parse("1.0.0-").is_err());
356 }
357
358 #[test]
359 fn rejects_empty_build() {
360 assert!(SemVer::parse("1.0.0+").is_err());
361 }
362
363 #[test]
364 fn rejects_build_with_plus() {
365 assert!(SemVer::parse("1.0.0+a+b").is_err());
366 }
367
368 #[test]
369 fn rejects_invalid_pre_release_identifiers() {
370 assert!(SemVer::parse("1.0.0-alpha..1").is_err());
371 assert!(SemVer::parse("1.0.0-alpha_1").is_err());
372 assert!(SemVer::parse("1.0.0-01").is_err());
373 }
374
375 #[test]
376 fn rejects_invalid_build_identifiers() {
377 assert!(SemVer::parse("1.0.0+build..1").is_err());
378 assert!(SemVer::parse("1.0.0+build_1").is_err());
379 }
380
381 #[test]
382 fn display() {
383 assert_eq!(SemVer::parse("1.2.3").unwrap().to_string(), "1.2.3");
384 assert_eq!(
385 SemVer::parse("2.0.0-beta.1").unwrap().to_string(),
386 "2.0.0-beta.1"
387 );
388 assert_eq!(
389 SemVer::parse("1.0.0+build").unwrap().to_string(),
390 "1.0.0+build"
391 );
392 assert_eq!(
393 SemVer::parse("1.0.0-alpha+build").unwrap().to_string(),
394 "1.0.0-alpha+build"
395 );
396 }
397
398 #[test]
399 fn new_constructor() {
400 let v = SemVer::new(1, 0, 0);
401 assert_eq!(v.to_string(), "1.0.0");
402 }
403
404 #[test]
405 fn ordering() {
406 let v1 = SemVer::parse("1.0.0").unwrap();
407 let v2 = SemVer::parse("2.0.0").unwrap();
408 let v3 = SemVer::parse("1.1.0").unwrap();
409 assert!(v1 < v2);
410 assert!(v1 < v3);
411 assert!(v3 < v2);
412 }
413
414 #[test]
415 fn pre_release_sorts_below_release() {
416 let release = SemVer::parse("1.0.0").unwrap();
417 let pre = SemVer::parse("1.0.0-alpha").unwrap();
418 assert!(pre < release);
419 assert!(release > pre);
420 }
421
422 #[test]
423 fn pre_release_compared_lexicographically() {
424 let alpha = SemVer::parse("1.0.0-alpha").unwrap();
425 let beta = SemVer::parse("1.0.0-beta").unwrap();
426 assert!(alpha < beta);
427 }
428
429 #[test]
430 fn pre_release_numeric_identifiers_compare_numerically() {
431 let two = SemVer::parse("1.0.0-alpha.2").unwrap();
432 let ten = SemVer::parse("1.0.0-alpha.10").unwrap();
433 assert!(two < ten);
434 }
435
436 #[test]
437 fn pre_release_numeric_identifier_comparison_does_not_overflow() {
438 let smaller = SemVer::parse("1.0.0-alpha.999999999999999999999999999999").unwrap();
439 let larger = SemVer::parse("1.0.0-alpha.1000000000000000000000000000000").unwrap();
440 assert!(smaller < larger);
441 }
442
443 #[test]
444 fn pre_release_numeric_identifiers_sort_before_non_numeric() {
445 let numeric = SemVer::parse("1.0.0-1").unwrap();
446 let alpha = SemVer::parse("1.0.0-alpha").unwrap();
447 assert!(numeric < alpha);
448 }
449
450 #[test]
451 fn precedence_ignores_build_metadata() {
452 let first = SemVer::parse("1.0.0+build.1").unwrap();
453 let second = SemVer::parse("1.0.0+build.2").unwrap();
454
455 assert_eq!(first.cmp_precedence(&second), core::cmp::Ordering::Equal);
456 }
457
458 #[test]
459 fn ord_is_consistent_with_eq_for_build_metadata() {
460 let first = SemVer::parse("1.0.0+build.1").unwrap();
461 let second = SemVer::parse("1.0.0+build.2").unwrap();
462
463 assert_ne!(first, second);
464 assert_ne!(first.cmp(&second), core::cmp::Ordering::Equal);
465 }
466
467 #[test]
468 fn btree_set_keeps_distinct_build_metadata() {
469 let mut versions = BTreeSet::new();
470
471 versions.insert(SemVer::parse("1.0.0+build.1").unwrap());
472 versions.insert(SemVer::parse("1.0.0+build.2").unwrap());
473
474 assert_eq!(versions.len(), 2);
475 }
476
477 #[test]
478 fn from_str_and_string_comparisons() {
479 let version = "1.2.3-beta.1".parse::<SemVer>().unwrap();
480 let owned = "1.2.3-beta.1".to_string();
481 assert_eq!(version, "1.2.3-beta.1");
482 assert_eq!(version, owned);
483 assert!("1.2".parse::<SemVer>().is_err());
484 }
485}