use crate::cancel::CancellationToken;
use crate::error::Result;
use crate::plugin::registry::{get_default_plugin, get_plugin_by_magic};
use crate::plugin::timeout::{run_with_timeout_and_cancel_scoped, run_with_timeout_scoped};
use crate::plugin::{CrushHeader, FileMetadata, PluginSelector, ScoringWeights};
use crc32fast::Hasher;
use std::sync::Arc;
use std::time::Duration;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(0);
#[derive(Clone)]
pub struct CompressionOptions {
plugin_name: Option<String>,
weights: ScoringWeights,
timeout: Duration,
file_metadata: Option<FileMetadata>,
cancel_token: Option<Arc<dyn CancellationToken>>,
}
impl CompressionOptions {
#[must_use]
pub fn new() -> Self {
Self {
plugin_name: None,
weights: ScoringWeights::default(),
timeout: DEFAULT_TIMEOUT,
file_metadata: None,
cancel_token: None,
}
}
#[must_use]
pub fn with_plugin(mut self, name: &str) -> Self {
self.plugin_name = Some(name.to_string());
self
}
#[must_use]
pub fn with_weights(mut self, weights: ScoringWeights) -> Self {
self.weights = weights;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_file_metadata(mut self, metadata: FileMetadata) -> Self {
self.file_metadata = Some(metadata);
self
}
#[must_use]
pub fn with_cancel_token(mut self, token: Arc<dyn CancellationToken>) -> Self {
self.cancel_token = Some(token);
self
}
}
impl std::fmt::Debug for CompressionOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompressionOptions")
.field("plugin_name", &self.plugin_name)
.field("weights", &self.weights)
.field("timeout", &self.timeout)
.field("file_metadata", &self.file_metadata)
.field(
"cancel_token",
&self.cancel_token.as_ref().map(|_| "Some(...)"),
)
.finish()
}
}
impl Default for CompressionOptions {
fn default() -> Self {
Self::new()
}
}
pub fn compress(input: &[u8]) -> Result<Vec<u8>> {
let plugin = get_default_plugin().ok_or_else(|| {
crate::error::PluginError::NotFound(
"Default DEFLATE plugin not found. Call init_plugins() first.".to_string(),
)
})?;
let default_magic = [0x43, 0x52, 0x01, 0x00];
let compressed_payload = std::thread::scope(|s| {
run_with_timeout_scoped(s, DEFAULT_TIMEOUT, |cancel_flag| {
plugin.compress(input, cancel_flag)
})
})?;
let mut hasher = Hasher::new();
hasher.update(&compressed_payload);
let crc32 = hasher.finalize();
let header = CrushHeader::new(default_magic, input.len() as u64).with_crc32();
let mut output = Vec::with_capacity(CrushHeader::SIZE + 4 + compressed_payload.len());
output.extend_from_slice(&header.to_bytes());
output.extend_from_slice(&crc32.to_le_bytes());
output.extend_from_slice(&compressed_payload);
Ok(output)
}
pub fn compress_with_options(input: &[u8], options: &CompressionOptions) -> Result<Vec<u8>> {
if let Some(ref token) = options.cancel_token {
if token.is_cancelled() {
return Err(crate::error::CrushError::Cancelled);
}
}
let selector = PluginSelector::new(options.weights);
let selected_metadata = if let Some(ref plugin_name) = options.plugin_name {
selector.select_by_name(plugin_name)?
} else {
selector.select()?
};
let plugin = get_plugin_by_magic(selected_metadata.magic_number).ok_or_else(|| {
crate::error::PluginError::NotFound(format!(
"Plugin '{}' metadata found but not in registry",
selected_metadata.name
))
})?;
let timeout = options.timeout;
let cancel_token = options.cancel_token.clone();
let compressed_payload = std::thread::scope(|s| {
run_with_timeout_and_cancel_scoped(s, timeout, cancel_token, |cancel_flag| {
plugin.compress(input, cancel_flag)
})
})?;
let metadata_bytes = options
.file_metadata
.as_ref()
.map_or(Vec::new(), super::plugin::metadata::FileMetadata::to_bytes);
let mut payload_with_metadata = Vec::new();
if !metadata_bytes.is_empty() {
#[allow(clippy::cast_possible_truncation)]
let metadata_len = metadata_bytes.len() as u16; payload_with_metadata.extend_from_slice(&metadata_len.to_le_bytes());
payload_with_metadata.extend_from_slice(&metadata_bytes);
}
payload_with_metadata.extend_from_slice(&compressed_payload);
let mut hasher = Hasher::new();
hasher.update(&payload_with_metadata);
let crc32 = hasher.finalize();
let mut header =
CrushHeader::new(selected_metadata.magic_number, input.len() as u64).with_crc32();
if !metadata_bytes.is_empty() {
header = header.with_metadata();
}
let mut output = Vec::with_capacity(CrushHeader::SIZE + 4 + payload_with_metadata.len());
output.extend_from_slice(&header.to_bytes());
output.extend_from_slice(&crc32.to_le_bytes());
output.extend_from_slice(&payload_with_metadata);
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::init_plugins;
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_basic() {
init_plugins().unwrap();
let data = b"Hello, Crush!";
let compressed = compress(data).unwrap();
assert!(compressed.len() >= CrushHeader::SIZE);
assert_eq!(&compressed[0..4], &[0x43, 0x52, 0x01, 0x00]);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_empty() {
init_plugins().unwrap();
let data = b"";
let compressed = compress(data).unwrap();
assert!(compressed.len() >= CrushHeader::SIZE);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_large() {
init_plugins().unwrap();
let data = vec![0x42; 1_000_000]; let compressed = compress(&data).unwrap();
assert!(compressed.len() < data.len() / 2);
}
#[test]
fn test_compression_options_default() {
let options = CompressionOptions::default();
assert_eq!(options.timeout, DEFAULT_TIMEOUT);
assert!(options.plugin_name.is_none());
assert!(options.file_metadata.is_none());
assert!(options.cancel_token.is_none());
}
#[test]
fn test_compression_options_new() {
let options = CompressionOptions::new();
assert_eq!(options.timeout, DEFAULT_TIMEOUT);
}
#[test]
fn test_compression_options_with_plugin() {
let options = CompressionOptions::new().with_plugin("deflate");
assert_eq!(options.plugin_name, Some("deflate".to_string()));
}
#[test]
fn test_compression_options_with_weights() {
let weights = ScoringWeights {
throughput: 0.5,
compression_ratio: 0.5,
};
let options = CompressionOptions::new().with_weights(weights);
assert!((options.weights.throughput - 0.5).abs() < f64::EPSILON);
assert!((options.weights.compression_ratio - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_compression_options_with_timeout() {
let timeout = Duration::from_secs(10);
let options = CompressionOptions::new().with_timeout(timeout);
assert_eq!(options.timeout, timeout);
}
#[test]
fn test_compression_options_with_file_metadata() {
let metadata = FileMetadata {
mtime: Some(1_234_567_890),
#[cfg(unix)]
permissions: Some(0o644),
};
let options = CompressionOptions::new().with_file_metadata(metadata);
assert!(options.file_metadata.is_some());
}
#[test]
fn test_compression_options_with_cancel_token() {
use crate::cancel::AtomicCancellationToken;
let token: Arc<dyn CancellationToken> = Arc::new(AtomicCancellationToken::new());
let options = CompressionOptions::new().with_cancel_token(token);
assert!(options.cancel_token.is_some());
}
#[test]
fn test_compression_options_debug() {
let options = CompressionOptions::new().with_plugin("test");
let debug_str = format!("{options:?}");
assert!(debug_str.contains("CompressionOptions"));
assert!(debug_str.contains("test"));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_default() {
init_plugins().unwrap();
let data = b"Test data for compression";
let options = CompressionOptions::default();
let compressed = compress_with_options(data, &options).unwrap();
assert!(compressed.len() >= CrushHeader::SIZE);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_manual_plugin() {
init_plugins().unwrap();
let data = b"Test data";
let options = CompressionOptions::default().with_plugin("deflate");
let compressed = compress_with_options(data, &options).unwrap();
assert!(compressed.len() >= CrushHeader::SIZE);
assert_eq!(&compressed[0..4], &[0x43, 0x52, 0x01, 0x00]); }
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_invalid_plugin() {
init_plugins().unwrap();
let data = b"Test data";
let options = CompressionOptions::default().with_plugin("nonexistent");
let result = compress_with_options(data, &options);
assert!(result.is_err());
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_with_metadata() {
init_plugins().unwrap();
let data = b"Test data with metadata";
let metadata = FileMetadata {
mtime: Some(1_234_567_890),
#[cfg(unix)]
permissions: Some(0o644),
};
let options = CompressionOptions::default().with_file_metadata(metadata);
let compressed = compress_with_options(data, &options).unwrap();
assert!(compressed.len() > CrushHeader::SIZE + 4); }
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_cancellation() {
use crate::cancel::AtomicCancellationToken;
init_plugins().unwrap();
let data = b"Test data";
let token = Arc::new(AtomicCancellationToken::new());
token.cancel();
let options = CompressionOptions::default().with_cancel_token(token);
let result = compress_with_options(data, &options);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::error::CrushError::Cancelled
));
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_timeout() {
init_plugins().unwrap();
let data = vec![0u8; 10_000_000]; let options = CompressionOptions::default().with_timeout(Duration::from_nanos(1));
let result = compress_with_options(&data, &options);
let _ = result;
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_with_options_zero_timeout() {
init_plugins().unwrap();
let data = b"Test with no timeout";
let options = CompressionOptions::default().with_timeout(Duration::from_secs(0));
let result = compress_with_options(data, &options);
assert!(result.is_ok());
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compress_roundtrip_with_options() {
use crate::decompress;
init_plugins().unwrap();
let original = b"Roundtrip test data with options";
let options = CompressionOptions::default();
let compressed = compress_with_options(original, &options).unwrap();
let result = decompress(&compressed).unwrap();
assert_eq!(result.data, original);
}
#[test]
#[allow(clippy::unwrap_used)]
fn test_compression_options_builder_chain() {
use crate::cancel::AtomicCancellationToken;
let metadata = FileMetadata {
mtime: Some(1_234_567_890),
#[cfg(unix)]
permissions: Some(0o644),
};
let weights = ScoringWeights {
throughput: 0.7,
compression_ratio: 0.3,
};
let token: Arc<dyn CancellationToken> = Arc::new(AtomicCancellationToken::new());
let options = CompressionOptions::new()
.with_plugin("deflate")
.with_weights(weights)
.with_timeout(Duration::from_secs(30))
.with_file_metadata(metadata)
.with_cancel_token(token);
assert_eq!(options.plugin_name, Some("deflate".to_string()));
assert!((options.weights.throughput - 0.7).abs() < f64::EPSILON);
assert_eq!(options.timeout, Duration::from_secs(30));
assert!(options.file_metadata.is_some());
assert!(options.cancel_token.is_some());
}
}