threatflux_binary_analysis/utils/
compression.rs1use crate::{Result, error::BinaryError};
2use flate2::read::{MultiGzDecoder, ZlibDecoder};
3use std::io::Read;
4
5pub const DEFAULT_MAX_DECOMPRESSED_SIZE: usize = 64 * 1024 * 1024;
7
8#[cfg(feature = "compression")]
12pub fn decompress(data: &[u8]) -> Result<Vec<u8>> {
13 decompress_with_limit(data, DEFAULT_MAX_DECOMPRESSED_SIZE)
14}
15
16#[cfg(feature = "compression")]
21pub fn decompress_with_limit(data: &[u8], max_size: usize) -> Result<Vec<u8>> {
22 if data.starts_with(b"\x1f\x8b") {
23 read_bounded(MultiGzDecoder::new(data), max_size)
24 } else if is_zlib_header(data) {
25 read_bounded(ZlibDecoder::new(data), max_size)
26 } else {
27 Err(BinaryError::invalid_data("unsupported compression format"))
28 }
29}
30
31fn is_zlib_header(data: &[u8]) -> bool {
32 let Some((&cmf, rest)) = data.split_first() else {
33 return false;
34 };
35 let Some(&flg) = rest.first() else {
36 return false;
37 };
38
39 cmf & 0x0f == 8 && cmf >> 4 <= 7 && (u16::from(cmf) << 8 | u16::from(flg)) % 31 == 0
43}
44
45fn read_bounded(mut reader: impl Read, max_size: usize) -> Result<Vec<u8>> {
46 const BUFFER_SIZE: usize = 8 * 1024;
47
48 let mut output = Vec::with_capacity(max_size.min(BUFFER_SIZE));
49 let mut buffer = [0_u8; BUFFER_SIZE];
50
51 loop {
52 let count = reader.read(&mut buffer)?;
53 if count == 0 {
54 return Ok(output);
55 }
56
57 let new_len = output
58 .len()
59 .checked_add(count)
60 .ok_or_else(|| BinaryError::invalid_data("decompressed size overflows usize"))?;
61 if new_len > max_size {
62 return Err(BinaryError::invalid_data(format!(
63 "decompressed data exceeds the {max_size}-byte limit"
64 )));
65 }
66
67 output
68 .try_reserve(count)
69 .map_err(|_| BinaryError::invalid_data("unable to allocate decompression output"))?;
70 output.extend_from_slice(&buffer[..count]);
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use flate2::Compression;
78 use flate2::write::{GzEncoder, ZlibEncoder};
79
80 #[cfg(feature = "compression")]
81 #[test]
82 fn test_decompress_gzip() {
83 let data = b"hello world";
84 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
85 use std::io::Write;
86 encoder.write_all(data).unwrap();
87 let compressed = encoder.finish().unwrap();
88
89 let decompressed = decompress(&compressed).unwrap();
90 assert_eq!(decompressed, data);
91 }
92
93 #[cfg(feature = "compression")]
94 #[test]
95 fn test_decompress_zlib() {
96 let data = b"another test";
97 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
98 use std::io::Write;
99 encoder.write_all(data).unwrap();
100 let compressed = encoder.finish().unwrap();
101
102 let decompressed = decompress(&compressed).unwrap();
103 assert_eq!(decompressed, data);
104 }
105
106 #[cfg(feature = "compression")]
107 #[test]
108 fn rejects_output_larger_than_limit() {
109 let data = vec![b'A'; 64 * 1024];
110 let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
111 use std::io::Write;
112 encoder.write_all(&data).unwrap();
113 let compressed = encoder.finish().unwrap();
114
115 let error = decompress_with_limit(&compressed, 1024).unwrap_err();
116 assert!(error.to_string().contains("1024-byte limit"));
117 }
118
119 #[cfg(feature = "compression")]
120 #[test]
121 fn accepts_exact_output_limit() {
122 let data = b"exact limit";
123 let mut encoder = ZlibEncoder::new(Vec::new(), Compression::none());
124 use std::io::Write;
125 encoder.write_all(data).unwrap();
126 let compressed = encoder.finish().unwrap();
127
128 assert_eq!(
129 decompress_with_limit(&compressed, data.len()).unwrap(),
130 data
131 );
132 }
133
134 #[test]
135 fn recognizes_all_valid_rfc_1950_headers() {
136 assert!(is_zlib_header(&[0x78, 0x01]));
137 assert!(is_zlib_header(&[0x78, 0x5e]));
138 assert!(is_zlib_header(&[0x78, 0x9c]));
139 assert!(is_zlib_header(&[0x78, 0xda]));
140 assert!(!is_zlib_header(&[0x78, 0x00]));
141 assert!(!is_zlib_header(&[0x88, 0x1c]));
142 }
143}