1use parking_lot::Mutex;
31use std::sync::atomic::{AtomicU64, Ordering};
32use std::sync::Arc;
33
34pub fn fnv1a_64(data: &[u8]) -> u64 {
40 let mut h: u64 = 14_695_981_039_346_656_037;
41 for &b in data {
42 h ^= b as u64;
43 h = h.wrapping_mul(1_099_511_628_211);
44 }
45 h
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ScpCompressionAlgorithm {
57 None,
59 Rle,
62 Lz77 {
68 window_size: usize,
70 },
71 Delta {
74 base_value: u8,
76 },
77 Xor {
82 key: Vec<u8>,
84 },
85}
86
87impl ScpCompressionAlgorithm {
88 pub fn name(&self) -> String {
90 match self {
91 Self::None => "none".to_string(),
92 Self::Rle => "rle".to_string(),
93 Self::Lz77 { window_size } => format!("lz77({})", window_size),
94 Self::Delta { base_value } => format!("delta({})", base_value),
95 Self::Xor { key } => format!("xor({})", key.len()),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
106pub struct CompressionStage {
107 pub algorithm: ScpCompressionAlgorithm,
109 pub enabled: bool,
111 pub min_size_bytes: usize,
114}
115
116#[derive(Debug, Clone)]
122pub struct ScpPipelineConfig {
123 pub stages: Vec<CompressionStage>,
125 pub max_input_size: usize,
128 pub enable_checksum: bool,
131}
132
133impl Default for ScpPipelineConfig {
134 fn default() -> Self {
135 Self {
136 stages: vec![
137 CompressionStage {
138 algorithm: ScpCompressionAlgorithm::Rle,
139 enabled: true,
140 min_size_bytes: 64,
141 },
142 CompressionStage {
143 algorithm: ScpCompressionAlgorithm::Lz77 { window_size: 4096 },
144 enabled: true,
145 min_size_bytes: 128,
146 },
147 ],
148 max_input_size: 64 * 1024 * 1024, enable_checksum: true,
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
160pub struct CompressedBlock {
161 pub original_size: usize,
163 pub compressed_size: usize,
165 pub stages_applied: Vec<String>,
167 pub data: Vec<u8>,
169 pub checksum: u64,
171 pub compression_ratio: f64,
173}
174
175#[derive(Debug, Clone, Default)]
181pub struct ScpPipelineStats {
182 pub total_blocks: u64,
184 pub total_input_bytes: u64,
186 pub total_output_bytes: u64,
188 pub avg_compression_ratio: f64,
190 pub stage_stats: Vec<(String, f64)>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
200pub enum ScpPipelineError {
201 #[error("input too large: {0} bytes")]
203 InputTooLarge(usize),
204 #[error("decompression failed: {0}")]
206 DecompressionFailed(String),
207 #[error("checksum mismatch: expected {expected:#x}, got {got:#x}")]
209 ChecksumMismatch {
210 expected: u64,
212 got: u64,
214 },
215 #[error("invalid configuration: {0}")]
217 InvalidConfiguration(String),
218 #[error("algorithm error: {0}")]
220 AlgorithmError(String),
221}
222
223pub fn rle_encode(data: &[u8]) -> Vec<u8> {
231 if data.is_empty() {
232 return Vec::new();
233 }
234 let mut out = Vec::with_capacity(data.len());
235 let mut i = 0;
236 while i < data.len() {
237 let byte = data[i];
238 let mut count: u8 = 1;
239 while i + (count as usize) < data.len() && data[i + (count as usize)] == byte && count < 255
241 {
242 count += 1;
243 }
244 out.push(count);
245 out.push(byte);
246 i += count as usize;
247 }
248 out
249}
250
251pub fn rle_decode(data: &[u8]) -> Result<Vec<u8>, ScpPipelineError> {
253 if !data.len().is_multiple_of(2) {
254 return Err(ScpPipelineError::DecompressionFailed(
255 "RLE stream has odd length".to_string(),
256 ));
257 }
258 let mut out = Vec::with_capacity(data.len() * 2);
259 let mut i = 0;
260 while i + 1 < data.len() {
261 let count = data[i] as usize;
262 let byte = data[i + 1];
263 if count == 0 {
264 return Err(ScpPipelineError::DecompressionFailed(
265 "RLE stream contains zero-count run".to_string(),
266 ));
267 }
268 for _ in 0..count {
269 out.push(byte);
270 }
271 i += 2;
272 }
273 Ok(out)
274}
275
276const LZ77_FLAG_LITERAL: u8 = 0x00;
284const LZ77_FLAG_MATCH: u8 = 0x01;
285const LZ77_MIN_MATCH: usize = 3;
286
287pub fn lz77_encode(data: &[u8], window_size: usize) -> Vec<u8> {
291 if data.is_empty() {
292 return Vec::new();
293 }
294 let effective_window = window_size.min(u16::MAX as usize);
295 let mut out = Vec::with_capacity(data.len() * 2);
297 let mut pos = 0;
298
299 while pos < data.len() {
300 let window_start = pos.saturating_sub(effective_window);
301 let lookahead_end = (pos + 258).min(data.len()); let mut best_offset: usize = 0;
305 let mut best_length: usize = 0;
306
307 if window_start < pos {
309 let lookahead = &data[pos..lookahead_end];
310 let lookahead_max = lookahead.len();
311
312 for start in window_start..pos {
313 let max_len = (pos - start).min(lookahead_max); let max_len = max_len.min(255 + LZ77_MIN_MATCH - 1); let mut length = 0;
317 while length < max_len && data[start + length] == lookahead[length] {
318 length += 1;
319 }
320 if length > best_length {
321 best_length = length;
322 best_offset = pos - start; }
324 }
325 }
326
327 if best_length >= LZ77_MIN_MATCH && best_offset <= u16::MAX as usize {
328 let stored_len = (best_length - LZ77_MIN_MATCH) as u8; let offset_u16 = best_offset as u16;
331 out.push(LZ77_FLAG_MATCH);
332 out.push((offset_u16 & 0xFF) as u8);
333 out.push((offset_u16 >> 8) as u8);
334 out.push(stored_len);
335 pos += best_length;
336 } else {
337 out.push(LZ77_FLAG_LITERAL);
339 out.push(data[pos]);
340 pos += 1;
341 }
342 }
343 out
344}
345
346pub fn lz77_decode(data: &[u8]) -> Result<Vec<u8>, ScpPipelineError> {
348 let mut out: Vec<u8> = Vec::with_capacity(data.len() * 2);
349 let mut i = 0;
350
351 while i < data.len() {
352 let flag = data[i];
353 i += 1;
354
355 match flag {
356 LZ77_FLAG_LITERAL => {
357 if i >= data.len() {
358 return Err(ScpPipelineError::DecompressionFailed(
359 "LZ77 stream truncated after literal flag".to_string(),
360 ));
361 }
362 out.push(data[i]);
363 i += 1;
364 }
365 LZ77_FLAG_MATCH => {
366 if i + 2 >= data.len() {
367 return Err(ScpPipelineError::DecompressionFailed(
368 "LZ77 stream truncated inside back-reference".to_string(),
369 ));
370 }
371 let offset_lo = data[i] as usize;
372 let offset_hi = data[i + 1] as usize;
373 let stored_len = data[i + 2] as usize;
374 i += 3;
375
376 let offset = offset_lo | (offset_hi << 8);
377 let length = stored_len + LZ77_MIN_MATCH;
378
379 if offset == 0 {
380 return Err(ScpPipelineError::DecompressionFailed(
381 "LZ77 back-reference has zero offset".to_string(),
382 ));
383 }
384 if offset > out.len() {
385 return Err(ScpPipelineError::DecompressionFailed(format!(
386 "LZ77 back-reference offset {} > output length {}",
387 offset,
388 out.len()
389 )));
390 }
391
392 let copy_start = out.len() - offset;
394 for j in 0..length {
395 let byte = out[copy_start + (j % offset)];
396 out.push(byte);
397 }
398 }
399 other => {
400 return Err(ScpPipelineError::DecompressionFailed(format!(
401 "LZ77 unknown flag byte: {:#x}",
402 other
403 )));
404 }
405 }
406 }
407 Ok(out)
408}
409
410pub fn delta_encode(data: &[u8], base: u8) -> Vec<u8> {
415 if data.is_empty() {
416 return Vec::new();
417 }
418 let mut out = Vec::with_capacity(data.len());
419 let first = data[0].wrapping_sub(base);
420 out.push(first);
421 let mut prev = data[0];
422 for &b in &data[1..] {
423 out.push(b.wrapping_sub(prev));
424 prev = b;
425 }
426 out
427}
428
429pub fn delta_decode(data: &[u8], base: u8) -> Vec<u8> {
431 if data.is_empty() {
432 return Vec::new();
433 }
434 let mut out = Vec::with_capacity(data.len());
435 let first = data[0].wrapping_add(base);
436 out.push(first);
437 let mut prev = first;
438 for &d in &data[1..] {
439 let b = d.wrapping_add(prev);
440 out.push(b);
441 prev = b;
442 }
443 out
444}
445
446pub fn xor_transform(data: &[u8], key: &[u8]) -> Vec<u8> {
450 if key.is_empty() {
451 return data.to_vec();
452 }
453 data.iter()
454 .enumerate()
455 .map(|(i, &b)| b ^ key[i % key.len()])
456 .collect()
457}
458
459fn apply_algorithm(
464 data: &[u8],
465 algo: &ScpCompressionAlgorithm,
466) -> Result<Vec<u8>, ScpPipelineError> {
467 match algo {
468 ScpCompressionAlgorithm::None => Ok(data.to_vec()),
469 ScpCompressionAlgorithm::Rle => Ok(rle_encode(data)),
470 ScpCompressionAlgorithm::Lz77 { window_size } => Ok(lz77_encode(data, *window_size)),
471 ScpCompressionAlgorithm::Delta { base_value } => Ok(delta_encode(data, *base_value)),
472 ScpCompressionAlgorithm::Xor { key } => {
473 if key.is_empty() {
474 return Err(ScpPipelineError::InvalidConfiguration(
475 "XOR key must not be empty".to_string(),
476 ));
477 }
478 Ok(xor_transform(data, key))
479 }
480 }
481}
482
483fn unapply_algorithm(data: &[u8], algo_name: &str) -> Result<Vec<u8>, ScpPipelineError> {
484 if algo_name == "none" {
486 return Ok(data.to_vec());
487 }
488 if algo_name == "rle" {
489 return rle_decode(data);
490 }
491 if algo_name.starts_with("lz77(") {
492 return lz77_decode(data);
493 }
494 if algo_name.starts_with("delta(") {
495 let inner = algo_name.trim_start_matches("delta(").trim_end_matches(')');
497 let base: u8 = inner.parse().map_err(|_| {
498 ScpPipelineError::DecompressionFailed(format!(
499 "Cannot parse delta base from stage name: {}",
500 algo_name
501 ))
502 })?;
503 return Ok(delta_decode(data, base));
504 }
505 if algo_name.starts_with("xor(") {
506 let inner = algo_name.trim_start_matches("xor(").trim_end_matches(')');
509 let key = hex::decode(inner).map_err(|_| {
510 ScpPipelineError::DecompressionFailed(format!(
511 "Cannot decode XOR key from stage name: {}",
512 algo_name
513 ))
514 })?;
515 return Ok(xor_transform(data, &key));
516 }
517 Err(ScpPipelineError::DecompressionFailed(format!(
518 "Unknown stage name in stages_applied: {}",
519 algo_name
520 )))
521}
522
523fn stage_name_for_storage(algo: &ScpCompressionAlgorithm) -> String {
527 match algo {
528 ScpCompressionAlgorithm::Xor { key } => format!("xor({})", hex::encode(key)),
529 other => other.name(),
530 }
531}
532
533#[derive(Debug, Default)]
538struct StatsAccumulator {
539 total_blocks: u64,
540 total_input_bytes: u64,
541 total_output_bytes: u64,
542 ratio_sum: f64,
544 stage_ratios: std::collections::HashMap<String, (f64, u64)>,
546}
547
548impl StatsAccumulator {
549 fn record(
550 &mut self,
551 input_bytes: usize,
552 output_bytes: usize,
553 stage_intermediate_ratios: &[(String, f64)],
554 ) {
555 self.total_blocks += 1;
556 self.total_input_bytes += input_bytes as u64;
557 self.total_output_bytes += output_bytes as u64;
558 let ratio = if output_bytes == 0 {
559 1.0
560 } else {
561 input_bytes as f64 / output_bytes as f64
562 };
563 self.ratio_sum += ratio;
564
565 for (name, r) in stage_intermediate_ratios {
566 let entry = self.stage_ratios.entry(name.clone()).or_insert((0.0, 0));
567 entry.0 += r;
568 entry.1 += 1;
569 }
570 }
571
572 fn snapshot(&self) -> ScpPipelineStats {
573 let avg = if self.total_blocks == 0 {
574 1.0
575 } else {
576 self.ratio_sum / self.total_blocks as f64
577 };
578 let stage_stats = self
579 .stage_ratios
580 .iter()
581 .map(|(name, (sum, count))| {
582 (
583 name.clone(),
584 if *count == 0 {
585 1.0
586 } else {
587 sum / *count as f64
588 },
589 )
590 })
591 .collect();
592 ScpPipelineStats {
593 total_blocks: self.total_blocks,
594 total_input_bytes: self.total_input_bytes,
595 total_output_bytes: self.total_output_bytes,
596 avg_compression_ratio: avg,
597 stage_stats,
598 }
599 }
600}
601
602pub struct ScpStorageCompressionPipeline {
614 config: ScpPipelineConfig,
615 stats: Arc<Mutex<StatsAccumulator>>,
616 _block_counter: Arc<AtomicU64>,
618}
619
620impl ScpStorageCompressionPipeline {
621 pub fn new(config: ScpPipelineConfig) -> Self {
626 Self {
627 config,
628 stats: Arc::new(Mutex::new(StatsAccumulator::default())),
629 _block_counter: Arc::new(AtomicU64::new(0)),
630 }
631 }
632
633 pub fn compress(&self, data: &[u8]) -> Result<CompressedBlock, ScpPipelineError> {
639 self.compress_with_stages(data, &self.config.stages)
640 }
641
642 pub fn decompress(&self, block: &CompressedBlock) -> Result<Vec<u8>, ScpPipelineError> {
648 let payload = if self.config.enable_checksum {
649 if block.data.len() < 8 {
651 return Err(ScpPipelineError::DecompressionFailed(
652 "Block too small to contain checksum".to_string(),
653 ));
654 }
655 let (payload, cs_bytes) = block.data.split_at(block.data.len() - 8);
656 let stored_cs = u64::from_le_bytes(cs_bytes.try_into().map_err(|_| {
657 ScpPipelineError::DecompressionFailed("Could not read checksum bytes".to_string())
658 })?);
659 let computed_cs = fnv1a_64(payload);
660 if stored_cs != computed_cs {
661 return Err(ScpPipelineError::ChecksumMismatch {
662 expected: stored_cs,
663 got: computed_cs,
664 });
665 }
666 payload.to_vec()
667 } else {
668 block.data.clone()
669 };
670
671 let mut current = payload;
673 for stage_name in block.stages_applied.iter().rev() {
674 current = unapply_algorithm(¤t, stage_name)?;
675 }
676 Ok(current)
677 }
678
679 pub fn compress_with_stages(
685 &self,
686 data: &[u8],
687 stages: &[CompressionStage],
688 ) -> Result<CompressedBlock, ScpPipelineError> {
689 if data.len() > self.config.max_input_size {
690 return Err(ScpPipelineError::InputTooLarge(data.len()));
691 }
692
693 let original_size = data.len();
694 let mut current = data.to_vec();
695 let mut stages_applied: Vec<String> = Vec::new();
696 let mut stage_intermediate_ratios: Vec<(String, f64)> = Vec::new();
697
698 for stage in stages {
699 if !stage.enabled {
700 continue;
701 }
702 if current.len() < stage.min_size_bytes {
703 continue;
704 }
705
706 let size_before = current.len();
707 let compressed = apply_algorithm(¤t, &stage.algorithm)?;
708 let size_after = compressed.len();
709
710 let stage_name = stage_name_for_storage(&stage.algorithm);
711 let ratio = if size_after == 0 {
712 1.0_f64
713 } else {
714 size_before as f64 / size_after as f64
715 };
716
717 stages_applied.push(stage_name.clone());
718 stage_intermediate_ratios.push((stage_name, ratio));
719 current = compressed;
720 }
721
722 let checksum = if self.config.enable_checksum {
724 let cs = fnv1a_64(¤t);
725 let cs_bytes = cs.to_le_bytes();
726 current.extend_from_slice(&cs_bytes);
727 cs
728 } else {
729 0
730 };
731
732 let compressed_size = current.len();
733 let compression_ratio = if compressed_size == 0 {
734 1.0
735 } else {
736 original_size as f64 / compressed_size as f64
737 };
738
739 self._block_counter.fetch_add(1, Ordering::Relaxed);
741 {
742 let mut acc = self.stats.lock();
743 acc.record(original_size, compressed_size, &stage_intermediate_ratios);
744 }
745
746 Ok(CompressedBlock {
747 original_size,
748 compressed_size,
749 stages_applied,
750 data: current,
751 checksum,
752 compression_ratio,
753 })
754 }
755
756 pub fn best_algorithm(&self, data: &[u8]) -> ScpCompressionAlgorithm {
761 let sample = &data[..data.len().min(1024)];
762 if sample.is_empty() {
763 return ScpCompressionAlgorithm::None;
764 }
765
766 let candidates: &[ScpCompressionAlgorithm] = &[
767 ScpCompressionAlgorithm::None,
768 ScpCompressionAlgorithm::Rle,
769 ScpCompressionAlgorithm::Lz77 { window_size: 4096 },
770 ];
771
772 let mut best_algo = ScpCompressionAlgorithm::None;
773 let mut best_ratio: f64 = 0.0;
774
775 for algo in candidates {
776 let encoded = match apply_algorithm(sample, algo) {
777 Ok(v) => v,
778 Err(_) => continue,
779 };
780 let ratio = if encoded.is_empty() {
781 1.0
782 } else {
783 sample.len() as f64 / encoded.len() as f64
784 };
785 if ratio > best_ratio {
786 best_ratio = ratio;
787 best_algo = algo.clone();
788 }
789 }
790 best_algo
791 }
792
793 pub fn stats(&self) -> ScpPipelineStats {
795 self.stats.lock().snapshot()
796 }
797}
798
799#[cfg(test)]
804mod tests {
805 use super::*;
806
807 struct Xorshift64(u64);
809
810 impl Xorshift64 {
811 fn new(seed: u64) -> Self {
812 Self(if seed == 0 { 1 } else { seed })
813 }
814 fn next(&mut self) -> u64 {
815 self.0 ^= self.0 << 13;
816 self.0 ^= self.0 >> 7;
817 self.0 ^= self.0 << 17;
818 self.0
819 }
820 fn next_byte(&mut self) -> u8 {
821 (self.next() & 0xFF) as u8
822 }
823 fn fill(&mut self, buf: &mut [u8]) {
824 for b in buf.iter_mut() {
825 *b = self.next_byte();
826 }
827 }
828 }
829
830 fn make_pipeline(enable_checksum: bool) -> ScpStorageCompressionPipeline {
831 let config = ScpPipelineConfig {
832 stages: vec![
833 CompressionStage {
834 algorithm: ScpCompressionAlgorithm::Rle,
835 enabled: true,
836 min_size_bytes: 4,
837 },
838 CompressionStage {
839 algorithm: ScpCompressionAlgorithm::Lz77 { window_size: 256 },
840 enabled: true,
841 min_size_bytes: 8,
842 },
843 ],
844 max_input_size: 1 << 20,
845 enable_checksum,
846 };
847 ScpStorageCompressionPipeline::new(config)
848 }
849
850 #[test]
853 fn test_rle_roundtrip_simple() {
854 let data = b"AAABBC";
855 let encoded = rle_encode(data);
856 let decoded = rle_decode(&encoded).unwrap();
857 assert_eq!(decoded, data);
858 }
859
860 #[test]
861 fn test_rle_roundtrip_empty() {
862 let encoded = rle_encode(&[]);
863 assert!(encoded.is_empty());
864 let decoded = rle_decode(&encoded).unwrap();
865 assert!(decoded.is_empty());
866 }
867
868 #[test]
869 fn test_rle_roundtrip_single_byte() {
870 let data = b"Z";
871 let enc = rle_encode(data);
872 assert_eq!(enc, vec![1, b'Z']);
873 let dec = rle_decode(&enc).unwrap();
874 assert_eq!(dec, data);
875 }
876
877 #[test]
878 fn test_rle_roundtrip_max_run() {
879 let data = vec![0xAA_u8; 256];
881 let enc = rle_encode(&data);
882 assert_eq!(enc.len(), 4);
884 assert_eq!(enc[0], 255);
885 assert_eq!(enc[2], 1);
886 let dec = rle_decode(&enc).unwrap();
887 assert_eq!(dec, data);
888 }
889
890 #[test]
891 fn test_rle_roundtrip_no_repeats() {
892 let data: Vec<u8> = (0u8..=255u8).collect();
893 let enc = rle_encode(&data);
894 let dec = rle_decode(&enc).unwrap();
895 assert_eq!(dec, data);
896 }
897
898 #[test]
899 fn test_rle_roundtrip_all_same() {
900 let data = vec![0x7F_u8; 1000];
901 let enc = rle_encode(&data);
902 let dec = rle_decode(&enc).unwrap();
903 assert_eq!(dec, data);
904 }
905
906 #[test]
907 fn test_rle_decode_odd_length_error() {
908 let bad = vec![3_u8, 0xAB, 0xFF]; assert!(rle_decode(&bad).is_err());
910 }
911
912 #[test]
913 fn test_rle_decode_zero_count_error() {
914 let bad = vec![0_u8, 0xAB]; assert!(rle_decode(&bad).is_err());
916 }
917
918 #[test]
921 fn test_lz77_roundtrip_empty() {
922 let enc = lz77_encode(&[], 256);
923 assert!(enc.is_empty());
924 let dec = lz77_decode(&enc).unwrap();
925 assert!(dec.is_empty());
926 }
927
928 #[test]
929 fn test_lz77_roundtrip_single_byte() {
930 let data = b"X";
931 let enc = lz77_encode(data, 256);
932 let dec = lz77_decode(&enc).unwrap();
933 assert_eq!(dec.as_slice(), data.as_ref());
934 }
935
936 #[test]
937 fn test_lz77_roundtrip_repeated_pattern() {
938 let data = b"abcabcabcabc";
939 let enc = lz77_encode(data, 256);
940 let dec = lz77_decode(&enc).unwrap();
941 assert_eq!(dec.as_slice(), data.as_ref());
942 }
943
944 #[test]
945 fn test_lz77_roundtrip_all_same() {
946 let data = vec![0x55_u8; 200];
947 let enc = lz77_encode(&data, 256);
948 let dec = lz77_decode(&enc).unwrap();
949 assert_eq!(dec, data);
950 }
951
952 #[test]
953 fn test_lz77_roundtrip_random() {
954 let mut rng = Xorshift64::new(42);
955 let mut data = vec![0u8; 512];
956 rng.fill(&mut data);
957 let enc = lz77_encode(&data, 256);
958 let dec = lz77_decode(&enc).unwrap();
959 assert_eq!(dec, data);
960 }
961
962 #[test]
963 fn test_lz77_roundtrip_text() {
964 let data = b"the quick brown fox jumps over the lazy dog. the quick brown fox.";
965 let enc = lz77_encode(data, 256);
966 let dec = lz77_decode(&enc).unwrap();
967 assert_eq!(dec.as_slice(), data.as_ref());
968 }
969
970 #[test]
971 fn test_lz77_roundtrip_large_window() {
972 let mut rng = Xorshift64::new(999);
973 let mut data = vec![0u8; 2048];
974 rng.fill(&mut data);
975 let repeated: Vec<u8> = data[0..128].to_vec();
977 data[1024..1024 + 128].copy_from_slice(&repeated);
978 let enc = lz77_encode(&data, 4096);
979 let dec = lz77_decode(&enc).unwrap();
980 assert_eq!(dec, data);
981 }
982
983 #[test]
984 fn test_lz77_decode_truncated_literal() {
985 let bad = vec![LZ77_FLAG_LITERAL];
987 assert!(lz77_decode(&bad).is_err());
988 }
989
990 #[test]
991 fn test_lz77_decode_truncated_backref() {
992 let bad = vec![LZ77_FLAG_MATCH, 0x01];
994 assert!(lz77_decode(&bad).is_err());
995 }
996
997 #[test]
998 fn test_lz77_decode_unknown_flag() {
999 let bad = vec![0x42_u8, 0x00];
1000 assert!(lz77_decode(&bad).is_err());
1001 }
1002
1003 #[test]
1006 fn test_delta_roundtrip_simple() {
1007 let data = b"Hello, World!";
1008 let enc = delta_encode(data, 0);
1009 let dec = delta_decode(&enc, 0);
1010 assert_eq!(dec.as_slice(), data.as_ref());
1011 }
1012
1013 #[test]
1014 fn test_delta_roundtrip_with_base() {
1015 let data = b"Hello, World!";
1016 let enc = delta_encode(data, 42);
1017 let dec = delta_decode(&enc, 42);
1018 assert_eq!(dec.as_slice(), data.as_ref());
1019 }
1020
1021 #[test]
1022 fn test_delta_roundtrip_empty() {
1023 let enc = delta_encode(&[], 0);
1024 assert!(enc.is_empty());
1025 let dec = delta_decode(&enc, 0);
1026 assert!(dec.is_empty());
1027 }
1028
1029 #[test]
1030 fn test_delta_roundtrip_monotone() {
1031 let data: Vec<u8> = (0u8..=100u8).collect();
1032 let enc = delta_encode(&data, 0);
1033 for &b in &enc[1..] {
1035 assert_eq!(b, 1);
1036 }
1037 let dec = delta_decode(&enc, 0);
1038 assert_eq!(dec, data);
1039 }
1040
1041 #[test]
1042 fn test_delta_roundtrip_random() {
1043 let mut rng = Xorshift64::new(12345);
1044 let mut data = vec![0u8; 256];
1045 rng.fill(&mut data);
1046 let enc = delta_encode(&data, 77);
1047 let dec = delta_decode(&enc, 77);
1048 assert_eq!(dec, data);
1049 }
1050
1051 #[test]
1054 fn test_xor_roundtrip_simple() {
1055 let data = b"Hello, World!";
1056 let key = b"secret";
1057 let enc = xor_transform(data, key);
1058 let dec = xor_transform(&enc, key);
1059 assert_eq!(dec.as_slice(), data.as_ref());
1060 }
1061
1062 #[test]
1063 fn test_xor_roundtrip_single_byte_key() {
1064 let data = b"AAABBBCCC";
1065 let key = b"\xFF";
1066 let enc = xor_transform(data, key);
1067 let dec = xor_transform(&enc, key);
1068 assert_eq!(dec.as_slice(), data.as_ref());
1069 }
1070
1071 #[test]
1072 fn test_xor_empty_key_passthrough() {
1073 let data = b"test";
1074 let enc = xor_transform(data, &[]);
1075 assert_eq!(enc.as_slice(), data.as_ref());
1076 }
1077
1078 #[test]
1079 fn test_xor_roundtrip_empty_data() {
1080 let enc = xor_transform(&[], b"key");
1081 assert!(enc.is_empty());
1082 }
1083
1084 #[test]
1085 fn test_xor_roundtrip_long_data() {
1086 let mut rng = Xorshift64::new(7777);
1087 let mut data = vec![0u8; 1024];
1088 rng.fill(&mut data);
1089 let key = b"COOLJAPAN";
1090 let enc = xor_transform(&data, key);
1091 let dec = xor_transform(&enc, key);
1092 assert_eq!(dec, data);
1093 }
1094
1095 #[test]
1098 fn test_fnv1a_known_values() {
1099 assert_eq!(fnv1a_64(&[]), 14_695_981_039_346_656_037_u64);
1101 let h1 = fnv1a_64(b"hello");
1103 let h2 = fnv1a_64(b"hello");
1104 assert_eq!(h1, h2);
1105 }
1106
1107 #[test]
1108 fn test_fnv1a_different_data_different_hash() {
1109 assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"abd"));
1110 }
1111
1112 #[test]
1115 fn test_pipeline_compress_decompress_with_checksum() {
1116 let pipeline = make_pipeline(true);
1117 let data = b"AAAAAABBBBBCCCCCDDDDD";
1118 let block = pipeline.compress(data).unwrap();
1119 assert_ne!(block.checksum, 0);
1120 let recovered = pipeline.decompress(&block).unwrap();
1121 assert_eq!(recovered, data);
1122 }
1123
1124 #[test]
1125 fn test_pipeline_compress_decompress_no_checksum() {
1126 let pipeline = make_pipeline(false);
1127 let data = b"AAAAAABBBBBCCCCCDDDDD";
1128 let block = pipeline.compress(data).unwrap();
1129 assert_eq!(block.checksum, 0);
1130 let recovered = pipeline.decompress(&block).unwrap();
1131 assert_eq!(recovered, data);
1132 }
1133
1134 #[test]
1135 fn test_pipeline_empty_input() {
1136 let pipeline = make_pipeline(true);
1137 let block = pipeline.compress(&[]).unwrap();
1138 assert!(block.stages_applied.is_empty());
1140 let recovered = pipeline.decompress(&block).unwrap();
1141 assert!(recovered.is_empty());
1142 }
1143
1144 #[test]
1145 fn test_pipeline_input_too_large() {
1146 let config = ScpPipelineConfig {
1147 stages: vec![],
1148 max_input_size: 10,
1149 enable_checksum: false,
1150 };
1151 let pipeline = ScpStorageCompressionPipeline::new(config);
1152 let data = vec![0u8; 11];
1153 let err = pipeline.compress(&data).unwrap_err();
1154 assert!(matches!(err, ScpPipelineError::InputTooLarge(11)));
1155 }
1156
1157 #[test]
1158 fn test_pipeline_checksum_mismatch() {
1159 let pipeline = make_pipeline(true);
1160 let data = b"test checksum mismatch";
1161 let mut block = pipeline.compress(data).unwrap();
1162 if !block.data.is_empty() {
1164 let mid = block.data.len() / 2;
1165 block.data[mid] ^= 0xFF;
1166 }
1167 let err = pipeline.decompress(&block).unwrap_err();
1168 assert!(matches!(err, ScpPipelineError::ChecksumMismatch { .. }));
1169 }
1170
1171 #[test]
1172 fn test_pipeline_compresses_repetitive_data() {
1173 let pipeline = make_pipeline(false);
1174 let data = vec![0x42_u8; 1024];
1175 let block = pipeline.compress(&data).unwrap();
1176 assert!(block.compressed_size < block.original_size);
1178 assert!(block.compression_ratio > 1.0);
1179 }
1180
1181 #[test]
1182 fn test_pipeline_stages_applied_tracked() {
1183 let pipeline = make_pipeline(false);
1184 let data = vec![b'A'; 200]; let block = pipeline.compress(&data).unwrap();
1186 assert!(!block.stages_applied.is_empty());
1187 let recovered = pipeline.decompress(&block).unwrap();
1188 assert_eq!(recovered, data);
1189 }
1190
1191 #[test]
1192 fn test_pipeline_min_size_bytes_skip() {
1193 let config = ScpPipelineConfig {
1194 stages: vec![CompressionStage {
1195 algorithm: ScpCompressionAlgorithm::Rle,
1196 enabled: true,
1197 min_size_bytes: 1000, }],
1199 max_input_size: 1 << 20,
1200 enable_checksum: false,
1201 };
1202 let pipeline = ScpStorageCompressionPipeline::new(config);
1203 let data = b"hello world"; let block = pipeline.compress(data).unwrap();
1205 assert!(block.stages_applied.is_empty(), "stage should be skipped");
1206 let recovered = pipeline.decompress(&block).unwrap();
1207 assert_eq!(recovered.as_slice(), data.as_ref());
1208 }
1209
1210 #[test]
1211 fn test_pipeline_stage_disabled_skip() {
1212 let config = ScpPipelineConfig {
1213 stages: vec![CompressionStage {
1214 algorithm: ScpCompressionAlgorithm::Rle,
1215 enabled: false, min_size_bytes: 0,
1217 }],
1218 max_input_size: 1 << 20,
1219 enable_checksum: false,
1220 };
1221 let pipeline = ScpStorageCompressionPipeline::new(config);
1222 let data = b"AAABBBCCC";
1223 let block = pipeline.compress(data).unwrap();
1224 assert!(block.stages_applied.is_empty());
1225 }
1226
1227 #[test]
1230 fn test_pipeline_delta_stage() {
1231 let config = ScpPipelineConfig {
1232 stages: vec![CompressionStage {
1233 algorithm: ScpCompressionAlgorithm::Delta { base_value: 0 },
1234 enabled: true,
1235 min_size_bytes: 1,
1236 }],
1237 max_input_size: 1 << 20,
1238 enable_checksum: true,
1239 };
1240 let pipeline = ScpStorageCompressionPipeline::new(config);
1241 let data: Vec<u8> = (0u8..=200u8).collect();
1242 let block = pipeline.compress(&data).unwrap();
1243 assert_eq!(block.stages_applied, vec!["delta(0)"]);
1244 let recovered = pipeline.decompress(&block).unwrap();
1245 assert_eq!(recovered, data);
1246 }
1247
1248 #[test]
1251 fn test_pipeline_xor_stage() {
1252 let key = b"mysecret".to_vec();
1253 let config = ScpPipelineConfig {
1254 stages: vec![CompressionStage {
1255 algorithm: ScpCompressionAlgorithm::Xor { key: key.clone() },
1256 enabled: true,
1257 min_size_bytes: 1,
1258 }],
1259 max_input_size: 1 << 20,
1260 enable_checksum: true,
1261 };
1262 let pipeline = ScpStorageCompressionPipeline::new(config);
1263 let data = b"sensitive payload data goes here";
1264 let block = pipeline.compress(data).unwrap();
1265 let recovered = pipeline.decompress(&block).unwrap();
1266 assert_eq!(recovered.as_slice(), data.as_ref());
1267 }
1268
1269 #[test]
1270 fn test_pipeline_xor_empty_key_error() {
1271 let config = ScpPipelineConfig {
1272 stages: vec![CompressionStage {
1273 algorithm: ScpCompressionAlgorithm::Xor { key: vec![] },
1274 enabled: true,
1275 min_size_bytes: 0,
1276 }],
1277 max_input_size: 1 << 20,
1278 enable_checksum: false,
1279 };
1280 let pipeline = ScpStorageCompressionPipeline::new(config);
1281 let err = pipeline.compress(b"test").unwrap_err();
1282 assert!(matches!(err, ScpPipelineError::InvalidConfiguration(_)));
1283 }
1284
1285 #[test]
1288 fn test_pipeline_multi_stage_rle_then_lz77() {
1289 let config = ScpPipelineConfig {
1290 stages: vec![
1291 CompressionStage {
1292 algorithm: ScpCompressionAlgorithm::Rle,
1293 enabled: true,
1294 min_size_bytes: 4,
1295 },
1296 CompressionStage {
1297 algorithm: ScpCompressionAlgorithm::Lz77 { window_size: 256 },
1298 enabled: true,
1299 min_size_bytes: 4,
1300 },
1301 ],
1302 max_input_size: 1 << 20,
1303 enable_checksum: true,
1304 };
1305 let pipeline = ScpStorageCompressionPipeline::new(config);
1306 let data = b"AAABBBCCCAAABBBCCCAAABBBCCC";
1307 let block = pipeline.compress(data).unwrap();
1308 assert_eq!(block.stages_applied.len(), 2);
1309 let recovered = pipeline.decompress(&block).unwrap();
1310 assert_eq!(recovered.as_slice(), data.as_ref());
1311 }
1312
1313 #[test]
1314 fn test_pipeline_multi_stage_delta_then_rle() {
1315 let config = ScpPipelineConfig {
1316 stages: vec![
1317 CompressionStage {
1318 algorithm: ScpCompressionAlgorithm::Delta { base_value: 0 },
1319 enabled: true,
1320 min_size_bytes: 1,
1321 },
1322 CompressionStage {
1323 algorithm: ScpCompressionAlgorithm::Rle,
1324 enabled: true,
1325 min_size_bytes: 4,
1326 },
1327 ],
1328 max_input_size: 1 << 20,
1329 enable_checksum: true,
1330 };
1331 let pipeline = ScpStorageCompressionPipeline::new(config);
1332 let data: Vec<u8> = (0u8..=200).collect();
1334 let block = pipeline.compress(&data).unwrap();
1335 let recovered = pipeline.decompress(&block).unwrap();
1336 assert_eq!(recovered, data);
1337 }
1338
1339 #[test]
1340 fn test_pipeline_three_stage() {
1341 let config = ScpPipelineConfig {
1342 stages: vec![
1343 CompressionStage {
1344 algorithm: ScpCompressionAlgorithm::Delta { base_value: 5 },
1345 enabled: true,
1346 min_size_bytes: 1,
1347 },
1348 CompressionStage {
1349 algorithm: ScpCompressionAlgorithm::Rle,
1350 enabled: true,
1351 min_size_bytes: 4,
1352 },
1353 CompressionStage {
1354 algorithm: ScpCompressionAlgorithm::Lz77 { window_size: 128 },
1355 enabled: true,
1356 min_size_bytes: 4,
1357 },
1358 ],
1359 max_input_size: 1 << 20,
1360 enable_checksum: true,
1361 };
1362 let pipeline = ScpStorageCompressionPipeline::new(config);
1363 let data = b"the quick brown fox jumps over the lazy dog. the quick brown fox.";
1364 let block = pipeline.compress(data).unwrap();
1365 let recovered = pipeline.decompress(&block).unwrap();
1366 assert_eq!(recovered.as_slice(), data.as_ref());
1367 }
1368
1369 #[test]
1372 fn test_compress_with_custom_stages() {
1373 let pipeline = make_pipeline(true);
1374 let data = b"custom stages test with some repeated data repeated data";
1375 let custom_stages = vec![CompressionStage {
1376 algorithm: ScpCompressionAlgorithm::Rle,
1377 enabled: true,
1378 min_size_bytes: 1,
1379 }];
1380 let block = pipeline.compress_with_stages(data, &custom_stages).unwrap();
1381 assert_eq!(block.stages_applied, vec!["rle"]);
1382 let recovered = pipeline.decompress(&block).unwrap();
1383 assert_eq!(recovered.as_slice(), data.as_ref());
1384 }
1385
1386 #[test]
1387 fn test_compress_with_empty_stages() {
1388 let pipeline = make_pipeline(true);
1389 let data = b"no compression applied";
1390 let block = pipeline.compress_with_stages(data, &[]).unwrap();
1391 assert!(block.stages_applied.is_empty());
1392 let recovered = pipeline.decompress(&block).unwrap();
1393 assert_eq!(recovered.as_slice(), data.as_ref());
1394 }
1395
1396 #[test]
1399 fn test_best_algorithm_repetitive_prefers_rle_or_lz77() {
1400 let pipeline = make_pipeline(false);
1401 let data = vec![0xCC_u8; 512];
1402 let best = pipeline.best_algorithm(&data);
1403 assert!(
1405 best != ScpCompressionAlgorithm::None || matches!(best, ScpCompressionAlgorithm::None)
1406 );
1407 let enc = apply_algorithm(&data, &best).unwrap();
1409 assert!(enc.len() <= data.len() + 10);
1410 }
1411
1412 #[test]
1413 fn test_best_algorithm_empty() {
1414 let pipeline = make_pipeline(false);
1415 let best = pipeline.best_algorithm(&[]);
1416 assert_eq!(best, ScpCompressionAlgorithm::None);
1417 }
1418
1419 #[test]
1420 fn test_best_algorithm_returns_valid_algo() {
1421 let pipeline = make_pipeline(false);
1422 let mut rng = Xorshift64::new(54321);
1423 let mut data = vec![0u8; 512];
1424 rng.fill(&mut data);
1425 let best = pipeline.best_algorithm(&data);
1426 assert!(
1428 best == ScpCompressionAlgorithm::None
1429 || best == ScpCompressionAlgorithm::Rle
1430 || matches!(best, ScpCompressionAlgorithm::Lz77 { .. })
1431 );
1432 }
1433
1434 #[test]
1437 fn test_stats_increments() {
1438 let pipeline = make_pipeline(false);
1439 let data = b"AAABBBCCC";
1440 pipeline.compress(data).unwrap();
1441 pipeline.compress(data).unwrap();
1442 let stats = pipeline.stats();
1443 assert_eq!(stats.total_blocks, 2);
1444 assert!(stats.total_input_bytes >= 18);
1445 }
1446
1447 #[test]
1448 fn test_stats_initial_zero() {
1449 let pipeline = make_pipeline(false);
1450 let stats = pipeline.stats();
1451 assert_eq!(stats.total_blocks, 0);
1452 assert_eq!(stats.total_input_bytes, 0);
1453 assert_eq!(stats.total_output_bytes, 0);
1454 }
1455
1456 #[test]
1457 fn test_stats_avg_ratio_reasonable() {
1458 let pipeline = make_pipeline(false);
1459 let data = vec![0xAB_u8; 500];
1460 for _ in 0..5 {
1461 pipeline.compress(&data).unwrap();
1462 }
1463 let stats = pipeline.stats();
1464 assert!(stats.avg_compression_ratio > 0.0);
1465 assert!(stats.avg_compression_ratio.is_finite());
1466 }
1467
1468 #[test]
1469 fn test_stats_stage_stats_populated() {
1470 let config = ScpPipelineConfig {
1471 stages: vec![CompressionStage {
1472 algorithm: ScpCompressionAlgorithm::Rle,
1473 enabled: true,
1474 min_size_bytes: 4,
1475 }],
1476 max_input_size: 1 << 20,
1477 enable_checksum: false,
1478 };
1479 let pipeline = ScpStorageCompressionPipeline::new(config);
1480 let data = vec![0xEE_u8; 128];
1481 pipeline.compress(&data).unwrap();
1482 let stats = pipeline.stats();
1483 assert!(!stats.stage_stats.is_empty());
1484 let (name, _ratio) = &stats.stage_stats[0];
1485 assert_eq!(name, "rle");
1486 }
1487
1488 #[test]
1491 fn test_error_input_too_large() {
1492 let config = ScpPipelineConfig {
1493 stages: vec![],
1494 max_input_size: 5,
1495 enable_checksum: false,
1496 };
1497 let pipeline = ScpStorageCompressionPipeline::new(config);
1498 assert!(matches!(
1499 pipeline.compress(&[0u8; 6]).unwrap_err(),
1500 ScpPipelineError::InputTooLarge(6)
1501 ));
1502 }
1503
1504 #[test]
1505 fn test_error_decompress_truncated_block() {
1506 let pipeline = make_pipeline(true);
1507 let block = CompressedBlock {
1509 original_size: 10,
1510 compressed_size: 4,
1511 stages_applied: vec![],
1512 data: vec![0u8; 4],
1513 checksum: 0,
1514 compression_ratio: 1.0,
1515 };
1516 assert!(pipeline.decompress(&block).is_err());
1517 }
1518
1519 #[test]
1520 fn test_error_rle_decode_odd_stream() {
1521 assert!(rle_decode(&[1, 0xAA, 0xBB]).is_err()); }
1523
1524 #[test]
1525 fn test_error_lz77_decode_bad_flag() {
1526 assert!(lz77_decode(&[0x99]).is_err());
1527 }
1528
1529 #[test]
1532 fn test_pipeline_roundtrip_binary_data() {
1533 let pipeline = make_pipeline(true);
1534 let mut rng = Xorshift64::new(2024);
1535 let mut data = vec![0u8; 512];
1536 rng.fill(&mut data);
1537 let block = pipeline.compress(&data).unwrap();
1538 let recovered = pipeline.decompress(&block).unwrap();
1539 assert_eq!(recovered, data);
1540 }
1541
1542 #[test]
1543 fn test_pipeline_roundtrip_zero_bytes() {
1544 let pipeline = make_pipeline(true);
1545 let data = vec![0u8; 500];
1546 let block = pipeline.compress(&data).unwrap();
1547 let recovered = pipeline.decompress(&block).unwrap();
1548 assert_eq!(recovered, data);
1549 }
1550
1551 #[test]
1552 fn test_pipeline_roundtrip_alternating() {
1553 let pipeline = make_pipeline(true);
1554 let data: Vec<u8> = (0..512)
1555 .map(|i| if i % 2 == 0 { 0x00 } else { 0xFF })
1556 .collect();
1557 let block = pipeline.compress(&data).unwrap();
1558 let recovered = pipeline.decompress(&block).unwrap();
1559 assert_eq!(recovered, data);
1560 }
1561
1562 #[test]
1563 fn test_pipeline_roundtrip_json_like() {
1564 let pipeline = make_pipeline(true);
1565 let data = br#"{"key":"value","count":42,"items":["a","b","c"],"nested":{"x":1,"y":2}}"#;
1566 let block = pipeline.compress(data).unwrap();
1567 let recovered = pipeline.decompress(&block).unwrap();
1568 assert_eq!(recovered.as_slice(), data.as_ref());
1569 }
1570
1571 #[test]
1572 fn test_compression_ratio_field() {
1573 let pipeline = make_pipeline(false);
1574 let data = vec![0xAA_u8; 200];
1575 let block = pipeline.compress(&data).unwrap();
1576 let ratio = block.original_size as f64 / block.compressed_size as f64;
1577 assert!((block.compression_ratio - ratio).abs() < 1e-9);
1578 }
1579
1580 #[test]
1581 fn test_none_algorithm_is_identity() {
1582 let config = ScpPipelineConfig {
1583 stages: vec![CompressionStage {
1584 algorithm: ScpCompressionAlgorithm::None,
1585 enabled: true,
1586 min_size_bytes: 0,
1587 }],
1588 max_input_size: 1 << 20,
1589 enable_checksum: false,
1590 };
1591 let pipeline = ScpStorageCompressionPipeline::new(config);
1592 let data = b"passthrough test";
1593 let block = pipeline.compress(data).unwrap();
1594 assert_eq!(block.stages_applied, vec!["none"]);
1595 let recovered = pipeline.decompress(&block).unwrap();
1596 assert_eq!(recovered.as_slice(), data.as_ref());
1597 }
1598
1599 #[test]
1600 fn test_pipeline_default_config() {
1601 let config = ScpPipelineConfig::default();
1602 let pipeline = ScpStorageCompressionPipeline::new(config);
1603 let data = vec![b'X'; 256];
1604 let block = pipeline.compress(&data).unwrap();
1605 let recovered = pipeline.decompress(&block).unwrap();
1606 assert_eq!(recovered, data);
1607 }
1608
1609 #[test]
1610 fn test_algo_name_display() {
1611 assert_eq!(ScpCompressionAlgorithm::None.name(), "none");
1612 assert_eq!(ScpCompressionAlgorithm::Rle.name(), "rle");
1613 assert_eq!(
1614 ScpCompressionAlgorithm::Lz77 { window_size: 4096 }.name(),
1615 "lz77(4096)"
1616 );
1617 assert_eq!(
1618 ScpCompressionAlgorithm::Delta { base_value: 7 }.name(),
1619 "delta(7)"
1620 );
1621 assert_eq!(
1622 ScpCompressionAlgorithm::Xor { key: vec![1, 2] }.name(),
1623 "xor(2)"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_stage_name_for_storage_xor_embeds_key() {
1629 let algo = ScpCompressionAlgorithm::Xor {
1630 key: vec![0xDE, 0xAD, 0xBE, 0xEF],
1631 };
1632 let name = stage_name_for_storage(&algo);
1633 assert!(name.starts_with("xor("));
1634 assert!(name.contains("deadbeef"));
1635 }
1636}