1use crate::{
29 BrotliCompressor, CompressionAlgorithm, CompressionLevel, CompressionResult, Compressor,
30 ZlibCompressor, ZstdCompressor,
31};
32use std::collections::HashMap;
33use std::sync::{Arc, Mutex};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum SizeClass {
38 Tiny,
40 Small,
42 Medium,
44 Large,
46 Huge,
48}
49
50impl SizeClass {
51 pub fn classify(size: usize) -> Self {
53 match size {
54 0..=1_024 => SizeClass::Tiny,
55 1_025..=102_400 => SizeClass::Small,
56 102_401..=10_485_760 => SizeClass::Medium,
57 10_485_761..=104_857_600 => SizeClass::Large,
58 _ => SizeClass::Huge,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum EntropyClass {
66 VeryLow,
68 Low,
70 Medium,
72 High,
74}
75
76impl EntropyClass {
77 pub fn classify(entropy: f64) -> Self {
79 match entropy {
80 e if e < 4.0 => EntropyClass::VeryLow,
81 e if e < 6.0 => EntropyClass::Low,
82 e if e < 7.5 => EntropyClass::Medium,
83 _ => EntropyClass::High,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum PatternClass {
91 Text,
93 Binary,
95 AlreadyCompressed,
97 Media,
99}
100
101impl PatternClass {
102 pub fn detect(data: &[u8]) -> Self {
104 if data.is_empty() {
105 return PatternClass::Binary;
106 }
107
108 if Self::is_compressed_format(data) {
110 return PatternClass::AlreadyCompressed;
111 }
112 if Self::is_media_format(data) {
113 return PatternClass::Media;
114 }
115
116 let text_chars = data
118 .iter()
119 .take(4096)
120 .filter(|&&b| {
121 b == b'\n' || b == b'\r' || b == b'\t' || (0x20..0x7F).contains(&b) || b >= 0x80
122 })
123 .count();
124
125 let sample_size = data.len().min(4096);
126 let text_ratio = text_chars as f64 / sample_size as f64;
127
128 if text_ratio > 0.85 {
129 PatternClass::Text
130 } else {
131 PatternClass::Binary
132 }
133 }
134
135 fn is_compressed_format(data: &[u8]) -> bool {
136 if data.len() < 4 {
137 return false;
138 }
139
140 matches!(
142 &data[0..4],
143 [0x1f, 0x8b, _, _] | [0x50, 0x4b, 0x03, 0x04] | [0x50, 0x4b, 0x05, 0x06] | [0x50, 0x4b, 0x07, 0x08] | [0x42, 0x5a, 0x68, _] | [0xfd, 0x37, 0x7a, 0x58] | [0x28, 0xb5, 0x2f, 0xfd] ) || (data.len() >= 2 && data[0] == 0x78) }
152
153 fn is_media_format(data: &[u8]) -> bool {
154 if data.len() < 4 {
155 return false;
156 }
157
158 matches!(
160 &data[0..4],
161 [0xff, 0xd8, 0xff, _] | [0x89, 0x50, 0x4e, 0x47] | [0x47, 0x49, 0x46, 0x38] | [0x42, 0x4d, _, _] | [0x66, 0x74, 0x79, 0x70] | [0x52, 0x49, 0x46, 0x46] ) || (data.len() >= 12 && &data[4..12] == b"ftypMSNV") || (data.len() >= 12 && &data[4..12] == b"ftypisom") }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
174pub struct FileProfile {
175 pub size: SizeClass,
177 pub entropy: EntropyClass,
179 pub pattern: PatternClass,
181}
182
183impl FileProfile {
184 pub fn analyze(data: &[u8]) -> Self {
186 let size = SizeClass::classify(data.len());
187 let entropy_value = calculate_entropy(data);
188 let entropy = EntropyClass::classify(entropy_value);
189 let pattern = PatternClass::detect(data);
190
191 FileProfile {
192 size,
193 entropy,
194 pattern,
195 }
196 }
197}
198
199pub fn calculate_entropy(data: &[u8]) -> f64 {
206 if data.is_empty() {
207 return 0.0;
208 }
209
210 let sample = &data[..data.len().min(4096)];
212
213 let mut frequencies = [0u32; 256];
215 for &byte in sample {
216 frequencies[byte as usize] += 1;
217 }
218
219 let len = sample.len() as f64;
221 let mut entropy = 0.0;
222
223 for &count in &frequencies {
224 if count > 0 {
225 let probability = count as f64 / len;
226 entropy -= probability * probability.log2();
227 }
228 }
229
230 entropy
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub struct CompressionStrategy {
236 pub algorithm: CompressionAlgorithm,
238 pub level: CompressionLevel,
240}
241
242impl CompressionStrategy {
243 pub fn select(profile: FileProfile) -> Self {
245 use CompressionAlgorithm::*;
246 use CompressionLevel::*;
247
248 if matches!(
250 profile.pattern,
251 PatternClass::AlreadyCompressed | PatternClass::Media
252 ) {
253 return CompressionStrategy {
254 algorithm: None,
255 level: Fast,
256 };
257 }
258
259 if profile.entropy == EntropyClass::High {
261 return CompressionStrategy {
262 algorithm: None,
263 level: Fast,
264 };
265 }
266
267 match (profile.size, profile.entropy, profile.pattern) {
268 (SizeClass::Tiny, EntropyClass::VeryLow | EntropyClass::Low, _) => {
270 CompressionStrategy {
271 algorithm: Brotli,
272 level: Best,
273 }
274 }
275
276 (SizeClass::Small, EntropyClass::VeryLow | EntropyClass::Low, PatternClass::Text) => {
278 CompressionStrategy {
279 algorithm: Brotli,
280 level: Best,
281 }
282 }
283
284 (SizeClass::Small, EntropyClass::VeryLow | EntropyClass::Low, PatternClass::Binary) => {
286 CompressionStrategy {
287 algorithm: Brotli,
288 level: Default,
289 }
290 }
291
292 (SizeClass::Medium, EntropyClass::VeryLow, _) => CompressionStrategy {
294 algorithm: Brotli,
295 level: Default,
296 },
297
298 (SizeClass::Medium, EntropyClass::Low, _) => CompressionStrategy {
300 algorithm: Zstd,
301 level: Default,
302 },
303
304 (SizeClass::Large | SizeClass::Huge, _, _) => CompressionStrategy {
306 algorithm: Zstd,
307 level: Fast,
308 },
309
310 _ => CompressionStrategy {
312 algorithm: Zstd,
313 level: Default,
314 },
315 }
316 }
317}
318
319#[derive(Debug, Clone)]
321struct CacheEntry {
322 strategy: CompressionStrategy,
323 hits: usize,
324}
325
326#[derive(Debug)]
328struct StrategyCache {
329 cache: HashMap<FileProfile, CacheEntry>,
330 max_size: usize,
331}
332
333impl StrategyCache {
334 fn new(max_size: usize) -> Self {
335 StrategyCache {
336 cache: HashMap::new(),
337 max_size,
338 }
339 }
340
341 fn get(&mut self, profile: &FileProfile) -> Option<CompressionStrategy> {
342 if let Some(entry) = self.cache.get_mut(profile) {
343 entry.hits += 1;
344 Some(entry.strategy)
345 } else {
346 None
347 }
348 }
349
350 fn insert(&mut self, profile: FileProfile, strategy: CompressionStrategy) {
351 if self.cache.len() >= self.max_size {
353 if let Some((&least_used, _)) = self.cache.iter().min_by_key(|(_, entry)| entry.hits) {
354 self.cache.remove(&least_used);
355 }
356 }
357
358 self.cache.insert(profile, CacheEntry { strategy, hits: 1 });
359 }
360}
361
362#[derive(Debug, Clone, Default)]
364pub struct PerformanceStats {
365 pub total_compressions: usize,
367 pub total_bytes_processed: usize,
369 pub total_bytes_compressed: usize,
371 pub cache_hits: usize,
373 pub cache_misses: usize,
375}
376
377impl PerformanceStats {
378 pub fn compression_ratio(&self) -> f64 {
380 if self.total_bytes_processed == 0 {
381 return 1.0;
382 }
383 self.total_bytes_compressed as f64 / self.total_bytes_processed as f64
384 }
385
386 pub fn cache_hit_rate(&self) -> f64 {
388 let total = self.cache_hits + self.cache_misses;
389 if total == 0 {
390 return 0.0;
391 }
392 self.cache_hits as f64 / total as f64
393 }
394}
395
396#[derive(Debug)]
398pub struct AdaptiveCompressor {
399 zstd: ZstdCompressor,
400 brotli: BrotliCompressor,
401 zlib: ZlibCompressor,
402 cache: Arc<Mutex<StrategyCache>>,
403 stats: Arc<Mutex<PerformanceStats>>,
404}
405
406impl AdaptiveCompressor {
407 pub fn new() -> Self {
409 Self::with_cache_size(1000)
410 }
411
412 pub fn with_cache_size(cache_size: usize) -> Self {
414 AdaptiveCompressor {
415 zstd: ZstdCompressor::new(CompressionLevel::Default),
416 brotli: BrotliCompressor::new(CompressionLevel::Default),
417 zlib: ZlibCompressor::new(CompressionLevel::Default),
418 cache: Arc::new(Mutex::new(StrategyCache::new(cache_size))),
419 stats: Arc::new(Mutex::new(PerformanceStats::default())),
420 }
421 }
422
423 pub fn select_strategy(&self, data: &[u8]) -> CompressionStrategy {
425 let profile = FileProfile::analyze(data);
426
427 if let Ok(mut cache) = self.cache.lock() {
429 if let Some(strategy) = cache.get(&profile) {
430 if let Ok(mut stats) = self.stats.lock() {
431 stats.cache_hits += 1;
432 }
433 return strategy;
434 }
435 }
436
437 let strategy = CompressionStrategy::select(profile);
439
440 if let Ok(mut cache) = self.cache.lock() {
442 cache.insert(profile, strategy);
443 }
444 if let Ok(mut stats) = self.stats.lock() {
445 stats.cache_misses += 1;
446 }
447
448 strategy
449 }
450
451 pub fn stats(&self) -> PerformanceStats {
453 self.stats
454 .lock()
455 .ok()
456 .map(|s| s.clone())
457 .unwrap_or_default()
458 }
459
460 pub fn reset_stats(&self) {
462 if let Ok(mut stats) = self.stats.lock() {
463 *stats = PerformanceStats::default();
464 }
465 }
466}
467
468impl Default for AdaptiveCompressor {
469 fn default() -> Self {
470 Self::new()
471 }
472}
473
474impl Compressor for AdaptiveCompressor {
475 fn compress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
476 let strategy = self.select_strategy(data);
477
478 let result = match strategy.algorithm {
479 CompressionAlgorithm::None => Ok(data.to_vec()),
480 CompressionAlgorithm::Zlib => {
481 let compressor = ZlibCompressor::new(strategy.level);
482 compressor.compress(data)
483 }
484 CompressionAlgorithm::Zstd => {
485 let compressor = ZstdCompressor::new(strategy.level);
486 compressor.compress(data)
487 }
488 CompressionAlgorithm::Brotli => {
489 let compressor = BrotliCompressor::new(strategy.level);
490 compressor.compress(data)
491 }
492 };
493
494 if let Ok(compressed) = &result {
496 if let Ok(mut stats) = self.stats.lock() {
497 stats.total_compressions += 1;
498 stats.total_bytes_processed += data.len();
499 stats.total_bytes_compressed += compressed.len();
500 }
501 }
502
503 result
504 }
505
506 fn decompress(&self, data: &[u8]) -> CompressionResult<Vec<u8>> {
507 let algorithm = CompressionAlgorithm::detect(data);
509
510 match algorithm {
511 CompressionAlgorithm::None => Ok(data.to_vec()),
512 CompressionAlgorithm::Zlib => self.zlib.decompress(data),
513 CompressionAlgorithm::Zstd => self.zstd.decompress(data),
514 CompressionAlgorithm::Brotli => self.brotli.decompress(data),
515 }
516 }
517}
518
519#[cfg(test)]
520#[allow(clippy::unwrap_used)]
521mod tests {
522 use super::*;
523
524 #[test]
525 fn test_size_classification() {
526 assert_eq!(SizeClass::classify(500), SizeClass::Tiny);
527 assert_eq!(SizeClass::classify(1024), SizeClass::Tiny);
528 assert_eq!(SizeClass::classify(1025), SizeClass::Small);
529 assert_eq!(SizeClass::classify(50_000), SizeClass::Small);
530 assert_eq!(SizeClass::classify(102_400), SizeClass::Small);
531 assert_eq!(SizeClass::classify(102_401), SizeClass::Medium);
532 assert_eq!(SizeClass::classify(5_000_000), SizeClass::Medium);
533 assert_eq!(SizeClass::classify(10_485_760), SizeClass::Medium);
534 assert_eq!(SizeClass::classify(10_485_761), SizeClass::Large);
535 assert_eq!(SizeClass::classify(50_000_000), SizeClass::Large);
536 assert_eq!(SizeClass::classify(104_857_600), SizeClass::Large);
537 assert_eq!(SizeClass::classify(104_857_601), SizeClass::Huge);
538 }
539
540 #[test]
541 fn test_entropy_classification() {
542 assert_eq!(EntropyClass::classify(3.5), EntropyClass::VeryLow);
543 assert_eq!(EntropyClass::classify(3.99), EntropyClass::VeryLow);
544 assert_eq!(EntropyClass::classify(4.0), EntropyClass::Low);
545 assert_eq!(EntropyClass::classify(5.5), EntropyClass::Low);
546 assert_eq!(EntropyClass::classify(5.99), EntropyClass::Low);
547 assert_eq!(EntropyClass::classify(6.0), EntropyClass::Medium);
548 assert_eq!(EntropyClass::classify(7.0), EntropyClass::Medium);
549 assert_eq!(EntropyClass::classify(7.49), EntropyClass::Medium);
550 assert_eq!(EntropyClass::classify(7.5), EntropyClass::High);
551 assert_eq!(EntropyClass::classify(8.0), EntropyClass::High);
552 }
553
554 #[test]
555 fn test_entropy_calculation() {
556 let uniform = vec![0u8; 1000];
558 let entropy = calculate_entropy(&uniform);
559 assert!(entropy < 0.01);
560
561 let random: Vec<u8> = (0..1000).map(|i| (i % 256) as u8).collect();
563 let entropy = calculate_entropy(&random);
564 assert!(entropy > 5.0); let text = b"Hello, World! This is a test of Shannon entropy calculation.";
568 let entropy = calculate_entropy(text);
569 assert!(entropy > 3.0 && entropy < 6.0);
570 }
571
572 #[test]
573 fn test_pattern_detection_text() {
574 let text = b"This is plain text content with newlines\nand tabs\t.";
575 assert_eq!(PatternClass::detect(text), PatternClass::Text);
576 }
577
578 #[test]
579 fn test_pattern_detection_compressed() {
580 let gzip = b"\x1f\x8b\x08\x00\x00\x00\x00\x00";
581 assert_eq!(PatternClass::detect(gzip), PatternClass::AlreadyCompressed);
582
583 let zip = b"\x50\x4b\x03\x04\x00\x00\x00\x00";
584 assert_eq!(PatternClass::detect(zip), PatternClass::AlreadyCompressed);
585
586 let zstd = b"\x28\xb5\x2f\xfd\x00\x00\x00\x00";
587 assert_eq!(PatternClass::detect(zstd), PatternClass::AlreadyCompressed);
588 }
589
590 #[test]
591 fn test_pattern_detection_media() {
592 let jpeg = b"\xff\xd8\xff\xe0\x00\x10JFIF";
593 assert_eq!(PatternClass::detect(jpeg), PatternClass::Media);
594
595 let png = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a";
596 assert_eq!(PatternClass::detect(png), PatternClass::Media);
597
598 let gif = b"\x47\x49\x46\x38\x39\x61";
599 assert_eq!(PatternClass::detect(gif), PatternClass::Media);
600 }
601
602 #[test]
603 fn test_strategy_selection_high_entropy() {
604 let profile = FileProfile {
606 size: SizeClass::Medium,
607 entropy: EntropyClass::High,
608 pattern: PatternClass::Binary,
609 };
610 let strategy = CompressionStrategy::select(profile);
611 assert_eq!(strategy.algorithm, CompressionAlgorithm::None);
612 }
613
614 #[test]
615 fn test_strategy_selection_compressed() {
616 let profile = FileProfile {
618 size: SizeClass::Small,
619 entropy: EntropyClass::Low,
620 pattern: PatternClass::AlreadyCompressed,
621 };
622 let strategy = CompressionStrategy::select(profile);
623 assert_eq!(strategy.algorithm, CompressionAlgorithm::None);
624 }
625
626 #[test]
627 fn test_strategy_selection_tiny_text() {
628 let profile = FileProfile {
630 size: SizeClass::Tiny,
631 entropy: EntropyClass::Low,
632 pattern: PatternClass::Text,
633 };
634 let strategy = CompressionStrategy::select(profile);
635 assert_eq!(strategy.algorithm, CompressionAlgorithm::Brotli);
636 assert_eq!(strategy.level, CompressionLevel::Best);
637 }
638
639 #[test]
640 fn test_strategy_selection_large_file() {
641 let profile = FileProfile {
643 size: SizeClass::Large,
644 entropy: EntropyClass::Low,
645 pattern: PatternClass::Binary,
646 };
647 let strategy = CompressionStrategy::select(profile);
648 assert_eq!(strategy.algorithm, CompressionAlgorithm::Zstd);
649 assert_eq!(strategy.level, CompressionLevel::Fast);
650 }
651
652 #[test]
653 fn test_file_profile_analysis() {
654 let text = b"Hello, World! This is a test.";
656 let profile = FileProfile::analyze(text);
657 assert_eq!(profile.size, SizeClass::Tiny);
658 assert_eq!(profile.pattern, PatternClass::Text);
659
660 let jpeg = b"\xff\xd8\xff\xe0\x00\x10JFIF\x00";
662 let profile = FileProfile::analyze(jpeg);
663 assert_eq!(profile.pattern, PatternClass::Media);
664 }
665
666 #[test]
667 fn test_adaptive_compressor_text() {
668 let compressor = AdaptiveCompressor::new();
669 let text = b"Hello, World! ".repeat(100); let compressed = compressor.compress(&text).unwrap();
672 let decompressed = compressor.decompress(&compressed).unwrap();
673
674 assert_eq!(&text[..], &decompressed[..]);
675 assert!(compressed.len() < text.len());
676 }
677
678 #[test]
679 fn test_adaptive_compressor_random() {
680 let compressor = AdaptiveCompressor::new();
681 let random: Vec<u8> = (0..1000).map(|i| ((i * 7 + 13) % 256) as u8).collect();
683
684 let compressed = compressor.compress(&random).unwrap();
685 let decompressed = compressor.decompress(&compressed).unwrap();
686
687 assert_eq!(&random[..], &decompressed[..]);
688 }
690
691 #[test]
692 fn test_adaptive_compressor_cache() {
693 let compressor = AdaptiveCompressor::new();
694 let text = b"Test data";
695
696 compressor.compress(text).unwrap();
698 let stats1 = compressor.stats();
699 assert_eq!(stats1.cache_misses, 1);
700 assert_eq!(stats1.cache_hits, 0);
701
702 compressor.compress(text).unwrap();
704 let stats2 = compressor.stats();
705 assert_eq!(stats2.cache_hits, 1);
706 }
707
708 #[test]
709 fn test_performance_stats() {
710 let compressor = AdaptiveCompressor::new();
711 let data1 = b"Hello, World!".repeat(10);
712 let data2 = b"Test data".repeat(20);
713
714 compressor.compress(&data1).unwrap();
715 compressor.compress(&data2).unwrap();
716
717 let stats = compressor.stats();
718 assert_eq!(stats.total_compressions, 2);
719 assert!(stats.total_bytes_processed > 0);
720 assert!(stats.total_bytes_compressed > 0);
721 assert!(stats.compression_ratio() < 1.0); }
723
724 #[test]
725 fn test_strategy_cache_lru_eviction() {
726 let mut cache = StrategyCache::new(2);
727
728 let profile1 = FileProfile {
729 size: SizeClass::Tiny,
730 entropy: EntropyClass::Low,
731 pattern: PatternClass::Text,
732 };
733 let profile2 = FileProfile {
734 size: SizeClass::Small,
735 entropy: EntropyClass::Medium,
736 pattern: PatternClass::Binary,
737 };
738 let profile3 = FileProfile {
739 size: SizeClass::Large,
740 entropy: EntropyClass::High,
741 pattern: PatternClass::Binary,
742 };
743
744 let strategy = CompressionStrategy {
745 algorithm: CompressionAlgorithm::Zstd,
746 level: CompressionLevel::Default,
747 };
748
749 cache.insert(profile1, strategy);
751 cache.insert(profile2, strategy);
752 assert_eq!(cache.cache.len(), 2);
753
754 cache.get(&profile1);
756
757 cache.insert(profile3, strategy);
759 assert_eq!(cache.cache.len(), 2);
760 assert!(cache.cache.contains_key(&profile1));
761 assert!(cache.cache.contains_key(&profile3));
762 assert!(!cache.cache.contains_key(&profile2));
763 }
764}