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 let compression_type = header.flags & 0x3F; match compression_type {
29 0 => {
30 Ok(compressed_data.to_vec())
32 }
33 2 | 3 => {
34 decompress(
36 compressed_data,
37 CompressionType::Lz4,
38 header.uncompressed_blocks_info_size as usize,
39 )
40 }
41 1 => {
42 decompress(
44 compressed_data,
45 CompressionType::Lzma,
46 header.uncompressed_blocks_info_size as usize,
47 )
48 }
49 4 => {
50 #[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 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 reader.read_bytes(16)?;
83
84 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 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 let mut data_pos = header.header_size() + header.compressed_blocks_info_size as u64;
114
115 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 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 data_pos += block.compressed_size as u64;
133 }
134
135 Ok(decompressed_data)
136 }
137
138 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 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 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 pub fn estimate_memory_usage(blocks: &[CompressionBlock]) -> usize {
202 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 total_uncompressed + max_block_size
212 }
213
214 pub fn is_compression_supported(compression_type: u32) -> bool {
216 match compression_type {
217 0 => true, 1 => true, 2 | 3 => true, #[cfg(feature = "brotli")]
221 4 => true, #[cfg(not(feature = "brotli"))]
223 4 => false, _ => false,
225 }
226 }
227}
228
229#[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 pub fn efficiency_percent(&self) -> f64 {
243 (1.0 - self.compression_ratio) * 100.0
244 }
245
246 pub fn is_effective(&self) -> bool {
248 self.compression_ratio < 0.9 }
250}
251
252#[derive(Debug, Clone)]
254pub struct CompressionOptions {
255 pub max_memory: Option<usize>,
257 pub validate_blocks: bool,
259 pub collect_stats: bool,
261 pub preferred_compression: CompressionType,
263}
264
265impl Default for CompressionOptions {
266 fn default() -> Self {
267 Self {
268 max_memory: Some(1024 * 1024 * 1024), validate_blocks: true,
270 collect_stats: false,
271 preferred_compression: CompressionType::Lz4,
272 }
273 }
274}
275
276impl CompressionOptions {
277 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 pub fn safe() -> Self {
289 Self {
290 max_memory: Some(512 * 1024 * 1024), 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)); assert!(BundleCompression::is_compression_supported(1)); assert!(BundleCompression::is_compression_supported(2)); assert!(BundleCompression::is_compression_supported(3)); assert!(!BundleCompression::is_compression_supported(99)); }
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}