use crate::error::{PluginError, Result, ValidationError};
use crate::plugin::registry::get_plugin_by_magic;
use crate::plugin::{list_plugins, CrushHeader, FileMetadata};
use crc32fast::Hasher;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
pub(crate) fn read_crc32_block(input: &[u8], payload_start: usize) -> Result<(u32, u32, usize)> {
if input.len() < payload_start + 4 {
return Err(ValidationError::InvalidHeader(
"Truncated: CRC32 flag set but no CRC32 data".to_string(),
)
.into());
}
let stored_crc = u32::from_le_bytes([
input[payload_start],
input[payload_start + 1],
input[payload_start + 2],
input[payload_start + 3],
]);
let new_payload_start = payload_start + 4;
let payload_for_crc = &input[new_payload_start..];
let mut hasher = Hasher::new();
hasher.update(payload_for_crc);
let computed_crc = hasher.finalize();
Ok((stored_crc, computed_crc, new_payload_start))
}
#[derive(Debug)]
pub struct DecompressionResult {
pub data: Vec<u8>,
pub metadata: FileMetadata,
}
pub fn decompress(input: &[u8]) -> Result<DecompressionResult> {
let cancel_flag = Arc::new(AtomicBool::new(false));
decompress_with_cancel(input, cancel_flag)
}
pub fn decompress_with_cancel(
input: &[u8],
cancel_flag: Arc<AtomicBool>,
) -> Result<DecompressionResult> {
if input.len() < CrushHeader::SIZE {
return Err(ValidationError::InvalidHeader(format!(
"Input too short: {} bytes, expected at least {}",
input.len(),
CrushHeader::SIZE
))
.into());
}
let header_bytes: [u8; CrushHeader::SIZE] = input[0..CrushHeader::SIZE]
.try_into()
.map_err(|_| ValidationError::InvalidHeader("Failed to read header".to_string()))?;
let header = CrushHeader::from_bytes(&header_bytes)?;
let mut payload_start = CrushHeader::SIZE;
if header.has_crc32() {
let (stored_crc, computed_crc, new_start) = read_crc32_block(input, payload_start)?;
payload_start = new_start;
if stored_crc != computed_crc {
return Err(ValidationError::CrcMismatch {
expected: stored_crc,
actual: computed_crc,
}
.into());
}
}
let metadata = if header.has_metadata() {
if input.len() < payload_start + 2 {
return Err(ValidationError::InvalidHeader(
"Truncated: metadata flag set but no metadata length".to_string(),
)
.into());
}
let metadata_len =
u16::from_le_bytes([input[payload_start], input[payload_start + 1]]) as usize;
payload_start += 2;
if input.len() < payload_start + metadata_len {
return Err(ValidationError::InvalidHeader(
"Truncated: metadata length exceeds payload size".to_string(),
)
.into());
}
let metadata_bytes = &input[payload_start..payload_start + metadata_len];
payload_start += metadata_len;
FileMetadata::from_bytes(metadata_bytes)?
} else {
FileMetadata::default()
};
let compressed_payload = &input[payload_start..];
let plugin = get_plugin_by_magic(header.magic).ok_or_else(|| {
let available = list_plugins()
.iter()
.map(|p| p.name)
.collect::<Vec<_>>()
.join(", ");
PluginError::NotFound(format!(
"No plugin found for magic number {:02X?}. \
Available plugins: {}. \
Did you call init_plugins()?",
header.magic, available
))
})?;
let decompressed = plugin.decompress(compressed_payload, cancel_flag)?;
let expected_size = usize::try_from(header.original_size).map_err(|_| {
ValidationError::InvalidHeader("Original size exceeds platform limits".to_string())
})?;
if decompressed.len() != expected_size {
return Err(ValidationError::CorruptedData(format!(
"Size mismatch: header says {} bytes, got {} bytes",
header.original_size,
decompressed.len()
))
.into());
}
Ok(DecompressionResult {
data: decompressed,
metadata,
})
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::unreadable_literal)]
mod tests {
use super::*;
use crate::{compress, init_plugins};
#[test]
#[allow(clippy::unwrap_used)]
fn test_decompress_valid() {
init_plugins().unwrap();
let original = b"Test data for decompression";
let compressed = compress(original).unwrap();
let decompressed = decompress(&compressed).unwrap().data;
assert_eq!(original.as_slice(), decompressed.as_slice());
}
#[test]
fn test_decompress_truncated() {
let truncated = &[0x43, 0x52, 0x01, 0x00, 0x01];
let result = decompress(truncated);
assert!(result.is_err());
}
#[test]
fn test_decompress_invalid_magic() {
let mut invalid = vec![0xFF, 0xFF, 0xFF, 0xFF]; invalid.extend_from_slice(&[0u8; 12]);
let result = decompress(&invalid);
assert!(result.is_err());
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_decompress_corrupted_crc() {
init_plugins().unwrap();
let original = b"Data to corrupt";
let mut compressed = compress(original).unwrap();
if compressed.len() > 16 {
compressed[16] ^= 0xFF;
}
let result = decompress(&compressed);
assert!(result.is_err());
}
#[test]
fn test_decompress_with_metadata() {
use crate::plugin::FileMetadata;
use crate::{compress_with_options, CompressionOptions};
init_plugins().expect("Failed to init");
let original = b"Data with metadata";
let metadata = FileMetadata {
mtime: Some(1234567890),
#[cfg(unix)]
permissions: Some(0o644),
};
let options = CompressionOptions::default().with_file_metadata(metadata.clone());
let compressed = compress_with_options(original, &options).expect("Compression failed");
let result = decompress(&compressed).expect("Decompression failed");
assert_eq!(original.as_slice(), result.data.as_slice());
assert_eq!(result.metadata.mtime, metadata.mtime);
#[cfg(unix)]
assert_eq!(result.metadata.permissions, metadata.permissions);
}
#[test]
fn test_decompress_truncated_crc32() {
init_plugins().expect("Failed to init");
let original = b"Test";
let mut compressed = compress(original).expect("Compression failed");
compressed.truncate(CrushHeader::SIZE);
let result = decompress(&compressed);
assert!(result.is_err()); }
#[test]
fn test_decompress_truncated_metadata_length() {
use crate::plugin::FileMetadata;
use crate::{compress_with_options, CompressionOptions};
init_plugins().expect("Failed to init");
let original = b"Test";
let metadata = FileMetadata {
mtime: Some(1234567890),
#[cfg(unix)]
permissions: Some(0o755),
};
let options = CompressionOptions::default().with_file_metadata(metadata);
let mut compressed = compress_with_options(original, &options).expect("Compression failed");
let truncate_pos = CrushHeader::SIZE + 4 + 1;
if compressed.len() > truncate_pos {
compressed.truncate(truncate_pos);
}
let result = decompress(&compressed);
assert!(result.is_err());
}
#[test]
fn test_decompress_truncated_metadata_payload() {
use crate::plugin::FileMetadata;
use crate::{compress_with_options, CompressionOptions};
init_plugins().expect("Failed to init");
let original = b"Test";
let metadata = FileMetadata {
mtime: Some(1234567890),
#[cfg(unix)]
permissions: Some(0o755),
};
let options = CompressionOptions::default().with_file_metadata(metadata);
let mut compressed = compress_with_options(original, &options).expect("Compression failed");
let truncate_pos = CrushHeader::SIZE + 4 + 2 + 3;
if compressed.len() > truncate_pos {
compressed.truncate(truncate_pos);
}
let result = decompress(&compressed);
assert!(result.is_err());
}
#[test]
fn test_decompress_plugin_not_found() {
let mut fake_compressed = vec![
0x43, 0x52, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
fake_compressed.extend_from_slice(&[0x78, 0x9c, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]);
let result = decompress(&fake_compressed);
assert!(result.is_err());
}
#[test]
fn test_decompress_empty_input() {
let result = decompress(&[]);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too short"));
}
#[test]
fn test_decompress_default_metadata() {
init_plugins().expect("Failed to init");
let original = b"No metadata test";
let compressed = compress(original).expect("Compression failed");
let result = decompress(&compressed).expect("Decompression failed");
assert!(result.metadata.mtime.is_none());
#[cfg(unix)]
assert!(result.metadata.permissions.is_none());
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_decompress_corrupted_payload() {
init_plugins().unwrap();
let original = b"Data to corrupt";
let mut compressed = compress(original).unwrap();
if compressed.len() > 24 {
compressed[24] ^= 0xFF;
}
let result = decompress(&compressed);
assert!(result.is_err());
}
}