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