Skip to main content

kernel/release/
number.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! A release named by its version number alone, the spelling crates.io
7//! carries (`crates/kernel/spec/Release.lean` §8-54-1).
8
9use crate::error::{AxCode, AxError};
10
11/// A release named by its version number alone, as crates.io names it
12/// (`crates/kernel/spec/Release.lean` §8-54-1, kernel D35).
13///
14/// Its own type rather than a [`Release`] with a date made up, because
15/// crates.io states no date, and a date filled in for it is a value the
16/// registry never said.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct Version {
19    major: u32,
20    minor: u32,
21    patch: u32,
22}
23
24impl Version {
25    /// The version crates.io carries: three dot-separated numbers, read by
26    /// the same rule as the version half of every other spelling.
27    ///
28    /// # Errors
29    /// When the string is not three dot-separated numbers.
30    pub fn from_crates_version(text: &str) -> Result<Version, AxError> {
31        Version::parse(text, &|msg| {
32            AxError::failure(
33                AxCode::ConfigInvalid,
34                "read a crates.io version",
35                text.to_owned(),
36            )
37            .with_recovery(msg)
38        })
39    }
40
41    /// The one reading of a version number, shared by every spelling so
42    /// none accepts what another refuses.
43    pub(super) fn parse(
44        version: &str,
45        refused: &dyn Fn(String) -> AxError,
46    ) -> Result<Version, AxError> {
47        let mut parts = version.split('.');
48        let mut next_number = || parts.next().and_then(number);
49        let (Some(major), Some(minor), Some(patch)) = (next_number(), next_number(), next_number())
50        else {
51            return Err(refused(format!(
52                "`{version}` is not three dot-separated numbers, as in `0.0.5`"
53            )));
54        };
55        if parts.next().is_some() {
56            return Err(refused(format!(
57                "`{version}` carries more than the three numbers a version of \
58                 this project has"
59            )));
60        }
61        Ok(Version {
62            major,
63            minor,
64            patch,
65        })
66    }
67}
68
69impl std::fmt::Display for Version {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        let Version {
72            major,
73            minor,
74            patch,
75        } = *self;
76        write!(f, "{major}.{minor}.{patch}")
77    }
78}
79
80/// One version component: digits only, and no leading zero that would
81/// make two spellings of one number.
82///
83/// Without the second rule `0.00.5` and `0.0.5` decode alike, and the
84/// tag that round-trips back out is not the tag that came in.
85fn number(text: &str) -> Option<u32> {
86    let mut chars = text.chars();
87    let first = chars.next()?;
88    if !first.is_ascii_digit() {
89        return None;
90    }
91    if first == '0' && chars.next().is_some() {
92        return None;
93    }
94    text.parse().ok()
95}