Skip to main content

unity_formats/
version.rs

1//! Unity editor versions, as files write them (`6000.3.12f1`) and as type tree packages order them.
2
3use std::fmt;
4
5use crate::{Result, invalid};
6
7/// A Unity version packed into one ordered number: major (16 bits), minor (16), build (16), release type (8: a 0,
8/// b 1, c 2, f 3, p 4, x 5), number (8). A version without a release type (`5.0.0`) is final, number 0.
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct UnityVersion(pub u64);
11
12const TYPES: [char; 6] = ['a', 'b', 'c', 'f', 'p', 'x'];
13
14impl UnityVersion {
15    /// Pack a version from its parts (`kind` 0 to 5: a, b, c, f, p, x).
16    pub fn new(major: u16, minor: u16, build: u16, kind: u8, number: u8) -> Self {
17        UnityVersion(
18            u64::from(major) << 48
19                | u64::from(minor) << 32
20                | u64::from(build) << 16
21                | u64::from(kind) << 8
22                | u64::from(number),
23        )
24    }
25
26    /// `major.minor.build[type number]`, e.g. `6000.3.12f1`; anything after the number is ignored.
27    pub fn parse(s: &str) -> Result<Self> {
28        let bad = || invalid(format!("Unity version {s:?}"));
29        let mut parts = s.splitn(3, '.');
30        let major: u16 = parts.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
31        let minor: u16 = parts.next().and_then(|p| p.parse().ok()).ok_or_else(bad)?;
32        let rest = parts.next().ok_or_else(bad)?;
33        let digits = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
34        let build: u16 = rest.get(..digits).and_then(|d| d.parse().ok()).ok_or_else(bad)?;
35        let tail = rest.get(digits..).ok_or_else(bad)?;
36        let Some(t) = tail.chars().next() else {
37            return Ok(Self::new(major, minor, build, 3, 0));
38        };
39        let kind = TYPES
40            .iter()
41            .position(|&c| c == t)
42            .and_then(|k| u8::try_from(k).ok())
43            .ok_or_else(bad)?;
44        let num = tail.get(t.len_utf8()..).ok_or_else(bad)?;
45        let n = num.find(|c: char| !c.is_ascii_digit()).unwrap_or(num.len());
46        let number: u8 = if n == 0 {
47            0
48        } else {
49            num.get(..n).and_then(|d| d.parse().ok()).ok_or_else(bad)?
50        };
51        Ok(Self::new(major, minor, build, kind, number))
52    }
53
54    /// The 16 bits at bit `shift`.
55    fn field(self, shift: u32) -> u16 {
56        u16::try_from((self.0 >> shift) & 0xFFFF).unwrap_or_default()
57    }
58
59    /// The major version (`6000`, `2019`).
60    pub fn major(self) -> u16 {
61        self.field(48)
62    }
63    /// The minor version.
64    pub fn minor(self) -> u16 {
65        self.field(32)
66    }
67    /// The build number (`12` of `6000.3.12f1`).
68    pub fn build(self) -> u16 {
69        self.field(16)
70    }
71}
72
73impl fmt::Display for UnityVersion {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        let kind = TYPES.get(((self.0 >> 8) & 0xFF) as usize).copied().unwrap_or('?');
76        write!(
77            f,
78            "{}.{}.{}{kind}{}",
79            self.major(),
80            self.minor(),
81            self.build(),
82            self.0 & 0xFF
83        )
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn versions_parse_and_order() {
93        let v = UnityVersion::parse("6000.3.12f1").unwrap();
94        assert_eq!(v, UnityVersion::new(6000, 3, 12, 3, 1));
95        assert_eq!(v.to_string(), "6000.3.12f1");
96        assert_eq!(
97            UnityVersion::parse("5.0.0").unwrap(),
98            UnityVersion::new(5, 0, 0, 3, 0)
99        );
100        assert!(UnityVersion::parse("2022.3.1b2").unwrap() < UnityVersion::parse("2022.3.1f1").unwrap());
101        assert!(UnityVersion::parse("2022.3.1f1-custom").is_ok());
102        for bad in ["", "6000", "6000.3", "6000.3.x", "6000.3.12q1"] {
103            assert!(UnityVersion::parse(bad).is_err(), "{bad}");
104        }
105    }
106}