atomcode_core/
version_check.rs1use crate::self_update::{Manifest, MANIFEST_URL};
10
11pub fn parse_and_compare(current: &str, body: &str) -> Option<String> {
19 let manifest: Manifest = serde_json::from_str(body).ok()?;
20 let latest = parse_version_line(&manifest.version)?;
21 let current = parse_version_line(current)?;
22 if latest > current {
23 Some(format_version(latest))
24 } else {
25 None
26 }
27}
28
29fn parse_version_line(s: &str) -> Option<(u64, u64, u64)> {
30 let trimmed = s.trim();
31 if trimmed.len() > 32 {
32 return None;
33 }
34 let rest = trimmed.strip_prefix('v')?;
35 let mut parts = rest.split('.');
36 let major = parts.next()?.parse::<u64>().ok()?;
37 let minor = parts.next()?.parse::<u64>().ok()?;
38 let patch = parts.next()?.parse::<u64>().ok()?;
39 if parts.next().is_some() {
40 return None;
41 }
42 Some((major, minor, patch))
43}
44
45fn format_version(v: (u64, u64, u64)) -> String {
46 format!("v{}.{}.{}", v.0, v.1, v.2)
47}
48
49pub async fn check_latest(current: &str) -> Option<String> {
53 let client = reqwest::Client::builder()
54 .timeout(std::time::Duration::from_secs(5))
55 .user_agent(crate::ATOMCODE_USER_AGENT)
56 .build()
57 .ok()?;
58 let resp = client.get(MANIFEST_URL).send().await.ok()?;
59 if !resp.status().is_success() {
60 return None;
61 }
62 let body = resp.text().await.ok()?;
63 parse_and_compare(current, &body)
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn manifest_body(version: &str) -> String {
71 format!(
72 r#"{{"version":"{}","binaries":{{"darwin-arm64":{{"sha256":"abcd","size":1024}}}}}}"#,
73 version
74 )
75 }
76
77 #[test]
78 fn newer_patch_version_returns_some() {
79 assert_eq!(
80 parse_and_compare("v4.15.3", &manifest_body("v4.15.4")),
81 Some("v4.15.4".to_string())
82 );
83 }
84
85 #[test]
86 fn newer_minor_version_returns_some() {
87 assert_eq!(
88 parse_and_compare("v4.15.3", &manifest_body("v4.16.0")),
89 Some("v4.16.0".to_string())
90 );
91 }
92
93 #[test]
94 fn newer_major_version_returns_some() {
95 assert_eq!(
96 parse_and_compare("v4.15.3", &manifest_body("v5.0.0")),
97 Some("v5.0.0".to_string())
98 );
99 }
100
101 #[test]
102 fn same_version_returns_none() {
103 assert_eq!(
104 parse_and_compare("v4.15.3", &manifest_body("v4.15.3")),
105 None
106 );
107 }
108
109 #[test]
110 fn older_version_returns_none() {
111 assert_eq!(
112 parse_and_compare("v4.15.3", &manifest_body("v4.15.2")),
113 None
114 );
115 assert_eq!(
116 parse_and_compare("v4.15.3", &manifest_body("v4.14.99")),
117 None
118 );
119 assert_eq!(
120 parse_and_compare("v4.15.3", &manifest_body("v3.99.99")),
121 None
122 );
123 }
124
125 #[test]
126 fn html_body_returns_none() {
127 assert_eq!(parse_and_compare("v4.15.3", "<html>404</html>"), None);
128 }
129
130 #[test]
131 fn empty_body_returns_none() {
132 assert_eq!(parse_and_compare("v4.15.3", ""), None);
133 assert_eq!(parse_and_compare("v4.15.3", " \n"), None);
134 }
135
136 #[test]
137 fn missing_v_prefix_returns_none() {
138 assert_eq!(parse_and_compare("v4.15.3", &manifest_body("4.15.4")), None);
139 }
140
141 #[test]
142 fn non_numeric_segment_returns_none() {
143 assert_eq!(
144 parse_and_compare("v4.15.3", &manifest_body("v4.15.x")),
145 None
146 );
147 assert_eq!(parse_and_compare("v4.15.3", &manifest_body("vX.Y.Z")), None);
148 }
149
150 #[test]
151 fn too_many_components_returns_none() {
152 assert_eq!(
153 parse_and_compare("v4.15.3", &manifest_body("v4.15.4.1")),
154 None
155 );
156 }
157
158 #[test]
159 fn too_few_components_returns_none() {
160 assert_eq!(parse_and_compare("v4.15.3", &manifest_body("v4.15")), None);
161 }
162
163 #[test]
164 fn missing_version_field_returns_none() {
165 let body = r#"{"binaries":{"darwin-arm64":{"sha256":"abcd","size":1024}}}"#;
166 assert_eq!(parse_and_compare("v4.15.3", body), None);
167 }
168
169 #[test]
170 fn missing_binaries_field_returns_none() {
171 let body = r#"{"version":"v4.16.0"}"#;
173 assert_eq!(parse_and_compare("v4.15.3", body), None);
174 }
175
176 #[test]
177 fn trims_whitespace_in_version() {
178 let body = r#"{"version":" v4.16.0\r\n","binaries":{"darwin-arm64":{"sha256":"abcd","size":1024}}}"#;
179 assert_eq!(
180 parse_and_compare("v4.15.3", body),
181 Some("v4.16.0".to_string())
182 );
183 }
184
185 #[test]
186 fn malformed_current_returns_none() {
187 assert_eq!(parse_and_compare("bad", &manifest_body("v4.16.0")), None);
188 }
189
190 #[test]
191 fn ignores_unknown_json_fields() {
192 let body = r#"{
193 "version": "v4.16.0",
194 "released_at": "2026-04-20T00:00:00Z",
195 "signature": "future-field",
196 "binaries": {"darwin-arm64": {"sha256": "abcd", "size": 1024}}
197 }"#;
198 assert_eq!(
199 parse_and_compare("v4.15.3", body),
200 Some("v4.16.0".to_string())
201 );
202 }
203}