Skip to main content

boxferry_engine/
target.rs

1//! Target implementation version ranges.
2
3use std::{error::Error, fmt, str::FromStr};
4
5/// Error returned when a platform version is not exactly `major.minor.patch`.
6#[derive(Clone, Debug, Eq, PartialEq)]
7pub struct ParsePlatformVersionError;
8
9impl fmt::Display for ParsePlatformVersionError {
10    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
11        formatter.write_str("platform version must contain exactly three unsigned numbers: major.minor.patch")
12    }
13}
14
15impl Error for ParsePlatformVersionError {}
16
17/// Numeric target implementation version.
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct PlatformVersion {
20    major: u64,
21    minor: u64,
22    patch: u64,
23}
24
25impl PlatformVersion {
26    /// Creates a numeric version.
27    #[must_use]
28    pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
29        Self { major, minor, patch }
30    }
31
32    /// Returns the major number.
33    #[must_use]
34    pub const fn major(self) -> u64 {
35        self.major
36    }
37
38    /// Returns the minor number.
39    #[must_use]
40    pub const fn minor(self) -> u64 {
41        self.minor
42    }
43
44    /// Returns the patch number.
45    #[must_use]
46    pub const fn patch(self) -> u64 {
47        self.patch
48    }
49}
50
51impl fmt::Display for PlatformVersion {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
54    }
55}
56
57impl FromStr for PlatformVersion {
58    type Err = ParsePlatformVersionError;
59
60    fn from_str(value: &str) -> Result<Self, Self::Err> {
61        let mut components = value.split('.');
62        let major = parse_component(components.next())?;
63        let minor = parse_component(components.next())?;
64        let patch = parse_component(components.next())?;
65        if components.next().is_some() {
66            return Err(ParsePlatformVersionError);
67        }
68        Ok(Self::new(major, minor, patch))
69    }
70}
71
72/// Inclusive minimum and optional maximum target versions.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct VersionRange {
75    minimum: PlatformVersion,
76    maximum: Option<PlatformVersion>,
77}
78
79impl VersionRange {
80    /// Creates an inclusive version range.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`TargetProfileError::MaximumBeforeMinimum`] for an inverted range.
85    pub const fn new(minimum: PlatformVersion, maximum: Option<PlatformVersion>) -> Result<Self, TargetProfileError> {
86        if let Some(maximum) = maximum {
87            if version_is_before(maximum, minimum) {
88                return Err(TargetProfileError::MaximumBeforeMinimum { minimum, maximum });
89            }
90        }
91        Ok(Self { minimum, maximum })
92    }
93
94    /// Returns the inclusive minimum version.
95    #[must_use]
96    pub const fn minimum(self) -> PlatformVersion {
97        self.minimum
98    }
99
100    /// Returns the inclusive optional maximum version.
101    #[must_use]
102    pub const fn maximum(self) -> Option<PlatformVersion> {
103        self.maximum
104    }
105
106    /// Returns whether a version is inside the inclusive range.
107    #[must_use]
108    pub const fn contains(self, version: PlatformVersion) -> bool {
109        !version_is_before(version, self.minimum)
110            && match self.maximum {
111                Some(maximum) => !version_is_before(maximum, version),
112                None => true,
113            }
114    }
115}
116
117/// Invalid target profile.
118#[derive(Clone, Debug, Eq, PartialEq)]
119#[non_exhaustive]
120pub enum TargetProfileError {
121    /// The target implementation name was empty or contained a NUL byte.
122    InvalidImplementation,
123    /// The optional maximum version was before the minimum.
124    MaximumBeforeMinimum {
125        /// Inclusive minimum.
126        minimum: PlatformVersion,
127        /// Invalid inclusive maximum.
128        maximum: PlatformVersion,
129    },
130}
131
132impl fmt::Display for TargetProfileError {
133    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
134        match self {
135            Self::InvalidImplementation => {
136                formatter.write_str("target implementation must be non-empty and contain no NUL byte")
137            }
138            Self::MaximumBeforeMinimum { minimum, maximum } => {
139                write!(formatter, "target maximum {maximum} is before minimum {minimum}")
140            }
141        }
142    }
143}
144
145impl Error for TargetProfileError {}
146
147/// Caller-selected target implementation and supported compatibility range.
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct TargetProfile {
150    implementation: String,
151    versions: VersionRange,
152}
153
154impl TargetProfile {
155    /// Creates a target profile without interpreting implementation-specific capabilities.
156    ///
157    /// # Errors
158    ///
159    /// Returns [`TargetProfileError`] for an invalid name or version range.
160    pub fn new(
161        implementation: impl Into<String>,
162        minimum_version: PlatformVersion,
163        maximum_version: Option<PlatformVersion>,
164    ) -> Result<Self, TargetProfileError> {
165        let implementation = implementation.into();
166        if implementation.is_empty() || implementation.contains('\0') {
167            return Err(TargetProfileError::InvalidImplementation);
168        }
169        Ok(Self {
170            implementation,
171            versions: VersionRange::new(minimum_version, maximum_version)?,
172        })
173    }
174
175    /// Returns the target implementation name, such as `podman`.
176    #[must_use]
177    pub fn implementation(&self) -> &str {
178        &self.implementation
179    }
180
181    /// Returns the requested compatibility range.
182    #[must_use]
183    pub const fn versions(&self) -> VersionRange {
184        self.versions
185    }
186}
187
188const fn version_is_before(left: PlatformVersion, right: PlatformVersion) -> bool {
189    left.major < right.major
190        || (left.major == right.major && left.minor < right.minor)
191        || (left.major == right.major && left.minor == right.minor && left.patch < right.patch)
192}
193
194fn parse_component(value: Option<&str>) -> Result<u64, ParsePlatformVersionError> {
195    value
196        .filter(|component| !component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit()))
197        .and_then(|component| component.parse().ok())
198        .ok_or(ParsePlatformVersionError)
199}
200
201#[cfg(test)]
202mod tests {
203    use std::str::FromStr;
204
205    use super::{ParsePlatformVersionError, PlatformVersion, TargetProfile, TargetProfileError};
206
207    #[test]
208    fn parses_exact_numeric_platform_versions() {
209        assert_eq!(PlatformVersion::from_str("5.4.0"), Ok(PlatformVersion::new(5, 4, 0)));
210        for value in ["5.4", "5.4.0.1", "5.4.x", "v5.4.0", "5..0", ""] {
211            assert_eq!(PlatformVersion::from_str(value), Err(ParsePlatformVersionError));
212        }
213    }
214
215    #[test]
216    fn minimum_and_maximum_are_inclusive() -> Result<(), String> {
217        let profile =
218            TargetProfile::new("podman", version(5, 4), Some(version(5, 6))).map_err(|error| error.to_string())?;
219        assert!(profile.versions().contains(version(5, 4)));
220        assert!(profile.versions().contains(version(5, 6)));
221        assert!(!profile.versions().contains(version(5, 3)));
222        assert!(!profile.versions().contains(version(5, 7)));
223        Ok(())
224    }
225
226    #[test]
227    fn rejects_maximum_before_minimum() {
228        assert!(matches!(
229            TargetProfile::new("podman", version(5, 4), Some(version(5, 3))),
230            Err(TargetProfileError::MaximumBeforeMinimum { .. })
231        ));
232    }
233
234    const fn version(major: u64, minor: u64) -> PlatformVersion {
235        PlatformVersion::new(major, minor, 0)
236    }
237}