use crate::self_update::{Manifest, MANIFEST_URL};
pub fn parse_and_compare(current: &str, body: &str) -> Option<String> {
let manifest: Manifest = serde_json::from_str(body).ok()?;
let latest = parse_version_line(&manifest.version)?;
let current = parse_version_line(current)?;
if latest > current {
Some(format_version(latest))
} else {
None
}
}
fn parse_version_line(s: &str) -> Option<(u64, u64, u64)> {
let trimmed = s.trim();
if trimmed.len() > 32 {
return None;
}
let rest = trimmed.strip_prefix('v')?;
let mut parts = rest.split('.');
let major = parts.next()?.parse::<u64>().ok()?;
let minor = parts.next()?.parse::<u64>().ok()?;
let patch = parts.next()?.parse::<u64>().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}
fn format_version(v: (u64, u64, u64)) -> String {
format!("v{}.{}.{}", v.0, v.1, v.2)
}
pub async fn check_latest(current: &str) -> Option<String> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.user_agent(crate::ATOMCODE_USER_AGENT)
.build()
.ok()?;
let resp = client.get(MANIFEST_URL).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let body = resp.text().await.ok()?;
parse_and_compare(current, &body)
}
#[cfg(test)]
mod tests {
use super::*;
fn manifest_body(version: &str) -> String {
format!(
r#"{{"version":"{}","binaries":{{"darwin-arm64":{{"sha256":"abcd","size":1024}}}}}}"#,
version
)
}
#[test]
fn newer_patch_version_returns_some() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.15.4")),
Some("v4.15.4".to_string())
);
}
#[test]
fn newer_minor_version_returns_some() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.16.0")),
Some("v4.16.0".to_string())
);
}
#[test]
fn newer_major_version_returns_some() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v5.0.0")),
Some("v5.0.0".to_string())
);
}
#[test]
fn same_version_returns_none() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.15.3")),
None
);
}
#[test]
fn older_version_returns_none() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.15.2")),
None
);
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.14.99")),
None
);
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v3.99.99")),
None
);
}
#[test]
fn html_body_returns_none() {
assert_eq!(parse_and_compare("v4.15.3", "<html>404</html>"), None);
}
#[test]
fn empty_body_returns_none() {
assert_eq!(parse_and_compare("v4.15.3", ""), None);
assert_eq!(parse_and_compare("v4.15.3", " \n"), None);
}
#[test]
fn missing_v_prefix_returns_none() {
assert_eq!(parse_and_compare("v4.15.3", &manifest_body("4.15.4")), None);
}
#[test]
fn non_numeric_segment_returns_none() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.15.x")),
None
);
assert_eq!(parse_and_compare("v4.15.3", &manifest_body("vX.Y.Z")), None);
}
#[test]
fn too_many_components_returns_none() {
assert_eq!(
parse_and_compare("v4.15.3", &manifest_body("v4.15.4.1")),
None
);
}
#[test]
fn too_few_components_returns_none() {
assert_eq!(parse_and_compare("v4.15.3", &manifest_body("v4.15")), None);
}
#[test]
fn missing_version_field_returns_none() {
let body = r#"{"binaries":{"darwin-arm64":{"sha256":"abcd","size":1024}}}"#;
assert_eq!(parse_and_compare("v4.15.3", body), None);
}
#[test]
fn missing_binaries_field_returns_none() {
let body = r#"{"version":"v4.16.0"}"#;
assert_eq!(parse_and_compare("v4.15.3", body), None);
}
#[test]
fn trims_whitespace_in_version() {
let body = r#"{"version":" v4.16.0\r\n","binaries":{"darwin-arm64":{"sha256":"abcd","size":1024}}}"#;
assert_eq!(
parse_and_compare("v4.15.3", body),
Some("v4.16.0".to_string())
);
}
#[test]
fn malformed_current_returns_none() {
assert_eq!(parse_and_compare("bad", &manifest_body("v4.16.0")), None);
}
#[test]
fn ignores_unknown_json_fields() {
let body = r#"{
"version": "v4.16.0",
"released_at": "2026-04-20T00:00:00Z",
"signature": "future-field",
"binaries": {"darwin-arm64": {"sha256": "abcd", "size": 1024}}
}"#;
assert_eq!(
parse_and_compare("v4.15.3", body),
Some("v4.16.0".to_string())
);
}
}