1use std::fmt;
31
32pub const MIN_NODE_VERSION: (u16, u16, u16) = (0, 4, 15);
39
40pub fn get_min_node_version() -> PeerVersion {
44 if let Ok(env_version) = std::env::var("ANT_MIN_NODE_VERSION")
45 && let Some(version) = PeerVersion::parse_semver(&env_version)
46 {
47 return version;
48 }
49 PeerVersion::new(MIN_NODE_VERSION.0, MIN_NODE_VERSION.1, MIN_NODE_VERSION.2)
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub struct PeerVersion {
55 pub major: u16,
57 pub minor: u16,
59 pub patch: u16,
61}
62
63impl PeerVersion {
64 pub fn new(major: u16, minor: u16, patch: u16) -> Self {
66 Self {
67 major,
68 minor,
69 patch,
70 }
71 }
72
73 pub fn parse_from_agent_string(agent: &str) -> Option<Self> {
83 let parts: Vec<&str> = agent.split('/').collect();
84
85 if parts.len() < 5 {
87 return None;
88 }
89
90 if parts[0] != "ant" {
92 return None;
93 }
94
95 Self::parse_semver(parts[3])
97 }
98
99 pub fn parse_semver(version_str: &str) -> Option<Self> {
101 let version_core = version_str.split('-').next()?;
103
104 let parts: Vec<&str> = version_core.split('.').collect();
105 if parts.len() < 3 {
106 return None;
107 }
108
109 let major = parts[0].parse::<u16>().ok()?;
110 let minor = parts[1].parse::<u16>().ok()?;
111 let patch = parts[2].parse::<u16>().ok()?;
112
113 Some(Self {
114 major,
115 minor,
116 patch,
117 })
118 }
119
120 pub fn meets_minimum(&self, min_version: &PeerVersion) -> bool {
124 (self.major, self.minor, self.patch)
125 >= (min_version.major, min_version.minor, min_version.patch)
126 }
127}
128
129impl fmt::Display for PeerVersion {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum VersionCheckResult {
138 Accepted {
140 version: PeerVersion,
142 },
143 Rejected {
145 detected: PeerVersion,
147 minimum: PeerVersion,
149 },
150 Legacy,
152 ParseError {
154 agent_string: String,
156 },
157}
158
159impl VersionCheckResult {
160 pub fn is_allowed(&self, allow_legacy: bool) -> bool {
162 match self {
163 VersionCheckResult::Accepted { .. } => true,
164 VersionCheckResult::Legacy => allow_legacy,
165 VersionCheckResult::Rejected { .. } | VersionCheckResult::ParseError { .. } => false,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub enum PeerType {
173 Node,
175 Client,
177 ReachabilityCheckClient,
179 Unknown,
181}
182
183impl PeerType {
184 pub fn from_agent_string(agent: &str) -> Self {
186 if agent.contains("/node/") {
187 PeerType::Node
188 } else if agent.contains("reachability-check-peer") {
189 PeerType::ReachabilityCheckClient
190 } else if agent.contains("/client/") || agent.contains("client") {
191 PeerType::Client
192 } else {
193 PeerType::Unknown
194 }
195 }
196}
197
198impl fmt::Display for PeerType {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match self {
202 PeerType::Node => write!(f, "Node"),
203 PeerType::Client => write!(f, "Client"),
204 PeerType::ReachabilityCheckClient => write!(f, "ReachabilityCheckClient"),
205 PeerType::Unknown => write!(f, "Unknown"),
206 }
207 }
208}
209
210pub fn check_peer_version(
219 agent_string: &str,
220 min_version: Option<&PeerVersion>,
221) -> VersionCheckResult {
222 let Some(min_version) = min_version else {
224 if let Some(version) = PeerVersion::parse_from_agent_string(agent_string) {
226 return VersionCheckResult::Accepted { version };
227 }
228 return VersionCheckResult::Legacy;
229 };
230
231 match PeerVersion::parse_from_agent_string(agent_string) {
233 Some(version) => {
234 if version.meets_minimum(min_version) {
235 VersionCheckResult::Accepted { version }
236 } else {
237 VersionCheckResult::Rejected {
238 detected: version,
239 minimum: *min_version,
240 }
241 }
242 }
243 None => {
244 if agent_string.starts_with("ant/") {
246 VersionCheckResult::Legacy
249 } else {
250 VersionCheckResult::ParseError {
251 agent_string: agent_string.to_string(),
252 }
253 }
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_parse_node_version() {
264 let agent = "ant/node/1.0/0.4.13/1";
265 let version = PeerVersion::parse_from_agent_string(agent).unwrap();
266 assert_eq!(version, PeerVersion::new(0, 4, 13));
267 }
268
269 #[test]
270 fn test_parse_client_version() {
271 let agent = "ant/client/1.0/0.9.0/1";
272 let version = PeerVersion::parse_from_agent_string(agent).unwrap();
273 assert_eq!(version, PeerVersion::new(0, 9, 0));
274 }
275
276 #[test]
277 fn test_parse_version_with_prerelease() {
278 let version = PeerVersion::parse_semver("0.4.13-alpha.1").unwrap();
279 assert_eq!(version, PeerVersion::new(0, 4, 13));
280 }
281
282 #[test]
283 fn test_parse_legacy_agent() {
284 let agent = "ant/1.0/1";
286 assert!(PeerVersion::parse_from_agent_string(agent).is_none());
287 }
288
289 #[test]
290 fn test_parse_invalid_agent() {
291 let agent = "some-other-agent/1.0.0";
292 assert!(PeerVersion::parse_from_agent_string(agent).is_none());
293 }
294
295 #[test]
296 fn test_version_comparison() {
297 let v1 = PeerVersion::new(0, 4, 10);
298 let v2 = PeerVersion::new(0, 4, 13);
299 let v3 = PeerVersion::new(0, 5, 0);
300 let v4 = PeerVersion::new(1, 0, 0);
301
302 assert!(v2.meets_minimum(&v1)); assert!(!v1.meets_minimum(&v2)); assert!(v3.meets_minimum(&v2)); assert!(v4.meets_minimum(&v3)); }
307
308 #[test]
309 fn test_version_display() {
310 let version = PeerVersion::new(0, 4, 13);
311 assert_eq!(version.to_string(), "0.4.13");
312 }
313
314 #[test]
315 fn test_check_peer_version_accepted() {
316 let agent = "ant/node/1.0/0.4.13/1";
317 let min = PeerVersion::new(0, 4, 10);
318 let result = check_peer_version(agent, Some(&min));
319 assert!(
320 matches!(result, VersionCheckResult::Accepted { version } if version == PeerVersion::new(0, 4, 13))
321 );
322 }
323
324 #[test]
325 fn test_check_peer_version_rejected() {
326 let agent = "ant/node/1.0/0.4.9/1";
327 let min = PeerVersion::new(0, 4, 10);
328 let result = check_peer_version(agent, Some(&min));
329 assert!(
330 matches!(result, VersionCheckResult::Rejected { detected, minimum }
331 if detected == PeerVersion::new(0, 4, 9) && minimum == PeerVersion::new(0, 4, 10))
332 );
333 }
334
335 #[test]
336 fn test_check_peer_version_legacy() {
337 let agent = "ant/1.0/1"; let min = PeerVersion::new(0, 4, 10);
339 let result = check_peer_version(agent, Some(&min));
340 assert!(matches!(result, VersionCheckResult::Legacy));
341 }
342
343 #[test]
344 fn test_check_peer_version_no_minimum() {
345 let agent = "ant/node/1.0/0.4.13/1";
346 let result = check_peer_version(agent, None);
347 assert!(matches!(result, VersionCheckResult::Accepted { .. }));
348 }
349
350 #[test]
351 fn test_peer_type_detection() {
352 assert_eq!(
353 PeerType::from_agent_string("ant/node/1.0/0.4.13/1"),
354 PeerType::Node
355 );
356 assert_eq!(
357 PeerType::from_agent_string("ant/client/1.0/0.9.0/1"),
358 PeerType::Client
359 );
360 assert_eq!(
361 PeerType::from_agent_string("ant/reachability-check-peer/1.0/0.1.0/1"),
362 PeerType::ReachabilityCheckClient
363 );
364 assert_eq!(
365 PeerType::from_agent_string("unknown-agent"),
366 PeerType::Unknown
367 );
368 }
369
370 #[test]
371 fn test_is_allowed() {
372 let accepted = VersionCheckResult::Accepted {
373 version: PeerVersion::new(0, 4, 13),
374 };
375 assert!(accepted.is_allowed(true));
376 assert!(accepted.is_allowed(false));
377
378 let legacy = VersionCheckResult::Legacy;
379 assert!(legacy.is_allowed(true));
380 assert!(!legacy.is_allowed(false));
381
382 let rejected = VersionCheckResult::Rejected {
383 detected: PeerVersion::new(0, 4, 9),
384 minimum: PeerVersion::new(0, 4, 10),
385 };
386 assert!(!rejected.is_allowed(true));
387 assert!(!rejected.is_allowed(false));
388 }
389
390 #[test]
391 fn test_get_min_node_version() {
392 let min_version = get_min_node_version();
394 assert_eq!(
395 min_version,
396 PeerVersion::new(MIN_NODE_VERSION.0, MIN_NODE_VERSION.1, MIN_NODE_VERSION.2)
397 );
398 }
399
400 #[test]
401 fn test_min_version_constant() {
402 let (major, minor, patch) = MIN_NODE_VERSION;
404 assert_eq!(major, 0);
405 assert_eq!(minor, 4);
406 assert_eq!(patch, 15);
407 }
408
409 #[test]
412 fn test_parse_rc_version_semver() {
413 let version = PeerVersion::parse_semver("0.4.14-rc.1").unwrap();
415 assert_eq!(version, PeerVersion::new(0, 4, 14));
416
417 let version = PeerVersion::parse_semver("0.4.14-rc.2").unwrap();
418 assert_eq!(version, PeerVersion::new(0, 4, 14));
419
420 let version = PeerVersion::parse_semver("1.0.0-rc.1").unwrap();
421 assert_eq!(version, PeerVersion::new(1, 0, 0));
422 }
423
424 #[test]
425 fn test_parse_rc_version_from_agent_string() {
426 let agent = "ant/node/1.0/0.4.14-rc.1/1";
428 let version = PeerVersion::parse_from_agent_string(agent).unwrap();
429 assert_eq!(version, PeerVersion::new(0, 4, 14));
430
431 let agent = "ant/node/1.0/0.5.0-rc.3/1";
432 let version = PeerVersion::parse_from_agent_string(agent).unwrap();
433 assert_eq!(version, PeerVersion::new(0, 5, 0));
434 }
435
436 #[test]
437 fn test_rc_version_comparison() {
438 let min_version = PeerVersion::new(0, 4, 10);
440
441 let rc_version = PeerVersion::parse_semver("0.4.14-rc.1").unwrap();
443 assert!(rc_version.meets_minimum(&min_version));
444
445 let old_rc_version = PeerVersion::parse_semver("0.4.9-rc.1").unwrap();
447 assert!(!old_rc_version.meets_minimum(&min_version));
448
449 let exact_rc_version = PeerVersion::parse_semver("0.4.10-rc.1").unwrap();
451 assert!(exact_rc_version.meets_minimum(&min_version));
452 }
453
454 #[test]
455 fn test_check_peer_version_with_rc_accepted() {
456 let agent = "ant/node/1.0/0.4.14-rc.1/1";
458 let min = PeerVersion::new(0, 4, 10);
459 let result = check_peer_version(agent, Some(&min));
460
461 assert!(
463 matches!(result, VersionCheckResult::Accepted { version } if version == PeerVersion::new(0, 4, 14))
464 );
465 }
466
467 #[test]
468 fn test_check_peer_version_with_rc_rejected() {
469 let agent = "ant/node/1.0/0.4.9-rc.2/1";
471 let min = PeerVersion::new(0, 4, 10);
472 let result = check_peer_version(agent, Some(&min));
473
474 assert!(
476 matches!(result, VersionCheckResult::Rejected { detected, minimum }
477 if detected == PeerVersion::new(0, 4, 9) && minimum == PeerVersion::new(0, 4, 10))
478 );
479 }
480
481 #[test]
482 fn test_various_prerelease_formats() {
483 assert_eq!(
485 PeerVersion::parse_semver("0.4.14-rc.1").unwrap(),
486 PeerVersion::new(0, 4, 14)
487 );
488 assert_eq!(
489 PeerVersion::parse_semver("0.4.14-alpha.1").unwrap(),
490 PeerVersion::new(0, 4, 14)
491 );
492 assert_eq!(
493 PeerVersion::parse_semver("0.4.14-beta.2").unwrap(),
494 PeerVersion::new(0, 4, 14)
495 );
496 assert_eq!(
497 PeerVersion::parse_semver("0.4.14-dev").unwrap(),
498 PeerVersion::new(0, 4, 14)
499 );
500 assert_eq!(
501 PeerVersion::parse_semver("0.4.14-SNAPSHOT").unwrap(),
502 PeerVersion::new(0, 4, 14)
503 );
504 assert_eq!(
506 PeerVersion::parse_semver("0.4.14-rc.1-build.123").unwrap(),
507 PeerVersion::new(0, 4, 14)
508 );
509 }
510}