unity_asset_binary/bundle/
compression.rs1use super::header::BundleHeader;
7use crate::compression::{CompressionBlock, CompressionType, decompress};
8use crate::error::{BinaryError, Result};
9use crate::reader::{BinaryReader, ByteOrder};
10
11pub struct BundleCompression;
16
17impl BundleCompression {
18 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; match compression_type {
46 0 => {
47 Ok(compressed_data.to_vec())
49 }
50 2 | 3 => {
51 decompress(compressed_data, CompressionType::Lz4, expected_uncompressed)
53 }
54 1 => {
55 decompress(
57 compressed_data,
58 CompressionType::Lzma,
59 expected_uncompressed,
60 )
61 }
62 4 => {
63 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 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 reader.read_bytes(16)?;
94
95 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 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 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 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 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 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 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 pub fn estimate_memory_usage(blocks: &[CompressionBlock]) -> usize {
262 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 total_uncompressed + max_block_size
272 }
273
274 pub fn is_compression_supported(compression_type: u32) -> bool {
276 match compression_type {
277 0 => true, 1 => true, 2 | 3 => true, 4 => true, _ => false,
282 }
283 }
284}
285
286#[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 pub fn efficiency_percent(&self) -> f64 {
300 (1.0 - self.compression_ratio) * 100.0
301 }
302
303 pub fn is_effective(&self) -> bool {
305 self.compression_ratio < 0.9 }
307}
308
309#[derive(Debug, Clone)]
311pub struct CompressionOptions {
312 pub max_memory: Option<usize>,
314 pub validate_blocks: bool,
316 pub collect_stats: bool,
318 pub preferred_compression: CompressionType,
320}
321
322impl Default for CompressionOptions {
323 fn default() -> Self {
324 Self {
325 max_memory: Some(1024 * 1024 * 1024), validate_blocks: true,
327 collect_stats: false,
328 preferred_compression: CompressionType::Lz4,
329 }
330 }
331}
332
333impl CompressionOptions {
334 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 pub fn safe() -> Self {
346 Self {
347 max_memory: Some(512 * 1024 * 1024), 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)); assert!(BundleCompression::is_compression_supported(1)); assert!(BundleCompression::is_compression_supported(2)); assert!(BundleCompression::is_compression_supported(3)); assert!(!BundleCompression::is_compression_supported(99)); }
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}