manabrew_server/
client_build.rs1use crate::protocol::{ClientPlatform, EngineGate};
11
12const MAX_VERSION_CHARS: usize = 32;
15
16const MIN_STATE_PATCH_VERSION: Version = Version(3, 17, 0);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23struct Version(u32, u32, u32);
24
25impl Version {
26 fn parse(raw: &str) -> Option<Self> {
29 let core = raw.split(['-', '+']).next()?;
30 let mut parts = core.split('.');
31 let mut next = || parts.next()?.parse::<u32>().ok();
32 let version = Self(next()?, next()?, next()?);
33 parts.next().is_none().then_some(version)
34 }
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct ClientBuild {
40 pub platform: ClientPlatform,
41 pub engine_gate: EngineGate,
42 version: Option<Version>,
43 raw_version: Option<String>,
44}
45
46impl ClientBuild {
47 pub fn new(
48 platform: ClientPlatform,
49 raw_version: Option<String>,
50 engine_gate: EngineGate,
51 ) -> Self {
52 let raw_version = raw_version.map(|raw| {
53 if raw.chars().count() > MAX_VERSION_CHARS {
54 raw.chars().take(MAX_VERSION_CHARS).collect()
55 } else {
56 raw
57 }
58 });
59 Self {
60 platform,
61 engine_gate,
62 version: raw_version.as_deref().and_then(Version::parse),
63 raw_version,
64 }
65 }
66
67 pub fn version(&self) -> Option<&str> {
70 self.raw_version.as_deref()
71 }
72
73 pub fn applies_state_patches(&self) -> bool {
76 self.version
77 .is_some_and(|version| version >= MIN_STATE_PATCH_VERSION)
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 fn build(version: &str) -> ClientBuild {
86 ClientBuild::new(
87 ClientPlatform::Desktop,
88 Some(version.to_string()),
89 EngineGate::Unknown,
90 )
91 }
92
93 #[test]
94 fn parses_plain_releases() {
95 assert_eq!(Version::parse("3.17.0"), Some(Version(3, 17, 0)));
96 assert_eq!(Version::parse("3.17.3-rc.1"), Some(Version(3, 17, 3)));
97 assert_eq!(Version::parse("3.17.3+build.9"), Some(Version(3, 17, 3)));
98 }
99
100 #[test]
101 fn rejects_anything_else() {
102 for raw in ["3.17", "3.17.0.1", "v3.17.0", "", "latest", "3.x.0"] {
103 assert_eq!(Version::parse(raw), None, "{raw}");
104 }
105 }
106
107 #[test]
108 fn orders_by_component_not_lexically() {
109 assert!(Version(3, 9, 6) < Version(3, 17, 0));
110 assert!(Version(3, 17, 0) < Version(3, 17, 3));
111 assert!(Version(4, 0, 0) > Version(3, 17, 3));
112 }
113
114 #[test]
115 fn state_patches_need_the_applier_release() {
116 assert!(!build("3.16.0").applies_state_patches());
117 assert!(!build("3.9.6").applies_state_patches());
118 assert!(build("3.17.0").applies_state_patches());
119 assert!(build("3.17.3").applies_state_patches());
120 assert!(build("4.0.0").applies_state_patches());
121 }
122
123 #[test]
124 fn unknown_clients_get_full_states() {
125 assert!(!ClientBuild::default().applies_state_patches());
126 assert!(!build("who knows").applies_state_patches());
127 assert!(
128 !ClientBuild::new(ClientPlatform::Web, None, EngineGate::Unknown)
129 .applies_state_patches()
130 );
131 }
132
133 #[test]
134 fn caps_the_reported_string() {
135 let build = ClientBuild::new(
136 ClientPlatform::Web,
137 Some("9".repeat(500)),
138 EngineGate::Unknown,
139 );
140 assert_eq!(build.version().map(str::len), Some(MAX_VERSION_CHARS));
141 assert!(!build.applies_state_patches());
142 }
143}