summaryrefslogtreecommitdiffstats
path: root/src/version.rs
blob: c1dcee5d796e99400743cc98fe3e32929adb179c (plain) (blame)
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())
}