1use crate::error::{BinaryError, Result};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
13pub enum UnityVersionType {
14 A = 0,
16 B = 1,
18 C = 2,
20 #[default]
22 F = 3,
23 P = 4,
25 X = 5,
27 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#[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>, }
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 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 pub fn parse_version(version: &str) -> Result<Self> {
107 if version.is_empty() {
108 return Ok(Self::default());
109 }
110
111 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 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 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 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 pub fn is_gte(&self, other: &UnityVersion) -> bool {
181 self.as_tuple() >= other.as_tuple()
182 }
183
184 pub fn is_lt(&self, other: &UnityVersion) -> bool {
186 self.as_tuple() < other.as_tuple()
187 }
188
189 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 pub fn get_alignment(&self) -> usize {
208 if self.major >= 2022 {
209 8 } else {
211 4 }
213 }
214
215 pub fn uses_big_endian(&self) -> bool {
217 false
219 }
220
221 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum UnityFeature {
274 BigIds,
276 TypeTreeEnabled,
278 ScriptTypeTree,
280 RefTypes,
282 UnityFS,
284 LZ4Compression,
286 LZMACompression,
288 BrotliCompression,
290 ModernSerialization,
292}
293
294pub struct VersionCompatibility;
296
297impl VersionCompatibility {
298 pub fn is_supported(version: &UnityVersion) -> bool {
300 version.major >= 3 && version.major <= 2023
302 }
303
304 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 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#[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 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}