use std::fmt;
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
static ref RELEASE_REGEX: Regex = Regex::new(r#"^(@?[^@]+)@(.*?)$"#).unwrap();
static ref VERSION_REGEX: Regex = Regex::new(
r#"(?x)
^
(?P<major>0|[1-9][0-9]*)
(?:\.(?P<minor>0|[1-9][0-9]*))?
(?:\.(?P<patch>0|[1-9][0-9]*))?
(?:-?
(?P<prerelease>(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)
(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?
(?:\+(?P<build_code>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
$
"#
)
.unwrap();
static ref DOTTED_BUILD_CODE_REGEX: Regex = Regex::new(
r#"(?x)
^
(?P<major>0|[1-9][0-9]*)
(?:\.(?P<minor>0|[1-9][0-9]*))?
(?:\.(?P<patch>0|[1-9][0-9]*))?
$
"#
)
.unwrap();
static ref HEX_REGEX: Regex = Regex::new(r#"^[a-fA-F0-9]+$"#).unwrap();
static ref VALID_RELEASE_REGEX: Regex = Regex::new(r"^[^/\r\n]*\z").unwrap();
}
#[derive(Debug, Clone, PartialEq)]
pub struct InvalidVersion;
impl std::error::Error for InvalidVersion {}
impl fmt::Display for InvalidVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid version")
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum InvalidRelease {
TooLong,
RestrictedName,
BadCharacters,
}
impl std::error::Error for InvalidRelease {}
impl fmt::Display for InvalidRelease {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"invalid release: {}",
match *self {
InvalidRelease::BadCharacters => "bad characters in release name",
InvalidRelease::RestrictedName => "restricted release name",
InvalidRelease::TooLong => "release name too long",
}
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Version<'a> {
raw: &'a str,
major: u64,
minor: u64,
patch: u64,
pre: &'a str,
build_code: &'a str,
}
fn is_build_hash(s: &str) -> bool {
match s.len() {
12 | 16 | 20 | 32 | 40 | 64 => HEX_REGEX.is_match(s),
_ => false,
}
}
impl<'a> Version<'a> {
pub fn parse(version: &'a str) -> Result<Version<'a>, InvalidVersion> {
let caps = if let Some(caps) = VERSION_REGEX.captures(version) {
caps
} else {
return Err(InvalidVersion);
};
Ok(Version {
raw: version,
major: caps[1].parse().unwrap(),
minor: caps
.get(2)
.and_then(|x| x.as_str().parse().ok())
.unwrap_or(0),
patch: caps
.get(3)
.and_then(|x| x.as_str().parse().ok())
.unwrap_or(0),
pre: caps.get(4).map(|x| x.as_str()).unwrap_or(""),
build_code: caps.get(5).map(|x| x.as_str()).unwrap_or(""),
})
}
#[cfg(feature = "semver")]
pub fn as_semver(&self) -> semver::Version {
fn split(s: &str) -> Vec<semver::Identifier> {
s.split('.')
.map(|item| {
if let Ok(val) = item.parse::<u64>() {
semver::Identifier::Numeric(val)
} else {
semver::Identifier::AlphaNumeric(item.into())
}
})
.collect()
}
semver::Version {
major: self.major,
minor: self.minor,
patch: self.patch,
pre: split(self.pre),
build: split(self.build_code),
}
}
pub fn major(&self) -> u64 {
self.major
}
pub fn minor(&self) -> u64 {
self.minor
}
pub fn patch(&self) -> u64 {
self.patch
}
pub fn pre(&self) -> Option<&str> {
if self.pre.is_empty() {
None
} else {
Some(self.pre)
}
}
pub fn build_code(&self) -> Option<&str> {
if self.build_code.is_empty() {
None
} else {
Some(self.build_code)
}
}
pub fn normalized_build_code(&self) -> String {
if let Some(caps) = DOTTED_BUILD_CODE_REGEX.captures(self.build_code) {
format!(
"{:012}{:010}{:010}",
caps[1].parse::<u64>().unwrap_or(0),
caps.get(2)
.and_then(|x| x.as_str().parse::<u64>().ok())
.unwrap_or(0),
caps.get(3)
.and_then(|x| x.as_str().parse::<u64>().ok())
.unwrap_or(0),
)
} else {
self.build_code.to_ascii_lowercase()
}
}
pub fn raw(&self) -> &str {
self.raw
}
pub fn triple(&self) -> (u64, u64, u64) {
(self.major, self.minor, self.patch)
}
pub fn quad(&self) -> (u64, u64, u64, Option<&str>) {
(self.major, self.minor, self.patch, self.pre())
}
}
impl<'a> fmt::Display for Version<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}.{}.{}", self.major(), self.minor(), self.patch())?;
if let Some(pre) = self.pre() {
write!(f, "-{}", pre)?;
}
if let Some(build_code) = self.build_code() {
write!(f, "+{}", build_code)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FormatType {
Unqualified,
Qualified,
Versioned,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Release<'a> {
raw: &'a str,
package: &'a str,
version_raw: &'a str,
version: Option<Version<'a>>,
ty: FormatType,
}
impl<'a> Release<'a> {
pub fn parse(release: &'a str) -> Result<Release<'a>, InvalidRelease> {
let release = release.trim();
if release.len() > 250 {
return Err(InvalidRelease::TooLong);
} else if release == "." || release == ".." || release == "latest" {
return Err(InvalidRelease::RestrictedName);
} else if !VALID_RELEASE_REGEX.is_match(release) {
return Err(InvalidRelease::BadCharacters);
}
if let Some(caps) = RELEASE_REGEX.captures(release) {
let (version, ty) = if let Ok(version) = Version::parse(caps.get(2).unwrap().as_str()) {
(Some(version), FormatType::Versioned)
} else {
(None, FormatType::Qualified)
};
Ok(Release {
raw: release,
package: caps.get(1).unwrap().as_str(),
version_raw: caps.get(2).unwrap().as_str(),
version,
ty,
})
} else {
Ok(Release {
raw: release,
package: "",
version_raw: release,
version: None,
ty: FormatType::Unqualified,
})
}
}
pub fn raw(&self) -> &str {
self.raw
}
pub fn package(&self) -> Option<&str> {
if self.package.is_empty() {
None
} else {
Some(self.package)
}
}
pub fn version_raw(&self) -> &str {
self.version_raw
}
pub fn version(&self) -> Option<&Version<'a>> {
self.version.as_ref()
}
pub fn build_hash(&self) -> Option<&str> {
self.version
.as_ref()
.and_then(|x| x.build_code())
.filter(|x| is_build_hash(x))
.or_else(|| {
if is_build_hash(self.version_raw()) {
Some(self.version_raw())
} else {
None
}
})
}
pub fn describe(&self) -> ReleaseDescription<'_> {
ReleaseDescription(self)
}
pub fn format_type(&self) -> FormatType {
self.ty
}
}
#[derive(Debug)]
pub struct ReleaseDescription<'a>(&'a Release<'a>);
impl<'a> fmt::Display for ReleaseDescription<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let short_hash = if let Some(hash) = self.0.build_hash() {
Some(hash.get(..12).unwrap_or(hash))
} else {
None
};
if let Some(ver) = self.0.version() {
write!(f, "{}.{}.{}", ver.major(), ver.minor(), ver.patch())?;
if let Some(pre) = ver.pre() {
write!(f, "-{}", pre)?;
}
if let Some(short_hash) = short_hash {
write!(f, " ({})", short_hash)?;
} else if let Some(build_code) = ver.build_code() {
write!(f, " ({})", build_code)?;
}
} else if let Some(short_hash) = short_hash {
write!(f, "{}", short_hash)?;
} else {
write!(f, "{}", self.0)?;
}
Ok(())
}
}
impl<'a> fmt::Display for Release<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut have_package = false;
if let Some(package) = self.package() {
write!(f, "{}", package)?;
have_package = true;
}
if let Some(version) = self.version() {
if have_package {
write!(f, "@")?;
}
write!(f, "{}", version)?;
} else {
if have_package {
write!(f, "@")?;
}
write!(f, "{}", self.version_raw)?;
}
Ok(())
}
}