utils/flexver: add flexver comparator
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I79b8d3745a8754619f810de1bac8b66f6a6a6964
This commit is contained in:
parent
83343bc3dd
commit
1c08e00ccf
2 changed files with 328 additions and 0 deletions
326
src/utils/flexver.rs
Normal file
326
src/utils/flexver.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
// FlexVer - Flexible Version Comparison
|
||||
//
|
||||
// This implementation is based on the original implementation of the
|
||||
// `flexver-rs` crate, which no longer appears to be maintained.
|
||||
//
|
||||
// See:
|
||||
// <https://git.sleeping.town/exa/FlexVer/src/branch/trunk/rust/src/lib.rs>
|
||||
//
|
||||
// This implementation provides semver-like version comparison with support for:
|
||||
//
|
||||
// - Flexible version string parsing (not strict semver)
|
||||
// - Pre-release handling (parts starting with `-`)
|
||||
// - Build metadata stripping (parts after `+`)
|
||||
// - Numerical vs lexical comparison
|
||||
|
||||
use std::{
|
||||
cmp::Ordering::{self, Equal, Greater, Less},
|
||||
collections::VecDeque,
|
||||
};
|
||||
|
||||
/// Type of version component for sorting purposes
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SortingType {
|
||||
/// A numeric component with both i64 value and string representation
|
||||
Numerical(i64, String),
|
||||
|
||||
/// A lexical string component
|
||||
Lexical(String),
|
||||
|
||||
/// A semver pre-release component (starting with `-`)
|
||||
SemverPrerelease(String),
|
||||
}
|
||||
|
||||
impl SortingType {
|
||||
fn into_string(self) -> String {
|
||||
match self {
|
||||
Self::Numerical(_, s) | Self::Lexical(s) | Self::SemverPrerelease(s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_semver_prerelease(s: &str) -> bool {
|
||||
s.len() > 1 && s.starts_with('-')
|
||||
}
|
||||
|
||||
/// Decompose a version string into its component parts
|
||||
fn decompose(str_in: &str) -> VecDeque<SortingType> {
|
||||
if str_in.is_empty() {
|
||||
return VecDeque::new();
|
||||
}
|
||||
|
||||
// Strip build metadata (after `+`)
|
||||
let s = if let Some((left, _)) = str_in.split_once('+') {
|
||||
left
|
||||
} else {
|
||||
str_in
|
||||
};
|
||||
|
||||
let mut out: VecDeque<SortingType> = VecDeque::new();
|
||||
let mut current = String::new();
|
||||
|
||||
let mut currently_numeric = s.starts_with(|c: char| c.is_ascii_digit());
|
||||
let mut skip = s.starts_with('-');
|
||||
|
||||
fn handle_split(
|
||||
current: &str,
|
||||
c: Option<&char>,
|
||||
currently_numeric: bool,
|
||||
) -> Option<SortingType> {
|
||||
let numeric = if let Some(c) = c {
|
||||
c.is_ascii_digit()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
use SortingType::*;
|
||||
|
||||
if currently_numeric {
|
||||
if numeric {
|
||||
return None;
|
||||
} else {
|
||||
return Some(Numerical(
|
||||
current.parse::<i64>().unwrap(),
|
||||
current.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !(numeric || c == Some(&'-') || c.is_none()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_semver_prerelease(current) {
|
||||
if c == Some(&'-') {
|
||||
// Pre-releases can have multiple dashes
|
||||
None
|
||||
} else {
|
||||
Some(SemverPrerelease(current.to_owned()))
|
||||
}
|
||||
} else {
|
||||
Some(Lexical(current.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
for c in s.chars() {
|
||||
if let Some(part) = handle_split(¤t, Some(&c), currently_numeric) {
|
||||
if skip {
|
||||
skip = false;
|
||||
} else {
|
||||
out.push_back(part);
|
||||
current.clear();
|
||||
currently_numeric = c.is_ascii_digit();
|
||||
}
|
||||
}
|
||||
current.push(c);
|
||||
}
|
||||
|
||||
if let Some(part) = handle_split(¤t, None, currently_numeric) {
|
||||
out.push_back(part);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Compare two version strings using FlexVer rules.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ordering::Less` if `a` < `b`
|
||||
/// - `Ordering::Equal` if `a` == `b`
|
||||
/// - `Ordering::Greater` if `a` > `b`
|
||||
///
|
||||
/// This matches the behavior of flexver-java:
|
||||
/// - "1.0.0" > "1.0.0-beta" (release > pre-release)
|
||||
/// - "1.0.0-beta" < "1.0.0+build123" (pre-release < build metadata)
|
||||
pub fn compare(left: &str, right: &str) -> Ordering {
|
||||
let iter = VersionComparisonIterator {
|
||||
left: decompose(left),
|
||||
right: decompose(right),
|
||||
};
|
||||
|
||||
for next in iter {
|
||||
use SortingType::*;
|
||||
|
||||
let current = match next {
|
||||
// Left ran out first
|
||||
(Some(l), None) => {
|
||||
if let SemverPrerelease(_) = l {
|
||||
Less
|
||||
} else {
|
||||
Greater
|
||||
}
|
||||
},
|
||||
// Right ran out first
|
||||
(None, Some(r)) => {
|
||||
if let SemverPrerelease(_) = r {
|
||||
Greater
|
||||
} else {
|
||||
Less
|
||||
}
|
||||
},
|
||||
// Both have components
|
||||
(Some(l), Some(r)) => {
|
||||
match (l, r) {
|
||||
(Numerical(l, _), Numerical(r, _)) => l.cmp(&r),
|
||||
(l, r) => l.into_string().cmp(&r.into_string()),
|
||||
}
|
||||
},
|
||||
(None, None) => unreachable!(),
|
||||
};
|
||||
|
||||
if current != Equal {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
Equal
|
||||
}
|
||||
|
||||
/// Version comparison iterator that yields pairs of components
|
||||
#[derive(Debug)]
|
||||
struct VersionComparisonIterator {
|
||||
left: VecDeque<SortingType>,
|
||||
right: VecDeque<SortingType>,
|
||||
}
|
||||
|
||||
impl Iterator for VersionComparisonIterator {
|
||||
type Item = (Option<SortingType>, Option<SortingType>);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = (self.left.pop_front(), self.right.pop_front());
|
||||
if let (None, None) = item {
|
||||
None
|
||||
} else {
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FlexVer type for use with standard library traits
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct FlexVer<'a>(pub &'a str);
|
||||
|
||||
impl PartialEq for FlexVer<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
compare(self.0, other.0) == Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FlexVer<'_> {}
|
||||
|
||||
impl PartialOrd for FlexVer<'_> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for FlexVer<'_> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
compare(self.0, other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn cmp(a: &str, b: &str) -> Ordering {
|
||||
compare(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_release_comparison() {
|
||||
assert_eq!(cmp("1.0.0", "1.0.0"), Ordering::Equal);
|
||||
assert_eq!(cmp("1.0.0", "1.0.1"), Ordering::Less);
|
||||
assert_eq!(cmp("1.0.1", "1.0.0"), Ordering::Greater);
|
||||
assert_eq!(cmp("1.0.0", "1.1.0"), Ordering::Less);
|
||||
assert_eq!(cmp("1.1.0", "1.0.0"), Ordering::Greater);
|
||||
assert_eq!(cmp("2.0.0", "1.9.9"), Ordering::Greater);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prerelease_comparison() {
|
||||
// Release > pre-release
|
||||
assert_eq!(cmp("1.0.0", "1.0.0-beta"), Ordering::Greater);
|
||||
assert_eq!(cmp("1.0.0-beta", "1.0.0"), Ordering::Less);
|
||||
|
||||
// Pre-release with tilde
|
||||
assert_eq!(cmp("1.0.0~1", "1.0.0~2"), Ordering::Less);
|
||||
assert_eq!(cmp("1.0.0~2", "1.0.0~1"), Ordering::Greater);
|
||||
assert_eq!(cmp("1.0.0~1", "1.0.0~1"), Ordering::Equal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prerelease_with_tilde_vs_alpha() {
|
||||
// In FlexVer, "~" is not treated as a pre-release marker
|
||||
// Only "-" followed by text marks a pre-release
|
||||
// So "1.0.0~1" is compared lexicographically vs "1.0.0-beta"
|
||||
// Since '~' (ASCII 126) > '-' (ASCII 45), "~1" > "-beta"
|
||||
assert_eq!(cmp("1.0.0~1", "1.0.0-beta"), Ordering::Greater);
|
||||
assert_eq!(cmp("1.0.0-beta", "1.0.0~1"), Ordering::Less);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_metadata() {
|
||||
// Build metadata with + is stripped for comparison
|
||||
assert_eq!(cmp("1.0.0+build", "1.0.0"), Ordering::Equal);
|
||||
assert_eq!(cmp("1.0.0+build", "1.0.0-alpha"), Ordering::Greater);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_file_extensions() {
|
||||
// File extensions should be handled by string comparison
|
||||
assert_eq!(cmp("mod-1.0.0.jar", "mod-1.0.0.jar"), Ordering::Equal);
|
||||
assert!(cmp("mod-1.0.0.jar", "mod-1.0.1.jar").is_lt());
|
||||
assert!(cmp("mod-1.0.1.jar", "mod-1.0.0.jar").is_gt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complex_versions() {
|
||||
// Simple version comparison
|
||||
assert!(cmp("sodium-1.0.0", "sodium-1.0.1").is_lt());
|
||||
assert!(cmp("sodium-1.0.1", "sodium-1.0.0").is_gt());
|
||||
|
||||
// File extensions are NOT stripped - they're part of the version string
|
||||
// "sodium-1.0.0.jar" < "sodium-1.0.0~1.jar" because '.' (48) < '~' (126)
|
||||
assert!(cmp("sodium-1.0.0.jar", "sodium-1.0.0~1.jar").is_lt());
|
||||
assert!(cmp("sodium-1.0.0~1.jar", "sodium-1.0.0.jar").is_gt());
|
||||
|
||||
assert!(cmp("fabric-0.15.0.1", "fabric-0.15.0.2").is_lt());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_min_max() {
|
||||
assert_eq!(FlexVer("1.0.0").min(FlexVer("1.0.0")), FlexVer("1.0.0"));
|
||||
assert_eq!(FlexVer("a1.2.6").min(FlexVer("b1.7.3")), FlexVer("a1.2.6"));
|
||||
assert_eq!(FlexVer("b1.7.3").max(FlexVer("a1.2.6")), FlexVer("b1.7.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commutative() {
|
||||
// If a > b, then b < a
|
||||
let pairs = vec![
|
||||
("1.0.0", "1.0.1"),
|
||||
("1.0.0-beta", "1.0.0"),
|
||||
("1.0.0~1", "1.0.0~2"),
|
||||
];
|
||||
|
||||
for (a, b) in pairs {
|
||||
let ordering = compare(a, b);
|
||||
let inverse = match ordering {
|
||||
Ordering::Less => Ordering::Greater,
|
||||
Ordering::Greater => Ordering::Less,
|
||||
Ordering::Equal => Ordering::Equal,
|
||||
};
|
||||
assert_eq!(
|
||||
compare(b, a),
|
||||
inverse,
|
||||
"Commutativity violation: {} vs {}",
|
||||
a,
|
||||
b
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
pub mod flexver;
|
||||
pub mod hash;
|
||||
pub mod id;
|
||||
|
||||
pub use flexver::FlexVer;
|
||||
pub use hash::verify_hash;
|
||||
pub use id::generate_pakku_id;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue