1use crate::error::{BinaryError, Result};
4use flate2::read::GzDecoder;
5use std::io::Read;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CompressionType {
10 None = 0,
12 Lzma = 1,
14 Lz4 = 2,
16 Lz4Hc = 3,
18 Lzham = 4,
20 Brotli = 5,
22}
23
24impl CompressionType {
25 pub fn from_flags(flags: u32) -> Result<Self> {
27 match flags & 0x3F {
28 0 => Ok(CompressionType::None),
29 1 => Ok(CompressionType::Lzma),
30 2 => Ok(CompressionType::Lz4),
31 3 => Ok(CompressionType::Lz4Hc),
32 4 => Ok(CompressionType::Lzham),
33 5 => Ok(CompressionType::Brotli),
34 other => Err(BinaryError::unsupported_compression(format!(
35 "Unknown compression type: {}",
36 other
37 ))),
38 }
39 }
40
41 pub fn is_supported(self) -> bool {
43 matches!(
44 self,
45 CompressionType::None
46 | CompressionType::Lz4
47 | CompressionType::Lz4Hc
48 | CompressionType::Lzma
49 | CompressionType::Brotli
50 )
51 }
52
53 pub fn name(self) -> &'static str {
55 match self {
56 CompressionType::None => "None",
57 CompressionType::Lzma => "LZMA",
58 CompressionType::Lz4 => "LZ4",
59 CompressionType::Lz4Hc => "LZ4HC",
60 CompressionType::Lzham => "LZHAM",
61 CompressionType::Brotli => "Brotli",
62 }
63 }
64}
65
66pub fn decompress(
68 data: &[u8],
69 compression: CompressionType,
70 uncompressed_size: usize,
71) -> Result<Vec<u8>> {
72 match compression {
73 CompressionType::None => {
74 Ok(data.to_vec())
76 }
77 CompressionType::Lz4 | CompressionType::Lz4Hc => {
78 decompress_lz4(data, uncompressed_size)
80 }
81 CompressionType::Lzma => {
82 decompress_lzma(data, uncompressed_size)
84 }
85 CompressionType::Lzham => {
86 Err(BinaryError::unsupported_compression(
88 "LZHAM compression not yet supported",
89 ))
90 }
91 CompressionType::Brotli => {
92 decompress_brotli(data)
94 }
95 }
96}
97
98fn decompress_lz4(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
100 let buffer_size = uncompressed_size
106 .checked_add(128)
107 .ok_or_else(|| BinaryError::invalid_data("LZ4 uncompressed_size overflow"))?; match lz4_flex::decompress(data, buffer_size) {
110 Ok(decompressed) => {
111 let size_diff = if decompressed.len() > uncompressed_size {
113 decompressed.len() - uncompressed_size
114 } else {
115 uncompressed_size - decompressed.len()
116 };
117
118 if size_diff <= 128 {
119 Ok(decompressed)
121 } else {
122 Err(BinaryError::decompression_failed(format!(
123 "LZ4 decompression size mismatch: expected {}, got {} (diff: {})",
124 uncompressed_size,
125 decompressed.len(),
126 size_diff
127 )))
128 }
129 }
130 Err(e) => {
131 match lz4_flex::decompress(data, uncompressed_size) {
133 Ok(decompressed) => Ok(decompressed),
134 Err(_) => Err(BinaryError::decompression_failed(format!(
135 "LZ4 block decompression failed: {}",
136 e
137 ))),
138 }
139 }
140 }
141}
142
143fn decompress_lzma(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
145 if data.is_empty() {
147 return Err(BinaryError::invalid_data("LZMA data is empty".to_string()));
148 }
149
150 let result = try_unity_lzma_strategies(data, uncompressed_size);
158 if result.is_ok() {
159 return result;
160 }
161
162 Err(BinaryError::decompression_failed(format!(
163 "LZMA decompression failed with all strategies. Input size: {}, expected output: {}",
164 data.len(),
165 uncompressed_size
166 )))
167}
168
169fn try_unity_lzma_strategies(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>> {
171 if let Ok(result) = try_unity_lzma_with_header(data, uncompressed_size) {
173 return Ok(result);
174 }
175
176 if let Ok(result) = try_unity_raw_lzma(data, uncompressed_size) {
178 return Ok(result);
179 }
180
181 let strategies = [
183 ("direct", data),
184 (
185 "skip_13_header",
186 if data.len() > 13 { &data[13..] } else { data },
187 ),
188 (
189 "skip_5_header",
190 if data.len() > 5 { &data[5..] } else { data },
191 ),
192 (
193 "skip_8_header",
194 if data.len() > 8 { &data[8..] } else { data },
195 ),
196 (
197 "unity_custom",
198 if data.len() > 9 { &data[9..] } else { data },
199 ),
200 ];
201
202 for (_strategy_name, test_data) in &strategies {
203 if test_data.is_empty() {
204 continue;
205 }
206
207 let mut output = Vec::new();
208 match lzma_rs::lzma_decompress(&mut std::io::Cursor::new(test_data), &mut output) {
209 Ok(_) => {
210 let size_ratio = output.len() as f64 / uncompressed_size as f64;
212 if (0.8..=1.2).contains(&size_ratio) {
213 return Ok(output);
215 } else if output.len() == uncompressed_size {
216 return Ok(output);
218 }
219 }
220 Err(_e) => {
221 }
223 }
224 }
225
226 Err(BinaryError::decompression_failed(
227 "All Unity LZMA strategies failed".to_string(),
228 ))
229}
230
231fn try_unity_lzma_with_header(data: &[u8], expected_size: usize) -> Result<Vec<u8>> {
233 if data.len() < 13 {
234 return Err(BinaryError::invalid_data(
235 "LZMA data too short for header".to_string(),
236 ));
237 }
238
239 let props = data[0];
247 let dict_size = u32::from_le_bytes([data[1], data[2], data[3], data[4]]);
248
249 let _lc = props % 9;
251 let remainder = props / 9;
252 let _pb = remainder / 5;
253 let _lp = remainder % 5;
254
255 let offsets_to_try = [5, 13]; for &data_offset in &offsets_to_try {
259 if data_offset >= data.len() {
260 continue;
261 }
262
263 let compressed_data = &data[data_offset..];
264
265 let _lc = props % 9;
267 let remainder = props / 9;
268 let _pb = remainder / 5;
269 let _lp = remainder % 5;
270
271 let mut unity_lzma_data = Vec::new();
273 unity_lzma_data.push(props);
274 unity_lzma_data.extend_from_slice(&dict_size.to_le_bytes());
275 unity_lzma_data.extend_from_slice(&(expected_size as u64).to_le_bytes());
276 unity_lzma_data.extend_from_slice(compressed_data);
277
278 let mut output = Vec::new();
279 match lzma_rs::lzma_decompress(&mut std::io::Cursor::new(&unity_lzma_data), &mut output) {
280 Ok(_) => {
281 if output.len() == expected_size {
282 return Ok(output);
283 } else if !output.is_empty() {
284 let ratio = output.len() as f64 / expected_size as f64;
285 if (0.8..=1.2).contains(&ratio) {
286 return Ok(output);
287 }
288 }
289 }
290 Err(_e) => {
291 }
293 }
294
295 let mut lzma_data = Vec::new();
297 lzma_data.push(props);
298 lzma_data.extend_from_slice(&dict_size.to_le_bytes());
299 lzma_data.extend_from_slice(&(expected_size as u64).to_le_bytes());
300 lzma_data.extend_from_slice(compressed_data);
301
302 let mut output = Vec::new();
303 match lzma_rs::lzma_decompress(&mut std::io::Cursor::new(&lzma_data), &mut output) {
304 Ok(_) => {
305 if output.len() == expected_size {
306 return Ok(output);
307 } else if !output.is_empty() {
308 let ratio = output.len() as f64 / expected_size as f64;
309 if (0.8..=1.2).contains(&ratio) {
310 return Ok(output);
311 }
312 }
313 }
314 Err(_e) => {
315 }
317 }
318 }
319
320 Err(BinaryError::decompression_failed(
321 "Unity LZMA header parsing failed".to_string(),
322 ))
323}
324
325fn try_unity_raw_lzma(data: &[u8], expected_size: usize) -> Result<Vec<u8>> {
327 if data.len() < 13 {
328 return Err(BinaryError::invalid_data(
329 "Data too short for Unity LZMA".to_string(),
330 ));
331 }
332
333 let offsets_to_try = [0, 5, 8, 9, 13, 16];
336
337 for &offset in &offsets_to_try {
338 if offset >= data.len() {
339 continue;
340 }
341
342 let lzma_stream = &data[offset..];
343 if lzma_stream.len() < 5 {
344 continue;
345 }
346
347 let mut output = Vec::new();
349 match lzma_rs::lzma_decompress(&mut std::io::Cursor::new(lzma_stream), &mut output) {
350 Ok(_) => {
351 if output.len() == expected_size {
353 return Ok(output);
354 } else if !output.is_empty() {
355 let ratio = output.len() as f64 / expected_size as f64;
356 if (0.5..=2.0).contains(&ratio) {
357 return Ok(output);
358 }
359 }
360 }
361 Err(_e) => {
362 }
364 }
365
366 if lzma_stream.len() >= 5 {
368 let mut reconstructed = Vec::new();
369 reconstructed.extend_from_slice(&lzma_stream[0..5]); reconstructed.extend_from_slice(&(expected_size as u64).to_le_bytes()); if lzma_stream.len() > 5 {
372 reconstructed.extend_from_slice(&lzma_stream[5..]); }
374
375 let mut output = Vec::new();
376 match lzma_rs::lzma_decompress(&mut std::io::Cursor::new(&reconstructed), &mut output) {
377 Ok(_) => {
378 if output.len() == expected_size {
379 return Ok(output);
380 }
381 }
382 Err(e) => {
383 let _ = e;
384 }
385 }
386 }
387 }
388
389 Err(BinaryError::decompression_failed(
390 "Unity raw LZMA failed".to_string(),
391 ))
392}
393
394pub fn decompress_brotli(data: &[u8]) -> Result<Vec<u8>> {
396 use std::io::Read;
397 let mut decompressed = Vec::new();
398 let mut decoder = brotli::Decompressor::new(data, 4096); match decoder.read_to_end(&mut decompressed) {
400 Ok(_) => Ok(decompressed),
401 Err(e) => Err(BinaryError::decompression_failed(format!(
402 "Brotli decompression failed: {}",
403 e
404 ))),
405 }
406}
407
408pub fn decompress_gzip(data: &[u8]) -> Result<Vec<u8>> {
410 let mut decoder = GzDecoder::new(data);
411 let mut decompressed = Vec::new();
412 decoder.read_to_end(&mut decompressed).map_err(|e| {
413 BinaryError::decompression_failed(format!("GZIP decompression failed: {}", e))
414 })?;
415 Ok(decompressed)
416}
417
418#[derive(Debug, Clone)]
420pub struct CompressionBlock {
421 pub uncompressed_size: u32,
423 pub compressed_size: u32,
425 pub flags: u16,
427}
428
429impl CompressionBlock {
430 pub fn new(uncompressed_size: u32, compressed_size: u32, flags: u16) -> Self {
432 Self {
433 uncompressed_size,
434 compressed_size,
435 flags,
436 }
437 }
438
439 pub fn compression_type(&self) -> Result<CompressionType> {
441 CompressionType::from_flags(self.flags as u32)
442 }
443
444 pub fn is_compressed(&self) -> bool {
446 self.uncompressed_size != self.compressed_size
447 }
448
449 pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
451 if data.len() != self.compressed_size as usize {
452 return Err(BinaryError::invalid_data(format!(
453 "Block data size mismatch: expected {}, got {}",
454 self.compressed_size,
455 data.len()
456 )));
457 }
458
459 let compression = self.compression_type()?;
460 decompress(data, compression, self.uncompressed_size as usize)
461 }
462}
463
464pub struct ArchiveFlags;
466
467impl ArchiveFlags {
468 pub const COMPRESSION_TYPE_MASK: u32 = 0x3F;
470 pub const BLOCKS_AND_DIRECTORY_INFO_COMBINED: u32 = 0x40;
472 pub const BLOCK_INFO_AT_END: u32 = 0x80;
474 pub const OLD_WEB_PLUGIN_COMPATIBILITY: u32 = 0x100;
476 pub const BLOCK_INFO_NEEDS_PADDING_AT_START: u32 = 0x200;
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_compression_type_from_flags() {
486 assert_eq!(
487 CompressionType::from_flags(0).unwrap(),
488 CompressionType::None
489 );
490 assert_eq!(
491 CompressionType::from_flags(1).unwrap(),
492 CompressionType::Lzma
493 );
494 assert_eq!(
495 CompressionType::from_flags(2).unwrap(),
496 CompressionType::Lz4
497 );
498 assert_eq!(
499 CompressionType::from_flags(3).unwrap(),
500 CompressionType::Lz4Hc
501 );
502 }
503
504 #[test]
505 fn test_compression_type_names() {
506 assert_eq!(CompressionType::None.name(), "None");
507 assert_eq!(CompressionType::Lz4.name(), "LZ4");
508 assert_eq!(CompressionType::Lzma.name(), "LZMA");
509 }
510
511 #[test]
512 fn test_compression_type_supported() {
513 assert!(CompressionType::None.is_supported());
514 assert!(CompressionType::Lz4.is_supported());
515 assert!(CompressionType::Lz4Hc.is_supported());
516 assert!(CompressionType::Lzma.is_supported());
517 assert!(!CompressionType::Lzham.is_supported());
518 }
519
520 #[test]
521 fn test_no_compression() {
522 let data = b"Hello, World!";
523 let result = decompress(data, CompressionType::None, data.len()).unwrap();
524 assert_eq!(result, data);
525 }
526
527 #[test]
528 fn test_compression_block() {
529 let block = CompressionBlock::new(100, 80, 2); assert!(block.is_compressed());
531 assert_eq!(block.compression_type().unwrap(), CompressionType::Lz4);
532 }
533
534 #[test]
535 fn test_archive_flags() {
536 let flags = 2 | ArchiveFlags::BLOCK_INFO_AT_END;
537 let compression =
538 CompressionType::from_flags(flags & ArchiveFlags::COMPRESSION_TYPE_MASK).unwrap();
539 assert_eq!(compression, CompressionType::Lz4);
540 assert_eq!(
541 flags & ArchiveFlags::BLOCK_INFO_AT_END,
542 ArchiveFlags::BLOCK_INFO_AT_END
543 );
544 }
545
546 #[test]
547 fn test_brotli_decompression() {
548 let test_data = b"Hello, World!";
551
552 match decompress_brotli(test_data) {
555 Ok(_) => {
556 }
558 Err(_) => {
559 }
561 }
562 }
563
564 #[test]
565 fn test_compression_detection() {
566 assert_eq!(
568 CompressionType::from_flags(0).unwrap(),
569 CompressionType::None
570 );
571 assert_eq!(
572 CompressionType::from_flags(1).unwrap(),
573 CompressionType::Lzma
574 );
575 assert_eq!(
576 CompressionType::from_flags(2).unwrap(),
577 CompressionType::Lz4
578 );
579 assert_eq!(
580 CompressionType::from_flags(3).unwrap(),
581 CompressionType::Lz4Hc
582 );
583 assert_eq!(
584 CompressionType::from_flags(4).unwrap(),
585 CompressionType::Lzham
586 );
587
588 assert_eq!(
590 CompressionType::from_flags(0x42).unwrap(),
591 CompressionType::Lz4
592 ); }
594
595 #[test]
596 fn test_gzip_decompression() {
597 let test_data = b"invalid gzip data";
600
601 match decompress_gzip(test_data) {
603 Ok(_) => panic!("Should fail with invalid GZIP data"),
604 Err(_) => {
605 }
607 }
608 }
609
610 #[test]
611 fn test_compression_support_matrix() {
612 let supported_types = [
614 CompressionType::None,
615 CompressionType::Lz4,
616 CompressionType::Lz4Hc,
617 CompressionType::Lzma,
618 ];
619
620 let unsupported_types = [CompressionType::Lzham];
621
622 for compression_type in supported_types {
623 assert!(
624 compression_type.is_supported(),
625 "Expected {} to be supported",
626 compression_type.name()
627 );
628 }
629
630 for compression_type in unsupported_types {
631 assert!(
632 !compression_type.is_supported(),
633 "Expected {} to be unsupported",
634 compression_type.name()
635 );
636 }
637 }
638}