use std::{
cmp::Ordering,
fmt::{Display, Formatter},
num::NonZeroUsize,
str::FromStr,
};
use serde::{Deserialize, Serialize};
use winnow::{
ModalResult,
Parser,
ascii::{dec_uint, digit1},
combinator::{Repeat, cut_err, eof, opt, preceded, repeat, seq, terminated},
error::{StrContext, StrContextValue},
token::one_of,
};
#[cfg(doc)]
use crate::Version;
use crate::{Error, VersionSegments};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Epoch(pub NonZeroUsize);
impl Epoch {
pub fn new(epoch: NonZeroUsize) -> Self {
Epoch(epoch)
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
terminated(dec_uint, eof)
.verify_map(NonZeroUsize::new)
.context(StrContext::Label("package epoch"))
.context(StrContext::Expected(StrContextValue::Description(
"positive non-zero decimal integer",
)))
.map(Self)
.parse_next(input)
}
}
impl FromStr for Epoch {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for Epoch {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.0)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PackageRelease {
pub major: usize,
pub minor: Option<usize>,
}
impl PackageRelease {
pub fn new(major: usize, minor: Option<usize>) -> Self {
PackageRelease { major, minor }
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
seq!(Self {
major: digit1.try_map(FromStr::from_str)
.context(StrContext::Label("package release"))
.context(StrContext::Expected(StrContextValue::Description(
"positive decimal integer",
))),
minor: opt(preceded('.', cut_err(digit1.try_map(FromStr::from_str))))
.context(StrContext::Label("package release"))
.context(StrContext::Expected(StrContextValue::Description(
"single '.' followed by positive decimal integer",
))),
_: eof.context(StrContext::Expected(StrContextValue::Description(
"end of package release value",
))),
})
.parse_next(input)
}
}
impl FromStr for PackageRelease {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for PackageRelease {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.major)?;
if let Some(minor) = self.minor {
write!(fmt, ".{minor}")?;
}
Ok(())
}
}
impl PartialOrd for PackageRelease {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PackageRelease {
fn cmp(&self, other: &Self) -> Ordering {
let major_order = self.major.cmp(&other.major);
if major_order != Ordering::Equal {
return major_order;
}
match (self.minor, other.minor) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, Serialize)]
pub struct PackageVersion(pub(crate) String);
impl PackageVersion {
pub fn new(pkgver: String) -> Result<Self, Error> {
PackageVersion::from_str(pkgver.as_str())
}
pub fn inner(&self) -> &str {
&self.0
}
pub fn segments(&self) -> VersionSegments<'_> {
VersionSegments::new(&self.0)
}
pub fn parser(input: &mut &str) -> ModalResult<Self> {
let allowed = |c: char| {
c.is_ascii() && ![':', '/', '-', '<', '>', '='].contains(&c) && !c.is_whitespace()
};
let pkgver: Repeat<_, _, _, (), _> = repeat(1.., one_of(allowed));
(
pkgver,
eof
)
.context(StrContext::Label("pkgver character"))
.context(StrContext::Expected(StrContextValue::Description(
"an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters",
)))
.take()
.map(|s: &str| Self(s.to_string()))
.parse_next(input)
}
}
impl FromStr for PackageVersion {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::parser.parse(s)?)
}
}
impl Display for PackageVersion {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
write!(fmt, "{}", self.inner())
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("1", Ok(Epoch(NonZeroUsize::new(1).unwrap())))]
fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
assert_eq!(result, Epoch::from_str(version));
}
#[rstest]
#[case("0", "expected positive non-zero decimal integer")]
#[case("-0", "expected positive non-zero decimal integer")]
#[case("z", "expected positive non-zero decimal integer")]
fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
panic!("'{input}' erroneously parsed as Epoch")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("foo")]
#[case("1.0.0")]
#[case(".xd")]
fn valid_pkgver(#[case] pkgver: &str) {
let parsed = PackageVersion::new(pkgver.to_string());
assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
assert_eq!(
parsed.as_ref().unwrap().to_string(),
pkgver,
"Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
parsed.unwrap(),
pkgver
);
}
#[rstest]
#[case("1:foo", "invalid pkgver character")]
#[case("foo-1", "invalid pkgver character")]
#[case("foo/1", "invalid pkgver character")]
#[case("ß", "invalid pkgver character")]
#[case("1.ß", "invalid pkgver character")]
#[case("", "invalid pkgver character")]
fn invalid_pkgver(#[case] pkgver: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
panic!("Expected pkgver {pkgver} to be invalid.")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("0")]
#[case("1")]
#[case("10")]
#[case("1.0")]
#[case("10.5")]
#[case("0.1")]
fn valid_pkgrel(#[case] pkgrel: &str) {
let parsed = PackageRelease::from_str(pkgrel);
assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
assert_eq!(
parsed.as_ref().unwrap().to_string(),
pkgrel,
"Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
parsed.unwrap(),
pkgrel
);
}
#[rstest]
#[case(".1", "expected positive decimal integer")]
#[case("1.", "expected single '.' followed by positive decimal integer")]
#[case("1..1", "expected single '.' followed by positive decimal integer")]
#[case("-1", "expected positive decimal integer")]
#[case("a", "expected positive decimal integer")]
#[case("1.a", "expected single '.' followed by positive decimal integer")]
#[case("1.0.0", "expected end of package release")]
#[case("", "expected positive decimal integer")]
fn invalid_pkgrel(#[case] pkgrel: &str, #[case] err_snippet: &str) {
let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
panic!("'{pkgrel}' erroneously parsed as PackageRelease")
};
assert!(
err_msg.contains(err_snippet),
"Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
);
}
#[rstest]
#[case("1", "1.0", Ordering::Less)]
#[case("1.0", "2", Ordering::Less)]
#[case("1", "1.1", Ordering::Less)]
#[case("1.0", "1.1", Ordering::Less)]
#[case("0", "1.1", Ordering::Less)]
#[case("1", "11", Ordering::Less)]
#[case("1", "1", Ordering::Equal)]
#[case("1.2", "1.2", Ordering::Equal)]
#[case("2.0", "2.0", Ordering::Equal)]
#[case("2", "1.0", Ordering::Greater)]
#[case("1.1", "1", Ordering::Greater)]
#[case("1.1", "1.0", Ordering::Greater)]
#[case("1.1", "0", Ordering::Greater)]
#[case("11", "1", Ordering::Greater)]
fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
let first = PackageRelease::from_str(first).unwrap();
let second = PackageRelease::from_str(second).unwrap();
assert_eq!(
first.cmp(&second),
order,
"{first} should be {order:?} to {second}"
);
}
}