1use crate::error::{PluginError, Result, ValidationError};
7use crate::plugin::registry::get_plugin_by_magic;
8use crate::plugin::{list_plugins, CrushHeader, FileMetadata};
9use crc32fast::Hasher;
10use std::sync::atomic::AtomicBool;
11use std::sync::Arc;
12
13pub(crate) fn read_crc32_block(input: &[u8], payload_start: usize) -> Result<(u32, u32, usize)> {
25 if input.len() < payload_start + 4 {
26 return Err(ValidationError::InvalidHeader(
27 "Truncated: CRC32 flag set but no CRC32 data".to_string(),
28 )
29 .into());
30 }
31 let stored_crc = u32::from_le_bytes([
32 input[payload_start],
33 input[payload_start + 1],
34 input[payload_start + 2],
35 input[payload_start + 3],
36 ]);
37 let new_payload_start = payload_start + 4;
38
39 let payload_for_crc = &input[new_payload_start..];
40 let mut hasher = Hasher::new();
41 hasher.update(payload_for_crc);
42 let computed_crc = hasher.finalize();
43
44 Ok((stored_crc, computed_crc, new_payload_start))
45}
46
47#[derive(Debug)]
48pub struct DecompressionResult {
49 pub data: Vec<u8>,
50 pub metadata: FileMetadata,
51}
52
53pub fn decompress(input: &[u8]) -> Result<DecompressionResult> {
80 let cancel_flag = Arc::new(AtomicBool::new(false));
81 decompress_with_cancel(input, cancel_flag)
82}
83
84pub fn decompress_with_cancel(
100 input: &[u8],
101 cancel_flag: Arc<AtomicBool>,
102) -> Result<DecompressionResult> {
103 if input.len() < CrushHeader::SIZE {
105 return Err(ValidationError::InvalidHeader(format!(
106 "Input too short: {} bytes, expected at least {}",
107 input.len(),
108 CrushHeader::SIZE
109 ))
110 .into());
111 }
112
113 let header_bytes: [u8; CrushHeader::SIZE] = input[0..CrushHeader::SIZE]
115 .try_into()
116 .map_err(|_| ValidationError::InvalidHeader("Failed to read header".to_string()))?;
117 let header = CrushHeader::from_bytes(&header_bytes)?;
118
119 let mut payload_start = CrushHeader::SIZE;
120
121 if header.has_crc32() {
123 let (stored_crc, computed_crc, new_start) = read_crc32_block(input, payload_start)?;
124 payload_start = new_start;
125 if stored_crc != computed_crc {
126 return Err(ValidationError::CrcMismatch {
127 expected: stored_crc,
128 actual: computed_crc,
129 }
130 .into());
131 }
132 }
133
134 let metadata = if header.has_metadata() {
136 if input.len() < payload_start + 2 {
137 return Err(ValidationError::InvalidHeader(
138 "Truncated: metadata flag set but no metadata length".to_string(),
139 )
140 .into());
141 }
142 let metadata_len =
143 u16::from_le_bytes([input[payload_start], input[payload_start + 1]]) as usize;
144 payload_start += 2;
145
146 if input.len() < payload_start + metadata_len {
147 return Err(ValidationError::InvalidHeader(
148 "Truncated: metadata length exceeds payload size".to_string(),
149 )
150 .into());
151 }
152 let metadata_bytes = &input[payload_start..payload_start + metadata_len];
153 payload_start += metadata_len;
154
155 FileMetadata::from_bytes(metadata_bytes)?
156 } else {
157 FileMetadata::default()
158 };
159
160 let compressed_payload = &input[payload_start..];
161
162 let plugin = get_plugin_by_magic(header.magic).ok_or_else(|| {
164 let available = list_plugins()
165 .iter()
166 .map(|p| p.name)
167 .collect::<Vec<_>>()
168 .join(", ");
169
170 PluginError::NotFound(format!(
171 "No plugin found for magic number {:02X?}. \
172 Available plugins: {}. \
173 Did you call init_plugins()?",
174 header.magic, available
175 ))
176 })?;
177
178 let decompressed = plugin.decompress(compressed_payload, cancel_flag)?;
180
181 let expected_size = usize::try_from(header.original_size).map_err(|_| {
183 ValidationError::InvalidHeader("Original size exceeds platform limits".to_string())
184 })?;
185
186 if decompressed.len() != expected_size {
187 return Err(ValidationError::CorruptedData(format!(
188 "Size mismatch: header says {} bytes, got {} bytes",
189 header.original_size,
190 decompressed.len()
191 ))
192 .into());
193 }
194
195 Ok(DecompressionResult {
196 data: decompressed,
197 metadata,
198 })
199}
200
201#[cfg(test)]
202#[allow(clippy::expect_used)]
203#[allow(clippy::unwrap_used)]
204#[allow(clippy::unreadable_literal)]
205mod tests {
206 use super::*;
207 use crate::{compress, init_plugins};
208
209 #[test]
210 #[allow(clippy::unwrap_used)]
211 fn test_decompress_valid() {
212 init_plugins().unwrap();
213 let original = b"Test data for decompression";
214 let compressed = compress(original).unwrap();
215 let decompressed = decompress(&compressed).unwrap().data;
216
217 assert_eq!(original.as_slice(), decompressed.as_slice());
218 }
219
220 #[test]
221 fn test_decompress_truncated() {
222 let truncated = &[0x43, 0x52, 0x01, 0x00, 0x01]; let result = decompress(truncated);
225 assert!(result.is_err());
226 }
227
228 #[test]
229 fn test_decompress_invalid_magic() {
230 let mut invalid = vec![0xFF, 0xFF, 0xFF, 0xFF]; invalid.extend_from_slice(&[0u8; 12]); let result = decompress(&invalid);
234 assert!(result.is_err());
235 }
236
237 #[test]
238 #[allow(clippy::unwrap_used)]
239 fn test_decompress_corrupted_crc() {
240 init_plugins().unwrap();
241 let original = b"Data to corrupt";
242 let mut compressed = compress(original).unwrap();
243
244 if compressed.len() > 16 {
246 compressed[16] ^= 0xFF;
247 }
248
249 let result = decompress(&compressed);
250 assert!(result.is_err());
251 }
252
253 #[test]
254 fn test_decompress_with_metadata() {
255 use crate::plugin::FileMetadata;
256 use crate::{compress_with_options, CompressionOptions};
257
258 init_plugins().expect("Failed to init");
259 let original = b"Data with metadata";
260 let metadata = FileMetadata {
261 mtime: Some(1234567890),
262 #[cfg(unix)]
263 permissions: Some(0o644),
264 };
265 let options = CompressionOptions::default().with_file_metadata(metadata.clone());
266 let compressed = compress_with_options(original, &options).expect("Compression failed");
267
268 let result = decompress(&compressed).expect("Decompression failed");
269
270 assert_eq!(original.as_slice(), result.data.as_slice());
271 assert_eq!(result.metadata.mtime, metadata.mtime);
272 #[cfg(unix)]
273 assert_eq!(result.metadata.permissions, metadata.permissions);
274 }
275
276 #[test]
277 fn test_decompress_truncated_crc32() {
278 init_plugins().expect("Failed to init");
279 let original = b"Test";
280 let mut compressed = compress(original).expect("Compression failed");
281
282 compressed.truncate(CrushHeader::SIZE); let result = decompress(&compressed);
286 assert!(result.is_err()); }
288
289 #[test]
290 fn test_decompress_truncated_metadata_length() {
291 use crate::plugin::FileMetadata;
292 use crate::{compress_with_options, CompressionOptions};
293
294 init_plugins().expect("Failed to init");
295 let original = b"Test";
296 let metadata = FileMetadata {
297 mtime: Some(1234567890),
298 #[cfg(unix)]
299 permissions: Some(0o755),
300 };
301 let options = CompressionOptions::default().with_file_metadata(metadata);
302 let mut compressed = compress_with_options(original, &options).expect("Compression failed");
303
304 let truncate_pos = CrushHeader::SIZE + 4 + 1;
306 if compressed.len() > truncate_pos {
307 compressed.truncate(truncate_pos);
308 }
309
310 let result = decompress(&compressed);
311 assert!(result.is_err());
312 }
313
314 #[test]
315 fn test_decompress_truncated_metadata_payload() {
316 use crate::plugin::FileMetadata;
317 use crate::{compress_with_options, CompressionOptions};
318
319 init_plugins().expect("Failed to init");
320 let original = b"Test";
321 let metadata = FileMetadata {
322 mtime: Some(1234567890),
323 #[cfg(unix)]
324 permissions: Some(0o755),
325 };
326 let options = CompressionOptions::default().with_file_metadata(metadata);
327 let mut compressed = compress_with_options(original, &options).expect("Compression failed");
328
329 let truncate_pos = CrushHeader::SIZE + 4 + 2 + 3;
331 if compressed.len() > truncate_pos {
332 compressed.truncate(truncate_pos);
333 }
334
335 let result = decompress(&compressed);
336 assert!(result.is_err());
337 }
338
339 #[test]
340 fn test_decompress_plugin_not_found() {
341 let mut fake_compressed = vec![
343 0x43, 0x52, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
348 fake_compressed.extend_from_slice(&[0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]);
350
351 let result = decompress(&fake_compressed);
352 assert!(result.is_err());
353 }
356
357 #[test]
358 fn test_decompress_empty_input() {
359 let result = decompress(&[]);
360 assert!(result.is_err());
361 assert!(result.unwrap_err().to_string().contains("too short"));
362 }
363
364 #[test]
365 fn test_decompress_default_metadata() {
366 init_plugins().expect("Failed to init");
367 let original = b"No metadata test";
368 let compressed = compress(original).expect("Compression failed");
369
370 let result = decompress(&compressed).expect("Decompression failed");
371
372 assert!(result.metadata.mtime.is_none());
374 #[cfg(unix)]
375 assert!(result.metadata.permissions.is_none());
376 }
377
378 #[test]
379 #[allow(clippy::unwrap_used)]
380 fn test_decompress_corrupted_payload() {
381 init_plugins().unwrap();
382 let original = b"Data to corrupt";
383 let mut compressed = compress(original).unwrap();
384
385 if compressed.len() > 24 {
387 compressed[24] ^= 0xFF;
388 }
389
390 let result = decompress(&compressed);
391 assert!(result.is_err());
393 }
394}