use std::fmt;
pub const MIN_NODE_VERSION: (u16, u16, u16) = (0, 4, 15);
pub fn get_min_node_version() -> PeerVersion {
if let Ok(env_version) = std::env::var("ANT_MIN_NODE_VERSION")
&& let Some(version) = PeerVersion::parse_semver(&env_version)
{
return version;
}
PeerVersion::new(MIN_NODE_VERSION.0, MIN_NODE_VERSION.1, MIN_NODE_VERSION.2)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PeerVersion {
pub major: u16,
pub minor: u16,
pub patch: u16,
}
impl PeerVersion {
pub fn new(major: u16, minor: u16, patch: u16) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn parse_from_agent_string(agent: &str) -> Option<Self> {
let parts: Vec<&str> = agent.split('/').collect();
if parts.len() < 5 {
return None;
}
if parts[0] != "ant" {
return None;
}
Self::parse_semver(parts[3])
}
pub fn parse_semver(version_str: &str) -> Option<Self> {
let version_core = version_str.split('-').next()?;
let parts: Vec<&str> = version_core.split('.').collect();
if parts.len() < 3 {
return None;
}
let major = parts[0].parse::<u16>().ok()?;
let minor = parts[1].parse::<u16>().ok()?;
let patch = parts[2].parse::<u16>().ok()?;
Some(Self {
major,
minor,
patch,
})
}
pub fn meets_minimum(&self, min_version: &PeerVersion) -> bool {
(self.major, self.minor, self.patch)
>= (min_version.major, min_version.minor, min_version.patch)
}
}
impl fmt::Display for PeerVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionCheckResult {
Accepted {
version: PeerVersion,
},
Rejected {
detected: PeerVersion,
minimum: PeerVersion,
},
Legacy,
ParseError {
agent_string: String,
},
}
impl VersionCheckResult {
pub fn is_allowed(&self, allow_legacy: bool) -> bool {
match self {
VersionCheckResult::Accepted { .. } => true,
VersionCheckResult::Legacy => allow_legacy,
VersionCheckResult::Rejected { .. } | VersionCheckResult::ParseError { .. } => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PeerType {
Node,
Client,
ReachabilityCheckClient,
Unknown,
}
impl PeerType {
pub fn from_agent_string(agent: &str) -> Self {
if agent.contains("/node/") {
PeerType::Node
} else if agent.contains("reachability-check-peer") {
PeerType::ReachabilityCheckClient
} else if agent.contains("/client/") || agent.contains("client") {
PeerType::Client
} else {
PeerType::Unknown
}
}
}
impl fmt::Display for PeerType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PeerType::Node => write!(f, "Node"),
PeerType::Client => write!(f, "Client"),
PeerType::ReachabilityCheckClient => write!(f, "ReachabilityCheckClient"),
PeerType::Unknown => write!(f, "Unknown"),
}
}
}
pub fn check_peer_version(
agent_string: &str,
min_version: Option<&PeerVersion>,
) -> VersionCheckResult {
let Some(min_version) = min_version else {
if let Some(version) = PeerVersion::parse_from_agent_string(agent_string) {
return VersionCheckResult::Accepted { version };
}
return VersionCheckResult::Legacy;
};
match PeerVersion::parse_from_agent_string(agent_string) {
Some(version) => {
if version.meets_minimum(min_version) {
VersionCheckResult::Accepted { version }
} else {
VersionCheckResult::Rejected {
detected: version,
minimum: *min_version,
}
}
}
None => {
if agent_string.starts_with("ant/") {
VersionCheckResult::Legacy
} else {
VersionCheckResult::ParseError {
agent_string: agent_string.to_string(),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_node_version() {
let agent = "ant/node/1.0/0.4.13/1";
let version = PeerVersion::parse_from_agent_string(agent).unwrap();
assert_eq!(version, PeerVersion::new(0, 4, 13));
}
#[test]
fn test_parse_client_version() {
let agent = "ant/client/1.0/0.9.0/1";
let version = PeerVersion::parse_from_agent_string(agent).unwrap();
assert_eq!(version, PeerVersion::new(0, 9, 0));
}
#[test]
fn test_parse_version_with_prerelease() {
let version = PeerVersion::parse_semver("0.4.13-alpha.1").unwrap();
assert_eq!(version, PeerVersion::new(0, 4, 13));
}
#[test]
fn test_parse_legacy_agent() {
let agent = "ant/1.0/1";
assert!(PeerVersion::parse_from_agent_string(agent).is_none());
}
#[test]
fn test_parse_invalid_agent() {
let agent = "some-other-agent/1.0.0";
assert!(PeerVersion::parse_from_agent_string(agent).is_none());
}
#[test]
fn test_version_comparison() {
let v1 = PeerVersion::new(0, 4, 10);
let v2 = PeerVersion::new(0, 4, 13);
let v3 = PeerVersion::new(0, 5, 0);
let v4 = PeerVersion::new(1, 0, 0);
assert!(v2.meets_minimum(&v1)); assert!(!v1.meets_minimum(&v2)); assert!(v3.meets_minimum(&v2)); assert!(v4.meets_minimum(&v3)); }
#[test]
fn test_version_display() {
let version = PeerVersion::new(0, 4, 13);
assert_eq!(version.to_string(), "0.4.13");
}
#[test]
fn test_check_peer_version_accepted() {
let agent = "ant/node/1.0/0.4.13/1";
let min = PeerVersion::new(0, 4, 10);
let result = check_peer_version(agent, Some(&min));
assert!(
matches!(result, VersionCheckResult::Accepted { version } if version == PeerVersion::new(0, 4, 13))
);
}
#[test]
fn test_check_peer_version_rejected() {
let agent = "ant/node/1.0/0.4.9/1";
let min = PeerVersion::new(0, 4, 10);
let result = check_peer_version(agent, Some(&min));
assert!(
matches!(result, VersionCheckResult::Rejected { detected, minimum }
if detected == PeerVersion::new(0, 4, 9) && minimum == PeerVersion::new(0, 4, 10))
);
}
#[test]
fn test_check_peer_version_legacy() {
let agent = "ant/1.0/1"; let min = PeerVersion::new(0, 4, 10);
let result = check_peer_version(agent, Some(&min));
assert!(matches!(result, VersionCheckResult::Legacy));
}
#[test]
fn test_check_peer_version_no_minimum() {
let agent = "ant/node/1.0/0.4.13/1";
let result = check_peer_version(agent, None);
assert!(matches!(result, VersionCheckResult::Accepted { .. }));
}
#[test]
fn test_peer_type_detection() {
assert_eq!(
PeerType::from_agent_string("ant/node/1.0/0.4.13/1"),
PeerType::Node
);
assert_eq!(
PeerType::from_agent_string("ant/client/1.0/0.9.0/1"),
PeerType::Client
);
assert_eq!(
PeerType::from_agent_string("ant/reachability-check-peer/1.0/0.1.0/1"),
PeerType::ReachabilityCheckClient
);
assert_eq!(
PeerType::from_agent_string("unknown-agent"),
PeerType::Unknown
);
}
#[test]
fn test_is_allowed() {
let accepted = VersionCheckResult::Accepted {
version: PeerVersion::new(0, 4, 13),
};
assert!(accepted.is_allowed(true));
assert!(accepted.is_allowed(false));
let legacy = VersionCheckResult::Legacy;
assert!(legacy.is_allowed(true));
assert!(!legacy.is_allowed(false));
let rejected = VersionCheckResult::Rejected {
detected: PeerVersion::new(0, 4, 9),
minimum: PeerVersion::new(0, 4, 10),
};
assert!(!rejected.is_allowed(true));
assert!(!rejected.is_allowed(false));
}
#[test]
fn test_get_min_node_version() {
let min_version = get_min_node_version();
assert_eq!(
min_version,
PeerVersion::new(MIN_NODE_VERSION.0, MIN_NODE_VERSION.1, MIN_NODE_VERSION.2)
);
}
#[test]
fn test_min_version_constant() {
let (major, minor, patch) = MIN_NODE_VERSION;
assert_eq!(major, 0);
assert_eq!(minor, 4);
assert_eq!(patch, 15);
}
#[test]
fn test_parse_rc_version_semver() {
let version = PeerVersion::parse_semver("0.4.14-rc.1").unwrap();
assert_eq!(version, PeerVersion::new(0, 4, 14));
let version = PeerVersion::parse_semver("0.4.14-rc.2").unwrap();
assert_eq!(version, PeerVersion::new(0, 4, 14));
let version = PeerVersion::parse_semver("1.0.0-rc.1").unwrap();
assert_eq!(version, PeerVersion::new(1, 0, 0));
}
#[test]
fn test_parse_rc_version_from_agent_string() {
let agent = "ant/node/1.0/0.4.14-rc.1/1";
let version = PeerVersion::parse_from_agent_string(agent).unwrap();
assert_eq!(version, PeerVersion::new(0, 4, 14));
let agent = "ant/node/1.0/0.5.0-rc.3/1";
let version = PeerVersion::parse_from_agent_string(agent).unwrap();
assert_eq!(version, PeerVersion::new(0, 5, 0));
}
#[test]
fn test_rc_version_comparison() {
let min_version = PeerVersion::new(0, 4, 10);
let rc_version = PeerVersion::parse_semver("0.4.14-rc.1").unwrap();
assert!(rc_version.meets_minimum(&min_version));
let old_rc_version = PeerVersion::parse_semver("0.4.9-rc.1").unwrap();
assert!(!old_rc_version.meets_minimum(&min_version));
let exact_rc_version = PeerVersion::parse_semver("0.4.10-rc.1").unwrap();
assert!(exact_rc_version.meets_minimum(&min_version));
}
#[test]
fn test_check_peer_version_with_rc_accepted() {
let agent = "ant/node/1.0/0.4.14-rc.1/1";
let min = PeerVersion::new(0, 4, 10);
let result = check_peer_version(agent, Some(&min));
assert!(
matches!(result, VersionCheckResult::Accepted { version } if version == PeerVersion::new(0, 4, 14))
);
}
#[test]
fn test_check_peer_version_with_rc_rejected() {
let agent = "ant/node/1.0/0.4.9-rc.2/1";
let min = PeerVersion::new(0, 4, 10);
let result = check_peer_version(agent, Some(&min));
assert!(
matches!(result, VersionCheckResult::Rejected { detected, minimum }
if detected == PeerVersion::new(0, 4, 9) && minimum == PeerVersion::new(0, 4, 10))
);
}
#[test]
fn test_various_prerelease_formats() {
assert_eq!(
PeerVersion::parse_semver("0.4.14-rc.1").unwrap(),
PeerVersion::new(0, 4, 14)
);
assert_eq!(
PeerVersion::parse_semver("0.4.14-alpha.1").unwrap(),
PeerVersion::new(0, 4, 14)
);
assert_eq!(
PeerVersion::parse_semver("0.4.14-beta.2").unwrap(),
PeerVersion::new(0, 4, 14)
);
assert_eq!(
PeerVersion::parse_semver("0.4.14-dev").unwrap(),
PeerVersion::new(0, 4, 14)
);
assert_eq!(
PeerVersion::parse_semver("0.4.14-SNAPSHOT").unwrap(),
PeerVersion::new(0, 4, 14)
);
assert_eq!(
PeerVersion::parse_semver("0.4.14-rc.1-build.123").unwrap(),
PeerVersion::new(0, 4, 14)
);
}
}