use crate::IggyError;
use std::borrow::Cow;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct SemanticVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: Option<Cow<'static, str>>,
}
const fn const_parse_u32_range(bytes: &[u8], start: usize, end: usize) -> u32 {
if start >= end {
panic!("Cannot parse empty range as u32");
}
let mut result = 0u32;
let mut i = start;
if bytes.is_empty() {
panic!("Can not parse empty string as u32");
}
while i < end {
let byte = bytes[i];
if !byte.is_ascii_digit() {
panic!("Invalid digit in version number");
}
let digit = bytes[i] - b'0';
result = result * 10 + digit as u32;
i += 1;
}
result
}
const fn find_byte_in_range(bytes: &[u8], target: u8, start: usize, end: usize) -> usize {
let mut i = start;
while i < end {
if bytes[i] == target {
return i;
}
i += 1;
}
end
}
const fn find_byte_pos_or_len(bytes: &[u8], target: u8) -> usize {
let mut i = 0;
while i < bytes.len() {
if bytes[i] == target {
return i;
}
i += 1;
}
bytes.len()
}
const fn const_str_slice(s: &str, start: usize, end: usize) -> &str {
let bytes = s.as_bytes();
if start > end {
panic!("Start index must be less than or equal to end index");
}
if end > bytes.len() {
panic!("End index out of bounds");
}
let slice = unsafe { core::slice::from_raw_parts(bytes.as_ptr().add(start), end - start) };
match core::str::from_utf8(slice) {
Ok(substr) => substr,
Err(_) => panic!("Invalid UTF-8 in version string"),
}
}
impl FromStr for SemanticVersion {
type Err = IggyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let version_core = s.split('+').next().unwrap();
let mut parts = version_core.split('-');
let version_numbers = parts.next().unwrap();
let prerelease = parts.next().map(|s| Cow::Owned(s.to_string()));
let mut version = version_numbers.split('.');
let major = version
.next()
.ok_or(IggyError::InvalidVersion(s.to_string()))?
.parse::<u32>()
.map_err(|_| IggyError::InvalidNumberValue)?;
let minor = version
.next()
.ok_or(IggyError::InvalidVersion(s.to_string()))?
.parse::<u32>()
.map_err(|_| IggyError::InvalidNumberValue)?;
let patch = version
.next()
.ok_or(IggyError::InvalidVersion(s.to_string()))?
.parse::<u32>()
.map_err(|_| IggyError::InvalidNumberValue)?;
Ok(SemanticVersion {
major,
minor,
patch,
prerelease,
})
}
}
impl SemanticVersion {
pub const fn parse_const(s: &'static str) -> Self {
let bytes = s.as_bytes();
let core_end = find_byte_pos_or_len(bytes, b'+');
let dash_pos = find_byte_in_range(bytes, b'-', 0, core_end);
let version_end = if dash_pos < core_end {
dash_pos
} else {
core_end
};
let first_dot = find_byte_in_range(bytes, b'.', 0, version_end);
let second_dot = find_byte_in_range(bytes, b'.', first_dot + 1, version_end);
let major = const_parse_u32_range(bytes, 0, first_dot);
let minor = const_parse_u32_range(bytes, first_dot + 1, second_dot);
let patch = const_parse_u32_range(bytes, second_dot + 1, version_end);
let prerelease = if dash_pos < core_end {
Some(Cow::Borrowed(const_str_slice(s, dash_pos + 1, core_end)))
} else {
None
};
Self {
major,
minor,
patch,
prerelease,
}
}
#[must_use]
pub fn is_equal_to(&self, other: &SemanticVersion) -> bool {
self.major == other.major && self.minor == other.minor && self.patch == other.patch
}
pub fn is_greater_than(&self, other: &SemanticVersion) -> bool {
if self.major > other.major {
return true;
}
if self.major < other.major {
return false;
}
if self.minor > other.minor {
return true;
}
if self.minor < other.minor {
return false;
}
if self.patch > other.patch {
return true;
}
if self.patch < other.patch {
return false;
}
false
}
pub fn get_numeric_version(&self) -> Result<u32, IggyError> {
let major = self.major;
let minor = format!("{:03}", self.minor);
let patch = format!("{:03}", self.patch);
if let Ok(version) = format!("{major}{minor}{patch}").parse::<u32>() {
return Ok(version);
}
Err(IggyError::InvalidVersion(self.to_string()))
}
}
impl Display for SemanticVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if let Some(ref prerelease) = self.prerelease {
write!(f, "-{prerelease}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_byte_pos_or_len() {
let bytes = b"1.2.3-beta+build";
assert_eq!(find_byte_pos_or_len(bytes, b'.'), 1);
assert_eq!(find_byte_pos_or_len(bytes, b'-'), 5);
assert_eq!(find_byte_pos_or_len(bytes, b'+'), 10);
assert_eq!(find_byte_pos_or_len(bytes, b'y'), bytes.len()); }
#[test]
fn test_find_byte_in_range() {
let bytes = b"1.2.3-beta";
assert_eq!(find_byte_in_range(bytes, b'.', 0, 5), 1);
assert_eq!(find_byte_in_range(bytes, b'.', 2, 5), 3);
assert_eq!(find_byte_in_range(bytes, b'.', 4, 5), 5); assert_eq!(find_byte_in_range(bytes, b'-', 0, 10), 5);
}
#[test]
fn test_const_parse_u32_range() {
let bytes = b"123";
assert_eq!(const_parse_u32_range(bytes, 0, 3), 123);
let bytes = b"1.52.999";
assert_eq!(const_parse_u32_range(bytes, 0, 1), 1);
assert_eq!(const_parse_u32_range(bytes, 2, 4), 52);
assert_eq!(const_parse_u32_range(bytes, 5, 8), 999);
}
#[test]
#[should_panic(expected = "Cannot parse empty range as u32")]
fn test_const_parse_u32_range_empty() {
let bytes = b"123";
const_parse_u32_range(bytes, 1, 1);
}
#[test]
#[should_panic(expected = "Invalid digit in version number")]
fn test_const_parse_u32_range_invalid() {
let bytes = b"12a";
const_parse_u32_range(bytes, 0, 3);
}
#[test]
fn test_const_str_slice() {
let s = "1.2.3-beta+build";
assert_eq!(const_str_slice(s, 0, 5), "1.2.3");
assert_eq!(const_str_slice(s, 6, 10), "beta");
assert_eq!(const_str_slice(s, 11, 16), "build");
assert_eq!(const_str_slice(s, 0, 0), "");
}
#[test]
fn should_parse_semver_on_compile_time() {
const SEMVER: SemanticVersion = SemanticVersion::parse_const("1.0.0-beta");
assert_eq!(SEMVER.major, 1);
assert_eq!(SEMVER.minor, 0);
assert_eq!(SEMVER.patch, 0);
assert_eq!(SEMVER.prerelease, Some(Cow::Borrowed("beta")));
const SEMVER_1: SemanticVersion = SemanticVersion::parse_const("2.1.5-rc.1");
assert_eq!(SEMVER_1.major, 2);
assert_eq!(SEMVER_1.minor, 1);
assert_eq!(SEMVER_1.patch, 5);
assert_eq!(SEMVER_1.prerelease, Some(Cow::Borrowed("rc.1")));
const SEMVER_2: SemanticVersion = SemanticVersion::parse_const("1.2.3+build.123");
assert_eq!(SEMVER_2.major, 1);
assert_eq!(SEMVER_2.minor, 2);
assert_eq!(SEMVER_2.patch, 3);
assert_eq!(SEMVER_2.prerelease, None);
const SEMVER_3: SemanticVersion = SemanticVersion::parse_const("3.2.1-alpha.2+build.456");
assert_eq!(SEMVER_3.major, 3);
assert_eq!(SEMVER_3.minor, 2);
assert_eq!(SEMVER_3.patch, 1);
assert_eq!(SEMVER_3.prerelease, Some(Cow::Borrowed("alpha.2")));
}
#[test]
fn should_parse_basic_semantic_version() {
let version = "1.2.3".parse::<SemanticVersion>().unwrap();
assert_eq!(version.major, 1);
assert_eq!(version.minor, 2);
assert_eq!(version.patch, 3);
assert_eq!(version.prerelease, None);
assert_eq!(version.to_string(), "1.2.3");
}
#[test]
fn should_parse_semantic_version_with_prerelease() {
let version = "0.6.0-rc1".parse::<SemanticVersion>().unwrap();
assert_eq!(version.major, 0);
assert_eq!(version.minor, 6);
assert_eq!(version.patch, 0);
assert_eq!(version.prerelease, Some(Cow::Borrowed("rc1")));
assert_eq!(version.to_string(), "0.6.0-rc1");
}
#[test]
fn should_parse_semantic_version_with_alpha() {
let version = "2.0.0-alpha.1".parse::<SemanticVersion>().unwrap();
assert_eq!(version.major, 2);
assert_eq!(version.minor, 0);
assert_eq!(version.patch, 0);
assert_eq!(version.prerelease, Some(Cow::Borrowed("alpha.1")));
assert_eq!(version.to_string(), "2.0.0-alpha.1");
}
#[test]
fn should_parse_semantic_version_with_build_metadata() {
let version = "1.0.0+20130313144700".parse::<SemanticVersion>().unwrap();
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
assert_eq!(version.patch, 0);
assert_eq!(version.prerelease, None);
assert_eq!(version.to_string(), "1.0.0");
}
#[test]
fn should_parse_semantic_version_with_prerelease_and_build_metadata() {
let version = "1.0.0-beta+exp.sha.5114f85"
.parse::<SemanticVersion>()
.unwrap();
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
assert_eq!(version.patch, 0);
assert_eq!(version.prerelease, Some(Cow::Borrowed("beta")));
assert_eq!(version.to_string(), "1.0.0-beta");
}
#[test]
fn should_compare_versions_correctly() {
let v1 = "1.0.0".parse::<SemanticVersion>().unwrap();
let v2 = "1.0.0-rc1".parse::<SemanticVersion>().unwrap();
let v3 = "2.0.0".parse::<SemanticVersion>().unwrap();
assert!(v1.is_equal_to(&v2)); assert!(!v1.is_equal_to(&v3));
assert!(v3.is_greater_than(&v1));
assert!(!v1.is_greater_than(&v3));
}
}