commonlibsse_ng\rel\version/mod.rs
1// C++ Original code
2// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/REL/Version.h
3// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
4// SPDX-License-Identifier: MIT
5//
6// SPDX-FileCopyrightText: (C) 2025 SARDONYX
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9mod win_api;
10
11pub use win_api::{FileVersionError, get_file_version};
12
13/// Represents a 4-part version number.
14///
15/// In binding, [`Copy`] is inherited, but it is omitted to avoid implicit copying in for loops, etc.
16///
17/// # Example
18/// ```
19/// use commonlibsse_ng::rel::version::Version;
20///
21/// let ver = Version::new(1, 6, 1170, 0);
22/// assert_eq!(ver.major(), 1);
23/// assert_eq!(ver.to_string(), "1.6.1170.0");
24/// ```
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct Version {
27 /// Internal representation of the version as a 4-element array.
28 ///
29 /// - Index 0: Major version
30 /// - Index 1: Minor version
31 /// - Index 2: Patch version
32 /// - Index 3: Build number
33 ///
34 /// This field is private, and access should be done through provided methods.
35 _impl: [u16; 4],
36}
37
38impl Version {
39 /// Create a empty version.
40 ///
41 /// # Examples
42 /// ```
43 /// use commonlibsse_ng::rel::version::Version;
44 ///
45 /// assert_eq!(Version::default_const(), Version::new(0, 0, 0, 0));
46 /// ```
47 #[inline]
48 pub const fn default_const() -> Self {
49 Self::new(0, 0, 0, 0)
50 }
51
52 /// Creates a new `Version` from four components.
53 ///
54 /// # Example
55 /// ```
56 /// use commonlibsse_ng::rel::version::Version;
57 ///
58 /// let ver = Version::new(1, 2, 3, 4);
59 /// assert_eq!(ver.major(), 1);
60 /// ```
61 #[inline]
62 pub const fn new(major: u16, minor: u16, patch: u16, build: u16) -> Self {
63 Self { _impl: [major, minor, patch, build] }
64 }
65
66 /// Parses a version string at compile time.
67 ///
68 /// # Panics
69 /// Errors are made under the following conditions.
70 ///
71 /// - When there is no number after a point.
72 /// - If there are more than 4 numbers.
73 /// - When there is a non-numeric character (other than a dot).
74 ///
75 /// # Examples
76 /// ```rust
77 /// use commonlibsse_ng::rel::version::{Version, VersionParseError};
78 ///
79 /// assert_eq!(Version::from_str_const("1.2.3"), Version::new(1, 2, 3, 0));
80 ///
81 /// // Panics
82 /// // assert_eq!(Version::from_str_const_("1.2.3.4.5")); // Too many numbers
83 /// // assert_eq!(Version::from_str_const("1.2.f.4.5")); // Invalid char `f`
84 /// // assert_eq!(Version::from_str_const("1.2.")); // Missing number
85 /// ```
86 #[inline]
87 pub const fn from_str_const(version: &str) -> Self {
88 let mut parts = [0_u16; 4];
89 let mut idx = 0;
90 let mut num = 0;
91 let mut has_digit = false;
92
93 let bytes = version.as_bytes();
94 let len = bytes.len();
95 let mut i = 0;
96 while i < len {
97 let b = bytes[i];
98 if b == b'.' {
99 if idx >= 4 {
100 panic!("Expected at most 4 parts, but got too many parts.");
101 }
102 parts[idx] = num;
103
104 num = 0;
105 idx += 1;
106 has_digit = false;
107 } else if b.is_ascii_digit() {
108 num = num * 10 + (b - b'0') as u16;
109 has_digit = true;
110 } else {
111 panic!("Expected a number but got invalid character.");
112 }
113 i += 1;
114 }
115
116 if has_digit {
117 if idx >= 4 {
118 panic!("Expected at most 4 parts, but got too many parts.");
119 }
120 parts[idx] = num;
121 } else {
122 panic!("Expected numbers after the dots, but got none in parts");
123 }
124
125 Self { _impl: parts }
126 }
127
128 /// Returns the major version component.
129 ///
130 /// # Examples
131 /// ```
132 /// use commonlibsse_ng::rel::version::Version;
133 ///
134 /// let v = Version::new(1, 2, 3, 4);
135 /// assert_eq!(v.major(), 1);
136 /// ```
137 #[inline]
138 pub const fn major(&self) -> u16 {
139 self._impl[0]
140 }
141
142 /// Returns the minor version component.
143 ///
144 /// # Examples
145 /// ```
146 /// use commonlibsse_ng::rel::version::Version;
147 ///
148 /// let v = Version::new(1, 2, 3, 4);
149 /// assert_eq!(v.minor(), 2);
150 /// ```
151 #[inline]
152 pub const fn minor(&self) -> u16 {
153 self._impl[1]
154 }
155
156 /// Returns the patch version component.
157 ///
158 /// # Examples
159 /// ```
160 /// use commonlibsse_ng::rel::version::Version;
161 ///
162 /// let v = Version::new(1, 2, 3, 4);
163 /// assert_eq!(v.patch(), 3);
164 /// ```
165 #[inline]
166 pub const fn patch(&self) -> u16 {
167 self._impl[2]
168 }
169
170 /// Returns the build version component.
171 ///
172 /// # Examples
173 /// ```
174 /// use commonlibsse_ng::rel::version::Version;
175 ///
176 /// let v = Version::new(1, 2, 3, 4);
177 /// assert_eq!(v.build(), 4);
178 /// ```
179 #[inline]
180 pub const fn build(&self) -> u16 {
181 self._impl[3]
182 }
183
184 /// Packs the version into a 32-bit integer.
185 /// # Examples
186 /// ```
187 /// use commonlibsse_ng::rel::version::Version;
188 ///
189 /// let v = Version::new(1, 2, 3, 4);
190 /// assert_eq!(v.pack(), 16908340);
191 /// ```
192 #[inline]
193 pub const fn pack(&self) -> u32 {
194 ((self._impl[0] as u32 & 0xFF) << 24)
195 | ((self._impl[1] as u32 & 0xFF) << 16)
196 | ((self._impl[2] as u32 & 0xFFF) << 4)
197 | (self._impl[3] as u32 & 0xF)
198 }
199
200 /// Unpacks a 32-bit integer into a `Version`.
201 #[inline]
202 pub const fn unpack(packed: u32) -> Self {
203 Self {
204 _impl: [
205 ((packed >> 24) & 0xFF) as u16,
206 ((packed >> 16) & 0xFF) as u16,
207 ((packed >> 4) & 0xFFF) as u16,
208 (packed & 0xF) as u16,
209 ],
210 }
211 }
212
213 /// Gets the inner parts.
214 ///
215 /// # Example
216 /// ```
217 /// # use commonlibsse_ng::rel::version::Version;
218 /// let v = Version::new(1, 2, 3, 4);
219 /// assert_eq!(v.parts(), [1, 2, 3, 4]);
220 /// ```
221 #[inline]
222 pub const fn parts(&self) -> [u16; 4] {
223 self._impl
224 }
225
226 /// To address library file name string.
227 ///
228 /// # Example
229 /// ```
230 /// # use commonlibsse_ng::rel::version::Version;
231 /// let v = Version::new(1, 2, 3, 4);
232 /// assert_eq!(v.to_address_library_string(), "1-2-3-4");
233 /// ```
234 #[inline]
235 pub fn to_address_library_string(&self) -> String {
236 let [major, minor, patch, build] = self._impl;
237 format!("{major}-{minor}-{patch}-{build}")
238 }
239}
240
241impl Default for Version {
242 fn default() -> Self {
243 Self::default_const()
244 }
245}
246
247impl core::fmt::Display for Version {
248 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
249 let major = self._impl[0];
250 let minor = self._impl[1];
251 let patch = self._impl[2];
252 let build = self._impl[3];
253 write!(f, "{major}.{minor}.{patch}.{build}",)
254 }
255}
256
257impl core::ops::Index<usize> for Version {
258 type Output = u16;
259
260 #[inline]
261 fn index(&self, index: usize) -> &Self::Output {
262 &self._impl[index]
263 }
264}
265
266impl core::ops::IndexMut<usize> for Version {
267 #[inline]
268 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
269 &mut self._impl[index]
270 }
271}
272
273impl core::str::FromStr for Version {
274 type Err = VersionParseError;
275
276 #[inline]
277 fn from_str(version: &str) -> Result<Self, Self::Err> {
278 let mut parts = [0_u16; 4];
279 let mut idx = 0;
280 let mut num = 0;
281 let mut has_digit = false;
282
283 let bytes = version.as_bytes();
284 let len = bytes.len();
285 let mut i = 0;
286 while i < len {
287 let b = bytes[i];
288 if b == b'.' {
289 if idx >= 4 {
290 return Err(VersionParseError::TooManyParts { parts: idx });
291 }
292 parts[idx] = num;
293
294 num = 0;
295 idx += 1;
296 has_digit = false;
297 } else if b.is_ascii_digit() {
298 num = num * 10 + (b - b'0') as u16;
299 has_digit = true;
300 } else {
301 return Err(VersionParseError::InvalidCharacter { character: b as char });
302 }
303 i += 1;
304 }
305
306 if has_digit {
307 if idx >= 4 {
308 return Err(VersionParseError::TooManyParts { parts: idx });
309 }
310 parts[idx] = num;
311 } else {
312 return Err(VersionParseError::MissingNumber { part: idx });
313 }
314
315 Ok(Self { _impl: parts })
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, snafu::Snafu)]
320pub enum VersionParseError {
321 /// Expected at most 4 parts, but got {parts} parts
322 TooManyParts { parts: usize },
323
324 /// Expected a number but got invalid character: `{character}`
325 InvalidCharacter { character: char },
326
327 /// Expected numbers after the dots, but got none in part {part}
328 MissingNumber { part: usize },
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_version_ord() {
337 let v1 = Version::new(1, 2, 3, 4);
338 let v2 = Version::new(1, 2, 3, 5);
339 let v3 = Version::new(2, 0, 0, 0);
340 let v4 = Version::new(1, 2, 3, 4);
341
342 assert!(v1 < v2);
343 assert!(v2 > v1);
344 assert!(v3 > v1);
345 assert!(v1 == v4);
346 }
347
348 #[test]
349 fn test_from_str() {
350 use core::str::FromStr as _;
351
352 assert_eq!(Version::from_str("1.2.3.4"), Ok(Version::new(1, 2, 3, 4)));
353 assert_eq!(Version::from_str("1.2.3"), Ok(Version::new(1, 2, 3, 0)));
354
355 assert_eq!(
356 Version::from_str("1.2.3.4.5"),
357 Err(VersionParseError::TooManyParts { parts: 4 }) // 0 based index. got 5 length
358 );
359 assert_eq!(
360 Version::from_str("1.2.f.4.5"),
361 Err(VersionParseError::InvalidCharacter { character: 'f' })
362 );
363 assert_eq!(Version::from_str("1.2."), Err(VersionParseError::MissingNumber { part: 2 }));
364 }
365}