1use std::{error::Error, fmt, str::FromStr};
4
5#[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#[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 #[must_use]
28 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
29 Self { major, minor, patch }
30 }
31
32 #[must_use]
34 pub const fn major(self) -> u64 {
35 self.major
36 }
37
38 #[must_use]
40 pub const fn minor(self) -> u64 {
41 self.minor
42 }
43
44 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct VersionRange {
75 minimum: PlatformVersion,
76 maximum: Option<PlatformVersion>,
77}
78
79impl VersionRange {
80 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 #[must_use]
96 pub const fn minimum(self) -> PlatformVersion {
97 self.minimum
98 }
99
100 #[must_use]
102 pub const fn maximum(self) -> Option<PlatformVersion> {
103 self.maximum
104 }
105
106 #[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#[derive(Clone, Debug, Eq, PartialEq)]
119#[non_exhaustive]
120pub enum TargetProfileError {
121 InvalidImplementation,
123 MaximumBeforeMinimum {
125 minimum: PlatformVersion,
127 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#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct TargetProfile {
150 implementation: String,
151 versions: VersionRange,
152}
153
154impl TargetProfile {
155 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 #[must_use]
177 pub fn implementation(&self) -> &str {
178 &self.implementation
179 }
180
181 #[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}