use crate::error::{BinaryError, Result};
use crate::reader::{BinaryReader, ByteOrder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializedFileHeader {
pub metadata_size: u32,
pub file_size: u64,
pub version: u32,
pub data_offset: u64,
pub endian: u8,
pub reserved: [u8; 3],
}
impl SerializedFileHeader {
pub fn from_reader(reader: &mut BinaryReader) -> Result<Self> {
let mut metadata_size = reader.read_u32()?;
let mut file_size = reader.read_u32()? as u64;
let version = reader.read_u32()?;
let mut data_offset = reader.read_u32()? as u64;
let endian;
let mut reserved = [0u8; 3];
if version >= 9 {
endian = reader.read_u8()?;
let reserved_bytes = reader.read_bytes(3)?;
reserved.copy_from_slice(&reserved_bytes);
} else {
let current_pos = reader.position();
let endian_pos = file_size.checked_sub(metadata_size as u64).ok_or_else(|| {
BinaryError::invalid_data("Invalid header: file_size < metadata_size")
})?;
reader.set_position(endian_pos)?;
endian = reader.read_u8()?;
reader.set_position(current_pos)?;
}
if version >= 22 {
metadata_size = reader.read_u32()?;
file_size = i64_to_u64_checked(reader.read_i64()?, "file_size")?;
data_offset = i64_to_u64_checked(reader.read_i64()?, "data_offset")?;
reader.read_i64()?; }
Ok(Self {
metadata_size,
file_size,
version,
data_offset,
endian,
reserved,
})
}
pub fn byte_order(&self) -> ByteOrder {
if self.endian == 0 {
ByteOrder::Little
} else {
ByteOrder::Big
}
}
pub fn is_valid(&self) -> bool {
self.version > 0
&& self.version < 100
&& self.data_offset > 0
&& self.file_size > self.data_offset
}
pub fn format_info(&self) -> HeaderFormatInfo {
HeaderFormatInfo {
version: self.version,
is_big_endian: self.endian != 0,
has_extended_format: self.version >= 22,
supports_large_files: self.version >= 22,
metadata_size: self.metadata_size,
data_offset: self.data_offset,
}
}
pub fn validate(&self) -> Result<()> {
if !self.is_valid() {
return Err(BinaryError::invalid_data("Invalid SerializedFile header"));
}
if self.metadata_size == 0 {
return Err(BinaryError::invalid_data("Metadata size cannot be zero"));
}
if self.data_offset < self.metadata_size as u64 {
return Err(BinaryError::invalid_data(
"Data offset cannot be less than metadata size",
));
}
if self.file_size < self.data_offset {
return Err(BinaryError::invalid_data(
"File size cannot be less than data offset",
));
}
Ok(())
}
pub fn header_size(&self) -> u32 {
if self.version >= 22 {
4 + 4 + 4 + 4 + 1 + 3 + 4 + 8 + 8 + 8 } else if self.version >= 9 {
4 + 4 + 4 + 4 + 1 + 3 } else {
4 + 4 + 4 + 4 }
}
pub fn supports_type_trees(&self) -> bool {
self.version >= 7
}
pub fn supports_script_types(&self) -> bool {
self.version >= 11
}
pub fn uses_new_object_format(&self) -> bool {
self.version >= 14
}
}
impl Default for SerializedFileHeader {
fn default() -> Self {
Self {
metadata_size: 0,
file_size: 0,
version: 19, data_offset: 0,
endian: 0, reserved: [0; 3],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderFormatInfo {
pub version: u32,
pub is_big_endian: bool,
pub has_extended_format: bool,
pub supports_large_files: bool,
pub metadata_size: u32,
pub data_offset: u64,
}
fn i64_to_u64_checked(value: i64, name: &'static str) -> Result<u64> {
if value < 0 {
return Err(BinaryError::invalid_data(format!(
"Invalid {}: negative value {}",
name, value
)));
}
Ok(value as u64)
}
#[derive(Debug, Clone)]
pub struct HeaderValidation {
pub is_valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl HeaderValidation {
pub fn new() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, error: String) {
self.is_valid = false;
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: String) {
self.warnings.push(warning);
}
}
impl Default for HeaderValidation {
fn default() -> Self {
Self::new()
}
}
pub fn validate_header(header: &SerializedFileHeader) -> HeaderValidation {
let mut validation = HeaderValidation::new();
if let Err(e) = header.validate() {
validation.add_error(e.to_string());
return validation;
}
if header.version < 7 {
validation.add_warning("Very old Unity version, limited feature support".to_string());
}
if header.version > 50 {
validation.add_warning("Very new Unity version, may have compatibility issues".to_string());
}
if header.endian != 0 {
validation.add_warning("Big-endian format detected, ensure proper handling".to_string());
}
if header.file_size > 1024_u64 * 1024 * 1024 {
validation.add_warning("Large file size (>1GB), may impact performance".to_string());
}
validation
}
pub mod versions {
pub const MIN_SUPPORTED: u32 = 5;
pub const FIRST_WITH_TYPETREE: u32 = 7;
pub const FIRST_WITH_ENDIAN_FLAG: u32 = 9;
pub const FIRST_WITH_SCRIPT_TYPES: u32 = 11;
pub const FIRST_WITH_NEW_OBJECTS: u32 = 14;
pub const FIRST_WITH_EXTENDED_FORMAT: u32 = 22;
pub const CURRENT_RECOMMENDED: u32 = 19;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_validation() {
let header = SerializedFileHeader {
version: 19,
file_size: 1000,
data_offset: 100,
metadata_size: 50,
..Default::default()
};
assert!(header.is_valid());
assert!(header.validate().is_ok());
}
#[test]
fn test_byte_order() {
#[allow(clippy::field_reassign_with_default)]
{
let mut header = SerializedFileHeader::default();
header.endian = 0;
assert_eq!(header.byte_order(), ByteOrder::Little);
header.endian = 1;
assert_eq!(header.byte_order(), ByteOrder::Big);
}
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_version_features() {
let mut header = SerializedFileHeader::default();
header.version = 6;
assert!(!header.supports_type_trees());
header.version = 7;
assert!(header.supports_type_trees());
header.version = 11;
assert!(header.supports_script_types());
header.version = 22;
assert!(header.uses_new_object_format());
}
}