unity_asset_binary/bundle/
types.rs

1//! Bundle data structures
2//!
3//! This module defines the core data structures used for bundle processing.
4
5use super::header::BundleHeader;
6use crate::asset::Asset;
7use crate::compression::CompressionBlock;
8use serde::{Deserialize, Serialize};
9
10/// Information about a file within the bundle
11///
12/// Represents a single file entry in the bundle's directory structure.
13#[derive(Debug, Clone, Serialize, Deserialize, Default)]
14pub struct BundleFileInfo {
15    /// Offset within the bundle data
16    pub offset: u64,
17    /// Size of the file
18    pub size: u64,
19    /// File name
20    pub name: String,
21}
22
23impl BundleFileInfo {
24    /// Create a new BundleFileInfo
25    pub fn new(name: String, offset: u64, size: u64) -> Self {
26        Self { name, offset, size }
27    }
28
29    /// Check if this file has valid properties
30    pub fn is_valid(&self) -> bool {
31        !self.name.is_empty() && self.size > 0
32    }
33
34    /// Get the end offset of this file
35    pub fn end_offset(&self) -> u64 {
36        self.offset + self.size
37    }
38}
39
40/// Directory node in the bundle
41///
42/// Represents a node in the bundle's internal directory structure,
43/// which can be either a file or a directory.
44#[derive(Debug, Clone, Serialize, Deserialize, Default)]
45pub struct DirectoryNode {
46    /// Node name
47    pub name: String,
48    /// Offset in the bundle
49    pub offset: u64,
50    /// Size of the data
51    pub size: u64,
52    /// Flags (indicates file type, compression, etc.)
53    pub flags: u32,
54}
55
56impl DirectoryNode {
57    /// Create a new DirectoryNode
58    pub fn new(name: String, offset: u64, size: u64, flags: u32) -> Self {
59        Self {
60            name,
61            offset,
62            size,
63            flags,
64        }
65    }
66
67    /// Check if this node represents a file
68    pub fn is_file(&self) -> bool {
69        // Unity uses bit 2 (0x4) to indicate files, not bit 0 (0x1)
70        (self.flags & 0x4) != 0
71    }
72
73    /// Check if this node represents a directory
74    pub fn is_directory(&self) -> bool {
75        !self.is_file()
76    }
77
78    /// Check if this node's data is compressed
79    pub fn is_compressed(&self) -> bool {
80        (self.flags & 0x2) != 0
81    }
82
83    /// Get the end offset of this node
84    pub fn end_offset(&self) -> u64 {
85        self.offset + self.size
86    }
87}
88
89/// A Unity AssetBundle
90///
91/// This structure represents a complete Unity AssetBundle with all its
92/// metadata, compression information, and contained assets.
93#[derive(Debug)]
94pub struct AssetBundle {
95    /// Bundle header
96    pub header: BundleHeader,
97    /// Compression blocks
98    pub blocks: Vec<CompressionBlock>,
99    /// Directory nodes
100    pub nodes: Vec<DirectoryNode>,
101    /// File information
102    pub files: Vec<BundleFileInfo>,
103    /// Contained assets
104    pub assets: Vec<Asset>,
105    /// Raw bundle data
106    data: Vec<u8>,
107}
108
109impl AssetBundle {
110    /// Create a new AssetBundle
111    pub fn new(header: BundleHeader, data: Vec<u8>) -> Self {
112        Self {
113            header,
114            blocks: Vec::new(),
115            nodes: Vec::new(),
116            files: Vec::new(),
117            assets: Vec::new(),
118            data,
119        }
120    }
121
122    /// Get the raw bundle data
123    pub fn data(&self) -> &[u8] {
124        &self.data
125    }
126
127    /// Get mutable access to the raw bundle data
128    pub fn data_mut(&mut self) -> &mut Vec<u8> {
129        &mut self.data
130    }
131
132    /// Get the total size of the bundle
133    pub fn size(&self) -> u64 {
134        self.data.len() as u64
135    }
136
137    /// Check if the bundle is compressed
138    pub fn is_compressed(&self) -> bool {
139        !self.blocks.is_empty()
140            && self.blocks.iter().any(|block| {
141                block
142                    .compression_type()
143                    .unwrap_or(crate::compression::CompressionType::None)
144                    != crate::compression::CompressionType::None
145            })
146    }
147
148    /// Get the number of files in the bundle
149    pub fn file_count(&self) -> usize {
150        self.files.len()
151    }
152
153    /// Get the number of assets in the bundle
154    pub fn asset_count(&self) -> usize {
155        self.assets.len()
156    }
157
158    /// Find a file by name
159    pub fn find_file(&self, name: &str) -> Option<&BundleFileInfo> {
160        self.files.iter().find(|file| file.name == name)
161    }
162
163    /// Find a node by name
164    pub fn find_node(&self, name: &str) -> Option<&DirectoryNode> {
165        self.nodes.iter().find(|node| node.name == name)
166    }
167
168    /// Get all file names
169    pub fn file_names(&self) -> Vec<&str> {
170        self.files.iter().map(|file| file.name.as_str()).collect()
171    }
172
173    /// Get all node names
174    pub fn node_names(&self) -> Vec<&str> {
175        self.nodes.iter().map(|node| node.name.as_str()).collect()
176    }
177
178    /// Extract data for a specific file
179    pub fn extract_file_data(&self, file: &BundleFileInfo) -> crate::error::Result<Vec<u8>> {
180        if file.offset + file.size > self.data.len() as u64 {
181            return Err(crate::error::BinaryError::invalid_data(
182                "File offset/size exceeds bundle data",
183            ));
184        }
185
186        let start = file.offset as usize;
187        let end = (file.offset + file.size) as usize;
188        Ok(self.data[start..end].to_vec())
189    }
190
191    /// Extract data for a specific node
192    pub fn extract_node_data(&self, node: &DirectoryNode) -> crate::error::Result<Vec<u8>> {
193        if node.offset + node.size > self.data.len() as u64 {
194            return Err(crate::error::BinaryError::invalid_data(
195                "Node offset/size exceeds bundle data",
196            ));
197        }
198
199        let start = node.offset as usize;
200        let end = (node.offset + node.size) as usize;
201        Ok(self.data[start..end].to_vec())
202    }
203
204    /// Get bundle statistics
205    pub fn statistics(&self) -> BundleStatistics {
206        let total_compressed_size: u64 = self.blocks.iter().map(|b| b.compressed_size as u64).sum();
207        let total_uncompressed_size: u64 =
208            self.blocks.iter().map(|b| b.uncompressed_size as u64).sum();
209
210        BundleStatistics {
211            total_size: self.size(),
212            header_size: self.header.header_size(),
213            compressed_size: total_compressed_size,
214            uncompressed_size: total_uncompressed_size,
215            compression_ratio: if total_uncompressed_size > 0 {
216                total_compressed_size as f64 / total_uncompressed_size as f64
217            } else {
218                1.0
219            },
220            file_count: self.file_count(),
221            asset_count: self.asset_count(),
222            block_count: self.blocks.len(),
223            node_count: self.nodes.len(),
224        }
225    }
226
227    /// Validate bundle consistency
228    pub fn validate(&self) -> crate::error::Result<()> {
229        // Validate header
230        self.header.validate()?;
231
232        // Validate files don't exceed bundle size
233        for file in &self.files {
234            if file.end_offset() > self.size() {
235                return Err(crate::error::BinaryError::invalid_data(format!(
236                    "File '{}' exceeds bundle size",
237                    file.name
238                )));
239            }
240        }
241
242        // Validate nodes don't exceed bundle size
243        for node in &self.nodes {
244            if node.end_offset() > self.size() {
245                return Err(crate::error::BinaryError::invalid_data(format!(
246                    "Node '{}' exceeds bundle size",
247                    node.name
248                )));
249            }
250        }
251
252        Ok(())
253    }
254}
255
256/// Bundle statistics
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct BundleStatistics {
259    pub total_size: u64,
260    pub header_size: u64,
261    pub compressed_size: u64,
262    pub uncompressed_size: u64,
263    pub compression_ratio: f64,
264    pub file_count: usize,
265    pub asset_count: usize,
266    pub block_count: usize,
267    pub node_count: usize,
268}
269
270/// Bundle loading options
271#[derive(Debug, Clone)]
272pub struct BundleLoadOptions {
273    /// Whether to load all assets immediately
274    pub load_assets: bool,
275    /// Whether to decompress all blocks immediately
276    pub decompress_blocks: bool,
277    /// Whether to validate the bundle structure
278    pub validate: bool,
279    /// Maximum memory usage for decompression (in bytes)
280    pub max_memory: Option<usize>,
281}
282
283impl Default for BundleLoadOptions {
284    fn default() -> Self {
285        Self {
286            load_assets: true,
287            decompress_blocks: false, // Lazy decompression by default
288            validate: true,
289            max_memory: Some(1024 * 1024 * 1024), // 1GB default limit
290        }
291    }
292}
293
294impl BundleLoadOptions {
295    /// Create options for fast loading (minimal processing)
296    pub fn fast() -> Self {
297        Self {
298            load_assets: false,
299            decompress_blocks: false,
300            validate: false,
301            max_memory: None,
302        }
303    }
304
305    /// Create options for complete loading (all processing)
306    pub fn complete() -> Self {
307        Self {
308            load_assets: true,
309            decompress_blocks: true,
310            validate: true,
311            max_memory: Some(2048 * 1024 * 1024), // 2GB for complete loading
312        }
313    }
314}