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