use std::{error::Error, fmt, str::FromStr};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsePlatformVersionError;
impl fmt::Display for ParsePlatformVersionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("platform version must contain exactly three unsigned numbers: major.minor.patch")
}
}
impl Error for ParsePlatformVersionError {}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct PlatformVersion {
major: u64,
minor: u64,
patch: u64,
}
impl PlatformVersion {
#[must_use]
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
Self { major, minor, patch }
}
#[must_use]
pub const fn major(self) -> u64 {
self.major
}
#[must_use]
pub const fn minor(self) -> u64 {
self.minor
}
#[must_use]
pub const fn patch(self) -> u64 {
self.patch
}
}
impl fmt::Display for PlatformVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl FromStr for PlatformVersion {
type Err = ParsePlatformVersionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let mut components = value.split('.');
let major = parse_component(components.next())?;
let minor = parse_component(components.next())?;
let patch = parse_component(components.next())?;
if components.next().is_some() {
return Err(ParsePlatformVersionError);
}
Ok(Self::new(major, minor, patch))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VersionRange {
minimum: PlatformVersion,
maximum: Option<PlatformVersion>,
}
impl VersionRange {
pub const fn new(minimum: PlatformVersion, maximum: Option<PlatformVersion>) -> Result<Self, TargetProfileError> {
if let Some(maximum) = maximum {
if version_is_before(maximum, minimum) {
return Err(TargetProfileError::MaximumBeforeMinimum { minimum, maximum });
}
}
Ok(Self { minimum, maximum })
}
#[must_use]
pub const fn minimum(self) -> PlatformVersion {
self.minimum
}
#[must_use]
pub const fn maximum(self) -> Option<PlatformVersion> {
self.maximum
}
#[must_use]
pub const fn contains(self, version: PlatformVersion) -> bool {
!version_is_before(version, self.minimum)
&& match self.maximum {
Some(maximum) => !version_is_before(maximum, version),
None => true,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum TargetProfileError {
InvalidImplementation,
MaximumBeforeMinimum {
minimum: PlatformVersion,
maximum: PlatformVersion,
},
}
impl fmt::Display for TargetProfileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidImplementation => {
formatter.write_str("target implementation must be non-empty and contain no NUL byte")
}
Self::MaximumBeforeMinimum { minimum, maximum } => {
write!(formatter, "target maximum {maximum} is before minimum {minimum}")
}
}
}
}
impl Error for TargetProfileError {}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TargetProfile {
implementation: String,
versions: VersionRange,
}
impl TargetProfile {
pub fn new(
implementation: impl Into<String>,
minimum_version: PlatformVersion,
maximum_version: Option<PlatformVersion>,
) -> Result<Self, TargetProfileError> {
let implementation = implementation.into();
if implementation.is_empty() || implementation.contains('\0') {
return Err(TargetProfileError::InvalidImplementation);
}
Ok(Self {
implementation,
versions: VersionRange::new(minimum_version, maximum_version)?,
})
}
#[must_use]
pub fn implementation(&self) -> &str {
&self.implementation
}
#[must_use]
pub const fn versions(&self) -> VersionRange {
self.versions
}
}
const fn version_is_before(left: PlatformVersion, right: PlatformVersion) -> bool {
left.major < right.major
|| (left.major == right.major && left.minor < right.minor)
|| (left.major == right.major && left.minor == right.minor && left.patch < right.patch)
}
fn parse_component(value: Option<&str>) -> Result<u64, ParsePlatformVersionError> {
value
.filter(|component| !component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit()))
.and_then(|component| component.parse().ok())
.ok_or(ParsePlatformVersionError)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use super::{ParsePlatformVersionError, PlatformVersion, TargetProfile, TargetProfileError};
#[test]
fn parses_exact_numeric_platform_versions() {
assert_eq!(PlatformVersion::from_str("5.4.0"), Ok(PlatformVersion::new(5, 4, 0)));
for value in ["5.4", "5.4.0.1", "5.4.x", "v5.4.0", "5..0", ""] {
assert_eq!(PlatformVersion::from_str(value), Err(ParsePlatformVersionError));
}
}
#[test]
fn minimum_and_maximum_are_inclusive() -> Result<(), String> {
let profile =
TargetProfile::new("podman", version(5, 4), Some(version(5, 6))).map_err(|error| error.to_string())?;
assert!(profile.versions().contains(version(5, 4)));
assert!(profile.versions().contains(version(5, 6)));
assert!(!profile.versions().contains(version(5, 3)));
assert!(!profile.versions().contains(version(5, 7)));
Ok(())
}
#[test]
fn rejects_maximum_before_minimum() {
assert!(matches!(
TargetProfile::new("podman", version(5, 4), Some(version(5, 3))),
Err(TargetProfileError::MaximumBeforeMinimum { .. })
));
}
const fn version(major: u64, minor: u64) -> PlatformVersion {
PlatformVersion::new(major, minor, 0)
}
}