1#[cfg(not(feature = "std"))]
2use alloc::boxed::Box;
3
4use core::cmp::Ordering;
5use core::fmt;
6use core::str::FromStr;
7
8use crate::SemverError;
9use crate::error::SemverErrorKind;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
12pub struct PreRelease(Box<str>);
14
15impl PreRelease {
16 pub(crate) fn zero() -> Self {
17 Self(Box::from("0"))
18 }
19
20 pub fn new(s: &str) -> Result<Self, SemverError> {
36 if s.is_empty() {
37 return Err(SemverErrorKind::Empty.into());
38 }
39 validate_prerelease(s)?;
40 Ok(Self(Box::from(s)))
41 }
42
43 #[must_use]
44 pub fn is_empty(&self) -> bool {
55 self.0.is_empty()
56 }
57
58 pub(crate) fn cmp_identifiers(&self, other: &Self) -> Ordering {
59 if self.is_empty() || other.is_empty() {
60 return self.0.len().cmp(&other.0.len());
61 }
62
63 cmp_dot_separated(&self.0, &other.0, parse_prerelease_identifier)
64 }
65}
66
67impl fmt::Display for PreRelease {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(&self.0)
70 }
71}
72
73impl PartialOrd for PreRelease {
74 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
75 Some(self.cmp(other))
76 }
77}
78
79impl Ord for PreRelease {
80 fn cmp(&self, other: &Self) -> Ordering {
81 self.cmp_identifiers(other)
82 }
83}
84
85impl FromStr for PreRelease {
86 type Err = SemverError;
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 Self::new(s)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Default)]
94pub struct BuildMetadata(Box<str>);
96
97impl BuildMetadata {
98 pub fn new(s: &str) -> Result<Self, SemverError> {
114 if s.is_empty() {
115 return Err(SemverErrorKind::Empty.into());
116 }
117 validate_build_metadata(s)?;
118 Ok(Self(Box::from(s)))
119 }
120
121 #[must_use]
122 pub fn is_empty(&self) -> bool {
133 self.0.is_empty()
134 }
135}
136
137impl fmt::Display for BuildMetadata {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.write_str(&self.0)
140 }
141}
142
143impl PartialOrd for BuildMetadata {
144 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
145 Some(self.cmp(other))
146 }
147}
148
149impl Ord for BuildMetadata {
150 fn cmp(&self, other: &Self) -> Ordering {
151 if self.is_empty() || other.is_empty() {
152 return self.0.len().cmp(&other.0.len());
153 }
154
155 cmp_dot_separated(&self.0, &other.0, parse_build_metadata_identifier)
156 }
157}
158
159impl FromStr for BuildMetadata {
160 type Err = SemverError;
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
163 Self::new(s)
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168enum IdentifierKind {
169 Numeric,
170 AlphaNumeric,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174struct Identifier<'a> {
175 raw: &'a str,
176 kind: IdentifierKind,
177}
178
179impl<'a> Identifier<'a> {
180 fn new(raw: &'a str, kind: IdentifierKind) -> Self {
181 Self { raw, kind }
182 }
183}
184
185fn parse_prerelease_identifier(raw: &str) -> Identifier<'_> {
186 let bytes = raw.as_bytes();
187 Identifier::new(
188 raw,
189 if bytes.iter().all(u8::is_ascii_digit) {
190 IdentifierKind::Numeric
191 } else {
192 IdentifierKind::AlphaNumeric
193 },
194 )
195}
196
197fn parse_build_metadata_identifier(raw: &str) -> Identifier<'_> {
198 Identifier::new(
199 raw,
200 if raw.bytes().all(|byte| byte.is_ascii_digit()) {
201 IdentifierKind::Numeric
202 } else {
203 IdentifierKind::AlphaNumeric
204 },
205 )
206}
207
208impl PartialOrd for Identifier<'_> {
209 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
210 Some(self.cmp(other))
211 }
212}
213
214impl Ord for Identifier<'_> {
215 fn cmp(&self, other: &Self) -> Ordering {
216 match (self.kind, other.kind) {
217 (IdentifierKind::Numeric, IdentifierKind::Numeric) => {
218 cmp_numeric_strings(self.raw, other.raw)
219 }
220 (IdentifierKind::Numeric, IdentifierKind::AlphaNumeric) => Ordering::Less,
221 (IdentifierKind::AlphaNumeric, IdentifierKind::Numeric) => Ordering::Greater,
222 (IdentifierKind::AlphaNumeric, IdentifierKind::AlphaNumeric) => self.raw.cmp(other.raw),
223 }
224 }
225}
226
227fn validate_prerelease(s: &str) -> Result<(), SemverError> {
228 let bytes = s.as_bytes();
229 let mut segment_start = 0;
230 let mut all_digits = true;
231
232 for (pos, &byte) in bytes.iter().enumerate() {
233 match byte {
234 b'.' => {
235 validate_prerelease_segment(bytes, segment_start, pos, all_digits)?;
236 segment_start = pos + 1;
237 all_digits = true;
238 }
239 b'0'..=b'9' => {}
240 b'A'..=b'Z' | b'a'..=b'z' | b'-' => all_digits = false,
241 _ => {
242 return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into());
243 }
244 }
245 }
246
247 validate_prerelease_segment(bytes, segment_start, bytes.len(), all_digits)
248}
249
250fn validate_prerelease_segment(
251 bytes: &[u8],
252 start: usize,
253 end: usize,
254 all_digits: bool,
255) -> Result<(), SemverError> {
256 if start == end {
257 return Err(SemverErrorKind::EmptySegment.into());
258 }
259 if all_digits && end - start > 1 && bytes[start] == b'0' {
260 return Err(SemverErrorKind::LeadingZero.into());
261 }
262 Ok(())
263}
264
265pub(crate) fn validate_build_metadata(s: &str) -> Result<(), SemverError> {
266 let bytes = s.as_bytes();
267 let mut segment_start = 0;
268
269 for (pos, &byte) in bytes.iter().enumerate() {
270 match byte {
271 b'.' => {
272 if pos == segment_start {
273 return Err(SemverErrorKind::EmptySegment.into());
274 }
275 segment_start = pos + 1;
276 }
277 b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' => {}
278 _ => {
279 return Err(SemverErrorKind::UnexpectedCharacter(char::from(byte)).into());
280 }
281 }
282 }
283
284 if segment_start == bytes.len() {
285 return Err(SemverErrorKind::EmptySegment.into());
286 }
287 Ok(())
288}
289
290fn cmp_dot_separated<'a>(
291 left: &'a str,
292 right: &'a str,
293 parser: fn(&'a str) -> Identifier<'a>,
294) -> Ordering {
295 let mut left_start = 0;
296 let mut right_start = 0;
297 loop {
298 let left_end = next_separator(left, left_start);
299 let right_end = next_separator(right, right_start);
300 let left_part = &left[left_start..left_end];
301 let right_part = &right[right_start..right_end];
302 let left_id = parser(left_part);
303 let right_id = parser(right_part);
304 match left_id.cmp(&right_id) {
305 Ordering::Equal => {}
306 ord @ (Ordering::Less | Ordering::Greater) => return ord,
307 }
308 let left_done = left_end == left.len();
309 let right_done = right_end == right.len();
310 if left_done {
311 return if right_done {
312 Ordering::Equal
313 } else {
314 Ordering::Less
315 };
316 }
317 if right_done {
318 return Ordering::Greater;
319 }
320 left_start = left_end + 1;
321 right_start = right_end + 1;
322 }
323}
324
325fn cmp_numeric_strings(left: &str, right: &str) -> Ordering {
326 match left.len().cmp(&right.len()) {
327 Ordering::Equal => left.cmp(right),
328 ord @ (Ordering::Less | Ordering::Greater) => ord,
329 }
330}
331
332fn next_separator(s: &str, start: usize) -> usize {
333 let bytes = s.as_bytes();
334 let mut pos = start;
335 while pos < bytes.len() && bytes[pos] != b'.' {
336 pos += 1;
337 }
338 pos
339}
340
341#[cfg(test)]
342mod tests {
343 #[cfg(not(feature = "std"))]
344 use alloc::string::ToString;
345
346 use super::*;
347
348 #[test]
349 fn identifier_ordering() {
350 assert_eq!(
351 parse_build_metadata_identifier("1").cmp(&parse_build_metadata_identifier("2")),
352 Ordering::Less
353 );
354 assert_eq!(
355 parse_build_metadata_identifier("18446744073709551615")
356 .cmp(&parse_build_metadata_identifier("18446744073709551616")),
357 Ordering::Less
358 );
359 assert_eq!(
360 parse_build_metadata_identifier("18446744073709551616")
361 .cmp(&parse_build_metadata_identifier("18446744073709551617")),
362 Ordering::Less
363 );
364 assert_eq!(
365 parse_prerelease_identifier("1").cmp(&parse_prerelease_identifier("alpha")),
366 Ordering::Less
367 );
368 assert_eq!(
369 parse_prerelease_identifier("beta").cmp(&parse_prerelease_identifier("1")),
370 Ordering::Greater
371 );
372 }
373
374 #[test]
375 fn prerelease_identifier_validation() {
376 assert!(PreRelease::new("").is_err());
377 assert!(PreRelease::new("01").is_err());
378 assert!(PreRelease::new("a!b").is_err());
379 assert_eq!(
380 parse_prerelease_identifier("alpha-1").kind,
381 IdentifierKind::AlphaNumeric
382 );
383 }
384
385 #[test]
386 fn prerelease_public_api() {
387 assert!(PreRelease::default().is_empty());
388 assert!(PreRelease::new("").is_err());
389 assert_eq!(PreRelease::new("alpha.1").unwrap().to_string(), "alpha.1");
390 assert_eq!(PreRelease::new("beta").unwrap().to_string(), "beta");
391 assert_eq!("rc.1".parse::<PreRelease>().unwrap().to_string(), "rc.1");
392 assert_eq!(PreRelease::zero().to_string(), "0");
393 assert!(PreRelease::new("alpha").unwrap() < PreRelease::new("beta").unwrap());
394 assert!(PreRelease::new("1").unwrap() < PreRelease::new("alpha").unwrap());
395 assert_eq!(
396 PreRelease::new("alpha.1")
397 .unwrap()
398 .partial_cmp(&PreRelease::new("alpha.1").unwrap()),
399 Some(Ordering::Equal)
400 );
401 }
402
403 #[test]
404 fn build_metadata_public_api() {
405 assert!(BuildMetadata::default().is_empty());
406 assert_eq!(
407 BuildMetadata::new("build.001").unwrap().to_string(),
408 "build.001"
409 );
410 assert_eq!(
411 BuildMetadata::new("sha.abcdef").unwrap().to_string(),
412 "sha.abcdef"
413 );
414 assert_eq!(
415 "meta.42".parse::<BuildMetadata>().unwrap().to_string(),
416 "meta.42"
417 );
418 assert_eq!(BuildMetadata::new("x.y").unwrap().to_string(), "x.y");
419 assert_eq!(
420 BuildMetadata::new("alpha")
421 .unwrap()
422 .partial_cmp(&BuildMetadata::new("1").unwrap()),
423 Some(Ordering::Greater)
424 );
425 }
426
427 #[test]
428 fn empty_component_shortcuts() {
429 assert_eq!(
430 PreRelease::default().cmp(&PreRelease::zero()),
431 Ordering::Less
432 );
433 assert_eq!(
434 PreRelease::zero().cmp(&PreRelease::default()),
435 Ordering::Greater
436 );
437 assert_eq!(
438 BuildMetadata::default().cmp(&BuildMetadata::new("meta").unwrap()),
439 Ordering::Less
440 );
441 assert_eq!(
442 BuildMetadata::new("meta")
443 .unwrap()
444 .cmp(&BuildMetadata::default()),
445 Ordering::Greater
446 );
447 }
448
449 #[test]
450 fn identifier_partial_cmp_and_prefix_order() {
451 assert_eq!(
452 parse_prerelease_identifier("alpha").partial_cmp(&parse_prerelease_identifier("alpha")),
453 Some(Ordering::Equal)
454 );
455 assert_eq!(
456 cmp_dot_separated("alpha", "alpha.1", parse_prerelease_identifier),
457 Ordering::Less
458 );
459 assert_eq!(
460 Ord::cmp(
461 &PreRelease::new("alpha").unwrap(),
462 &PreRelease::new("beta").unwrap()
463 ),
464 Ordering::Less
465 );
466 assert_eq!(
467 PartialOrd::partial_cmp(
468 &BuildMetadata::new("build.1").unwrap(),
469 &BuildMetadata::new("build.2").unwrap()
470 ),
471 Some(Ordering::Less)
472 );
473 assert_eq!(
474 parse_build_metadata_identifier("1").partial_cmp(&parse_build_metadata_identifier("2")),
475 Some(Ordering::Less)
476 );
477 assert_eq!("rc.1".parse::<PreRelease>().unwrap().to_string(), "rc.1");
478 assert_eq!(
479 "meta.42".parse::<BuildMetadata>().unwrap().to_string(),
480 "meta.42"
481 );
482 }
483
484 #[test]
485 fn prerelease_cmp_identifiers_covers_empty_and_equal_cases() {
486 assert_eq!(
487 PreRelease::default().cmp(&PreRelease::default()),
488 Ordering::Equal
489 );
490 assert_eq!(
491 PreRelease::default().cmp(&PreRelease::new("0").unwrap()),
492 Ordering::Less
493 );
494 assert_eq!(
495 PreRelease::new("alpha")
496 .unwrap()
497 .cmp_identifiers(&PreRelease::new("alpha").unwrap()),
498 Ordering::Equal
499 );
500 assert!(PreRelease::new("").is_err());
501 assert!(PreRelease::new("alpha!1").is_err());
502 assert!(BuildMetadata::new("").is_err());
503 assert!(BuildMetadata::new("meta!1").is_err());
504 }
505}