Skip to main content

crush_core/plugin/
default.rs

1//! Default DEFLATE compression plugin
2//!
3//! Provides standard DEFLATE compression using the flate2 crate.
4//! This plugin is always available and serves as the default compression algorithm.
5
6use crate::error::{PluginError, Result};
7use crate::plugin::{CompressionAlgorithm, PluginMetadata, COMPRESSION_ALGORITHMS};
8use flate2::read::{DeflateDecoder, DeflateEncoder};
9use flate2::Compression;
10use linkme::distributed_slice;
11use std::io::Read;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::Arc;
14
15/// DEFLATE compression plugin (RFC 1951)
16///
17/// Uses flate2's DEFLATE implementation with default compression level (6).
18/// This is the standard compression algorithm used by gzip, zlib, and PNG.
19pub struct DeflatePlugin;
20
21impl CompressionAlgorithm for DeflatePlugin {
22    fn name(&self) -> &'static str {
23        "deflate"
24    }
25
26    fn metadata(&self) -> PluginMetadata {
27        PluginMetadata {
28            name: "deflate",
29            version: "1.0.0",
30            // Magic number: CR (Crush) + V1 + ID 0x00 (default DEFLATE)
31            magic_number: [0x43, 0x52, 0x01, 0x00],
32            // Measured throughput: ~200 MB/s compression (typical on modern CPU)
33            throughput: 200.0,
34            // Compression ratio: ~0.35 (65% size reduction on text)
35            compression_ratio: 0.35,
36            description: "Standard DEFLATE compression (RFC 1951)",
37        }
38    }
39
40    fn compress(&self, input: &[u8], cancel_flag: Arc<AtomicBool>) -> Result<Vec<u8>> {
41        // Check cancellation before starting
42        if cancel_flag.load(Ordering::Acquire) {
43            return Err(PluginError::Cancelled.into());
44        }
45
46        let mut encoder = DeflateEncoder::new(input, Compression::default());
47        let mut compressed = Vec::new();
48
49        // Read compressed data in chunks, checking cancellation periodically
50        let mut buffer = vec![0u8; 64 * 1024]; // 64KB chunks
51        loop {
52            if cancel_flag.load(Ordering::Acquire) {
53                return Err(PluginError::Cancelled.into());
54            }
55
56            match encoder.read(&mut buffer) {
57                Ok(0) => break, // EOF
58                Ok(n) => compressed.extend_from_slice(&buffer[..n]),
59                Err(e) => {
60                    return Err(PluginError::OperationFailed(format!(
61                        "DEFLATE compression failed: {e}"
62                    ))
63                    .into())
64                }
65            }
66        }
67
68        Ok(compressed)
69    }
70
71    fn decompress(&self, input: &[u8], cancel_flag: Arc<AtomicBool>) -> Result<Vec<u8>> {
72        // Check cancellation before starting
73        if cancel_flag.load(Ordering::Acquire) {
74            return Err(PluginError::Cancelled.into());
75        }
76
77        let mut decoder = DeflateDecoder::new(input);
78        let mut decompressed = Vec::new();
79
80        // Read decompressed data in chunks, checking cancellation periodically
81        let mut buffer = vec![0u8; 64 * 1024]; // 64KB chunks
82        loop {
83            if cancel_flag.load(Ordering::Acquire) {
84                return Err(PluginError::Cancelled.into());
85            }
86
87            match decoder.read(&mut buffer) {
88                Ok(0) => break, // EOF
89                Ok(n) => decompressed.extend_from_slice(&buffer[..n]),
90                Err(e) => {
91                    return Err(PluginError::OperationFailed(format!(
92                        "DEFLATE decompression failed: {e}"
93                    ))
94                    .into())
95                }
96            }
97        }
98
99        Ok(decompressed)
100    }
101
102    fn detect(&self, _file_header: &[u8]) -> bool {
103        // DEFLATE plugin accepts all data (it's the default fallback)
104        // File type detection is primarily for routing during compression.
105        // For decompression, we use the magic number in the Crush header.
106        true
107    }
108}
109
110/// Register DEFLATE plugin at compile-time
111#[distributed_slice(COMPRESSION_ALGORITHMS)]
112static DEFLATE_PLUGIN: &dyn CompressionAlgorithm = &DeflatePlugin;
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_deflate_metadata() {
120        let plugin = DeflatePlugin;
121        let metadata = plugin.metadata();
122
123        assert_eq!(metadata.name, "deflate");
124        assert_eq!(metadata.magic_number, [0x43, 0x52, 0x01, 0x00]);
125    }
126
127    #[test]
128    #[allow(clippy::unwrap_used)]
129    fn test_deflate_roundtrip() {
130        let plugin = DeflatePlugin;
131        let cancel_flag = Arc::new(AtomicBool::new(false));
132
133        let original = b"Hello, DEFLATE! This is a test of the compression algorithm.";
134        let compressed = plugin.compress(original, Arc::clone(&cancel_flag)).unwrap();
135        let decompressed = plugin.decompress(&compressed, cancel_flag).unwrap();
136
137        assert_eq!(original.as_slice(), decompressed.as_slice());
138    }
139
140    #[test]
141    #[allow(clippy::unwrap_used)]
142    fn test_deflate_empty() {
143        let plugin = DeflatePlugin;
144        let cancel_flag = Arc::new(AtomicBool::new(false));
145
146        let original = b"";
147        let compressed = plugin.compress(original, Arc::clone(&cancel_flag)).unwrap();
148        let decompressed = plugin.decompress(&compressed, cancel_flag).unwrap();
149
150        assert_eq!(original.as_slice(), decompressed.as_slice());
151    }
152
153    #[test]
154    #[allow(clippy::unwrap_used)]
155    fn test_deflate_cancellation() {
156        let plugin = DeflatePlugin;
157        let cancel_flag = Arc::new(AtomicBool::new(true)); // Pre-cancelled
158
159        let original = b"This should be cancelled";
160        let result = plugin.compress(original, cancel_flag);
161
162        assert!(result.is_err());
163        // Verify it's a cancellation error by checking the error message
164        let err = result.unwrap_err();
165        assert!(
166            matches!(
167                err,
168                crate::error::CrushError::Plugin(crate::error::PluginError::Cancelled)
169            ),
170            "Expected PluginError::Cancelled, got: {err:?}"
171        );
172    }
173
174    #[test]
175    #[allow(clippy::unwrap_used)]
176    fn test_deflate_decompress_invalid_data() {
177        let plugin = DeflatePlugin;
178        let cancel_flag = Arc::new(AtomicBool::new(false));
179
180        // Invalid DEFLATE data
181        let invalid_data = b"This is not compressed data!";
182        let result = plugin.decompress(invalid_data, cancel_flag);
183
184        assert!(result.is_err());
185        let err_msg = result.unwrap_err().to_string();
186        assert!(err_msg.contains("DEFLATE decompression failed"));
187    }
188
189    #[test]
190    #[allow(clippy::unwrap_used)]
191    fn test_deflate_decompress_cancellation() {
192        let plugin = DeflatePlugin;
193        let cancel_flag = Arc::new(AtomicBool::new(true)); // Pre-cancelled
194
195        let data = b"Some data";
196        let result = plugin.decompress(data, cancel_flag);
197
198        assert!(result.is_err());
199        let err = result.unwrap_err();
200        assert!(
201            matches!(
202                err,
203                crate::error::CrushError::Plugin(crate::error::PluginError::Cancelled)
204            ),
205            "Expected PluginError::Cancelled, got: {err:?}"
206        );
207    }
208
209    #[test]
210    fn test_deflate_detect() {
211        let plugin = DeflatePlugin;
212
213        // DEFLATE plugin accepts all data (it's the default fallback)
214        assert!(plugin.detect(b"any data"));
215        assert!(plugin.detect(&[]));
216        assert!(plugin.detect(b"\x00\x01\x02\x03"));
217    }
218
219    #[test]
220    fn test_deflate_name() {
221        let plugin = DeflatePlugin;
222        assert_eq!(plugin.name(), "deflate");
223    }
224
225    #[test]
226    #[allow(clippy::unwrap_used)]
227    fn test_deflate_large_data() {
228        let plugin = DeflatePlugin;
229        let cancel_flag = Arc::new(AtomicBool::new(false));
230
231        // Test with data larger than 64KB buffer to ensure loop iteration
232        let original = vec![0x42u8; 128 * 1024]; // 128KB
233        let compressed = plugin
234            .compress(&original, Arc::clone(&cancel_flag))
235            .unwrap();
236        let decompressed = plugin.decompress(&compressed, cancel_flag).unwrap();
237
238        assert_eq!(original, decompressed);
239        // Verify compression actually happened
240        assert!(compressed.len() < original.len());
241    }
242
243    #[test]
244    fn test_deflate_metadata_values() {
245        let plugin = DeflatePlugin;
246        let metadata = plugin.metadata();
247
248        assert_eq!(metadata.version, "1.0.0");
249        assert!(metadata.throughput > 0.0);
250        assert!(metadata.compression_ratio > 0.0 && metadata.compression_ratio <= 1.0);
251        assert!(!metadata.description.is_empty());
252    }
253}