use std::str::FromStr;
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct Version {
epoch: Option<u64>,
upstream_version: String,
debian_revision: Option<String>,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Error {
Empty,
Malformed,
InvalidEpoch,
NoUpstreamVersion,
NoDebianRevision,
InvalidUpstreamVersion,
InvalidDebianRevision,
}
impl Version {
pub fn from_parts(
epoch: Option<u64>,
upstream_version: &str,
debian_revision: Option<&str>,
) -> Result<Self, Error> {
let ret = Version {
epoch,
upstream_version: upstream_version.to_owned(),
debian_revision: debian_revision.map(|v| v.to_owned()),
};
ret.check()?;
Ok(ret)
}
pub fn epoch(&self) -> Option<u64> {
self.epoch
}
pub fn upstream_version(&self) -> &str {
&self.upstream_version
}
pub fn debian_revision(&self) -> Option<&str> {
self.debian_revision.as_deref()
}
fn check(&self) -> Result<(), Error> {
if let Some(ch) = self.upstream_version.chars().next()
&& !ch.is_ascii_digit()
{
return Err(Error::InvalidUpstreamVersion);
}
for ch in self.upstream_version.chars() {
if ch.is_ascii_lowercase()
|| ch.is_ascii_uppercase()
|| ch.is_ascii_digit()
|| ch == '~'
|| ch == '+'
|| ch == '.'
{
continue;
}
if ch == ':' && self.epoch.is_some() {
continue;
}
if ch == '-' && self.debian_revision.is_some() {
continue;
}
return Err(Error::InvalidUpstreamVersion);
}
if let Some(debian_revision) = &self.debian_revision {
for ch in debian_revision.chars() {
if ch.is_ascii_lowercase()
|| ch.is_ascii_uppercase()
|| ch.is_ascii_digit()
|| ch == '~'
|| ch == '+'
|| ch == '.'
{
continue;
}
return Err(Error::InvalidDebianRevision);
}
}
Ok(())
}
}
impl FromStr for Version {
type Err = Error;
fn from_str(mut ver: &str) -> Result<Self, Error> {
ver = ver.trim();
let mut ret: Self = Default::default();
match ver.splitn(2, ':').collect::<Vec<_>>()[..] {
[version] => {
ver = version;
}
[epoch, version] => {
let epoch = epoch.parse().map_err(|_| Error::InvalidEpoch)?;
if epoch > (i32::MAX as u64) {
return Err(Error::InvalidEpoch);
}
ret.epoch = Some(epoch);
ver = version;
}
_ => {
return Err(Error::Malformed);
}
}
if ver.is_empty() {
return Err(Error::Empty);
}
match ver.rsplitn(2, '-').collect::<Vec<_>>()[..] {
[upstream_version] => {
ret.upstream_version = upstream_version.to_owned();
}
[debian_revision, upstream_version] => {
if debian_revision.is_empty() {
return Err(Error::NoDebianRevision);
}
if upstream_version.is_empty() {
return Err(Error::NoUpstreamVersion);
}
ret.upstream_version = upstream_version.to_owned();
ret.debian_revision = Some(debian_revision.to_owned());
}
_ => {
return Err(Error::Malformed);
}
}
if ret.upstream_version.is_empty() {
return Err(Error::NoUpstreamVersion);
}
ret.check()?;
Ok(ret)
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match (&self.debian_revision, &self.epoch) {
(Some(debian_revision), Some(epoch)) => {
format!("{}:{}-{}", epoch, self.upstream_version, debian_revision)
}
(None, Some(epoch)) => {
format!("{}:{}", epoch, self.upstream_version)
}
(Some(debian_revision), None) => {
format!("{}-{}", self.upstream_version, debian_revision)
}
(None, None) => self.upstream_version.clone(),
}
)
}
}
#[cfg(feature = "serde")]
mod serde {
use super::Version;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
String::serialize(&self.to_string(), serializer)
}
}
impl<'de> Deserialize<'de> for Version {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
s.parse().map_err(|e| D::Error::custom(format!("{e:?}")))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::control;
use std::io::{BufReader, Cursor};
#[test]
fn serde_version() {
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct Test {
#[serde(rename = "Version")]
version: Version,
}
let test: Test = control::de::from_reader(&mut BufReader::new(Cursor::new(
"\
Version: 1:1.0-1
",
)))
.unwrap();
assert_eq!(test.version.epoch, Some(1));
assert_eq!(test.version.upstream_version, "1.0");
assert_eq!(test.version.debian_revision, Some("1".to_owned()));
}
}
}
#[cfg(test)]
mod test {
use super::*;
macro_rules! check_matches {
($name:ident, $version:expr, $check:expr) => {
#[test]
fn $name() {
let v: Version = $version.parse().unwrap();
assert_eq!($check, v);
}
};
}
macro_rules! check_validation_fails {
($name:ident, $version:expr) => {
#[test]
fn $name() {
assert!($version.check().is_err());
}
};
}
macro_rules! check_parse_fails {
($name:ident, $version:expr) => {
#[test]
fn $name() {
assert!($version.parse::<Version>().is_err());
}
};
}
macro_rules! check_fuzz_regression {
($name:ident, $version:expr) => {
#[test]
fn $name() {
let Ok(v) = $version.parse::<Version>() else {
return;
};
v.to_string();
let v2: Version = "100:100.100+100-100onehundred100~100".parse().unwrap();
let _ = v.cmp(&v2);
let _ = v2.cmp(&v);
}
};
}
check_matches!(
simple_version,
"1.0-1",
Version {
upstream_version: "1.0".to_owned(),
debian_revision: Some("1".to_owned()),
..Default::default()
}
);
check_matches!(
simple_version_epoch,
"1:1.0-1",
Version {
epoch: Some(1),
upstream_version: "1.0".to_owned(),
debian_revision: Some("1".to_owned()),
}
);
check_matches!(
spaces,
" 1.0-1 ",
Version {
upstream_version: "1.0".to_owned(),
debian_revision: Some("1".to_owned()),
..Default::default()
}
);
check_matches!(
all_zeros,
"0:0:0:0-0",
Version {
epoch: Some(0),
upstream_version: "0:0:0".to_owned(),
debian_revision: Some("0".to_owned()),
}
);
check_matches!(
all_the_things,
"0:09azAZ.-+~:-0",
Version {
epoch: Some(0),
upstream_version: "09azAZ.-+~:".to_owned(),
debian_revision: Some("0".to_owned()),
}
);
check_matches!(
all_the_things_revision,
"0:0-azAZ09.+~",
Version {
epoch: Some(0),
upstream_version: "0".to_owned(),
debian_revision: Some("azAZ09.+~".to_owned()),
}
);
check_parse_fails!(empty, "");
check_parse_fails!(empty_space, " ");
check_parse_fails!(invalid_epoch, "-1:1.0-1");
check_parse_fails!(invalid_epoch2, "1.0:1-1");
check_parse_fails!(invalid_epoch3, "1:");
check_parse_fails!(invalid_epoch4, "a:1.0");
check_parse_fails!(invalid_upstream, "-1");
check_parse_fails!(starting_number, "abc3-0");
check_parse_fails!(space_twixt, "0:0 0-1");
check_parse_fails!(invalid_chars1, "1.0@");
check_parse_fails!(invalid_chars2, "1.0#");
check_parse_fails!(empty_revision, "7-");
check_parse_fails!(epoch_too_large, "333333333333333333:3");
check_validation_fails!(
invalid_construction_col,
Version {
epoch: None,
upstream_version: "1:0".to_string(),
debian_revision: Some("1".to_string()),
}
);
check_validation_fails!(
invalid_construction_dash,
Version {
epoch: Some(1),
upstream_version: "1-1".to_string(),
debian_revision: None,
}
);
#[test]
fn version_sort() {
let mut versions = vec![
"1.3",
"1.0",
"1.0+dfsg1-1",
"1.0-1",
"1.1",
"0:1.2",
"1:0.1",
"1.0+dfsg1",
"1.0~dfsg1",
]
.into_iter()
.map(|v| v.parse::<Version>().unwrap())
.collect::<Vec<_>>();
versions.sort();
assert_eq!(
vec![
"1.0~dfsg1",
"1.0",
"1.0-1",
"1.0+dfsg1",
"1.0+dfsg1-1",
"1.1",
"0:1.2",
"1.3",
"1:0.1",
]
.into_iter()
.map(|v| v.parse::<Version>().unwrap())
.collect::<Vec<_>>(),
versions
);
}
check_fuzz_regression!(
long_number,
"100:222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222221~~~~~~~~~~~~~~~~~1~1~0"
);
}