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
107fn parse_version_component(s: &str) -> PrimitiveResult<u64> {
108 if s.is_empty() {
109 return Err(PrimitiveError::Invalid {
110 message: "semver component must not be empty",
111 });
112 }
113 if s.len() > 1 && s.starts_with('0') {
114 return Err(PrimitiveError::Invalid {
115 message: "semver component must not have leading zeros",
116 });
117 }
118 parse_u64(s).ok_or(PrimitiveError::Invalid {
119 message: "semver component must be a non-negative integer",
120 })
121}
122
123fn parse_u64(s: &str) -> Option<u64> {
124 if s.is_empty() {
125 return None;
126 }
127 let mut result: u64 = 0;
128 for b in s.bytes() {
129 if !b.is_ascii_digit() {
130 return None;
131 }
132 let digit = (b - b'0') as u64;
133 result = result.checked_mul(10)?.checked_add(digit)?;
134 }
135 Some(result)
136}
137
138#[derive(Copy, Clone)]
139enum IdentifierKind {
140 PreRelease,
141 Build,
142}
143
144fn validate_identifier_set(s: &str, kind: IdentifierKind) -> PrimitiveResult<()> {
145 if s.is_empty() {
146 return Err(PrimitiveError::Invalid {
147 message: match kind {
148 IdentifierKind::PreRelease => "pre-release identifier must not be empty after '-'",
149 IdentifierKind::Build => "build metadata must not be empty after '+'",
150 },
151 });
152 }
153
154 for identifier in s.split('.') {
155 if identifier.is_empty() {
156 return Err(PrimitiveError::Invalid {
157 message: "semver identifiers must not be empty",
158 });
159 }
160
161 if !identifier
162 .bytes()
163 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
164 {
165 return Err(PrimitiveError::Invalid {
166 message: "semver identifiers must contain only ASCII alphanumerics and hyphens",
167 });
168 }
169
170 if matches!(kind, IdentifierKind::PreRelease)
171 && is_numeric_identifier(identifier)
172 && identifier.len() > 1
173 && identifier.starts_with('0')
174 {
175 return Err(PrimitiveError::Invalid {
176 message: "numeric pre-release identifiers must not have leading zeros",
177 });
178 }
179 }
180
181 Ok(())
182}
183
184fn is_numeric_identifier(s: &str) -> bool {
185 s.bytes().all(|b| b.is_ascii_digit())
186}
187
188fn compare_numeric_identifier(a: &str, b: &str) -> core::cmp::Ordering {
189 a.len().cmp(&b.len()).then_with(|| a.cmp(b))
190}
191
192fn compare_pre_release(a: &str, b: &str) -> core::cmp::Ordering {
193 for (left, right) in a.split('.').zip(b.split('.')) {
194 let left_numeric = is_numeric_identifier(left);
195 let right_numeric = is_numeric_identifier(right);
196
197 let ordering = match (left_numeric, right_numeric) {
198 (true, true) => compare_numeric_identifier(left, right),
199 (true, false) => core::cmp::Ordering::Less,
200 (false, true) => core::cmp::Ordering::Greater,
201 (false, false) => left.cmp(right),
202 };
203
204 if ordering != core::cmp::Ordering::Equal {
205 return ordering;
206 }
207 }
208
209 a.split('.').count().cmp(&b.split('.').count())
210}
211
212impl fmt::Display for SemVer {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
215 if let Some(pre) = &self.pre {
216 write!(f, "-{pre}")?;
217 }
218 if let Some(build) = &self.build {
219 write!(f, "+{build}")?;
220 }
221 Ok(())
222 }
223}
224
225impl FromStr for SemVer {
226 type Err = PrimitiveError;
227
228 fn from_str(s: &str) -> Result<Self, Self::Err> {
229 Self::parse(s)
230 }
231}
232
233impl PartialEq<str> for SemVer {
234 fn eq(&self, other: &str) -> bool {
235 Self::parse(other).is_ok_and(|other| self == &other)
236 }
237}
238
239impl PartialEq<&str> for SemVer {
240 fn eq(&self, other: &&str) -> bool {
241 self.eq(*other)
242 }
243}
244
245impl PartialEq<String> for SemVer {
246 fn eq(&self, other: &String) -> bool {
247 self.eq(other.as_str())
248 }
249}
250
251impl PartialEq<&String> for SemVer {
252 fn eq(&self, other: &&String) -> bool {
253 self.eq(other.as_str())
254 }
255}
256
257impl PartialOrd for SemVer {
258 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
259 Some(self.cmp(other))
260 }
261}
262
263impl Ord for SemVer {
264 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
265 let v = (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
266 if v != core::cmp::Ordering::Equal {
267 return v;
268 }
269 match (&self.pre, &other.pre) {
271 (None, None) => core::cmp::Ordering::Equal,
272 (Some(_), None) => core::cmp::Ordering::Less,
273 (None, Some(_)) => core::cmp::Ordering::Greater,
274 (Some(a), Some(b)) => compare_pre_release(a, b),
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::SemVer;
282 use crate::PrimitiveError;
283 use alloc::string::ToString;
284
285 #[test]
286 fn parses_simple() {
287 let v = SemVer::parse("1.2.3").unwrap();
288 assert_eq!(v.major(), 1);
289 assert_eq!(v.minor(), 2);
290 assert_eq!(v.patch(), 3);
291 assert!(v.pre().is_none());
292 assert!(v.build().is_none());
293 }
294
295 #[test]
296 fn parses_with_pre_release() {
297 let v = SemVer::parse("2.0.0-beta.1").unwrap();
298 assert_eq!(v.pre(), Some("beta.1"));
299 assert!(v.is_pre_release());
300 }
301
302 #[test]
303 fn parses_with_build() {
304 let v = SemVer::parse("1.0.0+build.456").unwrap();
305 assert_eq!(v.build(), Some("build.456"));
306 }
307
308 #[test]
309 fn parses_pre_and_build() {
310 let v = SemVer::parse("1.0.0-alpha.1+build.001").unwrap();
311 assert_eq!(v.pre(), Some("alpha.1"));
312 assert_eq!(v.build(), Some("build.001"));
313 }
314
315 #[test]
316 fn rejects_empty() {
317 assert_eq!(SemVer::parse("").unwrap_err(), PrimitiveError::Empty);
318 }
319
320 #[test]
321 fn rejects_missing_components() {
322 assert!(SemVer::parse("1.2").is_err());
323 }
324
325 #[test]
326 fn rejects_too_many_components() {
327 assert!(SemVer::parse("1.2.3.4").is_err());
328 }
329
330 #[test]
331 fn rejects_leading_zeros() {
332 assert!(SemVer::parse("1.02.3").is_err());
333 }
334
335 #[test]
336 fn rejects_non_numeric() {
337 assert!(SemVer::parse("a.b.c").is_err());
338 }
339
340 #[test]
341 fn rejects_empty_pre_release() {
342 assert!(SemVer::parse("1.0.0-").is_err());
343 }
344
345 #[test]
346 fn rejects_empty_build() {
347 assert!(SemVer::parse("1.0.0+").is_err());
348 }
349
350 #[test]
351 fn rejects_build_with_plus() {
352 assert!(SemVer::parse("1.0.0+a+b").is_err());
353 }
354
355 #[test]
356 fn rejects_invalid_pre_release_identifiers() {
357 assert!(SemVer::parse("1.0.0-alpha..1").is_err());
358 assert!(SemVer::parse("1.0.0-alpha_1").is_err());
359 assert!(SemVer::parse("1.0.0-01").is_err());
360 }
361
362 #[test]
363 fn rejects_invalid_build_identifiers() {
364 assert!(SemVer::parse("1.0.0+build..1").is_err());
365 assert!(SemVer::parse("1.0.0+build_1").is_err());
366 }
367
368 #[test]
369 fn display() {
370 assert_eq!(SemVer::parse("1.2.3").unwrap().to_string(), "1.2.3");
371 assert_eq!(
372 SemVer::parse("2.0.0-beta.1").unwrap().to_string(),
373 "2.0.0-beta.1"
374 );
375 assert_eq!(
376 SemVer::parse("1.0.0+build").unwrap().to_string(),
377 "1.0.0+build"
378 );
379 assert_eq!(
380 SemVer::parse("1.0.0-alpha+build").unwrap().to_string(),
381 "1.0.0-alpha+build"
382 );
383 }
384
385 #[test]
386 fn new_constructor() {
387 let v = SemVer::new(1, 0, 0);
388 assert_eq!(v.to_string(), "1.0.0");
389 }
390
391 #[test]
392 fn ordering() {
393 let v1 = SemVer::parse("1.0.0").unwrap();
394 let v2 = SemVer::parse("2.0.0").unwrap();
395 let v3 = SemVer::parse("1.1.0").unwrap();
396 assert!(v1 < v2);
397 assert!(v1 < v3);
398 assert!(v3 < v2);
399 }
400
401 #[test]
402 fn pre_release_sorts_below_release() {
403 let release = SemVer::parse("1.0.0").unwrap();
404 let pre = SemVer::parse("1.0.0-alpha").unwrap();
405 assert!(pre < release);
406 assert!(release > pre);
407 }
408
409 #[test]
410 fn pre_release_compared_lexicographically() {
411 let alpha = SemVer::parse("1.0.0-alpha").unwrap();
412 let beta = SemVer::parse("1.0.0-beta").unwrap();
413 assert!(alpha < beta);
414 }
415
416 #[test]
417 fn pre_release_numeric_identifiers_compare_numerically() {
418 let two = SemVer::parse("1.0.0-alpha.2").unwrap();
419 let ten = SemVer::parse("1.0.0-alpha.10").unwrap();
420 assert!(two < ten);
421 }
422
423 #[test]
424 fn pre_release_numeric_identifier_comparison_does_not_overflow() {
425 let smaller = SemVer::parse("1.0.0-alpha.999999999999999999999999999999").unwrap();
426 let larger = SemVer::parse("1.0.0-alpha.1000000000000000000000000000000").unwrap();
427 assert!(smaller < larger);
428 }
429
430 #[test]
431 fn pre_release_numeric_identifiers_sort_before_non_numeric() {
432 let numeric = SemVer::parse("1.0.0-1").unwrap();
433 let alpha = SemVer::parse("1.0.0-alpha").unwrap();
434 assert!(numeric < alpha);
435 }
436
437 #[test]
438 fn from_str_and_string_comparisons() {
439 let version = "1.2.3-beta.1".parse::<SemVer>().unwrap();
440 let owned = "1.2.3-beta.1".to_string();
441 assert_eq!(version, "1.2.3-beta.1");
442 assert_eq!(version, owned);
443 assert!("1.2".parse::<SemVer>().is_err());
444 }
445}