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                let size = reader.read_i64()?;
65                if size < 0 {
66                    return Err(BinaryError::invalid_data(format!(
67                        "Negative bundle size in header: {}",
68                        size
69                    )));
70                }
71                header.size = size as u64;
72                header.compressed_blocks_info_size = reader.read_u32()?;
73                header.uncompressed_blocks_info_size = reader.read_u32()?;
74                header.flags = reader.read_u32()?;
75            }
76            "UnityWeb" | "UnityRaw" => {
77                // Legacy formats
78                header.size = reader.read_u32()? as u64;
79                // Legacy formats don't have block info sizes or flags
80                header.compressed_blocks_info_size = 0;
81                header.uncompressed_blocks_info_size = 0;
82                header.flags = 0;
83
84                // Skip padding byte for some legacy versions
85                if version < 6 {
86                    reader.read_u8()?;
87                }
88            }
89            _ => {
90                return Err(BinaryError::unsupported(format!(
91                    "Unknown bundle signature: {}",
92                    signature
93                )));
94            }
95        }
96
97        // Record the actual header size
98        header.actual_header_size = reader.position();
99
100        Ok(header)
101    }
102
103    /// Get the compression type from flags
104    pub fn compression_type(&self) -> Result<CompressionType> {
105        CompressionType::from_flags(self.flags & ArchiveFlags::COMPRESSION_TYPE_MASK)
106    }
107
108    /// Check if block info is at the end of the file
109    pub fn block_info_at_end(&self) -> bool {
110        (self.flags & ArchiveFlags::BLOCK_INFO_AT_END) != 0
111    }
112
113    /// Check if this is a UnityFS format bundle
114    pub fn is_unity_fs(&self) -> bool {
115        self.signature == "UnityFS"
116    }
117
118    /// Check if this is a legacy format bundle
119    pub fn is_legacy(&self) -> bool {
120        matches!(self.signature.as_str(), "UnityWeb" | "UnityRaw")
121    }
122
123    /// Get the expected data offset after the header
124    pub fn data_offset(&self) -> u64 {
125        // This is typically calculated based on header size and block info location
126        if self.block_info_at_end() {
127            // Block info is at the end, data starts right after header
128            self.header_size()
129        } else {
130            // Block info is at the beginning, data starts after block info
131            self.header_size() + self.compressed_blocks_info_size as u64
132        }
133    }
134
135    /// Calculate the size of the header itself
136    pub fn header_size(&self) -> u64 {
137        // Use the actual header size recorded during parsing
138        // This is more accurate than calculating it
139        if self.actual_header_size > 0 {
140            self.actual_header_size
141        } else {
142            // Fallback to calculation if actual size not recorded
143            let base_size = match self.signature.as_str() {
144                "UnityFS" => {
145                    // Signature + version + unity_version + unity_revision + size + compressed_size + uncompressed_size + flags
146                    self.signature.len()
147                        + 1
148                        + 4
149                        + self.unity_version.len()
150                        + 1
151                        + self.unity_revision.len()
152                        + 1
153                        + 8
154                        + 4
155                        + 4
156                        + 4
157                }
158                "UnityWeb" | "UnityRaw" => {
159                    // Signature + version + unity_version + unity_revision + size
160                    self.signature.len()
161                        + 1
162                        + 4
163                        + self.unity_version.len()
164                        + 1
165                        + self.unity_revision.len()
166                        + 1
167                        + 4
168                }
169                _ => 0,
170            };
171
172            // Add padding for alignment
173            let aligned_size = (base_size + 15) & !15; // Align to 16 bytes
174            aligned_size as u64
175        }
176    }
177
178    /// Validate the header for consistency
179    pub fn validate(&self) -> Result<()> {
180        if self.signature.is_empty() {
181            return Err(BinaryError::invalid_data("Empty bundle signature"));
182        }
183
184        if !matches!(self.signature.as_str(), "UnityFS" | "UnityWeb" | "UnityRaw") {
185            return Err(BinaryError::unsupported(format!(
186                "Unsupported bundle signature: {}",
187                self.signature
188            )));
189        }
190
191        if self.version == 0 {
192            return Err(BinaryError::invalid_data("Invalid bundle version"));
193        }
194
195        if self.size == 0 {
196            return Err(BinaryError::invalid_data("Invalid bundle size"));
197        }
198
199        // UnityFS specific validations
200        if self.is_unity_fs() {
201            if self.compressed_blocks_info_size == 0 && self.uncompressed_blocks_info_size == 0 {
202                return Err(BinaryError::invalid_data("Invalid block info sizes"));
203            }
204
205            // Validate compression type
206            self.compression_type()?;
207        }
208
209        Ok(())
210    }
211
212    /// Get bundle format information
213    pub fn format_info(&self) -> BundleFormatInfo {
214        BundleFormatInfo {
215            signature: self.signature.clone(),
216            version: self.version,
217            is_compressed: self
218                .compression_type()
219                .map(|ct| ct != CompressionType::None)
220                .unwrap_or(false),
221            supports_streaming: self.is_unity_fs(),
222            has_directory_info: self.is_unity_fs(),
223        }
224    }
225}
226
227/// Bundle format information
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct BundleFormatInfo {
230    pub signature: String,
231    pub version: u32,
232    pub is_compressed: bool,
233    pub supports_streaming: bool,
234    pub has_directory_info: bool,
235}
236
237/// Bundle signature constants
238pub mod signatures {
239    pub const UNITY_FS: &str = "UnityFS";
240    pub const UNITY_WEB: &str = "UnityWeb";
241    pub const UNITY_RAW: &str = "UnityRaw";
242}
243
244/// Bundle version constants
245pub mod versions {
246    pub const UNITY_FS_MIN: u32 = 6;
247    pub const UNITY_FS_CURRENT: u32 = 7;
248    pub const UNITY_WEB_MIN: u32 = 3;
249    pub const UNITY_RAW_MIN: u32 = 1;
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_bundle_header_validation() {
258        // Empty header should fail validation
259        let empty = BundleHeader::default();
260        assert!(empty.validate().is_err());
261
262        // Minimum required fields should pass validation
263        let header = BundleHeader {
264            signature: "UnityFS".to_string(),
265            version: 6,
266            size: 1000,
267            compressed_blocks_info_size: 100,
268            uncompressed_blocks_info_size: 200,
269            ..Default::default()
270        };
271        assert!(header.validate().is_ok());
272    }
273
274    #[test]
275    fn test_bundle_format_detection() {
276        let header = BundleHeader {
277            signature: "UnityFS".to_string(),
278            version: 6,
279            ..Default::default()
280        };
281
282        assert!(header.is_unity_fs());
283        assert!(!header.is_legacy());
284
285        let legacy_header = BundleHeader {
286            signature: "UnityWeb".to_string(),
287            version: 3,
288            ..Default::default()
289        };
290
291        assert!(!legacy_header.is_unity_fs());
292        assert!(legacy_header.is_legacy());
293    }
294}