use std::borrow::Cow;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use super::{ContentType, InvalidValueError};
mod fedora {
use once_cell::sync::Lazy;
use regex::Regex;
use super::{ContentType, FedoraRelease, InvalidValueError};
static RELEASE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new("^F(?P<number>[1-9][0-9]*)(?P<ctype>[CFM]?)$").expect("Failed to compile hard-coded regex!")
});
pub fn release_parse(release: &str) -> Result<(u32, String), InvalidValueError> {
let invalid = || InvalidValueError::new("FedoraRelease", release.to_owned());
let parsed = RELEASE_RE.captures(release).ok_or_else(invalid)?;
let number: u32 = parsed
.name("number")
.ok_or_else(invalid)?
.as_str()
.parse::<u32>()
.map_err(|_| invalid())?;
let ctype: String = parsed.name("ctype").ok_or_else(invalid)?.as_str().to_owned();
Ok((number, ctype))
}
pub const MIN_RELEASE: u32 = 21;
pub const MIN_CONTAINER_RELEASE: u32 = 28;
pub const MIN_FLATPAK_RELEASE: u32 = 29;
pub const MIN_MODULE_RELEASE: u32 = 27;
pub fn is_valid_release(number: u32, ctype: ContentType) -> bool {
use ContentType::*;
match ctype {
RPM => number >= MIN_RELEASE,
Container => number >= MIN_CONTAINER_RELEASE,
Flatpak => number >= MIN_FLATPAK_RELEASE,
Module => number >= MIN_MODULE_RELEASE,
}
}
pub fn release_validate(release: &str) -> Result<FedoraRelease, InvalidValueError> {
let (num, ctype) = release_parse(release)?;
if !is_valid_release(num, ContentType::try_from_suffix(&ctype)?) {
return Err(InvalidValueError::new("FedoraRelease", release.to_string()));
}
Ok(FedoraRelease::from_str(release))
}
}
mod epel {
use once_cell::sync::Lazy;
use regex::Regex;
use super::{ContentType, FedoraRelease, InvalidValueError};
static RELEASE_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new("^EPEL-(?P<number>[1-9][0-9]*)(?P<ctype>[CFM]?)(?P<next>[N]?)$")
.expect("Failed to compile hard-coded regex!")
});
pub fn release_parse(release: &str) -> Result<(u32, String, bool), InvalidValueError> {
let invalid = || InvalidValueError::new("FedoraRelease", release.to_owned());
let parsed = RELEASE_RE.captures(release).ok_or_else(invalid)?;
let number: u32 = parsed
.name("number")
.ok_or_else(invalid)?
.as_str()
.parse::<u32>()
.map_err(|_| invalid())?;
let ctype: String = parsed.name("ctype").ok_or_else(invalid)?.as_str().to_owned();
let next: bool = parsed.name("next").ok_or_else(invalid)?.as_str() == "N";
Ok((number, ctype, next))
}
pub const MIN_RELEASE: u32 = 7;
pub const MIN_MODULE_RELEASE: u32 = 8;
pub const MIN_NEXT_RELEASE: u32 = 8;
pub fn is_valid_release(number: u32, ctype: ContentType, next: bool) -> bool {
use ContentType::*;
let valid_type = match ctype {
RPM => number >= MIN_RELEASE,
Container => false,
Flatpak => false,
Module => number >= MIN_MODULE_RELEASE,
};
let valid_next = match next {
false => number >= MIN_RELEASE,
true => number >= MIN_NEXT_RELEASE,
};
let valid_combo = (ctype == RPM) || !next;
valid_type && valid_next && valid_combo
}
pub fn release_validate(release: &str) -> Result<FedoraRelease, InvalidValueError> {
let (num, ctype, next) = release_parse(release)?;
if !(is_valid_release(num, ContentType::try_from_suffix(&ctype)?, next)) {
return Err(InvalidValueError::new("FedoraRelease", release.to_string()));
}
Ok(FedoraRelease::from_str(release))
}
}
mod el {
use once_cell::sync::Lazy;
use regex::Regex;
use super::{FedoraRelease, InvalidValueError};
static RELEASE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new("^EL-(?P<number>[1-9][0-9]*)$").expect("Failed to compile hard-coded regex!"));
pub fn release_parse(release: &str) -> Result<u32, InvalidValueError> {
let invalid = || InvalidValueError::new("FedoraRelease", release.to_owned());
let parsed = RELEASE_RE.captures(release).ok_or_else(invalid)?;
let number: u32 = parsed
.name("number")
.ok_or_else(invalid)?
.as_str()
.parse::<u32>()
.map_err(|_| invalid())?;
Ok(number)
}
pub const MIN_RELEASE: u32 = 5;
pub const MAX_RELEASE: u32 = 6;
pub fn is_valid_release(number: u32) -> bool {
(MIN_RELEASE..=MAX_RELEASE).contains(&number)
}
pub fn release_validate(release: &str) -> Result<FedoraRelease, InvalidValueError> {
let num = release_parse(release)?;
if !(is_valid_release(num)) {
return Err(InvalidValueError::new("FedoraRelease", release.to_string()));
}
Ok(FedoraRelease::from_str(release))
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(transparent)]
pub struct FedoraRelease {
release: Cow<'static, str>,
}
impl FedoraRelease {
pub const CURRENT: Self = Self::from_static_str("__current__");
pub const PENDING: Self = Self::from_static_str("__pending__");
pub const ARCHIVED: Self = Self::from_static_str("__archived__");
pub const ELN: Self = Self::from_static_str("ELN");
const fn from_static_str(string: &'static str) -> Self {
FedoraRelease {
release: Cow::Borrowed(string),
}
}
fn from_str(string: &str) -> Self {
FedoraRelease {
release: Cow::Owned(String::from(string)),
}
}
pub fn fedora(number: u32, ctype: ContentType) -> Result<Self, InvalidValueError> {
let string = format!("F{}{}", number, ctype.suffix());
string.parse()
}
pub fn epel(number: u32, ctype: ContentType, next: bool) -> Result<Self, InvalidValueError> {
let prefix = if number < 7 { "EL" } else { "EPEL" };
let suffix = if next { "N" } else { "" };
let string = format!("{}-{}{}{}", prefix, number, ctype.suffix(), suffix);
string.parse()
}
}
impl Display for FedoraRelease {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "{}", self.release)
}
}
impl TryFrom<&str> for FedoraRelease {
type Error = InvalidValueError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"" => Err(InvalidValueError::new("FedoraRelease", String::from("(empty string)"))),
"ELN" => Ok(FedoraRelease::from_str("ELN")),
f if f.starts_with('F') => fedora::release_validate(f),
epel if epel.starts_with("EPEL") => epel::release_validate(epel),
el if el.starts_with("EL") => el::release_validate(el),
_ => Err(InvalidValueError::new("FedoraRelease", value.to_owned())),
}
}
}
impl FromStr for FedoraRelease {
type Err = InvalidValueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
TryFrom::try_from(s)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use quickcheck::{Arbitrary, Gen, QuickCheck};
use super::*;
impl Arbitrary for ContentType {
fn arbitrary(g: &mut Gen) -> Self {
match (bool::arbitrary(g), bool::arbitrary(g)) {
(true, true) => ContentType::RPM,
(true, false) => ContentType::Container,
(false, true) => ContentType::Flatpak,
(false, false) => ContentType::Module,
}
}
}
fn quickchecker() -> QuickCheck {
QuickCheck::new().tests(1_000_000)
}
#[test]
fn parse_print_all() {
#[rustfmt::skip]
let known =[
"F36", "F36C",
"F35", "F35C", "F35F", "F35M",
"F34", "F34C", "F34F", "F34M",
"F33", "F33C", "F33F", "F33M",
"F32", "F32C", "F32F", "F32M",
"F31", "F31C", "F31F", "F31M",
"F30", "F30C", "F30F", "F30M",
"F29", "F29C", "F29F", "F29M",
"F28", "F28C", "F28M",
"F27", "F27M",
"F26",
"F25",
"F24",
"F23",
"F22",
"F21",
"EPEL-9", "EPEL-9N",
"EPEL-8", "EPEL-8M", "EPEL-8N",
"EPEL-7",
"EL-6",
"EL-5",
"ELN",
];
for value in known {
let parsed = FedoraRelease::try_from(value).unwrap();
assert_eq!(value, parsed.to_string());
}
}
#[test]
fn parse_eln() {
let eln = FedoraRelease::try_from("ELN").unwrap();
assert_eq!(FedoraRelease::ELN.to_string(), "ELN");
assert_eq!(FedoraRelease::ELN, eln);
}
#[test]
fn parse_fedora() {
#[rustfmt::skip]
let fixtures = [
("F36", (36, "")), ("F36C", (36, "C")), ("F36F", (36, "F")), ("F36M", (36, "M")),
("F35", (35, "")), ("F35C", (35, "C")), ("F35F", (35, "F")), ("F35M", (35, "M")),
("F34", (34, "")), ("F34C", (34, "C")), ("F34F", (34, "F")), ("F34M", (34, "M")),
("F33", (33, "")), ("F33C", (33, "C")), ("F33F", (33, "F")), ("F33M", (33, "M")),
("F32", (32, "")), ("F32C", (32, "C")), ("F32F", (32, "F")), ("F32M", (32, "M")),
("F31", (31, "")), ("F31C", (31, "C")), ("F31F", (31, "F")), ("F31M", (31, "M")),
("F30", (30, "")), ("F30C", (30, "C")), ("F30F", (30, "F")), ("F30M", (30, "M")),
("F29", (29, "")), ("F29C", (29, "C")), ("F29F", (29, "F")), ("F29M", (29, "M")),
("F28", (28, "")), ("F28C", (28, "C")), ("F28M", (28, "M")),
("F27", (27, "")), ("F27M", (27, "M")),
("F26", (26, "")),
("F25", (25, "")),
("F24", (24, "")),
("F23", (23, "")),
("F22", (22, "")),
("F21", (21, "")),
];
for (value, expected) in fixtures {
let parsed = fedora::release_parse(value).unwrap();
assert_eq!(parsed.0, expected.0);
assert_eq!(parsed.1, expected.1);
let release = FedoraRelease::try_from(value).unwrap();
assert_eq!(release.to_string(), value);
}
}
#[test]
fn parse_epel() {
#[rustfmt::skip]
let fixtures = [
("EPEL-9", (9, "", false)), ("EPEL-9N", (9, "", true)),
("EPEL-8", (8, "", false)), ("EPEL-8M", (8, "M", false)), ("EPEL-8N", (8, "", true)),
("EPEL-7", (7, "", false)),
];
for (value, expected) in fixtures {
let parsed = epel::release_parse(value).unwrap();
assert_eq!(parsed.0, expected.0);
assert_eq!(parsed.1, expected.1);
assert_eq!(parsed.2, expected.2);
let release = FedoraRelease::try_from(value).unwrap();
assert_eq!(release.to_string(), value);
}
}
#[test]
fn parse_el() {
#[rustfmt::skip]
let fixtures = [("EL-6", 6), ("EL-5", 5)];
for (value, expected) in fixtures {
let parsed = el::release_parse(value).unwrap();
assert_eq!(parsed, expected);
let release = FedoraRelease::try_from(value).unwrap();
assert_eq!(release.to_string(), value);
}
}
#[test]
fn parse_invalid() {
#[rustfmt::skip]
let values = [
"F20", "F21C", "F22F", "F23M", "EPEL-2", "EPEL-3N", "EPEL-7M", "EPEL-8CN", "EPEL-8FN", "EPEL-8MN", "EPEL-9CN", "EPEL-9FN", "EPEL-9MN", "EL-10", ];
for value in values {
value.parse::<FedoraRelease>().unwrap_err();
}
}
#[test]
fn check_fedora() {
fn prop(number: u32) -> bool {
if number < fedora::MIN_RELEASE {
return true;
}
let built = FedoraRelease::fedora(number, ContentType::RPM).unwrap().to_string();
let (num, ctype) = fedora::release_parse(&built).unwrap();
number == num && ctype.is_empty()
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_fedora_container() {
fn prop(number: u32) -> bool {
if number < fedora::MIN_RELEASE {
return true;
}
let built = FedoraRelease::fedora(number, ContentType::Container)
.unwrap()
.to_string();
let (num, ctype) = fedora::release_parse(&built).unwrap();
number == num && ctype == "C"
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_fedora_flatpak() {
fn prop(number: u32) -> bool {
if number < fedora::MIN_RELEASE {
return true;
}
let built = FedoraRelease::fedora(number, ContentType::Flatpak).unwrap().to_string();
let (num, ctype) = fedora::release_parse(&built).unwrap();
number == num && ctype == "F"
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_fedora_module() {
fn prop(number: u32) -> bool {
if number < fedora::MIN_RELEASE {
return true;
}
let built = FedoraRelease::fedora(number, ContentType::Module).unwrap().to_string();
let (num, ctype) = fedora::release_parse(&built).unwrap();
number == num && ctype == "M"
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_epel() {
fn prop(number: u32) -> bool {
if number < epel::MIN_RELEASE {
return true;
}
let built = FedoraRelease::epel(number, ContentType::RPM, false)
.unwrap()
.to_string();
let num = if number < 7 {
el::release_parse(&built).unwrap()
} else {
let (num, ctype, next) = epel::release_parse(&built).unwrap();
assert!(ctype.is_empty());
assert!(!next);
num
};
number == num
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_epel_modules() {
fn prop(number: u32) -> bool {
if number < epel::MIN_MODULE_RELEASE {
return true;
}
let built = FedoraRelease::epel(number, ContentType::Module, false)
.unwrap()
.to_string();
let (num, ctype, next) = epel::release_parse(&built).unwrap();
number == num && ctype == "M" && !next
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_epel_next() {
fn prop(number: u32) -> bool {
if number < epel::MIN_NEXT_RELEASE {
return true;
}
let built = FedoraRelease::epel(number, ContentType::RPM, true).unwrap().to_string();
let (num, ctype, next) = epel::release_parse(&built).unwrap();
number == num && ctype.is_empty() && next
}
quickchecker().quickcheck(prop as fn(u32) -> bool)
}
#[test]
fn check_epel_container() {
fn prop(number: u32, next: bool) -> bool {
FedoraRelease::epel(number, ContentType::Container, next).is_err()
}
quickchecker().quickcheck(prop as fn(u32, bool) -> bool)
}
#[test]
fn check_epel_flatpak() {
fn prop(number: u32, next: bool) -> bool {
FedoraRelease::epel(number, ContentType::Flatpak, next).is_err()
}
quickchecker().quickcheck(prop as fn(u32, bool) -> bool)
}
#[test]
fn check_epel_combo() {
fn prop(number: u32, ctype: ContentType) -> bool {
if number < epel::MIN_RELEASE {
return true;
}
(ctype == ContentType::RPM) != FedoraRelease::epel(number, ctype, true).is_err()
}
quickchecker().quickcheck(prop as fn(u32, ContentType) -> bool)
}
}