unity_asset_binary/
unity_version.rs

1//! Unity Version Management System
2//!
3//! This module provides comprehensive Unity version parsing, comparison, and compatibility
4//! handling based on UnityPy's implementation.
5
6use crate::error::{BinaryError, Result};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10
11/// Unity version type (release channel)
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
13pub enum UnityVersionType {
14    /// Alpha release
15    A = 0,
16    /// Beta release
17    B = 1,
18    /// China release
19    C = 2,
20    /// Final release
21    #[default]
22    F = 3,
23    /// Patch release
24    P = 4,
25    /// Experimental release
26    X = 5,
27    /// Unknown/Custom release
28    U = 6,
29}
30
31impl fmt::Display for UnityVersionType {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            UnityVersionType::A => write!(f, "a"),
35            UnityVersionType::B => write!(f, "b"),
36            UnityVersionType::C => write!(f, "c"),
37            UnityVersionType::F => write!(f, "f"),
38            UnityVersionType::P => write!(f, "p"),
39            UnityVersionType::X => write!(f, "x"),
40            UnityVersionType::U => write!(f, "u"),
41        }
42    }
43}
44
45impl FromStr for UnityVersionType {
46    type Err = BinaryError;
47
48    fn from_str(s: &str) -> Result<Self> {
49        match s.to_lowercase().as_str() {
50            "a" => Ok(UnityVersionType::A),
51            "b" => Ok(UnityVersionType::B),
52            "c" => Ok(UnityVersionType::C),
53            "f" => Ok(UnityVersionType::F),
54            "p" => Ok(UnityVersionType::P),
55            "x" => Ok(UnityVersionType::X),
56            _ => Ok(UnityVersionType::U),
57        }
58    }
59}
60
61/// Unity version representation
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct UnityVersion {
64    pub major: u16,
65    pub minor: u16,
66    pub build: u16,
67    pub version_type: UnityVersionType,
68    pub type_number: u8,
69    pub type_str: Option<String>, // For custom/unknown types
70}
71
72impl Default for UnityVersion {
73    fn default() -> Self {
74        Self {
75            major: 2020,
76            minor: 3,
77            build: 0,
78            version_type: UnityVersionType::F,
79            type_number: 1,
80            type_str: None,
81        }
82    }
83}
84
85impl UnityVersion {
86    /// Create a new Unity version
87    pub fn new(
88        major: u16,
89        minor: u16,
90        build: u16,
91        version_type: UnityVersionType,
92        type_number: u8,
93    ) -> Self {
94        Self {
95            major,
96            minor,
97            build,
98            version_type,
99            type_number,
100            type_str: None,
101        }
102    }
103
104    /// Parse Unity version from string
105    /// Supports formats like: "2020.3.12f1", "5.6.0", "2018.1.1b2"
106    pub fn parse_version(version: &str) -> Result<Self> {
107        if version.is_empty() {
108            return Ok(Self::default());
109        }
110
111        // Use regex to parse version string
112        let version_regex = regex::Regex::new(r"^(\d+)\.(\d+)\.(\d+)([a-zA-Z]?)(\d*)$")
113            .map_err(|e| BinaryError::invalid_data(format!("Regex error: {}", e)))?;
114
115        if let Some(captures) = version_regex.captures(version) {
116            let major = captures
117                .get(1)
118                .unwrap()
119                .as_str()
120                .parse::<u16>()
121                .map_err(|e| BinaryError::invalid_data(format!("Invalid major version: {}", e)))?;
122            let minor = captures
123                .get(2)
124                .unwrap()
125                .as_str()
126                .parse::<u16>()
127                .map_err(|e| BinaryError::invalid_data(format!("Invalid minor version: {}", e)))?;
128            let build = captures
129                .get(3)
130                .unwrap()
131                .as_str()
132                .parse::<u16>()
133                .map_err(|e| BinaryError::invalid_data(format!("Invalid build version: {}", e)))?;
134
135            let type_str = captures.get(4).map(|m| m.as_str()).unwrap_or("");
136            let type_number_str = captures.get(5).map(|m| m.as_str()).unwrap_or("0");
137
138            // If no type letter is provided, default to "f" (final release)
139            let version_type = if type_str.is_empty() {
140                UnityVersionType::F
141            } else {
142                UnityVersionType::from_str(type_str)?
143            };
144            let type_number = if type_number_str.is_empty() {
145                0
146            } else {
147                type_number_str
148                    .parse::<u8>()
149                    .map_err(|e| BinaryError::invalid_data(format!("Invalid type number: {}", e)))?
150            };
151
152            let mut version = Self::new(major, minor, build, version_type, type_number);
153
154            // Store custom type string for unknown types
155            if version_type == UnityVersionType::U {
156                version.type_str = Some(type_str.to_string());
157            }
158
159            Ok(version)
160        } else {
161            Err(BinaryError::invalid_data(format!(
162                "Invalid version format: {}",
163                version
164            )))
165        }
166    }
167
168    /// Convert to tuple for comparison
169    pub fn as_tuple(&self) -> (u16, u16, u16, u8, u8) {
170        (
171            self.major,
172            self.minor,
173            self.build,
174            self.version_type as u8,
175            self.type_number,
176        )
177    }
178
179    /// Check if this version is greater than or equal to another
180    pub fn is_gte(&self, other: &UnityVersion) -> bool {
181        self.as_tuple() >= other.as_tuple()
182    }
183
184    /// Check if this version is less than another
185    pub fn is_lt(&self, other: &UnityVersion) -> bool {
186        self.as_tuple() < other.as_tuple()
187    }
188
189    /// Check if this version supports a specific feature
190    pub fn supports_feature(&self, feature: UnityFeature) -> bool {
191        match feature {
192            UnityFeature::BigIds => self.major >= 2019 || (self.major == 2018 && self.minor >= 2),
193            UnityFeature::TypeTreeEnabled => {
194                self.major >= 5 || (self.major == 4 && self.minor >= 5)
195            }
196            UnityFeature::ScriptTypeTree => self.major >= 2018,
197            UnityFeature::RefTypes => self.major >= 2019,
198            UnityFeature::UnityFS => self.major >= 5 && self.minor >= 3,
199            UnityFeature::LZ4Compression => self.major >= 5 && self.minor >= 3,
200            UnityFeature::LZMACompression => self.major >= 3,
201            UnityFeature::BrotliCompression => self.major >= 2020,
202            UnityFeature::ModernSerialization => self.major >= 2018,
203        }
204    }
205
206    /// Get the appropriate byte alignment for this version
207    pub fn get_alignment(&self) -> usize {
208        if self.major >= 2022 {
209            8 // Unity 2022+ uses 8-byte alignment
210        } else {
211            4 // Unity 2019+ and older versions use 4-byte alignment
212        }
213    }
214
215    /// Check if this version uses big endian by default
216    pub fn uses_big_endian(&self) -> bool {
217        // Most Unity versions use little endian, but some platforms/versions may differ
218        false
219    }
220
221    /// Get the serialized file format version for this Unity version
222    pub fn get_serialized_file_format_version(&self) -> u32 {
223        if self.major >= 2022 {
224            22
225        } else if self.major >= 2020 {
226            21
227        } else if self.major >= 2019 {
228            20
229        } else if self.major >= 2018 {
230            19
231        } else if self.major >= 2017 {
232            17
233        } else if self.major >= 5 {
234            15
235        } else {
236            10
237        }
238    }
239}
240
241impl fmt::Display for UnityVersion {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        if let Some(ref custom_type) = self.type_str {
244            write!(
245                f,
246                "{}.{}.{}{}{}",
247                self.major, self.minor, self.build, custom_type, self.type_number
248            )
249        } else {
250            write!(
251                f,
252                "{}.{}.{}{}{}",
253                self.major, self.minor, self.build, self.version_type, self.type_number
254            )
255        }
256    }
257}
258
259impl PartialOrd for UnityVersion {
260    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
261        Some(self.cmp(other))
262    }
263}
264
265impl Ord for UnityVersion {
266    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
267        self.as_tuple().cmp(&other.as_tuple())
268    }
269}
270
271/// Unity features that depend on version
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum UnityFeature {
274    /// Support for 64-bit object IDs
275    BigIds,
276    /// TypeTree is enabled by default
277    TypeTreeEnabled,
278    /// Script type tree support
279    ScriptTypeTree,
280    /// Reference types support
281    RefTypes,
282    /// UnityFS format support
283    UnityFS,
284    /// LZ4 compression support
285    LZ4Compression,
286    /// LZMA compression support
287    LZMACompression,
288    /// Brotli compression support
289    BrotliCompression,
290    /// Modern serialization format
291    ModernSerialization,
292}
293
294/// Unity version compatibility checker
295pub struct VersionCompatibility;
296
297impl VersionCompatibility {
298    /// Check if a version is supported by this parser
299    pub fn is_supported(version: &UnityVersion) -> bool {
300        // We support Unity 3.4 to 2023.x
301        version.major >= 3 && version.major <= 2023
302    }
303
304    /// Get recommended settings for a Unity version
305    pub fn get_recommended_settings(version: &UnityVersion) -> VersionSettings {
306        VersionSettings {
307            use_type_tree: version.supports_feature(UnityFeature::TypeTreeEnabled),
308            alignment: version.get_alignment(),
309            big_endian: version.uses_big_endian(),
310            supports_big_ids: version.supports_feature(UnityFeature::BigIds),
311            supports_ref_types: version.supports_feature(UnityFeature::RefTypes),
312            serialized_file_format: version.get_serialized_file_format_version(),
313        }
314    }
315
316    /// Get a list of known Unity versions for testing
317    pub fn get_known_versions() -> Vec<UnityVersion> {
318        vec![
319            UnityVersion::parse_version("3.4.0f5").unwrap(),
320            UnityVersion::parse_version("4.7.2f1").unwrap(),
321            UnityVersion::parse_version("5.0.0f4").unwrap(),
322            UnityVersion::parse_version("5.6.7f1").unwrap(),
323            UnityVersion::parse_version("2017.4.40f1").unwrap(),
324            UnityVersion::parse_version("2018.4.36f1").unwrap(),
325            UnityVersion::parse_version("2019.4.40f1").unwrap(),
326            UnityVersion::parse_version("2020.3.48f1").unwrap(),
327            UnityVersion::parse_version("2021.3.21f1").unwrap(),
328            UnityVersion::parse_version("2022.3.21f1").unwrap(),
329            UnityVersion::parse_version("2023.2.20f1").unwrap(),
330        ]
331    }
332}
333
334/// Version-specific settings
335#[derive(Debug, Clone)]
336pub struct VersionSettings {
337    pub use_type_tree: bool,
338    pub alignment: usize,
339    pub big_endian: bool,
340    pub supports_big_ids: bool,
341    pub supports_ref_types: bool,
342    pub serialized_file_format: u32,
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[test]
350    fn test_version_parsing() {
351        let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
352        assert_eq!(version.major, 2020);
353        assert_eq!(version.minor, 3);
354        assert_eq!(version.build, 12);
355        assert_eq!(version.version_type, UnityVersionType::F);
356        assert_eq!(version.type_number, 1);
357    }
358
359    #[test]
360    fn test_version_comparison() {
361        let v1 = UnityVersion::parse_version("2020.3.12f1").unwrap();
362        let v2 = UnityVersion::parse_version("2021.1.0f1").unwrap();
363
364        assert!(v1 < v2);
365        assert!(v2.is_gte(&v1));
366        assert!(v1.is_lt(&v2));
367    }
368
369    #[test]
370    fn test_feature_support() {
371        let old_version = UnityVersion::parse_version("5.0.0f1").unwrap();
372        let unity_fs_version = UnityVersion::parse_version("5.3.0f1").unwrap();
373        let new_version = UnityVersion::parse_version("2020.3.12f1").unwrap();
374
375        assert!(!old_version.supports_feature(UnityFeature::BigIds));
376        assert!(new_version.supports_feature(UnityFeature::BigIds));
377
378        // Unity 5.0 doesn't support UnityFS (introduced in 5.3)
379        assert!(!old_version.supports_feature(UnityFeature::UnityFS));
380        assert!(unity_fs_version.supports_feature(UnityFeature::UnityFS));
381        assert!(new_version.supports_feature(UnityFeature::UnityFS));
382    }
383
384    #[test]
385    fn test_version_display() {
386        let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
387        assert_eq!(version.to_string(), "2020.3.12f1");
388    }
389
390    #[test]
391    fn test_compatibility_check() {
392        let supported = UnityVersion::parse_version("2020.3.12f1").unwrap();
393        let unsupported = UnityVersion::parse_version("2.0.0f1").unwrap();
394
395        assert!(VersionCompatibility::is_supported(&supported));
396        assert!(!VersionCompatibility::is_supported(&unsupported));
397    }
398}