1use std::num::NonZeroU32;
4
5#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord, Hash)]
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
8pub struct Version {
9 pub major: u32,
10 pub minor: u32,
11 pub patch: u32,
12 pub build: Option<NonZeroU32>,
15}
16
17impl Version {
18 pub fn from_client_exe(version: &str) -> Version {
19 let parts: Vec<_> = version.split(",").collect();
20 assert!(parts.len() == 4);
21 Version {
22 major: parts[0].trim().parse::<u32>().unwrap(),
23 minor: parts[1].trim().parse::<u32>().unwrap(),
24 patch: parts[2].trim().parse::<u32>().unwrap(),
25 build: NonZeroU32::new(parts[3].trim().parse::<u32>().unwrap()),
26 }
27 }
28
29 pub fn try_from_client_exe(version: &str) -> Option<Version> {
33 let parts: Vec<_> = version.split(',').collect();
34 if parts.len() != 4 {
35 return None;
36 }
37 Some(Version {
38 major: parts[0].trim().parse::<u32>().ok()?,
39 minor: parts[1].trim().parse::<u32>().ok()?,
40 patch: parts[2].trim().parse::<u32>().ok()?,
41 build: NonZeroU32::new(parts[3].trim().parse::<u32>().ok()?),
42 })
43 }
44
45 pub fn build_number(&self) -> Option<u32> {
47 self.build.map(NonZeroU32::get)
48 }
49
50 #[cfg(feature = "parsing")]
65 pub fn from_account_def(xml: &str) -> Option<Version> {
66 let doc = roxmltree::Document::parse(xml).ok()?;
67 for node in doc.descendants() {
68 if let Some(rest) = node.tag_name().name().strip_prefix("curVersion_") {
69 let rest = rest.strip_prefix("Release_").or_else(|| rest.strip_prefix("release_")).unwrap_or(rest);
71 let parts: Vec<&str> = rest.split('_').collect();
72 let build_idx = match parts.len() {
74 4 => 3,
75 5 => 4,
76 _ => continue,
77 };
78 let (major, minor, patch) = Self::friendly_from_account_def_parts(
79 parts[0].parse().ok()?,
80 parts[1].parse().ok()?,
81 parts[2].parse().ok()?,
82 );
83 return Some(Version { major, minor, patch, build: NonZeroU32::new(parts[build_idx].parse().ok()?) });
84 }
85 }
86 None
87 }
88
89 #[cfg(feature = "parsing")]
96 fn friendly_from_account_def_parts(major: u32, minor: u32, patch: u32) -> (u32, u32, u32) {
97 if (1..=11).contains(&major) { (0, major, minor) } else { (major, minor, patch) }
98 }
99
100 pub fn to_path(&self) -> String {
101 format!("{}.{}.{}", self.major, self.minor, self.patch)
102 }
103
104 pub const fn base(major: u32, minor: u32, patch: u32) -> Version {
109 Version { major, minor, patch, build: None }
110 }
111
112 pub fn is_at_least(&self, other: &Version) -> bool {
113 if self.major > other.major {
114 true
115 } else if self.major < other.major {
116 false
117 } else if self.minor > other.minor {
118 true
119 } else if self.minor < other.minor {
120 false
121 } else {
122 self.patch >= other.patch
123 }
124 }
125}
126
127#[cfg(test)]
128mod test {
129 use super::*;
130
131 fn assert_older_newer(older: Version, newer: Version) {
132 assert!(newer.is_at_least(&older));
133 assert!(newer.is_at_least(&newer));
134 assert!(!older.is_at_least(&newer));
135 }
136
137 #[test]
138 fn different_patch() {
139 let older = Version::from_client_exe("0,10,9,0");
140 let newer = Version::from_client_exe("0,10,10,0");
141 assert_older_newer(older, newer);
142 }
143
144 #[test]
145 fn different_minor() {
146 let older = Version::from_client_exe("0,10,9,0");
147 let newer = Version::from_client_exe("0,11,0,0");
148 assert_older_newer(older, newer);
149 }
150
151 #[test]
152 fn different_major() {
153 let older = Version::from_client_exe("0,11,5,0");
154 let newer = Version::from_client_exe("1,0,0,0");
155 assert_older_newer(older, newer);
156 }
157
158 #[cfg(feature = "parsing")]
159 #[test]
160 fn from_account_def_parses_version() {
161 let xml = r#"<root><Properties><curVersion_15_1_0_11965230></curVersion_15_1_0_11965230></Properties></root>"#;
162 let v = Version::from_account_def(xml).unwrap();
163 assert_eq!(v.major, 15);
164 assert_eq!(v.minor, 1);
165 assert_eq!(v.patch, 0);
166 assert_eq!(v.build, NonZeroU32::new(11965230));
167 }
168
169 #[cfg(feature = "parsing")]
170 #[test]
171 fn from_account_def_release_prefix_normalizes_stripped_version() {
172 let xml = r#"<root><Properties><curVersion_release_11_4_0_5624555></curVersion_release_11_4_0_5624555></Properties></root>"#;
176 let v = Version::from_account_def(xml).unwrap();
177 assert_eq!((v.major, v.minor, v.patch), (0, 11, 4));
178 assert_eq!(v.build, NonZeroU32::new(5624555));
179 }
180
181 #[cfg(feature = "parsing")]
182 #[test]
183 fn from_account_def_stripped_versions_match_replay_header() {
184 for (tag, build, want) in [
186 ("curVersion_release_11_0_0_5045210", 5045210u32, (0, 11, 0)),
187 ("curVersion_release_7_11_0_1167524", 1167524, (0, 7, 11)),
188 ("curVersion_release_10_0_0_3343484", 3343484, (0, 10, 0)),
189 ] {
190 let xml = format!("<root><{tag}></{tag}></root>");
191 let v = Version::from_account_def(&xml).unwrap();
192 assert_eq!((v.major, v.minor, v.patch), want, "tag {tag}");
193 assert_eq!(v.build, NonZeroU32::new(build), "tag {tag}");
194 }
195 }
196
197 #[cfg(feature = "parsing")]
198 #[test]
199 fn from_account_def_friendly_versions_untouched() {
200 let xml = r#"<root><curVersion_0_11_8_0_6223574></curVersion_0_11_8_0_6223574></root>"#;
203 let v = Version::from_account_def(xml).unwrap();
204 assert_eq!((v.major, v.minor, v.patch), (0, 11, 8));
205 let xml = r#"<root><curVersion_12_0_0_6775398></curVersion_12_0_0_6775398></root>"#;
206 let v = Version::from_account_def(xml).unwrap();
207 assert_eq!((v.major, v.minor, v.patch), (12, 0, 0));
208 }
209
210 #[cfg(feature = "parsing")]
211 #[test]
212 fn from_account_def_legacy_5part_version() {
213 let xml = r#"<root><Properties><curVersion_Release_0_6_13_0_296659></curVersion_Release_0_6_13_0_296659></Properties></root>"#;
214 let v = Version::from_account_def(xml).unwrap();
215 assert_eq!(v.major, 0);
216 assert_eq!(v.minor, 6);
217 assert_eq!(v.patch, 13);
218 assert_eq!(v.build, NonZeroU32::new(296659));
219 }
220
221 #[cfg(feature = "parsing")]
222 #[test]
223 fn from_account_def_returns_none_on_missing() {
224 let xml = r#"<root><Properties><someOtherNode/></Properties></root>"#;
225 assert!(Version::from_account_def(xml).is_none());
226 }
227}