1#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord, Hash)]
4#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
5#[cfg_attr(feature = "rkyv", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]
6pub struct Version {
7 pub major: u32,
8 pub minor: u32,
9 pub patch: u32,
10 pub build: u32,
11}
12
13impl Version {
14 pub fn from_client_exe(version: &str) -> Version {
15 let parts: Vec<_> = version.split(",").collect();
16 assert!(parts.len() == 4);
17 Version {
18 major: parts[0].trim().parse::<u32>().unwrap(),
19 minor: parts[1].trim().parse::<u32>().unwrap(),
20 patch: parts[2].trim().parse::<u32>().unwrap(),
21 build: parts[3].trim().parse::<u32>().unwrap(),
22 }
23 }
24
25 pub fn try_from_client_exe(version: &str) -> Option<Version> {
29 let parts: Vec<_> = version.split(',').collect();
30 if parts.len() != 4 {
31 return None;
32 }
33 Some(Version {
34 major: parts[0].trim().parse::<u32>().ok()?,
35 minor: parts[1].trim().parse::<u32>().ok()?,
36 patch: parts[2].trim().parse::<u32>().ok()?,
37 build: parts[3].trim().parse::<u32>().ok()?,
38 })
39 }
40
41 #[cfg(feature = "parsing")]
50 pub fn from_account_def(xml: &str) -> Option<Version> {
51 let doc = roxmltree::Document::parse(xml).ok()?;
52 for node in doc.descendants() {
53 if let Some(rest) = node.tag_name().name().strip_prefix("curVersion_") {
54 let rest = rest.strip_prefix("Release_").or_else(|| rest.strip_prefix("release_")).unwrap_or(rest);
56 let parts: Vec<&str> = rest.split('_').collect();
57 match parts.len() {
58 4 => {
60 return Some(Version {
61 major: parts[0].parse().ok()?,
62 minor: parts[1].parse().ok()?,
63 patch: parts[2].parse().ok()?,
64 build: parts[3].parse().ok()?,
65 });
66 }
67 5 => {
69 return Some(Version {
70 major: parts[0].parse().ok()?,
71 minor: parts[1].parse().ok()?,
72 patch: parts[2].parse().ok()?,
73 build: parts[4].parse().ok()?,
74 });
75 }
76 _ => {}
77 }
78 }
79 }
80 None
81 }
82
83 pub fn to_path(&self) -> String {
84 format!("{}.{}.{}", self.major, self.minor, self.patch)
85 }
86
87 pub const fn base(major: u32, minor: u32, patch: u32) -> Version {
92 Version { major, minor, patch, build: 0 }
93 }
94
95 pub fn is_at_least(&self, other: &Version) -> bool {
96 if self.major > other.major {
97 true
98 } else if self.major < other.major {
99 false
100 } else if self.minor > other.minor {
101 true
102 } else if self.minor < other.minor {
103 false
104 } else {
105 self.patch >= other.patch
106 }
107 }
108}
109
110#[cfg(test)]
111mod test {
112 use super::*;
113
114 fn assert_older_newer(older: Version, newer: Version) {
115 assert!(newer.is_at_least(&older));
116 assert!(newer.is_at_least(&newer));
117 assert!(!older.is_at_least(&newer));
118 }
119
120 #[test]
121 fn different_patch() {
122 let older = Version::from_client_exe("0,10,9,0");
123 let newer = Version::from_client_exe("0,10,10,0");
124 assert_older_newer(older, newer);
125 }
126
127 #[test]
128 fn different_minor() {
129 let older = Version::from_client_exe("0,10,9,0");
130 let newer = Version::from_client_exe("0,11,0,0");
131 assert_older_newer(older, newer);
132 }
133
134 #[test]
135 fn different_major() {
136 let older = Version::from_client_exe("0,11,5,0");
137 let newer = Version::from_client_exe("1,0,0,0");
138 assert_older_newer(older, newer);
139 }
140
141 #[cfg(feature = "parsing")]
142 #[test]
143 fn from_account_def_parses_version() {
144 let xml = r#"<root><Properties><curVersion_15_1_0_11965230></curVersion_15_1_0_11965230></Properties></root>"#;
145 let v = Version::from_account_def(xml).unwrap();
146 assert_eq!(v.major, 15);
147 assert_eq!(v.minor, 1);
148 assert_eq!(v.patch, 0);
149 assert_eq!(v.build, 11965230);
150 }
151
152 #[cfg(feature = "parsing")]
153 #[test]
154 fn from_account_def_with_release_prefix() {
155 let xml = r#"<root><Properties><curVersion_release_11_4_0_5624555></curVersion_release_11_4_0_5624555></Properties></root>"#;
156 let v = Version::from_account_def(xml).unwrap();
157 assert_eq!(v.major, 11);
158 assert_eq!(v.minor, 4);
159 assert_eq!(v.patch, 0);
160 assert_eq!(v.build, 5624555);
161 }
162
163 #[cfg(feature = "parsing")]
164 #[test]
165 fn from_account_def_legacy_5part_version() {
166 let xml = r#"<root><Properties><curVersion_Release_0_6_13_0_296659></curVersion_Release_0_6_13_0_296659></Properties></root>"#;
167 let v = Version::from_account_def(xml).unwrap();
168 assert_eq!(v.major, 0);
169 assert_eq!(v.minor, 6);
170 assert_eq!(v.patch, 13);
171 assert_eq!(v.build, 296659);
172 }
173
174 #[cfg(feature = "parsing")]
175 #[test]
176 fn from_account_def_returns_none_on_missing() {
177 let xml = r#"<root><Properties><someOtherNode/></Properties></root>"#;
178 assert!(Version::from_account_def(xml).is_none());
179 }
180}