blob: 3356b1bad84908012b20f075ef7b49956cd3bdc0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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>>,
}
/// Parses a string slice to u32 at compile time.
///
/// # Panics
/// - If the string is empty
/// - If the string contains non-digit characters
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");
}
// ASCII '0' - '9' to 0-9
let digit = bytes[i] - b'0';
result = result * 10 + digit as u32;
i += 1;
}
result
}
/// Find pos of a byte in a range, return `end` if not found any
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
}
/// Find pos of a byte in a slice, return bytes.len() if not found any
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()
}
/// Extract substring in const context
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");
}
// SAFETY: Creating a slice within the bound of original byte slice.
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> {
// Split on '+' to separate build metadata (which we ignore)
let version_core = s.split('+').next().unwrap();
// Split on '-' to separate prerelease identifier
let mut parts = version_core.split('-');
let version_numbers = parts.next().unwrap();
let prerelease = parts.next().map(|s| Cow::Owned(s.to_string()));
// Parse major.minor.patch
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();
// Split on '+' to ignore build metadata
let core_end = find_byte_pos_or_len(bytes, b'+');
// Split on '-' to separate prerelease
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
};
// Split version number on '.'
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);
// Parse major.minor.patch
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);
// Extract prerelease if it present
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()); // Not found
}
#[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); // Not found in range
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);
// Build metadata is not included in the display
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)); // Prerelease is ignored in comparison
assert!(!v1.is_equal_to(&v3));
assert!(v3.is_greater_than(&v1));
assert!(!v1.is_greater_than(&v3));
}
}