use versions::Versioning;
pub(crate) fn normalize_version(version: &str) -> String {
if version.is_empty() {
return String::new();
}
let start = version
.find(|c: char| c.is_ascii_digit())
.unwrap_or(version.len());
let trimmed = &version[start..];
if trimmed.is_empty() {
return String::new();
}
let mut result = String::with_capacity(trimmed.len());
let mut last_was_dot = false;
for c in trimmed.chars() {
if c.is_ascii_alphanumeric() {
last_was_dot = false;
result.push(c);
} else {
if !last_was_dot && !result.is_empty() {
result.push('.');
last_was_dot = true;
}
}
}
if result.ends_with('.') {
result.pop();
}
result
}
pub(crate) fn is_update_available_with_date(
installed_version: &str,
available_version: &str,
installed_date: &str,
available_date: &str,
) -> bool {
if !installed_version.is_empty()
&& !available_version.is_empty()
&& installed_version == available_version
{
return is_date_newer(installed_date, available_date);
}
let inst_orig = Versioning::new(installed_version);
let avail_orig = Versioning::new(available_version);
if let (Some(inst), Some(avail)) = (&inst_orig, &avail_orig) {
if inst < avail {
return true;
}
if inst == avail {
return is_date_newer(installed_date, available_date);
}
return false;
}
let inst_norm = normalize_version(installed_version);
let avail_norm = normalize_version(available_version);
if !inst_norm.is_empty() && !avail_norm.is_empty() && inst_norm == avail_norm {
return is_date_newer(installed_date, available_date);
}
let inst_parsed = Versioning::new(&inst_norm);
let avail_parsed = Versioning::new(&avail_norm);
if let (Some(inst), Some(avail)) = (&inst_parsed, &avail_parsed) {
if inst < avail {
return true;
}
if inst == avail {
return is_date_newer(installed_date, available_date);
}
return false;
}
if avail_parsed.is_some() && inst_parsed.is_none() {
return true;
}
is_date_newer(installed_date, available_date)
}
fn is_date_newer(installed_date: &str, available_date: &str) -> bool {
if installed_date.is_empty() || available_date.is_empty() {
return false;
}
let local_date = installed_date.get(..10).unwrap_or(installed_date);
let store_date = available_date.get(..10).unwrap_or(available_date);
store_date > local_date
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_strips_v_prefix() {
assert_eq!(normalize_version("v1.2.3"), "1.2.3");
}
#[test]
fn normalize_strips_non_numeric_prefix() {
assert_eq!(normalize_version("Version 2.0"), "2.0");
}
#[test]
fn normalize_preserves_semver() {
assert_eq!(normalize_version("1.2.3"), "1.2.3");
}
#[test]
fn normalize_handles_date_format() {
assert_eq!(normalize_version("2024.01.15"), "2024.01.15");
}
#[test]
fn normalize_collapses_dots() {
assert_eq!(normalize_version("1..2...3"), "1.2.3");
}
#[test]
fn normalize_replaces_non_numeric_separators() {
assert_eq!(normalize_version("1.2.3-beta1"), "1.2.3.beta1");
}
#[test]
fn normalize_empty() {
assert_eq!(normalize_version(""), "");
}
#[test]
fn normalized_versions_detect_update() {
assert!(is_update_available_with_date("v1.0", "v2.0", "", ""));
}
#[test]
fn normalized_versions_no_update_when_equal() {
assert!(!is_update_available_with_date("v1.0.0", "v1.0.0", "", ""));
}
#[test]
fn date_comparison_does_not_panic_on_non_ascii() {
assert!(is_date_newer("2024-é1-01", "2025-01-01"));
assert!(is_date_newer("2024-01-01", "2025-ñ1-01"));
assert!(!is_date_newer("ëëëëëëëëëë", "2025-01-01"));
}
#[test]
fn unparseable_versions_fall_back_to_date_comparison() {
assert!(!is_update_available_with_date("!@#", "***", "2025-06-01", "2024-01-01"));
assert!(is_update_available_with_date("!@#", "***", "2024-01-01", "2025-06-01"));
assert!(!is_update_available_with_date("!@#", "***", "", ""));
}
#[test]
fn prerelease_detected_as_update_to_release() {
assert!(is_update_available_with_date("1.2.3-beta1", "1.2.3", "", ""));
}
#[test]
fn release_not_downgraded_to_prerelease() {
assert!(!is_update_available_with_date("1.2.3", "1.2.3-beta1", "", ""));
}
#[test]
fn v_prefix_still_works_after_normalization_fallback() {
assert!(is_update_available_with_date("v1.0", "v2.0", "", ""));
assert!(!is_update_available_with_date("v2.0", "v1.0", "", ""));
}
}