unity_asset_binary/asset/
header.rs

1//! SerializedFile header parsing
2//!
3//! This module handles the parsing of Unity SerializedFile headers,
4//! supporting different Unity versions and formats.
5
6use crate::error::{BinaryError, Result};
7use crate::reader::{BinaryReader, ByteOrder};
8use serde::{Deserialize, Serialize};
9
10/// Header of a Unity SerializedFile
11///
12/// Contains metadata about the serialized file including version information,
13/// data layout, and endianness settings.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SerializedFileHeader {
16    /// Size of the metadata section
17    pub metadata_size: u32,
18    /// Total file size
19    pub file_size: u64,
20    /// File format version
21    pub version: u32,
22    /// Offset to the data section
23    pub data_offset: u64,
24    /// Endianness (0 = little, 1 = big)
25    pub endian: u8,
26    /// Reserved bytes
27    pub reserved: [u8; 3],
28}
29
30impl SerializedFileHeader {
31    /// Parse header from binary data (improved based on unity-rs)
32    pub fn from_reader(reader: &mut BinaryReader) -> Result<Self> {
33        let mut metadata_size = reader.read_u32()?;
34        let mut file_size = reader.read_u32()? as u64;
35        let version = reader.read_u32()?;
36        let mut data_offset = reader.read_u32()? as u64;
37
38        let endian;
39        let mut reserved = [0u8; 3];
40
41        // Handle different Unity versions (based on unity-rs logic)
42        if version >= 9 {
43            endian = reader.read_u8()?;
44            let reserved_bytes = reader.read_bytes(3)?;
45            reserved.copy_from_slice(&reserved_bytes);
46        } else {
47            // For older versions, endian is at the end of metadata
48            let current_pos = reader.position();
49            let endian_pos = file_size.checked_sub(metadata_size as u64).ok_or_else(|| {
50                BinaryError::invalid_data("Invalid header: file_size < metadata_size")
51            })?;
52            reader.set_position(endian_pos)?;
53            endian = reader.read_u8()?;
54            reader.set_position(current_pos)?;
55        }
56
57        // Handle version 22+ format changes
58        if version >= 22 {
59            metadata_size = reader.read_u32()?;
60            file_size = i64_to_u64_checked(reader.read_i64()?, "file_size")?;
61            data_offset = i64_to_u64_checked(reader.read_i64()?, "data_offset")?;
62            reader.read_i64()?; // Skip unknown field
63        }
64
65        Ok(Self {
66            metadata_size,
67            file_size,
68            version,
69            data_offset,
70            endian,
71            reserved,
72        })
73    }
74
75    /// Get the byte order from the endian flag
76    pub fn byte_order(&self) -> ByteOrder {
77        if self.endian == 0 {
78            ByteOrder::Little
79        } else {
80            ByteOrder::Big
81        }
82    }
83
84    /// Check if this is a valid Unity file header
85    pub fn is_valid(&self) -> bool {
86        // Basic sanity checks
87        self.version > 0
88            && self.version < 100
89            && self.data_offset > 0
90            && self.file_size > self.data_offset
91    }
92
93    /// Get header format information
94    pub fn format_info(&self) -> HeaderFormatInfo {
95        HeaderFormatInfo {
96            version: self.version,
97            is_big_endian: self.endian != 0,
98            has_extended_format: self.version >= 22,
99            supports_large_files: self.version >= 22,
100            metadata_size: self.metadata_size,
101            data_offset: self.data_offset,
102        }
103    }
104
105    /// Validate header consistency
106    pub fn validate(&self) -> Result<()> {
107        if !self.is_valid() {
108            return Err(BinaryError::invalid_data("Invalid SerializedFile header"));
109        }
110
111        if self.metadata_size == 0 {
112            return Err(BinaryError::invalid_data("Metadata size cannot be zero"));
113        }
114
115        if self.data_offset < self.metadata_size as u64 {
116            return Err(BinaryError::invalid_data(
117                "Data offset cannot be less than metadata size",
118            ));
119        }
120
121        if self.file_size < self.data_offset {
122            return Err(BinaryError::invalid_data(
123                "File size cannot be less than data offset",
124            ));
125        }
126
127        Ok(())
128    }
129
130    /// Get the size of the header itself
131    pub fn header_size(&self) -> u32 {
132        if self.version >= 22 {
133            // Extended format: metadata_size + file_size + version + data_offset + endian + reserved + extended fields
134            4 + 4 + 4 + 4 + 1 + 3 + 4 + 8 + 8 + 8 // 48 bytes
135        } else if self.version >= 9 {
136            // Standard format: metadata_size + file_size + version + data_offset + endian + reserved
137            4 + 4 + 4 + 4 + 1 + 3 // 20 bytes
138        } else {
139            // Legacy format: metadata_size + file_size + version + data_offset (endian at end)
140            4 + 4 + 4 + 4 // 16 bytes
141        }
142    }
143
144    /// Check if this version supports TypeTrees
145    pub fn supports_type_trees(&self) -> bool {
146        self.version >= 7
147    }
148
149    /// Check if this version supports script types
150    pub fn supports_script_types(&self) -> bool {
151        self.version >= 11
152    }
153
154    /// Check if this version uses the new object format
155    pub fn uses_new_object_format(&self) -> bool {
156        self.version >= 14
157    }
158}
159
160impl Default for SerializedFileHeader {
161    fn default() -> Self {
162        Self {
163            metadata_size: 0,
164            file_size: 0,
165            version: 19, // Default to Unity 2019+ format
166            data_offset: 0,
167            endian: 0, // Little endian by default
168            reserved: [0; 3],
169        }
170    }
171}
172
173/// Header format information
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct HeaderFormatInfo {
176    pub version: u32,
177    pub is_big_endian: bool,
178    pub has_extended_format: bool,
179    pub supports_large_files: bool,
180    pub metadata_size: u32,
181    pub data_offset: u64,
182}
183
184fn i64_to_u64_checked(value: i64, name: &'static str) -> Result<u64> {
185    if value < 0 {
186        return Err(BinaryError::invalid_data(format!(
187            "Invalid {}: negative value {}",
188            name, value
189        )));
190    }
191    Ok(value as u64)
192}
193
194/// Header validation result
195#[derive(Debug, Clone)]
196pub struct HeaderValidation {
197    pub is_valid: bool,
198    pub errors: Vec<String>,
199    pub warnings: Vec<String>,
200}
201
202impl HeaderValidation {
203    pub fn new() -> Self {
204        Self {
205            is_valid: true,
206            errors: Vec::new(),
207            warnings: Vec::new(),
208        }
209    }
210
211    pub fn add_error(&mut self, error: String) {
212        self.is_valid = false;
213        self.errors.push(error);
214    }
215
216    pub fn add_warning(&mut self, warning: String) {
217        self.warnings.push(warning);
218    }
219}
220
221impl Default for HeaderValidation {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227/// Comprehensive header validation
228pub fn validate_header(header: &SerializedFileHeader) -> HeaderValidation {
229    let mut validation = HeaderValidation::new();
230
231    // Basic validation
232    if let Err(e) = header.validate() {
233        validation.add_error(e.to_string());
234        return validation;
235    }
236
237    // Version-specific warnings
238    if header.version < 7 {
239        validation.add_warning("Very old Unity version, limited feature support".to_string());
240    }
241
242    if header.version > 50 {
243        validation.add_warning("Very new Unity version, may have compatibility issues".to_string());
244    }
245
246    // Endianness warnings
247    if header.endian != 0 {
248        validation.add_warning("Big-endian format detected, ensure proper handling".to_string());
249    }
250
251    // Size warnings
252    if header.file_size > 1024_u64 * 1024 * 1024 {
253        validation.add_warning("Large file size (>1GB), may impact performance".to_string());
254    }
255
256    validation
257}
258
259/// Unity version constants for header validation
260pub mod versions {
261    pub const MIN_SUPPORTED: u32 = 5;
262    pub const FIRST_WITH_TYPETREE: u32 = 7;
263    pub const FIRST_WITH_ENDIAN_FLAG: u32 = 9;
264    pub const FIRST_WITH_SCRIPT_TYPES: u32 = 11;
265    pub const FIRST_WITH_NEW_OBJECTS: u32 = 14;
266    pub const FIRST_WITH_EXTENDED_FORMAT: u32 = 22;
267    pub const CURRENT_RECOMMENDED: u32 = 19;
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn test_header_validation() {
276        let header = SerializedFileHeader {
277            version: 19,
278            file_size: 1000,
279            data_offset: 100,
280            metadata_size: 50,
281            ..Default::default()
282        };
283
284        assert!(header.is_valid());
285        assert!(header.validate().is_ok());
286    }
287
288    #[test]
289    fn test_byte_order() {
290        #[allow(clippy::field_reassign_with_default)]
291        {
292            let mut header = SerializedFileHeader::default();
293
294            header.endian = 0;
295            assert_eq!(header.byte_order(), ByteOrder::Little);
296
297            header.endian = 1;
298            assert_eq!(header.byte_order(), ByteOrder::Big);
299        }
300    }
301
302    #[test]
303    #[allow(clippy::field_reassign_with_default)]
304    fn test_version_features() {
305        let mut header = SerializedFileHeader::default();
306
307        header.version = 6;
308        assert!(!header.supports_type_trees());
309
310        header.version = 7;
311        assert!(header.supports_type_trees());
312
313        header.version = 11;
314        assert!(header.supports_script_types());
315
316        header.version = 22;
317        assert!(header.uses_new_object_format());
318    }
319}