1use std::fmt;
4
5use crate::{Result, invalid};
6
7#[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 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 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 fn field(self, shift: u32) -> u16 {
56 u16::try_from((self.0 >> shift) & 0xFFFF).unwrap_or_default()
57 }
58
59 pub fn major(self) -> u16 {
61 self.field(48)
62 }
63 pub fn minor(self) -> u16 {
65 self.field(32)
66 }
67 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}