1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
//! 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 <version>`, 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<Self> {
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 <version> latest=<version> url=<url>`.
pub fn parse_hello(line: &str) -> Option<HelloReply> {
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())
}
|