//! 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; use std::time::{SystemTime, UNIX_EPOCH}; /// 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. `time` is Unix milliseconds. pub fn hello_line(version: &str, latest: &str, url: &str, time: u64) -> String { format!("HELLO {version} latest={latest} url={url} time={time}") } #[derive(Debug, Clone, PartialEq, Eq)] pub struct HelloReply { pub version: String, pub latest: String, pub url: String, /// Server clock at the moment the reply was built, Unix milliseconds. pub time: u64, } /// Parse `HELLO latest= url= time=`. 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; let mut time = 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()); } else if let Some(value) = token.strip_prefix("time=") { time = value.parse().ok(); } } Some(HelloReply { version, latest: latest?, url: url?, time: time?, }) } /// The reply to `PING`: `PONG `. pub fn pong_line(time: u64) -> String { format!("PONG {time}") } /// Parse a `PONG` reply's server time. pub fn parse_pong(line: &str) -> Option { line.trim().strip_prefix("PONG ")?.trim().parse().ok() } /// Current Unix time in milliseconds. Used for handshake timestamps and crash /// anchors; a system clock before the epoch reads as zero. pub fn unix_time_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64) .unwrap_or(0) } /// 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()) }