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        let compression_type = header.flags & 0x3F; // CompressionTypeMask
27
28        match compression_type {
29            0 => {
30                // No compression
31                Ok(compressed_data.to_vec())
32            }
33            2 | 3 => {
34                // LZ4 or LZ4HC
35                decompress(
36                    compressed_data,
37                    CompressionType::Lz4,
38                    header.uncompressed_blocks_info_size as usize,
39                )
40            }
41            1 => {
42                // LZMA
43                decompress(
44                    compressed_data,
45                    CompressionType::Lzma,
46                    header.uncompressed_blocks_info_size as usize,
47                )
48            }
49            4 => {
50                // Brotli (newer Unity versions)
51                #[cfg(feature = "brotli")]
52                {
53                    decompress(
54                        compressed_data,
55                        CompressionType::Brotli,
56                        header.uncompressed_blocks_info_size as usize,
57                    )
58                }
59                #[cfg(not(feature = "brotli"))]
60                {
61                    Err(BinaryError::unsupported(
62                        "Brotli compression requires brotli feature",
63                    ))
64                }
65            }
66            _ => Err(BinaryError::unsupported(format!(
67                "Unknown compression type: {}",
68                compression_type
69            ))),
70        }
71    }
72
73    /// Parse compression blocks from decompressed blocks info
74    ///
75    /// This method parses the compression block metadata from the
76    /// decompressed blocks info data.
77    pub fn parse_compression_blocks(data: &[u8]) -> Result<Vec<CompressionBlock>> {
78        let mut reader = BinaryReader::new(data, ByteOrder::Big);
79        let mut blocks = Vec::new();
80
81        // Skip uncompressed data hash (16 bytes) - critical step
82        reader.read_bytes(16)?;
83
84        // Read compression blocks
85        let block_count = reader.read_i32()? as usize;
86
87        for _ in 0..block_count {
88            let uncompressed_size = reader.read_u32()?;
89            let compressed_size = reader.read_u32()?;
90            let flags = reader.read_u16()?;
91
92            let block = CompressionBlock::new(uncompressed_size, compressed_size, flags);
93            blocks.push(block);
94        }
95
96        Ok(blocks)
97    }
98
99    /// Decompress all data blocks
100    ///
101    /// This method reads and decompresses all data blocks from the bundle,
102    /// returning the complete decompressed data.
103    pub fn decompress_data_blocks(
104        header: &BundleHeader,
105        blocks: &[CompressionBlock],
106        reader: &mut BinaryReader,
107    ) -> Result<Vec<u8>> {
108        let mut decompressed_data = Vec::new();
109
110        // Calculate the position where block data starts
111        // TEMPORARY FIX: Always assume blocks info is after header, ignore the flag
112        // This matches our fix in read_blocks_info
113        let mut data_pos = header.header_size() + header.compressed_blocks_info_size as u64;
114
115        // Process each compression block
116        for block in blocks.iter() {
117            reader.set_position(data_pos)?;
118            let compressed_data = reader.read_bytes(block.compressed_size as usize)?;
119
120            // Check if we have enough data
121            if compressed_data.len() != block.compressed_size as usize {
122                return Err(BinaryError::not_enough_data(
123                    block.compressed_size as usize,
124                    compressed_data.len(),
125                ));
126            }
127
128            let block_data = block.decompress(&compressed_data)?;
129            decompressed_data.extend_from_slice(&block_data);
130
131            // Move to next block position
132            data_pos += block.compressed_size as u64;
133        }
134
135        Ok(decompressed_data)
136    }
137
138    /// Get compression statistics for blocks
139    pub fn get_compression_stats(blocks: &[CompressionBlock]) -> CompressionStats {
140        let total_compressed: u64 = blocks.iter().map(|b| b.compressed_size as u64).sum();
141        let total_uncompressed: u64 = blocks.iter().map(|b| b.uncompressed_size as u64).sum();
142
143        let compression_ratio = if total_uncompressed > 0 {
144            total_compressed as f64 / total_uncompressed as f64
145        } else {
146            1.0
147        };
148
149        let space_saved = total_uncompressed.saturating_sub(total_compressed);
150
151        CompressionStats {
152            block_count: blocks.len(),
153            total_compressed_size: total_compressed,
154            total_uncompressed_size: total_uncompressed,
155            compression_ratio,
156            space_saved,
157            average_block_size: if !blocks.is_empty() {
158                total_uncompressed / blocks.len() as u64
159            } else {
160                0
161            },
162        }
163    }
164
165    /// Validate compression blocks
166    pub fn validate_blocks(blocks: &[CompressionBlock]) -> Result<()> {
167        if blocks.is_empty() {
168            return Err(BinaryError::invalid_data("No compression blocks found"));
169        }
170
171        for (i, block) in blocks.iter().enumerate() {
172            if block.compressed_size == 0 {
173                return Err(BinaryError::invalid_data(format!(
174                    "Block {} has zero compressed size",
175                    i
176                )));
177            }
178
179            if block.uncompressed_size == 0 {
180                return Err(BinaryError::invalid_data(format!(
181                    "Block {} has zero uncompressed size",
182                    i
183                )));
184            }
185
186            // Sanity check: compressed size shouldn't be much larger than uncompressed
187            // (except for very small blocks or incompressible data)
188            if block.compressed_size > block.uncompressed_size * 2 && block.uncompressed_size > 1024
189            {
190                return Err(BinaryError::invalid_data(format!(
191                    "Block {} has suspicious compression ratio: {}/{}",
192                    i, block.compressed_size, block.uncompressed_size
193                )));
194            }
195        }
196
197        Ok(())
198    }
199
200    /// Estimate memory usage for decompression
201    pub fn estimate_memory_usage(blocks: &[CompressionBlock]) -> usize {
202        // Estimate peak memory usage during decompression
203        let total_uncompressed: usize = blocks.iter().map(|b| b.uncompressed_size as usize).sum();
204        let max_block_size: usize = blocks
205            .iter()
206            .map(|b| b.uncompressed_size as usize)
207            .max()
208            .unwrap_or(0);
209
210        // Peak usage: total output + largest single block for temporary decompression
211        total_uncompressed + max_block_size
212    }
213
214    /// Check if compression type is supported
215    pub fn is_compression_supported(compression_type: u32) -> bool {
216        match compression_type {
217            0 => true,     // None
218            1 => true,     // LZMA
219            2 | 3 => true, // LZ4/LZ4HC
220            #[cfg(feature = "brotli")]
221            4 => true, // Brotli
222            #[cfg(not(feature = "brotli"))]
223            4 => false, // Brotli
224            _ => false,
225        }
226    }
227}
228
229/// Compression statistics
230#[derive(Debug, Clone)]
231pub struct CompressionStats {
232    pub block_count: usize,
233    pub total_compressed_size: u64,
234    pub total_uncompressed_size: u64,
235    pub compression_ratio: f64,
236    pub space_saved: u64,
237    pub average_block_size: u64,
238}
239
240impl CompressionStats {
241    /// Get compression efficiency as a percentage
242    pub fn efficiency_percent(&self) -> f64 {
243        (1.0 - self.compression_ratio) * 100.0
244    }
245
246    /// Check if compression was effective
247    pub fn is_effective(&self) -> bool {
248        self.compression_ratio < 0.9 // Less than 90% of original size
249    }
250}
251
252/// Compression options for bundle processing
253#[derive(Debug, Clone)]
254pub struct CompressionOptions {
255    /// Maximum memory to use for decompression
256    pub max_memory: Option<usize>,
257    /// Whether to validate blocks before decompression
258    pub validate_blocks: bool,
259    /// Whether to collect compression statistics
260    pub collect_stats: bool,
261    /// Preferred compression type for new bundles
262    pub preferred_compression: CompressionType,
263}
264
265impl Default for CompressionOptions {
266    fn default() -> Self {
267        Self {
268            max_memory: Some(1024 * 1024 * 1024), // 1GB
269            validate_blocks: true,
270            collect_stats: false,
271            preferred_compression: CompressionType::Lz4,
272        }
273    }
274}
275
276impl CompressionOptions {
277    /// Create options for fast decompression (minimal validation)
278    pub fn fast() -> Self {
279        Self {
280            max_memory: None,
281            validate_blocks: false,
282            collect_stats: false,
283            preferred_compression: CompressionType::Lz4,
284        }
285    }
286
287    /// Create options for safe decompression (full validation)
288    pub fn safe() -> Self {
289        Self {
290            max_memory: Some(512 * 1024 * 1024), // 512MB
291            validate_blocks: true,
292            collect_stats: true,
293            preferred_compression: CompressionType::Lz4,
294        }
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_compression_support() {
304        assert!(BundleCompression::is_compression_supported(0)); // None
305        assert!(BundleCompression::is_compression_supported(1)); // LZMA
306        assert!(BundleCompression::is_compression_supported(2)); // LZ4
307        assert!(BundleCompression::is_compression_supported(3)); // LZ4HC
308        assert!(!BundleCompression::is_compression_supported(99)); // Unknown
309    }
310
311    #[test]
312    fn test_compression_stats() {
313        let blocks = vec![
314            CompressionBlock::new(1000, 500, 0),
315            CompressionBlock::new(2000, 1000, 0),
316        ];
317
318        let stats = BundleCompression::get_compression_stats(&blocks);
319        assert_eq!(stats.block_count, 2);
320        assert_eq!(stats.total_compressed_size, 1500);
321        assert_eq!(stats.total_uncompressed_size, 3000);
322        assert_eq!(stats.compression_ratio, 0.5);
323        assert_eq!(stats.space_saved, 1500);
324        assert!(stats.is_effective());
325    }
326}