#[macro_use]
extern crate lazy_static;
use regex::{Captures, Regex};
use std::cmp::Ordering;
use std::fmt;
lazy_static! {
static ref CANONICAL_RE: Regex = Regex::new(r#"(?xi)
^([1-9][0-9]*!)? # epoch
(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))* # release segment
((a|b|rc)(0|[1-9][0-9]*))? # pre-release
(\.post(0|[1-9][0-9]*))? # post release
(\.dev(0|[1-9][0-9]*))? # dev release
$"#).unwrap();
static ref VERSION_RE: Regex = Regex::new(r#"(?xi)
^v?
(?:
(?:(?P<epoch>[0-9]+)!)? # epoch
(?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
(?P<pre> # pre-release
[-_\.]?
(?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
[-_\.]?
(?P<pre_n>[0-9]+)?
)?
(?P<post> # post release
(?:-(?P<post_n1>[0-9]+))
|
(?:
[-_\.]?
(?P<post_l>post|rev|r)
[-_\.]?
(?P<post_n2>[0-9]+)?
)
)?
(?P<dev> # dev release
[-_\.]?
(?P<dev_l>dev)
[-_\.]?
(?P<dev_n>[0-9]+)?
)?
)
(?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
$"#).unwrap();
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Version {
pub epoch: u32,
pub release: Vec<u32>,
pub pre: Option<PreRelease>,
pub post: Option<u32>,
pub dev: Option<u32>,
pub local: Vec<LocalVersion>,
}
impl Version {
pub fn is_canonical(input: &str) -> bool {
CANONICAL_RE.is_match(input)
}
pub fn parse(input: &str) -> Option<Version> {
let captures = VERSION_RE.captures(input)?;
fn pre_release_constructor(label: &str) -> Option<fn(u32) -> PreRelease> {
match label {
"a" => Some(PreRelease::A),
"alpha" => Some(PreRelease::A),
"b" => Some(PreRelease::B),
"beta" => Some(PreRelease::B),
"c" => Some(PreRelease::RC),
"rc" => Some(PreRelease::RC),
"pre" => Some(PreRelease::RC),
"preview" => Some(PreRelease::RC),
_ => None
}
}
fn get_pre_release(captures: &Captures) -> Option<PreRelease> {
let label = captures.name("pre_l").map(|pre_l| pre_l.as_str())?;
let constructor = pre_release_constructor(&label.to_lowercase())?;
let pre_n = captures
.name("pre_n")
.and_then(|pre_n| pre_n.as_str().parse().ok())
.unwrap_or(0);
Some(constructor(pre_n))
}
fn get_post_release(captures: &Captures) -> Option<u32> {
match captures.name("post_n1") {
Some(n1) => n1.as_str().parse().ok(),
None => match captures.name("post_n2") {
Some(n2) => n2.as_str().parse().ok(),
None => match captures.name("post_l") {
Some(_) => Some(0),
None => None
}
}
}
}
fn get_dev_release(captures: &Captures) -> Option<u32> {
captures.name("dev_l")?;
match captures.name("dev_n") {
Some(dev_n) => dev_n.as_str().parse().ok(),
None => Some(0),
}
}
fn get_local_component(component: &str) -> LocalVersion {
if let Ok(num) = component.parse::<u32>() {
LocalVersion::NumericComponent(num)
} else {
LocalVersion::StringComponent(component.to_lowercase())
}
}
let epoch: u32 = captures
.name("epoch")
.map(|epoch| epoch.as_str())
.unwrap_or("0")
.parse()
.ok()?;
let release = captures
.name("release")?
.as_str()
.split('.')
.map(|part| part.parse().ok())
.collect::<Option<Vec<u32>>>()?;
let pre = get_pre_release(&captures);
let post = get_post_release(&captures);
let dev = get_dev_release(&captures);
let local: Vec<LocalVersion> = captures
.name("local")
.map(|local| local.as_str().split(&['-', '_', '.'][..]).collect::<Vec<&str>>())
.unwrap_or_default()
.iter()
.map(|local| get_local_component(local))
.collect();
Some(Version { epoch, release, pre, post, dev, local })
}
pub fn epoch_str(&self) -> String {
if self.epoch != 0 {
format!("{}!", self.epoch)
} else {
"".to_string()
}
}
pub fn release_str(&self) -> String {
self.release
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join(".")
}
pub fn pre_str(&self) -> String {
self.pre
.clone()
.map(|x| format!("{}", x))
.unwrap_or_default()
}
pub fn post_str(&self) -> String {
self.post
.map(|x| format!(".post{}", x))
.unwrap_or_default()
}
pub fn dev_str(&self) -> String {
self.dev
.map(|x| format!(".dev{}", x))
.unwrap_or_default()
}
pub fn local_str(&self) -> String {
let glued = self.local
.iter()
.map(|x| format!("{}", x))
.collect::<Vec<String>>()
.join(".");
if !glued.is_empty() {
format!("+{}", glued)
} else {
"".to_string()
}
}
pub fn public_str(&self) -> String {
format!(
"{}{}{}{}{}",
self.epoch_str(),
self.release_str(),
self.pre_str(),
self.post_str(),
self.dev_str())
}
pub fn normalize(&self) -> String {
format!("{}{}", self.public_str(), self.local_str())
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.normalize())
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
fn drop_right_zeros(vec: &[u32]) -> Vec<u32> {
vec
.to_vec()
.into_iter()
.rev()
.skip_while(|x| *x == 0)
.collect::<Vec<u32>>()
.into_iter()
.rev()
.collect()
}
fn post_dev_local(me: &Version, other: &Version) -> Ordering {
if me.post != other.post {
return me.post.unwrap_or(0).cmp(&other.post.unwrap_or(0))
}
match (me.dev, other.dev) {
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(ref sv), Some(ref ov)) => sv.cmp(ov),
(None, None) => me.local.cmp(&other.local),
}
}
if self.epoch != other.epoch {
return self.epoch.cmp(&other.epoch);
}
let me = drop_right_zeros(&self.release);
let notme = drop_right_zeros(&other.release);
if me != notme {
return me.cmp(¬me);
}
if self.dev.is_some() & self.pre.is_none() & self.post.is_none() {
if other.pre.is_some() | other.post.is_some() {
return Ordering::Less;
}
if other.pre.is_none() & other.post.is_none() & other.dev.is_none() {
return Ordering::Less;
}
}
if other.dev.is_some() & other.pre.is_none() & other.post.is_none() {
if self.pre.is_some() | self.post.is_some() {
return Ordering::Greater;
}
if self.pre.is_none() & self.post.is_none() & self.dev.is_none() {
return Ordering::Greater;
}
}
use PreRelease::*;
match (&self.pre, &other.pre) {
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(x, y) if x == y => post_dev_local(self, other),
(None, None) => post_dev_local(self, other),
(Some(pre1), Some(pre2)) => match (pre1, pre2) {
(RC(ref sv), RC(ref ov)) => sv.cmp(ov),
(RC(_), _) => Ordering::Greater,
(A(ref sv), A(ref ov)) => sv.cmp(ov),
(A(_), _) => Ordering::Less,
(B(ref sv), B(ref ov)) => sv.cmp(ov),
(B(_), A(_)) => Ordering::Greater,
(B(_), _) => Ordering::Less,
},
}
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum LocalVersion {
NumericComponent(u32),
StringComponent(String),
}
impl fmt::Display for LocalVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use LocalVersion::*;
match self {
NumericComponent(n) => write!(f, "{}", n),
StringComponent(s) => write!(f, "{}", s),
}
}
}
impl PartialOrd for LocalVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LocalVersion {
fn cmp(&self, other: &Self) -> Ordering {
use LocalVersion::*;
match (self, other) {
(NumericComponent(n1), NumericComponent(n2)) => n1.cmp(n2),
(StringComponent(s1), StringComponent(s2)) => s1.cmp(s2),
(NumericComponent(_), StringComponent(_)) => Ordering::Greater,
(StringComponent(_), NumericComponent(_)) => Ordering::Less,
}
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum PreRelease {
RC(u32),
A(u32),
B(u32),
}
impl fmt::Display for PreRelease {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PreRelease::RC(v) => write!(f, "rc{}", v),
PreRelease::A(v) => write!(f, "a{}", v),
PreRelease::B(v) => write!(f, "b{}", v),
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
const CANONICAL_VERSIONS: &[&str] = &[
"1!1.0", "1!1.0.dev456", "1!1.0.post456",
"1!1.0.post456.dev34", "1!1.0a1", "1!1.0a12",
"1!1.0a12.dev456", "1!1.0a2.dev456", "1!1.0b1.dev456",
"1!1.0b2", "1!1.0b2.post345", "1!1.0b2.post345.dev456",
"1!1.0b2.post346", "1!1.0rc1", "1!1.0rc1.dev456", "1!1.0rc2",
"1!1.0rc3", "1!1.1.dev1", "1.0", "1.0.dev456", "1.0.post456",
"1.0.post456.dev34", "1.0a1", "1.0a12", "1.0a12.dev456",
"1.0a2.dev456", "1.0b1.dev456", "1.0b2", "1.0b2.post345",
"1.0b2.post345.dev456", "1.0b2.post346", "1.0rc1",
"1.0rc1.dev456", "1.0rc2", "1.0rc3", "1.1.dev1",
];
const NON_CANONICAL_VERSIONS: &[&str] = &[
"1!1.2+1234.abc", "1!1.2+123456", "1!1.2+123abc", "1!1.2+123abc456",
"1!1.2+abc", "1!1.2+abc123", "1!1.2+abc123def", "1!1.2.post32+123456",
"1!1.2.post33+123456", "1.2+1234.abc", "1.2+123456", "1.2+123abc",
"1.2+123abc456", "1.2+abc", "1.2+abc123", "1.2+abc123def",
"1.2.post32+123456", "1.2.post33+123456", "1!1.2.post32+12_34.56",
"1!1.2.post32+12.oh.no.whyyyyy.56",
];
const INVALID_VERSIONS: &[&str] = &[
"foo bar", "1.0+a+", "1.0++", "1.0+_foobar", "1.0+foo&asd", "1.0+1+1",
"-3.0", "version 4", "beta alpha", "4.0ba3", "seven", "7..34", "1.0bb3",
"1.0omega4",
];
const NORMALIZATION: &[(&str, &str)] = &[
("1.0dev", "1.0.dev0"), ("1.0.dev", "1.0.dev0"),
("1.0dev1", "1.0.dev1"), ("1.0dev", "1.0.dev0"),
("1.0-dev", "1.0.dev0"), ("1.0-dev1", "1.0.dev1"),
("1.0DEV", "1.0.dev0"), ("1.0.DEV", "1.0.dev0"),
("1.0DEV1", "1.0.dev1"), ("1.0DEV", "1.0.dev0"),
("1.0.DEV1", "1.0.dev1"), ("1.0-DEV", "1.0.dev0"),
("1.0-DEV1", "1.0.dev1"), ("1.0a", "1.0a0"), ("1.0.a", "1.0a0"),
("1.0.a1", "1.0a1"), ("1.0-a", "1.0a0"), ("1.0-a1", "1.0a1"),
("1.0alpha", "1.0a0"), ("1.0.alpha", "1.0a0"), ("1.0.alpha1", "1.0a1"),
("1.0-alpha", "1.0a0"), ("1.0-alpha1", "1.0a1"), ("1.0A", "1.0a0"),
("1.0.A", "1.0a0"), ("1.0.A1", "1.0a1"), ("1.0-A", "1.0a0"),
("1.0-A1", "1.0a1"), ("1.0ALPHA", "1.0a0"), ("1.0.ALPHA", "1.0a0"),
("1.0.ALPHA1", "1.0a1"), ("1.0-ALPHA", "1.0a0"),
("1.0-ALPHA1", "1.0a1"), ("1.0b", "1.0b0"), ("1.0.b", "1.0b0"),
("1.0.b1", "1.0b1"), ("1.0-b", "1.0b0"), ("1.0-b1", "1.0b1"),
("1.0beta", "1.0b0"), ("1.0.beta", "1.0b0"), ("1.0.beta1", "1.0b1"),
("1.0-beta", "1.0b0"), ("1.0-beta1", "1.0b1"), ("1.0B", "1.0b0"),
("1.0.B", "1.0b0"), ("1.0.B1", "1.0b1"), ("1.0-B", "1.0b0"),
("1.0-B1", "1.0b1"), ("1.0BETA", "1.0b0"), ("1.0.BETA", "1.0b0"),
("1.0.BETA1", "1.0b1"), ("1.0-BETA", "1.0b0"), ("1.0-BETA1", "1.0b1"),
("1.0c", "1.0rc0"), ("1.0.c", "1.0rc0"), ("1.0.c1", "1.0rc1"),
("1.0-c", "1.0rc0"), ("1.0-c1", "1.0rc1"), ("1.0rc", "1.0rc0"),
("1.0.rc", "1.0rc0"), ("1.0.rc1", "1.0rc1"), ("1.0-rc", "1.0rc0"),
("1.0-rc1", "1.0rc1"), ("1.0C", "1.0rc0"), ("1.0.C", "1.0rc0"),
("1.0.C1", "1.0rc1"), ("1.0-C", "1.0rc0"), ("1.0-C1", "1.0rc1"),
("1.0RC", "1.0rc0"), ("1.0.RC", "1.0rc0"), ("1.0.RC1", "1.0rc1"),
("1.0-RC", "1.0rc0"), ("1.0-RC1", "1.0rc1"), ("1.0post", "1.0.post0"),
("1.0.post", "1.0.post0"), ("1.0post1", "1.0.post1"),
("1.0post", "1.0.post0"), ("1.0-post", "1.0.post0"),
("1.0-post1", "1.0.post1"), ("1.0POST", "1.0.post0"),
("1.0.POST", "1.0.post0"), ("1.0POST1", "1.0.post1"),
("1.0POST", "1.0.post0"), ("1.0r", "1.0.post0"),
("1.0rev", "1.0.post0"), ("1.0.POST1", "1.0.post1"),
("1.0.r1", "1.0.post1"), ("1.0.rev1", "1.0.post1"),
("1.0-POST", "1.0.post0"), ("1.0-POST1", "1.0.post1"),
("1.0-5", "1.0.post5"), ("1.0-r5", "1.0.post5"),
("1.0-rev5", "1.0.post5"), ("1.0+AbC", "1.0+abc"), ("1.01", "1.1"),
("1.0a05", "1.0a5"), ("1.0b07", "1.0b7"), ("1.0c056", "1.0rc56"),
("1.0rc09", "1.0rc9"), ("1.0.post000", "1.0.post0"),
("1.1.dev09000", "1.1.dev9000"), ("00!1.2", "1.2"),
("0100!0.0", "100!0.0"), ("v1.0", "1.0"),
("1.0.dev456", "1.0.dev456"), ("1.0a1", "1.0a1"),
("1.0a2.dev456", "1.0a2.dev456"), ("1.0a12.dev456", "1.0a12.dev456"),
("1.0a12", "1.0a12"), ("1.0b1.dev456", "1.0b1.dev456"),
("1.0b2", "1.0b2"), ("1.0b2.post345.dev456", "1.0b2.post345.dev456"),
("1.0b2.post345", "1.0b2.post345"), ("1.0rc1.dev456", "1.0rc1.dev456"),
("1.0rc1", "1.0rc1"), ("1.0.post456.dev34", "1.0.post456.dev34"),
("1.0.post456", "1.0.post456"), ("1.0.1", "1.0.1"),
("0!1.0.2", "1.0.2"), ("1.0.3+7", "1.0.3+7"),
("0!1.0.4+8.0", "1.0.4+8.0"), ("1.0.5+9.5", "1.0.5+9.5"),
("1.2+1234.abc", "1.2+1234.abc"), ("1.2+123456", "1.2+123456"),
("1.2+123abc", "1.2+123abc"), ("1.2+123abc456", "1.2+123abc456"),
("1.2+abc", "1.2+abc"), ("1.2+abc123", "1.2+abc123"),
("1.2+abc123def", "1.2+abc123def"), ("1.1.dev1", "1.1.dev1"),
("7!1.0.dev456", "7!1.0.dev456"), ("7!1.0a1", "7!1.0a1"),
("7!1.0a2.dev456", "7!1.0a2.dev456"),
("7!1.0a12.dev456", "7!1.0a12.dev456"), ("7!1.0a12", "7!1.0a12"),
("7!1.0b1.dev456", "7!1.0b1.dev456"), ("7!1.0b2", "7!1.0b2"),
("7!1.0b2.post345.dev456", "7!1.0b2.post345.dev456"),
("7!1.0b2.post345", "7!1.0b2.post345"),
("7!1.0rc1.dev456", "7!1.0rc1.dev456"), ("7!1.0rc1", "7!1.0rc1"),
("7!1.0", "7!1.0"), ("7!1.0.post456.dev34", "7!1.0.post456.dev34"),
("7!1.0.post456", "7!1.0.post456"), ("7!1.0.1", "7!1.0.1"),
("7!1.0.2", "7!1.0.2"), ("7!1.0.3+7", "7!1.0.3+7"),
("7!1.0.4+8.0", "7!1.0.4+8.0"), ("7!1.0.5+9.5", "7!1.0.5+9.5"),
("7!1.1.dev1", "7!1.1.dev1"),
];
#[test]
fn test_is_canonical() {
for version in CANONICAL_VERSIONS {
assert!(
Version::is_canonical(version),
"Expected '{}' to be valid", version);
}
for version in NON_CANONICAL_VERSIONS {
assert!(
!Version::is_canonical(version),
"Expected '{}' to NOT be valid", version);
}
}
#[test]
fn test_parse() {
for version in [CANONICAL_VERSIONS, NON_CANONICAL_VERSIONS].concat() {
assert!(
Version::parse(version).is_some(),
"Failed to parse version: '{}'", version);
}
for version in INVALID_VERSIONS {
assert!(
Version::parse(version).is_none(),
"Parsed version but should not have: '{}'", version);
}
}
#[test]
fn test_normalization() {
for (input, expected) in NORMALIZATION {
let ver = Version::parse(input);
assert!(ver.is_some(), "Failed to parse version: {}", input);
let normalized = ver.unwrap().normalize();
let expected = expected.to_string();
assert_eq!(
normalized,
expected,
"input={}, expected={}, actual={}",
input,
expected,
normalized);
}
}
}