//! Version compatibility and the `HELLO` handshake. //! //! Client and server must share MAJOR.MINOR; only the patch component may //! differ. The first line after TLS is `HELLO `, and nothing else is //! accepted until the server has answered it. use std::env; /// Version this build reports. The client and the server share it. pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Where players download new releases when the server advertises one and the /// operator has not set `DOWNLOAD_URL`. pub const DEFAULT_DOWNLOAD_URL: &str = "https://zagan.2137697.xyz/"; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Version { pub major: u64, pub minor: u64, pub patch: u64, } impl Version { /// Parse `MAJOR.MINOR.PATCH`, tolerating a leading `v` and a pre-release or /// build suffix (`4.0.0-rc1`, `4.0.0+build`). pub fn parse(raw: &str) -> Option { let raw = raw.trim(); let raw = raw.strip_prefix('v').unwrap_or(raw); // Drop a pre-release or build suffix, but not an empty one. let (core, suffix) = match raw.find(['-', '+']) { Some(index) => (&raw[..index], Some(&raw[index + 1..])), None => (raw, None), }; if suffix == Some("") { return None; } let mut parts = core.split('.'); let major = parts.next()?.parse().ok()?; let minor = parts.next()?.parse().ok()?; let patch = parts.next()?.parse().ok()?; if parts.next().is_some() { return None; } Some(Self { major, minor, patch, }) } pub fn compatible_with(self, other: Self) -> bool { self.major == other.major && self.minor == other.minor } } /// True when both versions parse and share MAJOR.MINOR. pub fn compatible(client: &str, server: &str) -> bool { match (Version::parse(client), Version::parse(server)) { (Some(client), Some(server)) => client.compatible_with(server), _ => false, } } /// True when `latest` is strictly newer than `current`. Unparseable versions /// are never "newer", so a broken reply cannot nag the player. pub fn is_newer(latest: &str, current: &str) -> bool { match (Version::parse(latest), Version::parse(current)) { (Some(latest), Some(current)) => latest > current, _ => false, } } /// The server line that accepts a handshake. pub fn hello_line(version: &str, latest: &str, url: &str) -> String { format!("HELLO {version} latest={latest} url={url}") } #[derive(Debug, Clone, PartialEq, Eq)] pub struct HelloReply { pub version: String, pub latest: String, pub url: String, } /// Parse `HELLO latest= url=`. pub fn parse_hello(line: &str) -> Option { let rest = line.trim().strip_prefix("HELLO ")?; let mut tokens = rest.split_whitespace(); let version = tokens.next()?.to_string(); let mut latest = None; let mut url = None; for token in tokens { if let Some(value) = token.strip_prefix("latest=") { latest = Some(value.to_string()); } else if let Some(value) = token.strip_prefix("url=") { url = Some(value.to_string()); } } Some(HelloReply { version, latest: latest?, url: url?, }) } /// Download page advertised by the server. `DOWNLOAD_URL` wins when set. pub fn download_url() -> String { env::var("DOWNLOAD_URL") .ok() .filter(|value| !value.trim().is_empty()) .unwrap_or_else(|| DEFAULT_DOWNLOAD_URL.to_string()) }