// Copyright (c) HashiCorp, Inc.
// Copyright (c) 2025 International Digital Economy Academy
// SPDX-License-Identifier: MPL-2.0
///|
// Error type for version parsing
pub suberror VersionError String
///|
impl Show for VersionError with output(self, logger) {
logger.write_string("VersionError: ")
match self {
VersionError(msg) => logger.write_string(msg)
}
}
///|
/// Version represents a single version.
pub struct Version {
metadata : String
pre : String
segments : Array[Int64]
si : Int
original : String
} derive(Eq, Show)
///|
/// Create a new Version from a string
pub fn Version::new(v : String) -> Version raise VersionError {
new_version(v)
}
///|
/// Create a new Version that adheres strictly to SemVer specs
pub fn Version::new_semver(v : String) -> Version raise VersionError {
new_version_semver(v)
}
///|
/// Helper function to compare strings character by character for proper lexicographic ordering
/// Returns -1 if a < b, 0 if a == b, 1 if a > b
fn string_compare(a : String, b : String) -> Int {
if a == b {
return 0
}
let len_a = a.length()
let len_b = b.length()
let min_len = if len_a < len_b { len_a } else { len_b }
for i = 0; i < min_len; i = i + 1 {
let char_a = a[i]
let char_b = b[i]
if char_a < char_b {
return -1
} else if char_a > char_b {
return 1
}
}
// If all compared characters are equal, the shorter string comes first
if len_a < len_b {
-1
} else if len_a > len_b {
1
} else {
0
}
}
///|
/// Helper function to check if a character code is a digit (optimized)
fn is_digit_code(code : Int) -> Bool {
// Ultra-optimized digit checking using bit operations
// ASCII '0' = 48 (0x30), '9' = 57 (0x39)
// For digit range check: subtract 48 and check if result is <= 9
// This is faster than two comparisons
let offset = code - 48
offset >= 0 && offset <= 9
}
///|
/// Helper function to convert a digit character code to Int64
fn char_code_to_digit(code : Int) -> Int64? {
if is_digit_code(code) {
Some((code - '0'.to_int()).to_int64())
} else {
None
}
}
///|
/// Helper function to check if a string contains only digits
fn is_all_digits(s : String) -> Bool {
if s.length() == 0 {
return false
}
for i = 0; i < s.length(); i = i + 1 {
if !is_digit_code(s[i]) {
return false
}
}
true
}
///|
/// Fast path check for simple versions like "1.2.3" (no prerelease, metadata, or 'v' prefix)
fn try_fast_parse_simple_version(s : String) -> Bool {
if s.length() == 0 {
return false
}
// Must start with digit
if !is_digit_code(s[0]) {
return false
}
// Check for presence of characters that indicate complex version
for i = 0; i < s.length(); i = i + 1 {
let c = s[i]
// Allow only digits and dots for simple versions
if !is_digit_code(c) && c != '.'.to_int() {
return false
}
}
// Must not start or end with dot, and no consecutive dots
if s[0] == '.'.to_int() || s[s.length() - 1] == '.'.to_int() {
return false
}
// Check for consecutive dots
for i = 0; i < s.length() - 1; i = i + 1 {
if s[i] == '.'.to_int() && s[i + 1] == '.'.to_int() {
return false
}
}
true
}
///|
/// Parse simple numeric version (fast path)
fn parse_simple_numeric_version(
s : String,
original : String,
) -> Version raise VersionError {
let segments_str = s.split(".")
let segments : Array[Int64] = []
let mut actual_segment_count = 0
for str in segments_str {
let str_string = str.to_string()
if str_string.length() == 0 {
raise VersionError(
"malformed version '\{original}': empty segment found in version components",
)
}
match try_parse_int(str_string) {
Some(val) => segments.push(val)
None => raise VersionError("malformed version: \{original}")
}
actual_segment_count = actual_segment_count + 1
}
// Pad to at least 3 segments
while segments.length() < 3 {
segments.push(0L)
}
Version::{
metadata: "",
pre: "",
segments,
si: actual_segment_count,
original,
}
}
///|
/// Helper function to parse a string to Int64, returns None if invalid
/// Optimized version using character operations
fn try_parse_int(s : String) -> Int64? {
if s.length() == 0 {
return None
}
// Quick check for all digits
if !is_all_digits(s) {
return None
}
let mut parsed_val = 0L
for i = 0; i < s.length(); i = i + 1 {
match char_code_to_digit(s[i]) {
Some(digit) => parsed_val = parsed_val * 10L + digit
None => return None // Should not happen due to is_all_digits check
}
}
Some(parsed_val)
}
///|
fn new_version_semver(v : String) -> Version raise VersionError {
let trimmed = v.trim_space()
// Check for empty version
if trimmed.length() == 0 {
raise VersionError(
"malformed version: cannot parse empty string as version",
)
}
// Remove optional 'v' prefix
let without_v = match trimmed.strip_prefix("v") {
Some(stripped) => stripped.to_string()
None => trimmed.to_string()
}
// Basic validation: version should start with a digit
if without_v.length() == 0 || !is_digit_code(without_v[0]) {
raise VersionError(
"malformed version '\{v}': version must start with a digit after optional 'v' prefix",
)
}
// Stricter SemVer validation: reject patterns like "1.7rc2", "1.0-"
// Split on + for metadata (only first occurrence)
let metadata_parts = without_v.split("+")
let mut version_part = ""
let mut metadata = ""
let mut part_count = 0
for part in metadata_parts {
if part_count == 0 {
version_part = part.to_string()
} else if metadata.length() > 0 {
metadata = metadata + "+" + part.to_string()
} else {
metadata = part.to_string()
}
part_count = part_count + 1
}
// Split on - for prerelease (only first occurrence)
let prerelease_parts = version_part.split("-")
let mut core_part = ""
let mut pre = ""
part_count = 0
for part in prerelease_parts {
if part_count == 0 {
core_part = part.to_string()
} else if pre.length() > 0 {
pre = pre + "-" + part.to_string()
} else {
pre = part.to_string()
}
part_count = part_count + 1
}
// Validate core part is not empty
if core_part.length() == 0 {
raise VersionError("malformed version: \{v}")
}
// SemVer stricter validation: reject if prerelease is empty but dash is present
if version_part.strip_suffix("-") is Some(_) && pre.length() == 0 {
raise VersionError("malformed version: \{v}")
}
// Check for invalid characters in prerelease section
if pre.length() > 0 {
// Must be separated properly from main version
if !version_part.contains("-") {
raise VersionError("malformed version: \{v}")
}
}
// Parse version segments - each must be a valid number
let segments_str = core_part.split(".")
let segments : Array[Int64] = []
let mut actual_segment_count = 0
for str in segments_str {
let str_string = str.to_string()
// Check for empty segments
if str_string.length() == 0 {
raise VersionError("malformed version: \{v}")
}
// Validate that segment contains only digits (no alpha characters)
if !is_all_digits(str_string) {
raise VersionError("malformed version: \{v}")
}
match try_parse_int(str_string) {
Some(val) => segments.push(val)
None => raise VersionError("malformed version: \{v}")
}
actual_segment_count = actual_segment_count + 1
}
// For SemVer, check if there are invalid characters attached to main version
// This catches cases like "1.7rc2" where "rc2" is not properly separated with a dash
if pre.length() == 0 {
// Check if the original version (without v prefix) contains letters mixed with numbers
// without proper dash separation
let mut has_letter_after_digit = false
let mut found_digit = false
for i = 0; i < core_part.length(); i = i + 1 {
let c = core_part[i]
if is_digit_code(c) {
found_digit = true
} else if c != '.'.to_int() {
if found_digit {
has_letter_after_digit = true
break
}
}
}
if has_letter_after_digit {
raise VersionError("malformed version: \{v}")
}
}
// Pad to at least 3 segments (MAJOR.MINOR.PATCH)
while segments.length() < 3 {
segments.push(0L)
}
Version::{ metadata, pre, segments, si: actual_segment_count, original: v }
}
///|
fn new_version(v : String) -> Version raise VersionError {
let trimmed = v.trim_space()
// Check for empty version
if trimmed.length() == 0 {
raise VersionError("malformed version: empty string")
}
// Fast path for simple numeric versions like "1.2.3" (most common case)
let trimmed_str = trimmed.to_string()
if try_fast_parse_simple_version(trimmed_str) {
return parse_simple_numeric_version(trimmed_str, v)
}
// Remove optional 'v' prefix
let without_v = match trimmed.strip_prefix("v") {
Some(stripped) => stripped.to_string()
None => trimmed.to_string()
}
// Basic validation: version should start with a digit
if without_v.length() == 0 || !is_digit_code(without_v[0]) {
raise VersionError("malformed version: \{v}")
}
// More permissive parsing like Go version
// Parse version similar to Go's regex: `v?([0-9]+(\.[0-9]+)*?)(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?`
// Split on + for metadata (only first occurrence)
let metadata_parts = without_v.split("+")
let mut version_part = ""
let mut metadata = ""
let mut part_count = 0
for part in metadata_parts {
if part_count == 0 {
version_part = part.to_string()
// Join remaining parts back (in case + appears in metadata)
} else if metadata.length() > 0 {
metadata = metadata + "+" + part.to_string()
} else {
metadata = part.to_string()
}
part_count = part_count + 1
}
// Now parse the version_part for numeric segments and prerelease
let mut core_part = ""
let mut pre = ""
// Find where the numeric part ends and prerelease begins
let mut i = 0
let mut found_non_digit_non_dot = false
while i < version_part.length() && !found_non_digit_non_dot {
let char_str = version_part.substring(start=i, end=i + 1)
match char_str {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "." =>
i = i + 1
"-" => {
// Found explicit prerelease separator
core_part = version_part.substring(start=0, end=i).to_string()
pre = version_part
.substring(start=i + 1, end=version_part.length())
.to_string()
found_non_digit_non_dot = true
}
_ => {
// Found alpha character without dash - this is an implicit prerelease like "1.2.3alpha1"
core_part = version_part.substring(start=0, end=i).to_string()
pre = version_part
.substring(start=i, end=version_part.length())
.to_string()
found_non_digit_non_dot = true
}
}
}
if !found_non_digit_non_dot {
// No prerelease found
core_part = version_part.to_string()
pre = ""
}
// Validate core part is not empty
if core_part.length() == 0 {
raise VersionError("malformed version: \{v}")
}
// Parse version segments - each must be a valid number
let segments_str = core_part.split(".")
let segments : Array[Int64] = []
let mut actual_segment_count = 0
for str in segments_str {
let str_string = str.to_string()
// Check for empty segments
if str_string.length() == 0 {
raise VersionError("malformed version: \{v}")
}
// Validate that segment contains only digits (no alpha characters)
if !is_all_digits(str_string) {
raise VersionError("malformed version: \{v}")
}
match try_parse_int(str_string) {
Some(val) => segments.push(val)
None => raise VersionError("malformed version: \{v}")
}
actual_segment_count = actual_segment_count + 1
}
// Pad to at least 3 segments (MAJOR.MINOR.PATCH)
while segments.length() < 3 {
segments.push(0L)
}
Version::{ metadata, pre, segments, si: actual_segment_count, original: v }
}
///|
/// Must is a helper that wraps a call to a function returning (Version, error)
/// and panics if error is non-nil.
pub fn must(v : Result[Version, VersionError]) -> Version {
match v {
Ok(version) => version
Err(e) => abort("Version parse error: \{e}")
}
}
///|
/// Must wrapper for Version::new
pub fn Version::must(v : String) -> Version {
Version::new(v) catch {
e => abort("Version parse error: \{e}")
}
}
///|
/// Must wrapper for Version::new_semver
pub fn Version::must_semver(v : String) -> Version {
Version::new_semver(v) catch {
e => abort("Version parse error: \{e}")
}
}
///|
/// Compare compares this version to another version. This
/// returns -1, 0, or 1 if this version is smaller, equal,
/// or larger than the other version, respectively.
pub fn Version::compare(self : Version, other : Version) -> Int {
// Quick equality check
if self.to_string() == other.to_string() {
return 0
}
// If the segments are the same, we must compare on prerelease info
if self.equal_segments(other) {
let pre_self = self.prerelease()
let pre_other = other.prerelease()
if pre_self == "" && pre_other == "" {
return 0
}
if pre_self == "" {
return 1
}
if pre_other == "" {
return -1
}
return compare_prereleases(pre_self, pre_other)
}
let segments_self = self.segments64()
let segments_other = other.segments64()
let len_self = segments_self.length()
let len_other = segments_other.length()
let hs = if len_self < len_other { len_other } else { len_self }
// Compare the segments
for i = 0; i < hs; i = i + 1 {
if i > len_self - 1 {
// Self had lower specificity
if !all_zero_from_index(segments_other, i) {
return -1
}
break
} else if i > len_other - 1 {
// Other had lower specificity
if !all_zero_from_index(segments_self, i) {
return 1
}
break
}
let lhs = segments_self[i]
let rhs = segments_other[i]
if lhs == rhs {
continue
} else if lhs < rhs {
return -1
}
return 1
}
0
}
///|
pub fn equal_segments(self : Version, other : Version) -> Bool {
let segments_self = self.segments64()
let segments_other = other.segments64()
if segments_self.length() != segments_other.length() {
return false
}
for i = 0; i < segments_self.length(); i = i + 1 {
if segments_self[i] != segments_other[i] {
return false
}
}
true
}
///|
fn all_zero_from_index(segs : Array[Int64], start_index : Int) -> Bool {
for i = start_index; i < segs.length(); i = i + 1 {
if segs[i] != 0L {
return false
}
}
true
}
///|
fn compare_part(pre_self : String, pre_other : String) -> Int {
if pre_self == pre_other {
return 0
}
let mut self_numeric = false
let mut other_numeric = false
let mut self_int = 0L
let mut other_int = 0L
// Parse pre_self as integer using try_parse_int helper
match try_parse_int(pre_self) {
Some(val) => {
self_numeric = true
self_int = val
}
None => self_numeric = false
}
// Parse pre_other as integer using try_parse_int helper
match try_parse_int(pre_other) {
Some(val) => {
other_numeric = true
other_int = val
}
None => other_numeric = false
}
// If a part is empty, we use the other to decide
if pre_self == "" {
return if other_numeric { -1 } else { 1 }
}
if pre_other == "" {
return if self_numeric { 1 } else { -1 }
}
if self_numeric && !other_numeric {
return -1
} else if !self_numeric && other_numeric {
return 1
} else if !self_numeric && !other_numeric {
return string_compare(pre_self, pre_other)
} else if self_int > other_int {
return 1
} else if self_int < other_int {
return -1
} else {
return 0
}
}
///|
fn compare_prereleases(v : String, other : String) -> Int {
// The same pre release!
if v == other {
return 0
}
// Split both pre releases to analyze their parts and convert directly to arrays
let self_parts : Array[String] = []
for part in v.split(".") {
self_parts.push(part.to_string())
}
let other_parts : Array[String] = []
for part in other.split(".") {
other_parts.push(part.to_string())
}
let self_pre_release_len = self_parts.length()
let other_pre_release_len = other_parts.length()
let biggest_len = if self_pre_release_len > other_pre_release_len {
self_pre_release_len
} else {
other_pre_release_len
}
// Loop for parts to find the first difference
for i = 0; i < biggest_len; i = i + 1 {
let part_self_pre = if i < self_pre_release_len {
self_parts[i]
} else {
""
}
let part_other_pre = if i < other_pre_release_len {
other_parts[i]
} else {
""
}
let compare_result = compare_part(part_self_pre, part_other_pre)
// If parts are equal, continue the loop
if compare_result != 0 {
return compare_result
}
}
0
}
///|
/// Core returns a new version constructed from only the MAJOR.MINOR.PATCH
/// segments of the version, without prerelease or metadata.
pub fn Version::core(self : Version) -> Version raise VersionError {
let segments = self.segments64()
let segments_only = "\{segments[0]}.\{segments[1]}.\{segments[2]}"
Version::new(segments_only)
}
///|
/// Equal tests if two versions are equal.
pub fn Version::equal(self : Version, other : Version) -> Bool {
self.compare(other) == 0
}
///|
/// GreaterThan tests if this version is greater than another version.
pub fn Version::greater_than(self : Version, other : Version) -> Bool {
self.compare(other) > 0
}
///|
/// GreaterThanOrEqual tests if this version is greater than or equal to another version.
pub fn Version::greater_than_or_equal(self : Version, other : Version) -> Bool {
self.compare(other) >= 0
}
///|
/// LessThan tests if this version is less than another version.
pub fn Version::less_than(self : Version, other : Version) -> Bool {
self.compare(other) < 0
}
///|
/// LessThanOrEqual tests if this version is less than or equal to another version.
pub fn Version::less_than_or_equal(self : Version, other : Version) -> Bool {
self.compare(other) <= 0
}
///|
/// Metadata returns any metadata that was part of the version string.
/// Metadata is anything that comes after the "+" in the version.
pub fn Version::metadata(self : Version) -> String {
self.metadata
}
///|
/// Prerelease returns any prerelease data that is part of the version.
/// Prerelease information is anything that comes after the "-" in the version.
pub fn Version::prerelease(self : Version) -> String {
self.pre
}
///|
/// Segments returns the numeric segments of the version as a slice of ints.
pub fn Version::segments(self : Version) -> Array[Int] {
let segment_slice : Array[Int] = []
for v in self.segments {
segment_slice.push(v.to_int())
}
segment_slice
}
///|
/// Segments64 returns the numeric segments of the version as a slice of int64s.
pub fn Version::segments64(self : Version) -> Array[Int64] {
let result : Array[Int64] = []
for seg in self.segments {
result.push(seg)
}
result
}
///|
/// String returns the full version string including pre-release and metadata information.
/// Optimized version using array-based concatenation for better performance
pub fn Version::to_string(self : Version) -> String {
// Use array-based approach for more efficient string building
let parts : Array[String] = []
// Add version segments
for i = 0; i < self.segments.length(); i = i + 1 {
if i > 0 {
parts.push(".")
}
parts.push(self.segments[i].to_string())
}
// Add prerelease if present
if self.pre != "" {
parts.push("-")
parts.push(self.pre)
}
// Add metadata if present
if self.metadata != "" {
parts.push("+")
parts.push(self.metadata)
}
// Join all parts efficiently
let mut result = ""
for part in parts {
result = result + part
}
result
}
///|
/// Original returns the original parsed version as-is
pub fn Version::original(self : Version) -> String {
self.original
}
///|
/// Marshal version to JSON string (equivalent to MarshalText)
pub fn Version::to_json(self : Version) -> String {
self.to_string()
}
///|
/// Unmarshal version from JSON string (equivalent to UnmarshalText)
pub fn Version::from_json(json_str : String) -> Version raise VersionError {
// Remove quotes if present (JSON strings are quoted)
let unquoted = if json_str.strip_prefix("\"") is Some(without_prefix) &&
without_prefix.strip_suffix("\"") is Some(without_suffix) {
without_suffix.to_string()
} else {
json_str
}
Version::new(unquoted)
}
///|
/// Convert version to a database-compatible string (equivalent to Value)
pub fn Version::to_db_string(self : Version) -> String {
self.to_string()
}
///|
/// Create version from a database-compatible value (equivalent to Scan)
pub fn Version::from_db_string(db_str : String) -> Version raise VersionError {
Version::new(db_str)
}
///|
/// Check if version is stable (no prerelease)
pub fn Version::is_stable(self : Version) -> Bool {
self.pre == ""
}
///|
/// Check if version is prerelease
pub fn Version::is_prerelease(self : Version) -> Bool {
self.pre != ""
}
///|
/// Get major version number (first segment)
pub fn Version::major(self : Version) -> Int64 {
if self.segments.length() > 0 {
self.segments[0]
} else {
0L
}
}
///|
/// Get minor version number (second segment)
pub fn Version::minor(self : Version) -> Int64 {
if self.segments.length() > 1 {
self.segments[1]
} else {
0L
}
}
///|
/// Get patch version number (third segment)
pub fn Version::patch(self : Version) -> Int64 {
if self.segments.length() > 2 {
self.segments[2]
} else {
0L
}
}
///|
/// Create a new version with incremented major version
pub fn Version::increment_major(self : Version) -> Version raise VersionError {
let new_version = "\{self.major() + 1L}.0.0"
Version::new(new_version)
}
///|
/// Create a new version with incremented minor version
pub fn Version::increment_minor(self : Version) -> Version raise VersionError {
let new_version = "\{self.major()}.\{self.minor() + 1L}.0"
Version::new(new_version)
}
///|
/// Create a new version with incremented patch version
pub fn Version::increment_patch(self : Version) -> Version raise VersionError {
let new_version = "\{self.major()}.\{self.minor()}.\{self.patch() + 1L}"
Version::new(new_version)
}