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