Skip to main content

js_semver/
identifier.rs

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)]
12/// A parsed pre-release identifier list such as `alpha.1`.
13pub struct PreRelease(Box<str>);
14
15impl PreRelease {
16    pub(crate) fn zero() -> Self {
17        Self(Box::from("0"))
18    }
19
20    /// Parse a pre-release identifier list such as `alpha.1`.
21    ///
22    /// # Examples
23    ///
24    /// ```rust
25    /// use js_semver::PreRelease;
26    ///
27    /// let pre = PreRelease::new("alpha.1").unwrap();
28    ///
29    /// assert_eq!(pre.to_string(), "alpha.1");
30    /// ```
31    ///
32    /// # Errors
33    ///
34    /// Returns [`SemverError`] if `s` is not valid pre-release metadata.
35    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    /// Returns `true` when there are no pre-release identifiers.
45    ///
46    /// # Examples
47    ///
48    /// ```rust
49    /// use js_semver::PreRelease;
50    ///
51    /// assert!(PreRelease::default().is_empty());
52    /// assert!(!PreRelease::new("rc.1").unwrap().is_empty());
53    /// ```
54    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)]
94/// Parsed build metadata such as `build.42`.
95pub struct BuildMetadata(Box<str>);
96
97impl BuildMetadata {
98    /// Parse build metadata such as `build.42`.
99    ///
100    /// # Examples
101    ///
102    /// ```rust
103    /// use js_semver::BuildMetadata;
104    ///
105    /// let build = BuildMetadata::new("build.42").unwrap();
106    ///
107    /// assert_eq!(build.to_string(), "build.42");
108    /// ```
109    ///
110    /// # Errors
111    ///
112    /// Returns [`SemverError`] if `s` is not valid build metadata.
113    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    /// Returns `true` when there is no build metadata.
123    ///
124    /// # Examples
125    ///
126    /// ```rust
127    /// use js_semver::BuildMetadata;
128    ///
129    /// assert!(BuildMetadata::default().is_empty());
130    /// assert!(!BuildMetadata::new("sha.abcdef").unwrap().is_empty());
131    /// ```
132    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}