unity_asset_binary/bundle/
header.rs

1//! AssetBundle header parsing
2//!
3//! This module handles the parsing of AssetBundle headers,
4//! supporting both legacy and UnityFS formats.
5
6use crate::compression::{ArchiveFlags, CompressionType};
7use crate::error::{BinaryError, Result};
8use crate::reader::BinaryReader;
9use serde::{Deserialize, Serialize};
10
11/// AssetBundle header information
12///
13/// Contains metadata about the bundle including version, compression settings,
14/// and structural information needed for parsing the bundle contents.
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct BundleHeader {
17    /// Bundle signature (e.g., "UnityFS", "UnityWeb", "UnityRaw")
18    pub signature: String,
19    /// Bundle format version
20    pub version: u32,
21    /// Unity version that created this bundle
22    pub unity_version: String,
23    /// Unity revision
24    pub unity_revision: String,
25    /// Total bundle size
26    pub size: u64,
27    /// Compressed blocks info size
28    pub compressed_blocks_info_size: u32,
29    /// Uncompressed blocks info size
30    pub uncompressed_blocks_info_size: u32,
31    /// Archive flags (compression type, block info location, etc.)
32    pub flags: u32,
33    /// Actual header size (recorded during parsing)
34    pub actual_header_size: u64,
35}
36
37impl BundleHeader {
38    /// Parse bundle header from binary data
39    ///
40    /// This method reads the bundle header from a binary reader,
41    /// handling different bundle formats (UnityFS, UnityWeb, etc.).
42    pub fn from_reader(reader: &mut BinaryReader) -> Result<Self> {
43        let signature = reader.read_cstring()?;
44        let version = reader.read_u32()?;
45        let unity_version = reader.read_cstring()?;
46        let unity_revision = reader.read_cstring()?;
47
48        let mut header = Self {
49            signature: signature.clone(),
50            version,
51            unity_version,
52            unity_revision,
53            size: 0,
54            compressed_blocks_info_size: 0,
55            uncompressed_blocks_info_size: 0,
56            flags: 0,
57            actual_header_size: 0,
58        };
59
60        // Read additional fields based on bundle format
61        match signature.as_str() {
62            "UnityFS" => {
63                // Modern UnityFS format
64                header.size = reader.read_i64()? as u64;
65                header.compressed_blocks_info_size = reader.read_u32()?;
66                header.uncompressed_blocks_info_size = reader.read_u32()?;
67                header.flags = reader.read_u32()?;
68            }
69            "UnityWeb" | "UnityRaw" => {
70                // Legacy formats
71                header.size = reader.read_u32()? as u64;
72                // Legacy formats don't have block info sizes or flags
73                header.compressed_blocks_info_size = 0;
74                header.uncompressed_blocks_info_size = 0;
75                header.flags = 0;
76
77                // Skip padding byte for some legacy versions
78                if version < 6 {
79                    reader.read_u8()?;
80                }
81            }
82            _ => {
83                return Err(BinaryError::unsupported(format!(
84                    "Unknown bundle signature: {}",
85                    signature
86                )));
87            }
88        }
89
90        // Record the actual header size
91        header.actual_header_size = reader.position();
92
93        Ok(header)
94    }
95
96    /// Get the compression type from flags
97    pub fn compression_type(&self) -> Result<CompressionType> {
98        CompressionType::from_flags(self.flags & ArchiveFlags::COMPRESSION_TYPE_MASK)
99    }
100
101    /// Check if block info is at the end of the file
102    pub fn block_info_at_end(&self) -> bool {
103        (self.flags & ArchiveFlags::BLOCK_INFO_AT_END) != 0
104    }
105
106    /// Check if this is a UnityFS format bundle
107    pub fn is_unity_fs(&self) -> bool {
108        self.signature == "UnityFS"
109    }
110
111    /// Check if this is a legacy format bundle
112    pub fn is_legacy(&self) -> bool {
113        matches!(self.signature.as_str(), "UnityWeb" | "UnityRaw")
114    }
115
116    /// Get the expected data offset after the header
117    pub fn data_offset(&self) -> u64 {
118        // This is typically calculated based on header size and block info location
119        if self.block_info_at_end() {
120            // Block info is at the end, data starts right after header
121            self.header_size()
122        } else {
123            // Block info is at the beginning, data starts after block info
124            self.header_size() + self.compressed_blocks_info_size as u64
125        }
126    }
127
128    /// Calculate the size of the header itself
129    pub fn header_size(&self) -> u64 {
130        // Use the actual header size recorded during parsing
131        // This is more accurate than calculating it
132        if self.actual_header_size > 0 {
133            self.actual_header_size
134        } else {
135            // Fallback to calculation if actual size not recorded
136            let base_size = match self.signature.as_str() {
137                "UnityFS" => {
138                    // Signature + version + unity_version + unity_revision + size + compressed_size + uncompressed_size + flags
139                    self.signature.len()
140                        + 1
141                        + 4
142                        + self.unity_version.len()
143                        + 1
144                        + self.unity_revision.len()
145                        + 1
146                        + 8
147                        + 4
148                        + 4
149                        + 4
150                }
151                "UnityWeb" | "UnityRaw" => {
152                    // Signature + version + unity_version + unity_revision + size
153                    self.signature.len()
154                        + 1
155                        + 4
156                        + self.unity_version.len()
157                        + 1
158                        + self.unity_revision.len()
159                        + 1
160                        + 4
161                }
162                _ => 0,
163            };
164
165            // Add padding for alignment
166            let aligned_size = (base_size + 15) & !15; // Align to 16 bytes
167            aligned_size as u64
168        }
169    }
170
171    /// Validate the header for consistency
172    pub fn validate(&self) -> Result<()> {
173        if self.signature.is_empty() {
174            return Err(BinaryError::invalid_data("Empty bundle signature"));
175        }
176
177        if !matches!(self.signature.as_str(), "UnityFS" | "UnityWeb" | "UnityRaw") {
178            return Err(BinaryError::unsupported(format!(
179                "Unsupported bundle signature: {}",
180                self.signature
181            )));
182        }
183
184        if self.version == 0 {
185            return Err(BinaryError::invalid_data("Invalid bundle version"));
186        }
187
188        if self.size == 0 {
189            return Err(BinaryError::invalid_data("Invalid bundle size"));
190        }
191
192        // UnityFS specific validations
193        if self.is_unity_fs() {
194            if self.compressed_blocks_info_size == 0 && self.uncompressed_blocks_info_size == 0 {
195                return Err(BinaryError::invalid_data("Invalid block info sizes"));
196            }
197
198            // Validate compression type
199            self.compression_type()?;
200        }
201
202        Ok(())
203    }
204
205    /// Get bundle format information
206    pub fn format_info(&self) -> BundleFormatInfo {
207        BundleFormatInfo {
208            signature: self.signature.clone(),
209            version: self.version,
210            is_compressed: self
211                .compression_type()
212                .map(|ct| ct != CompressionType::None)
213                .unwrap_or(false),
214            supports_streaming: self.is_unity_fs(),
215            has_directory_info: self.is_unity_fs(),
216        }
217    }
218}
219
220/// Bundle format information
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BundleFormatInfo {
223    pub signature: String,
224    pub version: u32,
225    pub is_compressed: bool,
226    pub supports_streaming: bool,
227    pub has_directory_info: bool,
228}
229
230/// Bundle signature constants
231pub mod signatures {
232    pub const UNITY_FS: &str = "UnityFS";
233    pub const UNITY_WEB: &str = "UnityWeb";
234    pub const UNITY_RAW: &str = "UnityRaw";
235}
236
237/// Bundle version constants
238pub mod versions {
239    pub const UNITY_FS_MIN: u32 = 6;
240    pub const UNITY_FS_CURRENT: u32 = 7;
241    pub const UNITY_WEB_MIN: u32 = 3;
242    pub const UNITY_RAW_MIN: u32 = 1;
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_bundle_header_validation() {
251        let mut header = BundleHeader::default();
252
253        // Empty header should fail validation
254        assert!(header.validate().is_err());
255
256        // Set minimum required fields
257        header.signature = "UnityFS".to_string();
258        header.version = 6;
259        header.size = 1000;
260        header.compressed_blocks_info_size = 100;
261        header.uncompressed_blocks_info_size = 200;
262
263        // Should now pass validation
264        assert!(header.validate().is_ok());
265    }
266
267    #[test]
268    fn test_bundle_format_detection() {
269        let header = BundleHeader {
270            signature: "UnityFS".to_string(),
271            version: 6,
272            ..Default::default()
273        };
274
275        assert!(header.is_unity_fs());
276        assert!(!header.is_legacy());
277
278        let legacy_header = BundleHeader {
279            signature: "UnityWeb".to_string(),
280            version: 3,
281            ..Default::default()
282        };
283
284        assert!(!legacy_header.is_unity_fs());
285        assert!(legacy_header.is_legacy());
286    }
287}