1use crate::{error::Error, Result};
4use std::io::Read;
5#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
9pub enum CompressionAlgorithm {
10 None,
12 #[default]
14 Lz4,
15 Snappy,
17 Deflate,
19 Zstd,
21}
22
23const MAX_DECOMPRESSED_SIZE: usize = 128 * 1024 * 1024;
25
26impl CompressionAlgorithm {
27 fn from_name_opt(s: &str) -> Option<Self> {
35 let simple = s.rsplit('.').next().unwrap_or(s);
37 match simple.to_uppercase().as_str() {
38 "NONE" | "NOOPCOMPRESSOR" | "NOCOMPRESSOR" | "NULLCOMPRESSOR" => {
39 Some(CompressionAlgorithm::None)
40 }
41 "LZ4" | "LZ4COMPRESSOR" => Some(CompressionAlgorithm::Lz4),
42 "SNAPPY" | "SNAPPYCOMPRESSOR" => Some(CompressionAlgorithm::Snappy),
43 "DEFLATE" | "DEFLATECOMPRESSOR" => Some(CompressionAlgorithm::Deflate),
44 "ZSTD" | "ZSTDCOMPRESSOR" => Some(CompressionAlgorithm::Zstd),
45 _ => None,
46 }
47 }
48
49 pub fn parse(s: &str) -> Result<Self> {
56 Self::from_name_opt(s).ok_or_else(|| {
57 Error::UnsupportedFormat(format!(
58 "Unsupported compression algorithm '{}'. CQLite supports: \
59 LZ4Compressor, SnappyCompressor, DeflateCompressor, ZstdCompressor \
60 (or NONE for uncompressed).",
61 s
62 ))
63 })
64 }
65}
66
67impl From<String> for CompressionAlgorithm {
68 fn from(s: String) -> Self {
69 Self::from(s.as_str())
70 }
71}
72
73impl From<&str> for CompressionAlgorithm {
74 fn from(s: &str) -> Self {
83 Self::from_name_opt(s).unwrap_or(CompressionAlgorithm::None)
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct ChunkedDecompressionConfig {
90 pub max_memory_mb: usize,
92 pub chunk_size: usize,
94 pub max_output_size: usize,
96}
97
98impl Default for ChunkedDecompressionConfig {
99 fn default() -> Self {
100 Self {
101 max_memory_mb: 32, chunk_size: 1024 * 1024, max_output_size: 128 * 1024 * 1024, }
105 }
106}
107
108pub struct StreamingDecompressor {
110 algorithm: CompressionAlgorithm,
111 config: ChunkedDecompressionConfig,
112 bytes_processed: usize,
113 bytes_output: usize,
114}
115
116fn validate_decompression_size(uncompressed_size: usize) -> Result<()> {
121 if uncompressed_size > MAX_DECOMPRESSED_SIZE {
122 return Err(Error::storage(format!(
123 "Decompression bomb protection: size {} exceeds limit {} (128MB)",
124 uncompressed_size, MAX_DECOMPRESSED_SIZE
125 )));
126 }
127 Ok(())
128}
129
130#[cfg(feature = "snappy")]
141fn snappy_decompress_raw(data: &[u8], decode_attempts: &mut usize) -> Result<Vec<u8>> {
142 use snap::raw::Decoder;
143 *decode_attempts += 1;
144
145 let advertised = snap::raw::decompress_len(data)
152 .map_err(|e| Error::storage(format!("Snappy (raw) length decode failed: {}", e)))?;
153 if advertised > MAX_DECOMPRESSED_SIZE {
154 return Err(Error::storage(format!(
155 "Decompression bomb protection: advertised size {} exceeds limit {} (128MB)",
156 advertised, MAX_DECOMPRESSED_SIZE
157 )));
158 }
159
160 let mut decoder = Decoder::new();
161 let decompressed = decoder
162 .decompress_vec(data)
163 .map_err(|e| Error::storage(format!("Snappy (raw) decompression failed: {}", e)))?;
164 if decompressed.len() > MAX_DECOMPRESSED_SIZE {
167 return Err(Error::storage(format!(
168 "Decompression bomb protection: decompressed size {} exceeds limit {} (128MB)",
169 decompressed.len(),
170 MAX_DECOMPRESSED_SIZE
171 )));
172 }
173 Ok(decompressed)
174}
175
176pub struct Compression {
178 algorithm: CompressionAlgorithm,
179}
180
181impl Compression {
182 pub fn new(algorithm: CompressionAlgorithm) -> Result<Self> {
184 Ok(Self { algorithm })
185 }
186
187 pub fn compress(&self, data: &[u8]) -> Result<Vec<u8>> {
189 match self.algorithm {
190 CompressionAlgorithm::None => Ok(data.to_vec()),
191 CompressionAlgorithm::Lz4 => {
192 #[cfg(feature = "lz4")]
193 {
194 use lz4_flex::compress_prepend_size;
196
197 let compressed = compress_prepend_size(data);
199 Ok(compressed)
200 }
201 #[cfg(not(feature = "lz4"))]
202 {
203 Err(Error::storage("LZ4 compression not available".to_string()))
204 }
205 }
206 CompressionAlgorithm::Snappy => {
207 #[cfg(feature = "snappy")]
208 {
209 use snap::raw::Encoder;
210
211 let mut encoder = Encoder::new();
217 encoder
218 .compress_vec(data)
219 .map_err(|e| Error::storage(format!("Snappy compression failed: {}", e)))
220 }
221 #[cfg(not(feature = "snappy"))]
222 {
223 Err(Error::storage(
224 "Snappy compression not available".to_string(),
225 ))
226 }
227 }
228 CompressionAlgorithm::Deflate => {
229 #[cfg(feature = "deflate")]
230 {
231 use flate2::write::ZlibEncoder;
232 use flate2::Compression as DeflateCompression;
233 use std::io::Write;
234
235 let mut encoder = ZlibEncoder::new(Vec::new(), DeflateCompression::new(6));
240 encoder.write_all(data).map_err(|e| {
241 Error::storage(format!("Deflate compression failed: {}", e))
242 })?;
243 encoder
244 .finish()
245 .map_err(|e| Error::storage(format!("Deflate finish failed: {}", e)))
246 }
247 #[cfg(not(feature = "deflate"))]
248 {
249 Err(Error::storage(
250 "Deflate compression not available".to_string(),
251 ))
252 }
253 }
254 CompressionAlgorithm::Zstd => {
255 #[cfg(feature = "zstd")]
256 {
257 use zstd::stream::encode_all;
258
259 encode_all(data, 3)
263 .map_err(|e| Error::storage(format!("Zstd compression failed: {}", e)))
264 }
265 #[cfg(not(feature = "zstd"))]
266 {
267 Err(Error::storage("Zstd compression not available".to_string()))
268 }
269 }
270 }
271 }
272
273 pub fn create_streaming_decompressor(
275 &self,
276 config: ChunkedDecompressionConfig,
277 ) -> StreamingDecompressor {
278 StreamingDecompressor {
279 algorithm: self.algorithm,
280 config,
281 bytes_processed: 0,
282 bytes_output: 0,
283 }
284 }
285
286 pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
288 crate::storage::sstable::read_work_counters::record_decompress();
292 match self.algorithm {
293 CompressionAlgorithm::None => Ok(data.to_vec()),
294 CompressionAlgorithm::Lz4 => {
295 #[cfg(feature = "lz4")]
296 {
297 use lz4_flex::decompress_size_prepended;
298
299 if data.len() < 4 {
301 return Err(Error::storage("Invalid LZ4 data: too short".to_string()));
302 }
303
304 let uncompressed_size =
306 u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
307
308 validate_decompression_size(uncompressed_size)?;
313
314 decompress_size_prepended(data)
316 .map_err(|e| Error::storage(format!("LZ4 decompression failed: {}", e)))
317 }
318 #[cfg(not(feature = "lz4"))]
319 {
320 Err(Error::storage("LZ4 compression not available".to_string()))
321 }
322 }
323 CompressionAlgorithm::Snappy => {
324 #[cfg(feature = "snappy")]
325 {
326 snappy_decompress_raw(data, &mut 0)
333 }
334 #[cfg(not(feature = "snappy"))]
335 {
336 Err(Error::storage(
337 "Snappy compression not available".to_string(),
338 ))
339 }
340 }
341 CompressionAlgorithm::Deflate => {
342 #[cfg(feature = "deflate")]
343 {
344 use flate2::read::ZlibDecoder;
345 use std::io::Read;
346
347 if data.is_empty() {
353 return Err(Error::storage(
354 "Invalid Deflate data: empty chunk".to_string(),
355 ));
356 }
357
358 let mut decoder = ZlibDecoder::new(data).take(MAX_DECOMPRESSED_SIZE as u64 + 1);
363 let mut decompressed = Vec::new();
364 decoder.read_to_end(&mut decompressed).map_err(|e| {
365 Error::storage(format!("Deflate decompression failed: {}", e))
366 })?;
367
368 if decompressed.len() > MAX_DECOMPRESSED_SIZE {
369 return Err(Error::storage(format!(
370 "Decompression bomb protection: Deflate output exceeds limit {} (128MB)",
371 MAX_DECOMPRESSED_SIZE
372 )));
373 }
374
375 Ok(decompressed)
376 }
377 #[cfg(not(feature = "deflate"))]
378 {
379 Err(Error::storage(
380 "Deflate compression not available".to_string(),
381 ))
382 }
383 }
384 CompressionAlgorithm::Zstd => {
385 #[cfg(feature = "zstd")]
386 {
387 use std::io::Read;
388 use zstd::stream::read::Decoder as ZstdDecoder;
389
390 if data.is_empty() {
400 return Err(Error::storage("Invalid Zstd data: empty chunk".to_string()));
401 }
402
403 let mut decoder = ZstdDecoder::new(data)
409 .map_err(|e| Error::storage(format!("Zstd decoder init failed: {}", e)))?
410 .take(MAX_DECOMPRESSED_SIZE as u64 + 1);
411 let mut decompressed = Vec::new();
412 decoder
413 .read_to_end(&mut decompressed)
414 .map_err(|e| Error::storage(format!("Zstd decompression failed: {}", e)))?;
415
416 if decompressed.len() > MAX_DECOMPRESSED_SIZE {
417 return Err(Error::storage(format!(
418 "Decompression bomb protection: Zstd output exceeds limit {} (128MB)",
419 MAX_DECOMPRESSED_SIZE
420 )));
421 }
422
423 Ok(decompressed)
424 }
425 #[cfg(not(feature = "zstd"))]
426 {
427 Err(Error::storage("Zstd compression not available".to_string()))
428 }
429 }
430 }
431 }
432
433 pub fn algorithm(&self) -> &CompressionAlgorithm {
435 &self.algorithm
436 }
437
438 pub fn should_use_streaming(
440 &self,
441 compressed_size: usize,
442 config: &ChunkedDecompressionConfig,
443 ) -> bool {
444 compressed_size > config.max_memory_mb * 1024 * 1024 / 4 }
446}
447
448impl StreamingDecompressor {
449 pub async fn decompress_streaming<R: Read + Send>(
451 &mut self,
452 reader: R,
453 expected_size: Option<usize>,
454 ) -> Result<Vec<u8>> {
455 let memory_limit_bytes = self.config.max_memory_mb * 1024 * 1024;
456
457 let mut output = if let Some(size) = expected_size {
459 if size > self.config.max_output_size {
460 return Err(Error::storage(format!(
461 "Expected decompressed size {} exceeds limit {}",
462 size, self.config.max_output_size
463 )));
464 }
465 Vec::with_capacity(size.min(memory_limit_bytes / 2))
466 } else {
467 Vec::with_capacity(self.config.chunk_size)
468 };
469
470 match self.algorithm {
471 CompressionAlgorithm::None => {
472 self.copy_chunks_with_limit(reader, &mut output, memory_limit_bytes)
474 .await?;
475 }
476 CompressionAlgorithm::Lz4 => {
477 self.decompress_lz4_streaming(reader, &mut output, memory_limit_bytes)
478 .await?;
479 }
480 CompressionAlgorithm::Snappy => {
481 self.decompress_snappy_streaming(reader, &mut output, memory_limit_bytes)
482 .await?;
483 }
484 CompressionAlgorithm::Deflate => {
485 self.decompress_deflate_streaming(reader, &mut output, memory_limit_bytes)
486 .await?;
487 }
488 CompressionAlgorithm::Zstd => {
489 self.decompress_zstd_streaming(reader, &mut output, memory_limit_bytes)
490 .await?;
491 }
492 }
493
494 self.bytes_output = output.len();
495 Ok(output)
496 }
497
498 async fn copy_chunks_with_limit<R: Read>(
500 &mut self,
501 mut reader: R,
502 output: &mut Vec<u8>,
503 memory_limit: usize,
504 ) -> Result<()> {
505 let mut buffer = vec![0u8; self.config.chunk_size];
506
507 loop {
508 let bytes_read = reader
509 .read(&mut buffer)
510 .map_err(|e| Error::storage(format!("Failed to read chunk: {}", e)))?;
511
512 if bytes_read == 0 {
513 break; }
515
516 if output.len() + bytes_read > memory_limit {
518 return Err(Error::storage(format!(
519 "Memory limit exceeded: {} bytes (limit: {} bytes)",
520 output.len() + bytes_read,
521 memory_limit
522 )));
523 }
524
525 output.extend_from_slice(&buffer[..bytes_read]);
526 self.bytes_processed += bytes_read;
527
528 if self.bytes_processed % (8 * 1024 * 1024) == 0 {
530 tokio::task::yield_now().await;
531 }
532 }
533
534 Ok(())
535 }
536
537 async fn decompress_lz4_streaming<R: Read>(
539 &mut self,
540 reader: R,
541 output: &mut Vec<u8>,
542 memory_limit: usize,
543 ) -> Result<()> {
544 #[cfg(feature = "lz4")]
545 {
546 let mut buf_reader = std::io::BufReader::new(reader);
548 let mut size_bytes = [0u8; 4];
549 use std::io::Read;
550
551 buf_reader
552 .read_exact(&mut size_bytes)
553 .map_err(|e| Error::storage(format!("Failed to read LZ4 size header: {}", e)))?;
554
555 let expected_size = u32::from_le_bytes(size_bytes) as usize;
556
557 if expected_size > memory_limit {
558 return Err(Error::storage(format!(
559 "LZ4 expected size {} exceeds memory limit {}",
560 expected_size, memory_limit
561 )));
562 }
563
564 let mut compressed_buffer = Vec::new();
566 let mut chunk_buffer = vec![0u8; self.config.chunk_size];
567
568 loop {
569 let bytes_read = buf_reader.read(&mut chunk_buffer).map_err(|e| {
570 Error::storage(format!("Failed to read LZ4 compressed chunk: {}", e))
571 })?;
572
573 if bytes_read == 0 {
574 break;
575 }
576
577 compressed_buffer.extend_from_slice(&chunk_buffer[..bytes_read]);
578 self.bytes_processed += bytes_read;
579
580 if self.bytes_processed % (4 * 1024 * 1024) == 0 {
582 tokio::task::yield_now().await;
583 }
584 }
585
586 use lz4_flex::decompress;
588 let decompressed = decompress(&compressed_buffer, expected_size)
589 .map_err(|e| Error::storage(format!("LZ4 decompression failed: {}", e)))?;
590
591 output.extend_from_slice(&decompressed);
592 Ok(())
593 }
594 #[cfg(not(feature = "lz4"))]
595 {
596 Err(Error::storage("LZ4 compression not available".to_string()))
597 }
598 }
599
600 async fn decompress_snappy_streaming<R: Read>(
602 &mut self,
603 reader: R,
604 output: &mut Vec<u8>,
605 memory_limit: usize,
606 ) -> Result<()> {
607 #[cfg(feature = "snappy")]
608 {
609 use std::io::BufReader;
610
611 let max_compressed = snap::raw::max_compress_len(memory_limit);
635 if max_compressed == 0 {
636 return Err(Error::storage(format!(
637 "Snappy streaming memory limit {} too large to bound compressed input",
638 memory_limit
639 )));
640 }
641 let read_cap = max_compressed
642 .checked_add(1)
643 .ok_or_else(|| Error::storage("Snappy compressed read cap overflow".to_string()))?;
644 let mut buf_reader = BufReader::new(reader).take(read_cap as u64);
645 let mut compressed = Vec::new();
646 buf_reader.read_to_end(&mut compressed).map_err(|e| {
647 Error::storage(format!("Failed to read Snappy compressed data: {}", e))
648 })?;
649 if compressed.len() > max_compressed {
650 return Err(Error::storage(format!(
651 "Snappy compressed input exceeds bound {} bytes (memory limit: {} bytes)",
652 max_compressed, memory_limit
653 )));
654 }
655 self.bytes_processed += compressed.len();
656
657 let advertised = snap::raw::decompress_len(&compressed)
664 .map_err(|e| Error::storage(format!("Snappy (raw) length decode failed: {}", e)))?;
665 let effective_limit = memory_limit.min(self.config.max_output_size);
666 let projected = output.len().checked_add(advertised);
667 if projected.is_none_or(|total| total > effective_limit) {
668 return Err(Error::storage(format!(
669 "Decompression bomb protection: advertised Snappy size {} exceeds limit {} bytes",
670 advertised, effective_limit
671 )));
672 }
673
674 let decompressed = snappy_decompress_raw(&compressed, &mut 0)?;
678
679 if output.len() + decompressed.len() > memory_limit {
680 return Err(Error::storage(format!(
681 "Memory limit exceeded during Snappy decompression: {} bytes (limit: {} bytes)",
682 output.len() + decompressed.len(),
683 memory_limit
684 )));
685 }
686
687 output.extend_from_slice(&decompressed);
688 tokio::task::yield_now().await;
691
692 Ok(())
693 }
694 #[cfg(not(feature = "snappy"))]
695 {
696 let _ = (reader, output, memory_limit);
697 Err(Error::storage(
698 "Snappy compression not available".to_string(),
699 ))
700 }
701 }
702
703 #[allow(clippy::ptr_arg)] async fn decompress_deflate_streaming<R: Read>(
706 &mut self,
707 #[cfg_attr(not(feature = "deflate"), allow(unused_variables))] reader: R,
708 #[cfg_attr(not(feature = "deflate"), allow(unused_variables))] output: &mut Vec<u8>,
709 #[cfg_attr(not(feature = "deflate"), allow(unused_variables))] memory_limit: usize,
710 ) -> Result<()> {
711 #[cfg(feature = "deflate")]
712 {
713 use flate2::read::ZlibDecoder;
714 use std::io::BufReader;
715
716 let buf_reader = BufReader::new(reader);
720 let mut decoder = ZlibDecoder::new(buf_reader);
721 let mut chunk_buffer = vec![0u8; self.config.chunk_size];
722
723 loop {
724 let bytes_read = decoder.read(&mut chunk_buffer).map_err(|e| {
725 Error::storage(format!("Deflate streaming decompression failed: {}", e))
726 })?;
727
728 if bytes_read == 0 {
729 break; }
731
732 if output.len() + bytes_read > memory_limit {
734 return Err(Error::storage(format!(
735 "Memory limit exceeded during Deflate decompression: {} bytes (limit: {} bytes)",
736 output.len() + bytes_read,
737 memory_limit
738 )));
739 }
740
741 output.extend_from_slice(&chunk_buffer[..bytes_read]);
742 self.bytes_processed += bytes_read;
743
744 if self.bytes_processed % (4 * 1024 * 1024) == 0 {
746 tokio::task::yield_now().await;
747 }
748 }
749
750 Ok(())
751 }
752 #[cfg(not(feature = "deflate"))]
753 {
754 Err(Error::storage(
755 "Deflate compression not available".to_string(),
756 ))
757 }
758 }
759
760 #[allow(clippy::ptr_arg)] async fn decompress_zstd_streaming<R: Read>(
763 &mut self,
764 #[cfg_attr(not(feature = "zstd"), allow(unused_variables))] reader: R,
765 #[cfg_attr(not(feature = "zstd"), allow(unused_variables))] output: &mut Vec<u8>,
766 #[cfg_attr(not(feature = "zstd"), allow(unused_variables))] memory_limit: usize,
767 ) -> Result<()> {
768 #[cfg(feature = "zstd")]
769 {
770 use std::io::BufReader;
771
772 let buf_reader = BufReader::new(reader);
773 let mut decoder = zstd::stream::read::Decoder::new(buf_reader)
774 .map_err(|e| Error::storage(format!("Failed to create Zstd decoder: {}", e)))?;
775 let mut chunk_buffer = vec![0u8; self.config.chunk_size];
776
777 loop {
778 let bytes_read = decoder.read(&mut chunk_buffer).map_err(|e| {
779 Error::storage(format!("Zstd streaming decompression failed: {}", e))
780 })?;
781
782 if bytes_read == 0 {
783 break; }
785
786 if output.len() + bytes_read > memory_limit {
788 return Err(Error::storage(format!(
789 "Memory limit exceeded during Zstd decompression: {} bytes (limit: {} bytes)",
790 output.len() + bytes_read,
791 memory_limit
792 )));
793 }
794
795 output.extend_from_slice(&chunk_buffer[..bytes_read]);
796 self.bytes_processed += bytes_read;
797
798 if self.bytes_processed % (4 * 1024 * 1024) == 0 {
800 tokio::task::yield_now().await;
801 }
802 }
803
804 Ok(())
805 }
806 #[cfg(not(feature = "zstd"))]
807 {
808 Err(Error::storage("Zstd compression not available".to_string()))
809 }
810 }
811
812 pub fn stats(&self) -> (usize, usize) {
814 (self.bytes_processed, self.bytes_output)
815 }
816
817 pub fn reset(&mut self) {
819 self.bytes_processed = 0;
820 self.bytes_output = 0;
821 }
822
823 pub fn estimated_ratio(&self) -> f64 {
825 match self.algorithm {
826 CompressionAlgorithm::None => 1.0,
827 CompressionAlgorithm::Lz4 => 0.6, CompressionAlgorithm::Snappy => 0.5, CompressionAlgorithm::Deflate => 0.3, CompressionAlgorithm::Zstd => 0.25, }
832 }
833
834 pub fn select_optimal_algorithm(
836 data_sample: &[u8],
837 performance_priority: CompressionPriority,
838 ) -> CompressionAlgorithm {
839 let entropy = calculate_entropy(data_sample);
841 let repetition_score = calculate_repetition_score(data_sample);
842 let data_size = data_sample.len();
843
844 match performance_priority {
845 CompressionPriority::Speed => {
846 if entropy > 0.9 {
848 CompressionAlgorithm::None } else {
850 CompressionAlgorithm::Lz4 }
852 }
853 CompressionPriority::Balanced => {
854 if entropy > 0.95 {
856 CompressionAlgorithm::None
857 } else if repetition_score > 0.7 || data_size > 1024 * 1024 {
858 CompressionAlgorithm::Snappy } else {
860 CompressionAlgorithm::Lz4
861 }
862 }
863 CompressionPriority::Ratio => {
864 if entropy > 0.98 {
866 CompressionAlgorithm::None
867 } else if repetition_score > 0.5 {
868 CompressionAlgorithm::Deflate } else {
870 CompressionAlgorithm::Snappy
871 }
872 }
873 }
874 }
875}
876
877#[derive(Debug, Clone, Copy, PartialEq)]
879pub enum CompressionPriority {
880 Speed,
882 Balanced,
884 Ratio,
886}
887
888#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
890pub struct CompressionStats {
891 pub original_size: u64,
893
894 pub compressed_size: u64,
896
897 pub ratio: f64,
899
900 pub algorithm: CompressionAlgorithm,
902}
903
904impl CompressionStats {
905 pub fn calculate(
907 original_size: u64,
908 compressed_size: u64,
909 algorithm: CompressionAlgorithm,
910 ) -> Self {
911 let ratio = if original_size > 0 {
912 compressed_size as f64 / original_size as f64
913 } else {
914 1.0
915 };
916
917 Self {
918 original_size,
919 compressed_size,
920 ratio,
921 algorithm,
922 }
923 }
924
925 pub fn space_saved(&self) -> u64 {
927 self.original_size.saturating_sub(self.compressed_size)
928 }
929
930 pub fn compression_percentage(&self) -> f64 {
932 (1.0 - self.ratio) * 100.0
933 }
934}
935
936fn calculate_entropy(data: &[u8]) -> f64 {
938 if data.is_empty() {
939 return 0.0;
940 }
941
942 let mut counts = [0u32; 256];
943 for &byte in data {
944 counts[byte as usize] += 1;
945 }
946
947 let total = data.len() as f64;
948 let mut entropy = 0.0;
949
950 for &count in &counts {
951 if count > 0 {
952 let probability = count as f64 / total;
953 entropy -= probability * probability.log2();
954 }
955 }
956
957 entropy / 8.0 }
960
961fn calculate_repetition_score(data: &[u8]) -> f64 {
963 if data.len() < 4 {
964 return 0.0;
965 }
966
967 let mut repeated_bytes = 0;
968 let mut pattern_matches = 0;
969
970 for i in 1..data.len() {
972 if data[i] == data[i - 1] {
973 repeated_bytes += 1;
974 }
975 }
976
977 for i in 3..data.len() {
981 if data[i] == data[i - 2] && data[i - 1] == data[i - 3] {
982 pattern_matches += 1;
983 }
984 }
985
986 let byte_repetition_score = repeated_bytes as f64 / (data.len() - 1) as f64;
987 let pattern_repetition_score = if data.len() > 3 {
988 pattern_matches as f64 / (data.len() - 3) as f64
989 } else {
990 0.0
991 };
992
993 (byte_repetition_score * 0.6 + pattern_repetition_score * 0.4).min(1.0)
995}
996
997fn normalize_algorithm_name(raw_name: &str) -> String {
999 match raw_name {
1000 "LZ4Compressor" => "LZ4".to_string(),
1001 "SnappyCompressor" => "SNAPPY".to_string(),
1002 "DeflateCompressor" => "DEFLATE".to_string(),
1003 "ZstdCompressor" => "ZSTD".to_string(),
1004 "NoCompressor" | "NullCompressor" => "NONE".to_string(),
1005 other => other.to_string(),
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::*;
1013
1014 #[test]
1015 fn test_no_compression() {
1016 let compression = Compression::new(CompressionAlgorithm::None).unwrap();
1017 let data = b"hello world";
1018
1019 let compressed = compression.compress(data).unwrap();
1020 assert_eq!(compressed, data);
1021
1022 let decompressed = compression.decompress(&compressed).unwrap();
1023 assert_eq!(decompressed, data);
1024 }
1025
1026 #[test]
1027 fn test_compression_stats() {
1028 let stats = CompressionStats::calculate(1000, 600, CompressionAlgorithm::Lz4);
1029
1030 assert_eq!(stats.original_size, 1000);
1031 assert_eq!(stats.compressed_size, 600);
1032 assert_eq!(stats.ratio, 0.6);
1033 assert_eq!(stats.space_saved(), 400);
1034 assert_eq!(stats.compression_percentage(), 40.0);
1035 }
1036
1037 #[cfg(feature = "snappy")]
1052 #[test]
1053 fn test_snappy_decode_is_strict_raw_only_no_format_guessing() {
1054 use snap::raw::Encoder;
1055 let p_wrong = b"WRONG-framed-decode-abcdefghijklmnopqrstuvwxyz".to_vec();
1056 let s = p_wrong.len() as u32;
1057 let mut enc = Encoder::new();
1058 let inner = enc.compress_vec(&p_wrong).unwrap(); let mut adversarial = s.to_be_bytes().to_vec(); adversarial.extend_from_slice(&inner);
1061
1062 let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1063 let got = compression.decompress(&adversarial).ok();
1064 assert!(
1069 got.is_none(),
1070 "strict raw decode must ERROR on the ambiguous chunk, not return bytes \
1071 (no-heuristics, #1588); got {got:?}"
1072 );
1073 }
1074
1075 #[cfg(feature = "snappy")]
1076 #[test]
1077 fn test_snappy_compression_cassandra_format() {
1078 let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1079 let data = b"This is test data for Snappy compression with Cassandra format validation. "
1080 .repeat(10);
1081
1082 let compressed = compression.compress(&data).unwrap();
1083
1084 let raw_roundtrip = {
1088 use snap::raw::Decoder;
1089 Decoder::new().decompress_vec(&compressed).unwrap()
1090 };
1091 assert_eq!(raw_roundtrip, data, "compress() output must be raw Snappy");
1092
1093 let decompressed = compression.decompress(&compressed).unwrap();
1094 assert_eq!(decompressed, data);
1095 }
1096
1097 #[cfg(feature = "snappy")]
1104 #[tokio::test]
1105 async fn test_snappy_streaming_roundtrip_raw() {
1106 use std::io::Cursor;
1107 let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1108 let data = b"streaming raw snappy round-trip payload for issue 1862. ".repeat(64);
1109
1110 let compressed = compression.compress(&data).unwrap();
1111
1112 let mut decompressor =
1113 compression.create_streaming_decompressor(ChunkedDecompressionConfig::default());
1114 let out = decompressor
1115 .decompress_streaming(Cursor::new(compressed), Some(data.len()))
1116 .await
1117 .expect("streaming decode of raw Snappy must succeed");
1118 assert_eq!(
1119 out, data,
1120 "streaming Snappy must decode raw compress() output byte-for-byte"
1121 );
1122 }
1123
1124 #[cfg(feature = "snappy")]
1126 fn snappy_len_prefix(mut n: u64) -> Vec<u8> {
1127 let mut out = Vec::new();
1128 loop {
1129 let mut b = (n & 0x7f) as u8;
1130 n >>= 7;
1131 if n != 0 {
1132 b |= 0x80;
1133 }
1134 out.push(b);
1135 if n == 0 {
1136 break;
1137 }
1138 }
1139 out
1140 }
1141
1142 #[cfg(feature = "snappy")]
1148 #[tokio::test]
1149 async fn test_snappy_streaming_advertised_len_bomb_rejected() {
1150 use std::io::Cursor;
1151
1152 let config = ChunkedDecompressionConfig {
1154 max_memory_mb: 1,
1155 chunk_size: 1024,
1156 max_output_size: 128 * 1024 * 1024,
1157 };
1158 let mut input = snappy_len_prefix(100 * 1024 * 1024);
1159 input.push(0x00); let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1162 let mut decompressor = compression.create_streaming_decompressor(config);
1163 let err = decompressor
1165 .decompress_streaming(Cursor::new(input), None)
1166 .await
1167 .expect_err("advertised-size bomb must be rejected");
1168 assert!(
1169 err.to_string().contains("Decompression bomb protection"),
1170 "expected pre-allocation bomb error, got: {err}"
1171 );
1172 }
1173
1174 #[cfg(feature = "snappy")]
1179 #[tokio::test]
1180 async fn test_snappy_streaming_output_cap_bounds_below_memory_limit() {
1181 use std::io::Cursor;
1182
1183 let config = ChunkedDecompressionConfig {
1186 max_memory_mb: 100,
1187 chunk_size: 1024,
1188 max_output_size: 1024 * 1024,
1189 };
1190 let mut input = snappy_len_prefix(50 * 1024 * 1024);
1191 input.push(0x00); let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1194 let mut decompressor = compression.create_streaming_decompressor(config);
1195 let err = decompressor
1196 .decompress_streaming(Cursor::new(input), None)
1197 .await
1198 .expect_err("advertised size over output cap must be rejected");
1199 assert!(
1200 err.to_string().contains("Decompression bomb protection"),
1201 "expected output-cap-bounded bomb error, got: {err}"
1202 );
1203 }
1204
1205 #[cfg(feature = "snappy")]
1209 #[tokio::test]
1210 async fn test_snappy_streaming_oversized_compressed_rejected() {
1211 use std::io::Cursor;
1212
1213 let config = ChunkedDecompressionConfig {
1214 max_memory_mb: 1,
1215 chunk_size: 1024,
1216 max_output_size: 128 * 1024 * 1024,
1217 };
1218 let oversized = vec![0u8; 4 * 1024 * 1024];
1220
1221 let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1222 let mut decompressor = compression.create_streaming_decompressor(config);
1223 let err = decompressor
1224 .decompress_streaming(Cursor::new(oversized), None)
1225 .await
1226 .expect_err("over-cap compressed input must be rejected");
1227 assert!(
1228 err.to_string()
1229 .contains("Snappy compressed input exceeds bound"),
1230 "expected compressed read-cap error, got: {err}"
1231 );
1232 }
1233
1234 #[cfg(feature = "snappy")]
1237 #[test]
1238 fn test_snappy_decode_single_attempt_and_byte_identical() {
1239 use snap::raw::Encoder;
1240 let good = b"the quick brown fox jumps over the lazy dog. ".repeat(8);
1241 let chunk = Encoder::new().compress_vec(&good).unwrap();
1242
1243 let mut attempts = 0usize;
1244 let out = super::snappy_decompress_raw(&chunk, &mut attempts).unwrap();
1245 assert_eq!(
1246 out, good,
1247 "raw decode is byte-identical to the known-good input"
1248 );
1249 assert_eq!(
1250 attempts, 1,
1251 "exactly one decode attempt (no format guessing)"
1252 );
1253 }
1254
1255 #[cfg(feature = "snappy")]
1261 #[test]
1262 fn test_snappy_raw_helper_advertised_len_bomb_rejected_no_alloc() {
1263 let mut chunk = snappy_len_prefix(200 * 1024 * 1024);
1266 chunk.push(0x00);
1267
1268 let mut attempts = 0usize;
1269 let err = super::snappy_decompress_raw(&chunk, &mut attempts)
1270 .expect_err("over-limit advertised length must be rejected at the helper");
1271 assert!(
1272 err.to_string().contains("Decompression bomb protection"),
1273 "expected typed decompression-bomb error, got: {err}"
1274 );
1275 assert!(
1276 err.to_string().contains("advertised size"),
1277 "guard must fire on the ADVERTISED length before decode/alloc, got: {err}"
1278 );
1279 assert_eq!(attempts, 1, "helper is entered exactly once");
1282 }
1283
1284 #[cfg(feature = "deflate")]
1285 #[test]
1286 fn test_deflate_compression_cassandra_format() {
1287 let compression = Compression::new(CompressionAlgorithm::Deflate).unwrap();
1288 let data = b"This is test data for Deflate compression with Cassandra format validation. "
1289 .repeat(10);
1290
1291 let compressed = compression.compress(&data).unwrap();
1292
1293 assert!(compressed.len() >= 2);
1295 assert_eq!(compressed[0], 0x78, "zlib stream must start with CMF 0x78");
1296
1297 let decompressed = compression.decompress(&compressed).unwrap();
1298 assert_eq!(decompressed, data);
1299 }
1300
1301 #[test]
1302 fn test_compression_reader() {
1303 let mut reader = CompressionReader::new(CompressionAlgorithm::None);
1304 let data = b"test data";
1305
1306 let result = reader.read(data).unwrap();
1307 assert_eq!(result, data);
1308 assert_eq!(reader.algorithm(), &CompressionAlgorithm::None);
1309 assert_eq!(reader.block_size(), 65536);
1310 }
1311
1312 #[test]
1313 fn test_compression_reader_with_block_size() {
1314 let reader = CompressionReader::with_block_size(CompressionAlgorithm::None, 32768);
1315 assert_eq!(reader.block_size(), 32768);
1316 }
1317
1318 #[test]
1319 fn test_compression_info_binary_parsing() {
1320 use crate::testing::{list_tables, resolve_table_to_sstable_path};
1321 use std::collections::HashMap;
1322 use std::fs;
1323 use std::path::Path;
1324
1325 fn find_compressioninfo_files(table_dir: &Path) -> Vec<std::path::PathBuf> {
1327 if let Ok(dir) = fs::read_dir(table_dir) {
1328 dir.filter_map(|entry| entry.ok())
1329 .map(|e| e.path())
1330 .filter(|p| p.is_file())
1331 .filter(|p| {
1332 p.file_name()
1333 .and_then(|n| n.to_str())
1334 .map(|n| n.ends_with("-CompressionInfo.db"))
1335 .unwrap_or(false)
1336 })
1337 .collect()
1338 } else {
1339 Vec::new()
1340 }
1341 }
1342
1343 let mut by_algo: HashMap<String, std::path::PathBuf> = HashMap::new();
1345 for table in list_tables(None).unwrap_or_default() {
1346 let table_dir = match resolve_table_to_sstable_path(&table.keyspace, &table.table) {
1347 Ok(p) => p,
1348 Err(_) => continue,
1349 };
1350
1351 for ci_path in find_compressioninfo_files(&table_dir) {
1352 if let Ok(data) = std::fs::read(&ci_path) {
1354 if let Ok(info) = CompressionInfo::parse_binary(&data) {
1355 let algo = info.algorithm.clone();
1356 by_algo.entry(algo).or_insert(ci_path.clone());
1357 if by_algo.len() >= 3 {
1359 break;
1360 }
1361 }
1362 }
1363 }
1364 if by_algo.len() >= 3 {
1365 break;
1366 }
1367 }
1368
1369 if by_algo.is_empty() {
1370 println!(
1372 "⚠️ No compressed tables found in canonical datasets - skipping binary parsing validation"
1373 );
1374 return;
1375 }
1376
1377 for (algo, ci_path) in by_algo {
1379 let data = std::fs::read(&ci_path).expect("Failed to read CompressionInfo.db");
1380 let info =
1381 CompressionInfo::parse_binary(&data).expect("Failed to parse CompressionInfo.db");
1382
1383 assert_eq!(info.algorithm, algo);
1385 if info.chunk_length == 0 {
1387 println!(
1388 "⚠️ Found CompressionInfo with zero chunk_length for {} - skipping validation",
1389 algo
1390 );
1391 continue;
1392 }
1393 assert!(info.chunk_length > 0);
1394 assert!(info.data_length > 0);
1395 assert!(!info.chunks.is_empty());
1396 }
1397 }
1398
1399 #[test]
1400 fn test_compression_info_json_parsing() {
1401 let json_data = r#"{
1402 "algorithm": "SNAPPY",
1403 "parameters": {"level": "6"},
1404 "chunk_length": 65536,
1405 "data_length": 2097152,
1406 "chunks": [
1407 {"offset": 0, "compressed_length": 32000, "uncompressed_length": 65536},
1408 {"offset": 32000, "compressed_length": 31500, "uncompressed_length": 65536}
1409 ]
1410 }"#;
1411
1412 let info = CompressionInfo::parse(json_data.as_bytes()).unwrap();
1413 assert_eq!(info.algorithm, "SNAPPY");
1414 assert_eq!(info.chunk_length, 65536);
1415 assert_eq!(info.data_length, 2097152);
1416 assert_eq!(info.chunk_count(), 2);
1417 assert_eq!(info.compressed_size(), 63500);
1418 assert!(info.compression_ratio() < 1.0);
1419 assert_eq!(info.get_algorithm(), CompressionAlgorithm::Snappy);
1420 }
1421
1422 #[test]
1423 fn test_compression_algorithm_from_string() {
1424 assert_eq!(
1425 CompressionAlgorithm::from("NONE".to_string()),
1426 CompressionAlgorithm::None
1427 );
1428 assert_eq!(
1429 CompressionAlgorithm::from("LZ4".to_string()),
1430 CompressionAlgorithm::Lz4
1431 );
1432 assert_eq!(
1433 CompressionAlgorithm::from("SNAPPY".to_string()),
1434 CompressionAlgorithm::Snappy
1435 );
1436 assert_eq!(
1437 CompressionAlgorithm::from("DEFLATE".to_string()),
1438 CompressionAlgorithm::Deflate
1439 );
1440 assert_eq!(
1441 CompressionAlgorithm::from("unknown".to_string()),
1442 CompressionAlgorithm::None
1443 );
1444 }
1445
1446 #[test]
1447 fn test_compression_invalid_data() {
1448 let compression = Compression::new(CompressionAlgorithm::Snappy).unwrap();
1449
1450 let short_data = &[1, 2];
1452 assert!(compression.decompress(short_data).is_err());
1453
1454 let invalid_data = &[0, 0, 0, 100, 1, 2, 3]; if cfg!(feature = "snappy") {
1457 assert!(compression.decompress(invalid_data).is_err());
1458 }
1459 }
1460
1461 #[test]
1462 fn test_compression_streaming() {
1463 let mut reader = CompressionReader::new(CompressionAlgorithm::None);
1464 let chunks = vec![
1465 b"chunk1".as_slice(),
1466 b"chunk2".as_slice(),
1467 b"chunk3".as_slice(),
1468 ];
1469
1470 let result = reader.read_streaming(&chunks).unwrap();
1471 assert_eq!(result, b"chunk1chunk2chunk3");
1472 }
1473
1474 #[test]
1475 fn test_decompression_bomb_protection() {
1476 #[cfg(feature = "snappy")]
1482 {
1483 }
1493
1494 #[cfg(feature = "deflate")]
1501 {
1502 let compression = Compression::new(CompressionAlgorithm::Deflate).unwrap();
1503 let malicious_size: u32 = 200 * 1024 * 1024;
1504 let mut malicious_data = malicious_size.to_be_bytes().to_vec();
1505 malicious_data.extend_from_slice(&[0u8; 10]);
1506
1507 let result = compression.decompress(&malicious_data);
1508 assert!(
1509 result.is_err(),
1510 "Should reject malformed (non-zlib) Deflate data"
1511 );
1512
1513 let data = b"deflate bomb-guard roundtrip".repeat(4);
1515 let compressed = compression.compress(&data).unwrap();
1516 let decompressed = compression.decompress(&compressed).unwrap();
1517 assert_eq!(decompressed, data);
1518 }
1519
1520 #[cfg(feature = "zstd")]
1526 {
1527 let compression = Compression::new(CompressionAlgorithm::Zstd).unwrap();
1528 let mut malicious_data = (200u32 * 1024 * 1024).to_be_bytes().to_vec();
1529 malicious_data.extend_from_slice(&[0u8; 10]);
1530
1531 let result = compression.decompress(&malicious_data);
1532 assert!(result.is_err(), "Should reject malformed Zstd frame");
1533
1534 let data = b"zstd bomb-guard roundtrip".repeat(4);
1535 let compressed = compression.compress(&data).unwrap();
1536 let decompressed = compression.decompress(&compressed).unwrap();
1537 assert_eq!(decompressed, data);
1538 }
1539
1540 #[cfg(feature = "lz4")]
1542 {
1543 let compression = Compression::new(CompressionAlgorithm::Lz4).unwrap();
1544 let malicious_size: u32 = 200 * 1024 * 1024; let mut malicious_data = malicious_size.to_le_bytes().to_vec(); malicious_data.extend_from_slice(&[0u8; 10]); let result = compression.decompress(&malicious_data);
1549 assert!(result.is_err(), "Should reject malicious LZ4 size");
1550 assert!(result
1551 .unwrap_err()
1552 .to_string()
1553 .contains("Decompression bomb"));
1554 }
1555 }
1556
1557 #[test]
1558 fn test_entropy_calculation() {
1559 let uniform_data: Vec<u8> = (0..=255).collect();
1561 let entropy = calculate_entropy(&uniform_data);
1562 assert!(entropy > 0.9); let repetitive_data = vec![0u8; 256];
1566 let entropy = calculate_entropy(&repetitive_data);
1567 assert!(entropy < 0.1); }
1569
1570 #[test]
1571 fn test_repetition_score() {
1572 let repetitive_data = vec![0u8, 0u8, 0u8, 0u8];
1574 let score = calculate_repetition_score(&repetitive_data);
1575 assert!(score > 0.8);
1576
1577 let random_data = vec![1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8];
1579 let score = calculate_repetition_score(&random_data);
1580 assert!(score < 0.2);
1581 }
1582
1583 }
1586
1587#[allow(dead_code)]
1589pub struct CompressionReader {
1590 algorithm: CompressionAlgorithm,
1591 buffer: Vec<u8>,
1592 block_size: usize,
1593}
1594
1595impl CompressionReader {
1596 pub fn new(algorithm: CompressionAlgorithm) -> Self {
1598 Self {
1599 algorithm,
1600 buffer: Vec::new(),
1601 block_size: 65536, }
1603 }
1604
1605 pub fn with_block_size(algorithm: CompressionAlgorithm, block_size: usize) -> Self {
1607 Self {
1608 algorithm,
1609 buffer: Vec::new(),
1610 block_size,
1611 }
1612 }
1613
1614 pub fn read(&mut self, compressed_data: &[u8]) -> Result<Vec<u8>> {
1616 let compression = Compression::new(self.algorithm)?;
1617 compression.decompress(compressed_data)
1618 }
1619
1620 pub fn read_streaming(&mut self, compressed_chunks: &[&[u8]]) -> Result<Vec<u8>> {
1622 let mut result = Vec::new();
1623
1624 for chunk in compressed_chunks {
1625 let decompressed = self.read(chunk)?;
1626 result.extend_from_slice(&decompressed);
1627 }
1628
1629 Ok(result)
1630 }
1631
1632 pub fn algorithm(&self) -> &CompressionAlgorithm {
1634 &self.algorithm
1635 }
1636
1637 pub fn block_size(&self) -> usize {
1639 self.block_size
1640 }
1641}
1642
1643#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1645pub struct CompressionInfo {
1646 pub algorithm: String,
1648 pub parameters: std::collections::HashMap<String, String>,
1650 pub chunk_length: u32,
1652 pub data_length: u64,
1654 pub chunks: Vec<ChunkInfo>,
1656}
1657
1658#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1660pub struct ChunkInfo {
1661 pub offset: u64,
1663 pub compressed_length: u32,
1665 pub uncompressed_length: u32,
1667}
1668
1669impl CompressionInfo {
1670 pub fn parse(data: &[u8]) -> Result<Self> {
1672 use serde_json;
1673
1674 let info: CompressionInfo = serde_json::from_slice(data)
1676 .map_err(|e| Error::storage(format!("Failed to parse CompressionInfo.db: {}", e)))?;
1677
1678 Ok(info)
1679 }
1680
1681 pub fn parse_binary(data: &[u8]) -> Result<Self> {
1683 if data.len() < 20 {
1691 return Err(Error::storage("CompressionInfo.db too short".to_string()));
1692 }
1693
1694 let mut offset = 0;
1695
1696 let algo_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
1699 offset += 2;
1700
1701 if offset + algo_len > data.len() {
1702 return Err(Error::storage(
1703 "Invalid algorithm name length in CompressionInfo.db".to_string(),
1704 ));
1705 }
1706
1707 let raw_algorithm = String::from_utf8(data[offset..offset + algo_len].to_vec())
1709 .map_err(|e| Error::storage(format!("Invalid UTF-8 in algorithm name: {}", e)))?;
1710
1711 if !crate::storage::sstable::compression_info::is_supported_compressor_name(&raw_algorithm)
1716 {
1717 return Err(Error::UnsupportedFormat(format!(
1718 "Unsupported compression algorithm '{}' in CompressionInfo.db. \
1719 CQLite only supports: {}. Cannot decompress this SSTable.",
1720 raw_algorithm,
1721 crate::storage::sstable::compression_info::SUPPORTED_COMPRESSOR_NAMES.join(", ")
1722 )));
1723 }
1724
1725 let algorithm = normalize_algorithm_name(&raw_algorithm);
1727 offset += algo_len;
1728
1729 if offset < data.len() && data[offset] == 0 {
1739 offset += 1;
1740 }
1741
1742 if offset + 4 > data.len() {
1744 return Err(Error::storage(
1745 "CompressionInfo.db too short for chunk_length".to_string(),
1746 ));
1747 }
1748 let chunk_length = u32::from_be_bytes([
1749 data[offset],
1750 data[offset + 1],
1751 data[offset + 2],
1752 data[offset + 3],
1753 ]);
1754 offset += 4;
1755
1756 if offset + 8 > data.len() {
1758 return Err(Error::storage(
1759 "CompressionInfo.db too short for data_length".to_string(),
1760 ));
1761 }
1762 let data_length = u64::from_be_bytes([
1763 data[offset],
1764 data[offset + 1],
1765 data[offset + 2],
1766 data[offset + 3],
1767 data[offset + 4],
1768 data[offset + 5],
1769 data[offset + 6],
1770 data[offset + 7],
1771 ]);
1772 offset += 8;
1773
1774 if offset + 4 > data.len() {
1776 return Err(Error::storage(
1777 "CompressionInfo.db too short for chunk_count".to_string(),
1778 ));
1779 }
1780 let chunk_count = u32::from_be_bytes([
1781 data[offset],
1782 data[offset + 1],
1783 data[offset + 2],
1784 data[offset + 3],
1785 ]);
1786 offset += 4;
1787
1788 let mut chunks = Vec::new();
1790 for i in 0..chunk_count {
1791 if offset + 16 > data.len() {
1792 return Err(Error::storage(format!(
1793 "CompressionInfo.db too short for chunk info: chunk {}, offset {}, data len {}",
1794 i,
1795 offset,
1796 data.len()
1797 )));
1798 }
1799
1800 let chunk_offset = u64::from_be_bytes([
1805 data[offset],
1806 data[offset + 1],
1807 data[offset + 2],
1808 data[offset + 3],
1809 data[offset + 4],
1810 data[offset + 5],
1811 data[offset + 6],
1812 data[offset + 7],
1813 ]);
1814 offset += 8;
1815
1816 let compressed_length = u32::from_be_bytes([
1818 data[offset],
1819 data[offset + 1],
1820 data[offset + 2],
1821 data[offset + 3],
1822 ]);
1823 offset += 4;
1824
1825 let uncompressed_length = u32::from_be_bytes([
1827 data[offset],
1828 data[offset + 1],
1829 data[offset + 2],
1830 data[offset + 3],
1831 ]);
1832 offset += 4;
1833
1834 chunks.push(ChunkInfo {
1835 offset: chunk_offset,
1836 compressed_length,
1837 uncompressed_length,
1838 });
1839 }
1840
1841 Ok(CompressionInfo {
1842 algorithm,
1843 parameters: std::collections::HashMap::new(),
1844 chunk_length,
1845 data_length,
1846 chunks,
1847 })
1848 }
1849
1850 pub fn get_algorithm(&self) -> CompressionAlgorithm {
1852 CompressionAlgorithm::from(self.algorithm.as_str())
1853 }
1854
1855 pub fn chunk_count(&self) -> usize {
1857 self.chunks.len()
1858 }
1859
1860 pub fn compressed_size(&self) -> u64 {
1862 self.chunks.iter().map(|c| c.compressed_length as u64).sum()
1863 }
1864
1865 pub fn compression_ratio(&self) -> f64 {
1867 if self.data_length > 0 {
1868 self.compressed_size() as f64 / self.data_length as f64
1869 } else {
1870 1.0
1871 }
1872 }
1873}