unity_asset_binary/bundle/
compression.rs

1//! Bundle compression handling
2//!
3//! This module provides compression and decompression functionality
4//! for Unity AssetBundle blocks, supporting LZ4, LZMA, and Brotli.
5
6use super::header::BundleHeader;
7use crate::compression::{CompressionBlock, CompressionType, decompress};
8use crate::error::{BinaryError, Result};
9use crate::reader::{BinaryReader, ByteOrder};
10
11/// Bundle compression handler
12///
13/// This struct provides methods for handling compressed bundle data,
14/// including block info decompression and data block processing.
15pub struct BundleCompression;
16
17impl BundleCompression {
18    /// Decompress blocks info data
19    ///
20    /// This method handles the decompression of the blocks information
21    /// section of a bundle, which contains metadata about all compression blocks.
22    pub fn decompress_blocks_info(
23        header: &BundleHeader,
24        compressed_data: &[u8],
25    ) -> Result<Vec<u8>> {
26        Self::decompress_blocks_info_limited(header, compressed_data, None)
27    }
28
29    pub fn decompress_blocks_info_limited(
30        header: &BundleHeader,
31        compressed_data: &[u8],
32        max_uncompressed_size: Option<usize>,
33    ) -> Result<Vec<u8>> {
34        let expected_uncompressed = header.uncompressed_blocks_info_size as usize;
35        if let Some(limit) = max_uncompressed_size
36            && expected_uncompressed > limit
37        {
38            return Err(BinaryError::ResourceLimitExceeded(format!(
39                "Blocks info uncompressed size {} exceeds limit {}",
40                expected_uncompressed, limit
41            )));
42        }
43        let compression_type = header.flags & 0x3F; // CompressionTypeMask
44
45        match compression_type {
46            0 => {
47                // No compression
48                Ok(compressed_data.to_vec())
49            }
50            2 | 3 => {
51                // LZ4 or LZ4HC
52                decompress(compressed_data, CompressionType::Lz4, expected_uncompressed)
53            }
54            1 => {
55                // LZMA
56                decompress(
57                    compressed_data,
58                    CompressionType::Lzma,
59                    expected_uncompressed,
60                )
61            }
62            4 => {
63                // Brotli (newer Unity versions)
64                decompress(
65                    compressed_data,
66                    CompressionType::Brotli,
67                    expected_uncompressed,
68                )
69            }
70            _ => Err(BinaryError::unsupported(format!(
71                "Unknown compression type: {}",
72                compression_type
73            ))),
74        }
75    }
76
77    /// Parse compression blocks from decompressed blocks info
78    ///
79    /// This method parses the compression block metadata from the
80    /// decompressed blocks info data.
81    pub fn parse_compression_blocks(data: &[u8]) -> Result<Vec<CompressionBlock>> {
82        Self::parse_compression_blocks_limited(data, &super::types::BundleLoadOptions::fast())
83    }
84
85    pub fn parse_compression_blocks_limited(
86        data: &[u8],
87        options: &super::types::BundleLoadOptions,
88    ) -> Result<Vec<CompressionBlock>> {
89        let mut reader = BinaryReader::new(data, ByteOrder::Big);
90        let mut blocks = Vec::new();
91
92        // Skip uncompressed data hash (16 bytes) - critical step
93        reader.read_bytes(16)?;
94
95        // Read compression blocks
96        let block_count_i32 = reader.read_i32()?;
97        if block_count_i32 < 0 {
98            return Err(BinaryError::invalid_data(format!(
99                "Negative compression block count: {}",
100                block_count_i32
101            )));
102        }
103        let block_count = block_count_i32 as usize;
104        if block_count > options.max_blocks {
105            return Err(BinaryError::ResourceLimitExceeded(format!(
106                "Compression block count {} exceeds limit {}",
107                block_count, options.max_blocks
108            )));
109        }
110
111        // Ensure the block table fits in the provided buffer.
112        let table_bytes = block_count
113            .checked_mul(10)
114            .ok_or_else(|| BinaryError::invalid_data("Compression block table size overflow"))?;
115        let required = 16usize
116            .checked_add(4)
117            .and_then(|v| v.checked_add(table_bytes))
118            .ok_or_else(|| BinaryError::invalid_data("Compression block table size overflow"))?;
119        if data.len() < required {
120            return Err(BinaryError::not_enough_data(required, data.len()));
121        }
122
123        for _ in 0..block_count {
124            let uncompressed_size = reader.read_u32()?;
125            let compressed_size = reader.read_u32()?;
126            let flags = reader.read_u16()?;
127
128            let block = CompressionBlock::new(uncompressed_size, compressed_size, flags);
129            blocks.push(block);
130        }
131
132        Ok(blocks)
133    }
134
135    /// Decompress all data blocks
136    ///
137    /// This method reads and decompresses all data blocks from the bundle,
138    /// returning the complete decompressed data.
139    pub fn decompress_data_blocks(
140        header: &BundleHeader,
141        blocks: &[CompressionBlock],
142        reader: &mut BinaryReader,
143    ) -> Result<Vec<u8>> {
144        Self::decompress_data_blocks_limited(header, blocks, reader, None)
145    }
146
147    pub fn decompress_data_blocks_limited(
148        header: &BundleHeader,
149        blocks: &[CompressionBlock],
150        reader: &mut BinaryReader,
151        max_memory: Option<usize>,
152    ) -> Result<Vec<u8>> {
153        let mut total_uncompressed: u64 = 0;
154        for block in blocks {
155            total_uncompressed = total_uncompressed
156                .checked_add(block.uncompressed_size as u64)
157                .ok_or_else(|| BinaryError::invalid_data("Total uncompressed size overflow"))?;
158        }
159        if let Some(limit) = max_memory
160            && total_uncompressed > limit as u64
161        {
162            return Err(BinaryError::ResourceLimitExceeded(format!(
163                "Bundle decompressed size {} exceeds max_memory {}",
164                total_uncompressed, limit
165            )));
166        }
167
168        let total_uncompressed_usize = usize::try_from(total_uncompressed).map_err(|_| {
169            BinaryError::ResourceLimitExceeded(format!(
170                "Bundle decompressed size {} does not fit in usize",
171                total_uncompressed
172            ))
173        })?;
174
175        let mut decompressed_data = Vec::with_capacity(total_uncompressed_usize);
176
177        // The caller is responsible for positioning `reader` at the start of block data, taking
178        // header alignment and `BlocksInfoAtEnd` into account.
179        let _ = header;
180
181        for block in blocks.iter() {
182            if let Some(limit) = max_memory
183                && (block.uncompressed_size as u64) > (limit as u64)
184            {
185                return Err(BinaryError::ResourceLimitExceeded(format!(
186                    "Block uncompressed size {} exceeds max_memory {}",
187                    block.uncompressed_size, limit
188                )));
189            }
190            let compressed = reader.read_bytes(block.compressed_size as usize)?;
191            let block_data = block.decompress(&compressed)?;
192            decompressed_data.extend_from_slice(&block_data);
193        }
194
195        Ok(decompressed_data)
196    }
197
198    /// Get compression statistics for blocks
199    pub fn get_compression_stats(blocks: &[CompressionBlock]) -> CompressionStats {
200        let total_compressed: u64 = blocks.iter().map(|b| b.compressed_size as u64).sum();
201        let total_uncompressed: u64 = blocks.iter().map(|b| b.uncompressed_size as u64).sum();
202
203        let compression_ratio = if total_uncompressed > 0 {
204            total_compressed as f64 / total_uncompressed as f64
205        } else {
206            1.0
207        };
208
209        let space_saved = total_uncompressed.saturating_sub(total_compressed);
210
211        CompressionStats {
212            block_count: blocks.len(),
213            total_compressed_size: total_compressed,
214            total_uncompressed_size: total_uncompressed,
215            compression_ratio,
216            space_saved,
217            average_block_size: if !blocks.is_empty() {
218                total_uncompressed / blocks.len() as u64
219            } else {
220                0
221            },
222        }
223    }
224
225    /// Validate compression blocks
226    pub fn validate_blocks(blocks: &[CompressionBlock]) -> Result<()> {
227        if blocks.is_empty() {
228            return Err(BinaryError::invalid_data("No compression blocks found"));
229        }
230
231        for (i, block) in blocks.iter().enumerate() {
232            if block.compressed_size == 0 {
233                return Err(BinaryError::invalid_data(format!(
234                    "Block {} has zero compressed size",
235                    i
236                )));
237            }
238
239            if block.uncompressed_size == 0 {
240                return Err(BinaryError::invalid_data(format!(
241                    "Block {} has zero uncompressed size",
242                    i
243                )));
244            }
245
246            // Sanity check: compressed size shouldn't be much larger than uncompressed
247            // (except for very small blocks or incompressible data)
248            if block.compressed_size > block.uncompressed_size * 2 && block.uncompressed_size > 1024
249            {
250                return Err(BinaryError::invalid_data(format!(
251                    "Block {} has suspicious compression ratio: {}/{}",
252                    i, block.compressed_size, block.uncompressed_size
253                )));
254            }
255        }
256
257        Ok(())
258    }
259
260    /// Estimate memory usage for decompression
261    pub fn estimate_memory_usage(blocks: &[CompressionBlock]) -> usize {
262        // Estimate peak memory usage during decompression
263        let total_uncompressed: usize = blocks.iter().map(|b| b.uncompressed_size as usize).sum();
264        let max_block_size: usize = blocks
265            .iter()
266            .map(|b| b.uncompressed_size as usize)
267            .max()
268            .unwrap_or(0);
269
270        // Peak usage: total output + largest single block for temporary decompression
271        total_uncompressed + max_block_size
272    }
273
274    /// Check if compression type is supported
275    pub fn is_compression_supported(compression_type: u32) -> bool {
276        match compression_type {
277            0 => true,     // None
278            1 => true,     // LZMA
279            2 | 3 => true, // LZ4/LZ4HC
280            4 => true,     // Brotli
281            _ => false,
282        }
283    }
284}
285
286/// Compression statistics
287#[derive(Debug, Clone)]
288pub struct CompressionStats {
289    pub block_count: usize,
290    pub total_compressed_size: u64,
291    pub total_uncompressed_size: u64,
292    pub compression_ratio: f64,
293    pub space_saved: u64,
294    pub average_block_size: u64,
295}
296
297impl CompressionStats {
298    /// Get compression efficiency as a percentage
299    pub fn efficiency_percent(&self) -> f64 {
300        (1.0 - self.compression_ratio) * 100.0
301    }
302
303    /// Check if compression was effective
304    pub fn is_effective(&self) -> bool {
305        self.compression_ratio < 0.9 // Less than 90% of original size
306    }
307}
308
309/// Compression options for bundle processing
310#[derive(Debug, Clone)]
311pub struct CompressionOptions {
312    /// Maximum memory to use for decompression
313    pub max_memory: Option<usize>,
314    /// Whether to validate blocks before decompression
315    pub validate_blocks: bool,
316    /// Whether to collect compression statistics
317    pub collect_stats: bool,
318    /// Preferred compression type for new bundles
319    pub preferred_compression: CompressionType,
320}
321
322impl Default for CompressionOptions {
323    fn default() -> Self {
324        Self {
325            max_memory: Some(1024 * 1024 * 1024), // 1GB
326            validate_blocks: true,
327            collect_stats: false,
328            preferred_compression: CompressionType::Lz4,
329        }
330    }
331}
332
333impl CompressionOptions {
334    /// Create options for fast decompression (minimal validation)
335    pub fn fast() -> Self {
336        Self {
337            max_memory: None,
338            validate_blocks: false,
339            collect_stats: false,
340            preferred_compression: CompressionType::Lz4,
341        }
342    }
343
344    /// Create options for safe decompression (full validation)
345    pub fn safe() -> Self {
346        Self {
347            max_memory: Some(512 * 1024 * 1024), // 512MB
348            validate_blocks: true,
349            collect_stats: true,
350            preferred_compression: CompressionType::Lz4,
351        }
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_compression_support() {
361        assert!(BundleCompression::is_compression_supported(0)); // None
362        assert!(BundleCompression::is_compression_supported(1)); // LZMA
363        assert!(BundleCompression::is_compression_supported(2)); // LZ4
364        assert!(BundleCompression::is_compression_supported(3)); // LZ4HC
365        assert!(!BundleCompression::is_compression_supported(99)); // Unknown
366    }
367
368    #[test]
369    fn test_compression_stats() {
370        let blocks = vec![
371            CompressionBlock::new(1000, 500, 0),
372            CompressionBlock::new(2000, 1000, 0),
373        ];
374
375        let stats = BundleCompression::get_compression_stats(&blocks);
376        assert_eq!(stats.block_count, 2);
377        assert_eq!(stats.total_compressed_size, 1500);
378        assert_eq!(stats.total_uncompressed_size, 3000);
379        assert_eq!(stats.compression_ratio, 0.5);
380        assert_eq!(stats.space_saved, 1500);
381        assert!(stats.is_effective());
382    }
383}