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}