use crate::error::{BinaryError, Result};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
pub enum UnityVersionType {
A = 0,
B = 1,
C = 2,
#[default]
F = 3,
P = 4,
X = 5,
U = 6,
}
impl fmt::Display for UnityVersionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UnityVersionType::A => write!(f, "a"),
UnityVersionType::B => write!(f, "b"),
UnityVersionType::C => write!(f, "c"),
UnityVersionType::F => write!(f, "f"),
UnityVersionType::P => write!(f, "p"),
UnityVersionType::X => write!(f, "x"),
UnityVersionType::U => write!(f, "u"),
}
}
}
impl FromStr for UnityVersionType {
type Err = BinaryError;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"a" => Ok(UnityVersionType::A),
"b" => Ok(UnityVersionType::B),
"c" => Ok(UnityVersionType::C),
"f" => Ok(UnityVersionType::F),
"p" => Ok(UnityVersionType::P),
"x" => Ok(UnityVersionType::X),
_ => Ok(UnityVersionType::U),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnityVersion {
pub major: u16,
pub minor: u16,
pub build: u16,
pub version_type: UnityVersionType,
pub type_number: u8,
pub type_str: Option<String>, }
impl Default for UnityVersion {
fn default() -> Self {
Self {
major: 2020,
minor: 3,
build: 0,
version_type: UnityVersionType::F,
type_number: 1,
type_str: None,
}
}
}
impl UnityVersion {
pub fn new(
major: u16,
minor: u16,
build: u16,
version_type: UnityVersionType,
type_number: u8,
) -> Self {
Self {
major,
minor,
build,
version_type,
type_number,
type_str: None,
}
}
pub fn parse_version(version: &str) -> Result<Self> {
if version.is_empty() {
return Ok(Self::default());
}
let version_regex = regex::Regex::new(r"^(\d+)\.(\d+)\.(\d+)([a-zA-Z]?)(\d*)$")
.map_err(|e| BinaryError::invalid_data(format!("Regex error: {}", e)))?;
if let Some(captures) = version_regex.captures(version) {
let major = captures
.get(1)
.unwrap()
.as_str()
.parse::<u16>()
.map_err(|e| BinaryError::invalid_data(format!("Invalid major version: {}", e)))?;
let minor = captures
.get(2)
.unwrap()
.as_str()
.parse::<u16>()
.map_err(|e| BinaryError::invalid_data(format!("Invalid minor version: {}", e)))?;
let build = captures
.get(3)
.unwrap()
.as_str()
.parse::<u16>()
.map_err(|e| BinaryError::invalid_data(format!("Invalid build version: {}", e)))?;
let type_str = captures.get(4).map(|m| m.as_str()).unwrap_or("");
let type_number_str = captures.get(5).map(|m| m.as_str()).unwrap_or("0");
let version_type = if type_str.is_empty() {
UnityVersionType::F
} else {
UnityVersionType::from_str(type_str)?
};
let type_number = if type_number_str.is_empty() {
0
} else {
type_number_str
.parse::<u8>()
.map_err(|e| BinaryError::invalid_data(format!("Invalid type number: {}", e)))?
};
let mut version = Self::new(major, minor, build, version_type, type_number);
if version_type == UnityVersionType::U {
version.type_str = Some(type_str.to_string());
}
Ok(version)
} else {
Err(BinaryError::invalid_data(format!(
"Invalid version format: {}",
version
)))
}
}
pub fn as_tuple(&self) -> (u16, u16, u16, u8, u8) {
(
self.major,
self.minor,
self.build,
self.version_type as u8,
self.type_number,
)
}
pub fn is_gte(&self, other: &UnityVersion) -> bool {
self.as_tuple() >= other.as_tuple()
}
pub fn is_lt(&self, other: &UnityVersion) -> bool {
self.as_tuple() < other.as_tuple()
}
pub fn supports_feature(&self, feature: UnityFeature) -> bool {
match feature {
UnityFeature::BigIds => self.major >= 2019 || (self.major == 2018 && self.minor >= 2),
UnityFeature::TypeTreeEnabled => {
self.major >= 5 || (self.major == 4 && self.minor >= 5)
}
UnityFeature::ScriptTypeTree => self.major >= 2018,
UnityFeature::RefTypes => self.major >= 2019,
UnityFeature::UnityFS => self.major >= 5 && self.minor >= 3,
UnityFeature::LZ4Compression => self.major >= 5 && self.minor >= 3,
UnityFeature::LZMACompression => self.major >= 3,
UnityFeature::BrotliCompression => self.major >= 2020,
UnityFeature::ModernSerialization => self.major >= 2018,
}
}
pub fn get_alignment(&self) -> usize {
if self.major >= 2022 {
8 } else {
4 }
}
pub fn uses_big_endian(&self) -> bool {
false
}
pub fn get_serialized_file_format_version(&self) -> u32 {
if self.major >= 2022 {
22
} else if self.major >= 2020 {
21
} else if self.major >= 2019 {
20
} else if self.major >= 2018 {
19
} else if self.major >= 2017 {
17
} else if self.major >= 5 {
15
} else {
10
}
}
}
impl fmt::Display for UnityVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref custom_type) = self.type_str {
write!(
f,
"{}.{}.{}{}{}",
self.major, self.minor, self.build, custom_type, self.type_number
)
} else {
write!(
f,
"{}.{}.{}{}{}",
self.major, self.minor, self.build, self.version_type, self.type_number
)
}
}
}
impl PartialOrd for UnityVersion {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for UnityVersion {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_tuple().cmp(&other.as_tuple())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnityFeature {
BigIds,
TypeTreeEnabled,
ScriptTypeTree,
RefTypes,
UnityFS,
LZ4Compression,
LZMACompression,
BrotliCompression,
ModernSerialization,
}
pub struct VersionCompatibility;
impl VersionCompatibility {
pub fn is_supported(version: &UnityVersion) -> bool {
version.major >= 3 && version.major <= 2023
}
pub fn get_recommended_settings(version: &UnityVersion) -> VersionSettings {
VersionSettings {
use_type_tree: version.supports_feature(UnityFeature::TypeTreeEnabled),
alignment: version.get_alignment(),
big_endian: version.uses_big_endian(),
supports_big_ids: version.supports_feature(UnityFeature::BigIds),
supports_ref_types: version.supports_feature(UnityFeature::RefTypes),
serialized_file_format: version.get_serialized_file_format_version(),
}
}
pub fn get_known_versions() -> Vec<UnityVersion> {
vec![
UnityVersion::parse_version("3.4.0f5").unwrap(),
UnityVersion::parse_version("4.7.2f1").unwrap(),
UnityVersion::parse_version("5.0.0f4").unwrap(),
UnityVersion::parse_version("5.6.7f1").unwrap(),
UnityVersion::parse_version("2017.4.40f1").unwrap(),
UnityVersion::parse_version("2018.4.36f1").unwrap(),
UnityVersion::parse_version("2019.4.40f1").unwrap(),
UnityVersion::parse_version("2020.3.48f1").unwrap(),
UnityVersion::parse_version("2021.3.21f1").unwrap(),
UnityVersion::parse_version("2022.3.21f1").unwrap(),
UnityVersion::parse_version("2023.2.20f1").unwrap(),
]
}
}
#[derive(Debug, Clone)]
pub struct VersionSettings {
pub use_type_tree: bool,
pub alignment: usize,
pub big_endian: bool,
pub supports_big_ids: bool,
pub supports_ref_types: bool,
pub serialized_file_format: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_parsing() {
let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
assert_eq!(version.major, 2020);
assert_eq!(version.minor, 3);
assert_eq!(version.build, 12);
assert_eq!(version.version_type, UnityVersionType::F);
assert_eq!(version.type_number, 1);
}
#[test]
fn test_version_comparison() {
let v1 = UnityVersion::parse_version("2020.3.12f1").unwrap();
let v2 = UnityVersion::parse_version("2021.1.0f1").unwrap();
assert!(v1 < v2);
assert!(v2.is_gte(&v1));
assert!(v1.is_lt(&v2));
}
#[test]
fn test_feature_support() {
let old_version = UnityVersion::parse_version("5.0.0f1").unwrap();
let unity_fs_version = UnityVersion::parse_version("5.3.0f1").unwrap();
let new_version = UnityVersion::parse_version("2020.3.12f1").unwrap();
assert!(!old_version.supports_feature(UnityFeature::BigIds));
assert!(new_version.supports_feature(UnityFeature::BigIds));
assert!(!old_version.supports_feature(UnityFeature::UnityFS));
assert!(unity_fs_version.supports_feature(UnityFeature::UnityFS));
assert!(new_version.supports_feature(UnityFeature::UnityFS));
}
#[test]
fn test_version_display() {
let version = UnityVersion::parse_version("2020.3.12f1").unwrap();
assert_eq!(version.to_string(), "2020.3.12f1");
}
#[test]
fn test_compatibility_check() {
let supported = UnityVersion::parse_version("2020.3.12f1").unwrap();
let unsupported = UnityVersion::parse_version("2.0.0f1").unwrap();
assert!(VersionCompatibility::is_supported(&supported));
assert!(!VersionCompatibility::is_supported(&unsupported));
}
}