vrc_get_vpm/version/
unity_version.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::num::NonZeroU8;
4use std::str::FromStr;
5
6use crate::version::Version;
7use serde::de::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct UnityVersion {
12    // major version such as 2019, 2022, and 6
13    // note: 5 < 2017 < 2023 < 6 < 7 ...
14    // this system assumes if this value is greater than 2000, it's year release
15    major: u16,
16    // minor version of unity. usually 1, 2 for tech and 3 for LTS
17    minor: u8,
18    // revision number, updated every few weeks
19    revision: u8,
20    // release type. a < b < f = c < p < x
21    //   'a' for alpha (expects revision to be zero)
22    //   'b' for beta (expects revision to be zero)
23    //   'f' and 'c' for normal ('c' is for china)
24    //   'p' for patches
25    //   'x' for experimental
26    type_: ReleaseType,
27    // revision increment
28    increment: u8,
29    // for china releases of Unity,
30    // we may see f1c1 so we have an additional one increment for china releases
31    // For example, https://unity.cn/releases/lts/2022 has 2022.3.22f1c1
32    china_increment: Option<NonZeroU8>,
33}
34
35impl UnityVersion {
36    pub const fn new(
37        major: u16,
38        minor: u8,
39        revision: u8,
40        type_: ReleaseType,
41        increment: u8,
42    ) -> Self {
43        Self {
44            major,
45            minor,
46            revision,
47            type_,
48            increment,
49            china_increment: None,
50        }
51    }
52
53    pub const fn new_f1(major: u16, minor: u8, revision: u8) -> Self {
54        Self {
55            major,
56            minor,
57            revision,
58            type_: ReleaseType::Normal,
59            increment: 1,
60            china_increment: None,
61        }
62    }
63
64    pub const fn new_china(
65        major: u16,
66        minor: u8,
67        revision: u8,
68        increment: u8,
69        china_increment: NonZeroU8,
70    ) -> Self {
71        Self {
72            major,
73            minor,
74            revision,
75            type_: ReleaseType::China,
76            increment,
77            china_increment: Some(china_increment),
78        }
79    }
80
81    // expects major.minor.revision[type]increment
82    pub fn parse(input: &str) -> Option<Self> {
83        let (major, rest) = input.split_once('.')?;
84        let major = u16::from_str(major).ok()?;
85        let (minor, rest) = rest.split_once('.')?;
86        let minor = u8::from_str(minor).ok()?;
87        let revision_delimiter = rest.find(is_release_type_char)?;
88        let revision = &rest[..revision_delimiter];
89        let revision = u8::from_str(revision).ok()?;
90        let type_ = ReleaseType::try_from(rest.as_bytes()[revision_delimiter]).ok()?;
91        let rest = &rest[revision_delimiter + 1..];
92
93        let (increment_part, _rest) = rest.split_once('-').unwrap_or((rest, ""));
94        let (increment, china_increment);
95        if increment_part.contains('c') {
96            let (increment_str, increment_china_str) = increment_part.split_once('c')?;
97            increment = u8::from_str(increment_str).ok()?;
98            china_increment = Some(NonZeroU8::from_str(increment_china_str).ok()?);
99        } else {
100            increment = u8::from_str(increment_part).ok()?;
101            china_increment = None;
102        }
103
104        return Some(Self {
105            major,
106            minor,
107            revision,
108            type_,
109            increment,
110            china_increment,
111        });
112
113        fn is_release_type_char(c: char) -> bool {
114            c == 'a' || c == 'b' || c == 'f' || c == 'c' || c == 'p' || c == 'x'
115        }
116    }
117
118    // expects major.minor.revision
119    pub fn parse_no_type_increment(input: &str) -> Option<Self> {
120        let (major, rest) = input.split_once('.')?;
121        let major = u16::from_str(major).ok()?;
122        let (minor, rest) = rest.split_once('.')?;
123        let minor = u8::from_str(minor).ok()?;
124        let revision = rest;
125        let revision = u8::from_str(revision).ok()?;
126
127        Some(Self::new_f1(major, minor, revision))
128    }
129
130    pub fn major(self) -> u16 {
131        self.major
132    }
133
134    pub fn minor(self) -> u8 {
135        self.minor
136    }
137
138    pub fn revision(self) -> u8 {
139        self.revision
140    }
141
142    pub fn type_(self) -> ReleaseType {
143        self.type_
144    }
145
146    pub fn increment(self) -> u8 {
147        self.increment
148    }
149
150    pub fn china_increment(self) -> Option<NonZeroU8> {
151        self.china_increment
152    }
153
154    pub fn as_semver(self) -> Version {
155        Version::new(self.major as u64, self.minor as u64, self.revision as u64)
156    }
157}
158
159impl fmt::Display for UnityVersion {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        if let Some(china) = self.china_increment {
162            write!(
163                f,
164                "{maj}.{min}.{rev}{ty}{inc}c{china}",
165                maj = self.major,
166                min = self.minor,
167                rev = self.revision,
168                ty = self.type_,
169                inc = self.increment,
170                china = china.get(),
171            )
172        } else {
173            write!(
174                f,
175                "{maj}.{min}.{rev}{ty}{inc}",
176                maj = self.major,
177                min = self.minor,
178                rev = self.revision,
179                ty = self.type_,
180                inc = self.increment,
181            )
182        }
183    }
184}
185
186impl Serialize for UnityVersion {
187    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188    where
189        S: Serializer,
190    {
191        serializer.serialize_str(&self.to_string())
192    }
193}
194
195impl<'de> Deserialize<'de> for UnityVersion {
196    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197    where
198        D: Deserializer<'de>,
199    {
200        UnityVersion::parse(&String::deserialize(deserializer)?)
201            .ok_or_else(|| D::Error::custom("invalid unity version"))
202    }
203}
204
205impl PartialOrd<Self> for UnityVersion {
206    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
207        Some(Ord::cmp(self, other))
208    }
209}
210
211impl Ord for UnityVersion {
212    fn cmp(&self, other: &Self) -> Ordering {
213        // We ignore china increment for comparing version
214        (self.major().cmp(&other.major()))
215            .then_with(|| self.minor().cmp(&other.minor()))
216            .then_with(|| self.revision().cmp(&other.revision()))
217            .then_with(|| self.type_().cmp(&other.type_()))
218            .then_with(|| self.increment().cmp(&other.increment()))
219    }
220}
221
222#[derive(Clone, Copy, Debug, Eq)]
223pub enum ReleaseType {
224    Alpha,
225    Beta,
226    Normal,
227    China,
228    Patch,
229    Experimental,
230}
231
232impl PartialEq for ReleaseType {
233    fn eq(&self, other: &Self) -> bool {
234        match (self, other) {
235            (Self::Alpha, Self::Alpha) => true,
236            (Self::Beta, Self::Beta) => true,
237            (Self::Normal, Self::Normal) => true,
238            (Self::China, Self::China) => true,
239            (Self::Patch, Self::Patch) => true,
240            (Self::Experimental, Self::Experimental) => true,
241
242            // exceptions!
243            (Self::Normal, Self::China) => true,
244            (Self::China, Self::Normal) => true,
245            _ => false,
246        }
247    }
248}
249
250impl PartialOrd for ReleaseType {
251    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
252        Some(Ord::cmp(self, other))
253    }
254}
255
256impl Ord for ReleaseType {
257    fn cmp(&self, other: &Self) -> Ordering {
258        use Ordering::*;
259        use ReleaseType::*;
260        match (*self, *other) {
261            (Alpha, Alpha) => Equal,
262            (Alpha, _) => Less,
263            (_, Alpha) => Greater,
264
265            (Beta, Beta) => Equal,
266            (Beta, _) => Less,
267            (_, Beta) => Greater,
268
269            (Normal, Normal) => Equal,
270            (Normal, China) => Equal,
271            (China, Normal) => Equal,
272            (China, China) => Equal,
273            (Normal, _) => Less,
274            (China, _) => Less,
275            (_, Normal) => Greater,
276            (_, China) => Greater,
277
278            (Patch, Patch) => Equal,
279            (Patch, _) => Less,
280            (_, Patch) => Greater,
281
282            (Experimental, Experimental) => Equal,
283        }
284    }
285}
286
287pub struct ReleaseTypeError(());
288
289impl fmt::Display for ReleaseType {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            ReleaseType::Alpha => f.write_str("a"),
293            ReleaseType::Beta => f.write_str("b"),
294            ReleaseType::Normal => f.write_str("f"),
295            ReleaseType::China => f.write_str("c"),
296            ReleaseType::Patch => f.write_str("p"),
297            ReleaseType::Experimental => f.write_str("x"),
298        }
299    }
300}
301
302impl TryFrom<u8> for ReleaseType {
303    type Error = ReleaseTypeError;
304
305    fn try_from(value: u8) -> Result<Self, Self::Error> {
306        match value {
307            b'a' => Ok(Self::Alpha),
308            b'b' => Ok(Self::Beta),
309            b'f' => Ok(Self::Normal),
310            b'c' => Ok(Self::China),
311            b'p' => Ok(Self::Patch),
312            b'x' => Ok(Self::Experimental),
313            _ => Err(ReleaseTypeError(())),
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn parse_unity_version() {
324        macro_rules! good {
325            ($string: literal, $major: literal, $minor: literal, $revision: literal, $type_: ident, $increment: literal) => {
326                let version = UnityVersion::parse($string).unwrap();
327                assert_eq!(version.major, $major);
328                assert_eq!(version.minor, $minor);
329                assert_eq!(version.revision, $revision);
330                assert!(matches!(version.type_, ReleaseType::$type_));
331                assert_eq!(version.increment, $increment);
332                assert_eq!(version.china_increment, None);
333            };
334        }
335
336        macro_rules! good_cn {
337            ($string: literal, $major: literal, $minor: literal, $revision: literal, $type_: ident, $increment: literal, $china_increment: literal) => {
338                let version = UnityVersion::parse($string).unwrap();
339                assert_eq!(version.major, $major);
340                assert_eq!(version.minor, $minor);
341                assert_eq!(version.revision, $revision);
342                assert!(matches!(version.type_, ReleaseType::$type_));
343                assert_eq!(version.increment, $increment);
344                assert_eq!(
345                    version.china_increment,
346                    Some(NonZeroU8::new($china_increment).unwrap())
347                );
348            };
349        }
350
351        macro_rules! bad {
352            ($string: literal) => {
353                assert!(UnityVersion::parse($string).is_none());
354            };
355        }
356
357        good!("5.6.6f1", 5, 6, 6, Normal, 1);
358
359        good!("2019.1.0a1", 2019, 1, 0, Alpha, 1);
360        good!("2019.1.0b1", 2019, 1, 0, Beta, 1);
361        good!("2019.4.31f1", 2019, 4, 31, Normal, 1);
362        good!("2023.3.6f1", 2023, 3, 6, Normal, 1);
363        good!("2023.3.6c1", 2023, 3, 6, China, 1);
364        good!("2023.3.6p1", 2023, 3, 6, Patch, 1);
365        good!("2023.3.6x1", 2023, 3, 6, Experimental, 1);
366
367        good!("2019.1.0a1-EXTRA", 2019, 1, 0, Alpha, 1);
368
369        good_cn!("2022.3.22f1c1", 2022, 3, 22, Normal, 1, 1);
370
371        bad!("2022");
372        bad!("2019.0");
373        bad!("5.6.6");
374        bad!("2023.4.6f");
375    }
376
377    #[test]
378    fn ord_version() {
379        macro_rules! test {
380            ($left: literal <  $right: literal) => {
381                let left = UnityVersion::parse($left).unwrap();
382                let right = UnityVersion::parse($right).unwrap();
383                assert!(left < right);
384                assert!(right > left);
385            };
386        }
387
388        test!("5.6.5f1" < "5.6.6f1");
389        test!("5.6.6f1" < "5.6.6f2");
390        test!("5.6.6f1" < "2022.1.0f1");
391        test!("2022.1.0a1" < "2022.1.0f1");
392    }
393
394    #[test]
395    fn ord_release_type() {
396        use ReleaseType::*;
397
398        macro_rules! test {
399            ($left: ident $right: ident $ordering: ident) => {
400                assert_eq!($left.cmp(&$right), Ordering::$ordering);
401            };
402        }
403
404        assert!(Alpha < Beta);
405        assert!(Beta < Normal);
406        assert!(Beta < China);
407        assert!(China < Patch);
408        assert!(Patch < Experimental);
409
410        test!(Alpha Alpha Equal);
411        test!(Alpha Beta Less);
412        test!(Alpha Normal Less);
413        test!(Alpha China Less);
414        test!(Alpha Patch Less);
415        test!(Alpha Experimental Less);
416
417        test!(Beta Alpha Greater);
418        test!(Beta Beta Equal);
419        test!(Beta Normal Less);
420        test!(Beta China Less);
421        test!(Beta Patch Less);
422        test!(Beta Experimental Less);
423
424        test!(Normal Alpha Greater);
425        test!(Normal Beta Greater);
426        test!(Normal Normal Equal);
427        test!(Normal China Equal);
428        test!(Normal Patch Less);
429        test!(Normal Experimental Less);
430
431        test!(China Alpha Greater);
432        test!(China Beta Greater);
433        test!(China Normal Equal);
434        test!(China China Equal);
435        test!(China Patch Less);
436        test!(China Experimental Less);
437
438        test!(Patch Alpha Greater);
439        test!(Patch Beta Greater);
440        test!(Patch Normal Greater);
441        test!(Patch China Greater);
442        test!(Patch Patch Equal);
443        test!(Patch Experimental Less);
444
445        test!(Experimental Alpha Greater);
446        test!(Experimental Beta Greater);
447        test!(Experimental Normal Greater);
448        test!(Experimental China Greater);
449        test!(Experimental Patch Greater);
450        test!(Experimental Experimental Equal);
451    }
452
453    #[test]
454    fn eq_release_type() {
455        use ReleaseType::*;
456
457        assert_eq!(Alpha, Alpha);
458        assert_ne!(Alpha, Beta);
459        assert_ne!(Alpha, Normal);
460        assert_ne!(Alpha, China);
461        assert_ne!(Alpha, Patch);
462        assert_ne!(Alpha, Experimental);
463
464        assert_ne!(Beta, Alpha);
465        assert_eq!(Beta, Beta);
466        assert_ne!(Beta, Normal);
467        assert_ne!(Beta, China);
468        assert_ne!(Beta, Patch);
469        assert_ne!(Beta, Experimental);
470
471        assert_ne!(Normal, Alpha);
472        assert_ne!(Normal, Beta);
473        assert_eq!(Normal, Normal);
474        assert_eq!(Normal, China);
475        assert_ne!(Normal, Patch);
476        assert_ne!(Normal, Experimental);
477
478        assert_ne!(China, Alpha);
479        assert_ne!(China, Beta);
480        assert_eq!(China, Normal);
481        assert_eq!(China, China);
482        assert_ne!(China, Patch);
483        assert_ne!(China, Experimental);
484
485        assert_ne!(Patch, Alpha);
486        assert_ne!(Patch, Beta);
487        assert_ne!(Patch, Normal);
488        assert_ne!(Patch, China);
489        assert_eq!(Patch, Patch);
490        assert_ne!(Patch, Experimental);
491
492        assert_ne!(Experimental, Alpha);
493        assert_ne!(Experimental, Beta);
494        assert_ne!(Experimental, Normal);
495        assert_ne!(Experimental, China);
496        assert_ne!(Experimental, Patch);
497        assert_eq!(Experimental, Experimental);
498    }
499}