1use crate::metrics::OperationMetrics;
9#[cfg(feature = "compression")]
10use lz4_flex;
11use serde::{Deserialize, Serialize};
12use std::sync::{Arc, Mutex};
13#[cfg(not(target_arch = "wasm32"))]
14use std::time::Instant;
15use thiserror::Error;
16
17#[derive(Debug, Error, Clone, PartialEq)]
19pub enum ByteStorageError {
20 #[error("input exceeds maximum size")]
21 InputTooLarge,
22
23 #[error("decompression ratio exceeds safety limit")]
24 DecompressionBomb,
25
26 #[error("integrity check failed")]
27 ChecksumMismatch,
28
29 #[error("compression failed")]
30 CompressionFailed,
31
32 #[error("decompression failed")]
33 DecompressionFailed,
34
35 #[error("size validation failed")]
36 SizeValidationFailed,
37
38 #[error("serialization failed: {0}")]
39 SerializationFailed(String),
40
41 #[error("deserialization failed: {0}")]
42 DeserializationFailed(String),
43}
44
45const MAX_UNCOMPRESSED_SIZE: usize = 512 * 1024 * 1024; const MAX_COMPRESSED_SIZE: usize = 512 * 1024 * 1024; const MAX_COMPRESSION_RATIO: u64 = 1000;
51
52#[derive(Serialize, Deserialize)]
55pub struct StorageEnvelope {
56 pub compressed_data: Vec<u8>,
58 pub checksum: [u8; 8],
60 pub original_size: u32,
62 pub format: String,
64}
65
66impl StorageEnvelope {
67 #[cfg(all(feature = "compression", feature = "checksum"))]
73 pub fn new(data: &[u8], format: String) -> Result<Self, ByteStorageError> {
74 if data.len() > MAX_UNCOMPRESSED_SIZE {
76 return Err(ByteStorageError::InputTooLarge);
77 }
78
79 let original_size = data.len() as u32;
80
81 let compressed_data = lz4_flex::compress(data);
83
84 if compressed_data.len() > MAX_COMPRESSED_SIZE {
86 return Err(ByteStorageError::InputTooLarge);
87 }
88
89 let checksum = crate::checksum::checksum(data);
91
92 Ok(StorageEnvelope {
93 compressed_data,
94 checksum,
95 original_size,
96 format,
97 })
98 }
99
100 #[cfg(all(feature = "compression", feature = "checksum"))]
102 pub fn extract(&self) -> Result<Vec<u8>, ByteStorageError> {
103 if self.compressed_data.len() > MAX_COMPRESSED_SIZE {
105 return Err(ByteStorageError::InputTooLarge);
106 }
107
108 if self.original_size as usize > MAX_UNCOMPRESSED_SIZE {
109 return Err(ByteStorageError::InputTooLarge);
110 }
111
112 let compressed_size = self.compressed_data.len() as u64;
115
116 if compressed_size == 0 {
118 return Err(ByteStorageError::DecompressionBomb);
119 }
120
121 let max_allowed_original = MAX_COMPRESSION_RATIO
123 .checked_mul(compressed_size)
124 .ok_or(ByteStorageError::DecompressionBomb)?;
125
126 if (self.original_size as u64) > max_allowed_original {
128 return Err(ByteStorageError::DecompressionBomb);
129 }
130
131 let decompressed = lz4_flex::decompress(&self.compressed_data, self.original_size as usize)
133 .map_err(|_| ByteStorageError::DecompressionFailed)?;
134
135 if !crate::checksum::verify_checksum(&decompressed, &self.checksum) {
138 return Err(ByteStorageError::ChecksumMismatch);
139 }
140
141 if decompressed.len() != self.original_size as usize {
143 return Err(ByteStorageError::SizeValidationFailed);
144 }
145
146 Ok(decompressed)
147 }
148}
149
150pub struct ByteStorage {
153 default_format: String,
154 last_metrics: Arc<Mutex<OperationMetrics>>,
156}
157
158impl ByteStorage {
159 pub fn new(default_format: Option<String>) -> Self {
161 ByteStorage {
162 default_format: default_format.unwrap_or_else(|| "msgpack".to_string()),
163 last_metrics: Arc::new(Mutex::new(OperationMetrics::new())),
164 }
165 }
166
167 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
171 pub fn store(&self, data: &[u8], format: Option<String>) -> Result<Vec<u8>, ByteStorageError> {
172 if data.len() > MAX_UNCOMPRESSED_SIZE {
174 return Err(ByteStorageError::InputTooLarge);
175 }
176
177 let format = format.unwrap_or_else(|| self.default_format.clone());
178
179 #[cfg(not(target_arch = "wasm32"))]
181 let compression_start = Instant::now();
182 let original_size = data.len();
183
184 let envelope = StorageEnvelope::new(data, format)?;
185
186 #[cfg(not(target_arch = "wasm32"))]
187 let compression_micros = compression_start.elapsed().as_micros() as u64;
188 #[cfg(target_arch = "wasm32")]
189 let compression_micros = 0u64;
190 let compressed_size = envelope.compressed_data.len();
191
192 let envelope_bytes = rmp_serde::to_vec(&envelope)
194 .map_err(|e| ByteStorageError::SerializationFailed(e.to_string()))?;
195
196 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
198 return Err(ByteStorageError::InputTooLarge);
199 }
200
201 if let Ok(mut metrics) = self.last_metrics.lock() {
203 *metrics = OperationMetrics::new().with_compression(
204 compression_micros,
205 original_size,
206 compressed_size,
207 );
208 }
209
210 Ok(envelope_bytes)
211 }
212
213 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
217 pub fn retrieve(&self, envelope_bytes: &[u8]) -> Result<(Vec<u8>, String), ByteStorageError> {
218 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
220 return Err(ByteStorageError::InputTooLarge);
221 }
222
223 let envelope: StorageEnvelope = rmp_serde::from_slice(envelope_bytes)
225 .map_err(|e| ByteStorageError::DeserializationFailed(e.to_string()))?;
226
227 #[cfg(not(target_arch = "wasm32"))]
229 let decompress_start = Instant::now();
230
231 let data = envelope.extract()?;
233
234 #[cfg(not(target_arch = "wasm32"))]
235 let decompress_micros = decompress_start.elapsed().as_micros() as u64;
236 #[cfg(target_arch = "wasm32")]
237 let decompress_micros = 0u64;
238
239 let compressed_size = envelope.compressed_data.len();
241 let original_size = envelope.original_size as usize;
242
243 if let Ok(mut metrics) = self.last_metrics.lock() {
245 *metrics = OperationMetrics::new().with_compression(
246 decompress_micros,
247 original_size,
248 compressed_size,
249 );
250 }
251
252 Ok((data, envelope.format))
253 }
254
255 #[cfg(feature = "compression")]
257 pub fn estimate_compression(&self, data: &[u8]) -> Result<f64, ByteStorageError> {
258 if data.len() > MAX_UNCOMPRESSED_SIZE {
260 return Err(ByteStorageError::InputTooLarge);
261 }
262
263 let compressed = lz4_flex::compress(data);
264
265 Ok(data.len() as f64 / compressed.len() as f64)
266 }
267
268 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
270 pub fn validate(&self, envelope_bytes: &[u8]) -> bool {
271 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
273 return false; }
275
276 match rmp_serde::from_slice::<StorageEnvelope>(envelope_bytes) {
277 Ok(envelope) => envelope.extract().is_ok(),
278 Err(_) => false,
279 }
280 }
281
282 pub fn get_last_metrics(&self) -> OperationMetrics {
286 self.last_metrics
287 .lock()
288 .map(|metrics| metrics.clone())
289 .unwrap_or_else(|_| OperationMetrics::new())
290 }
291
292 pub fn max_uncompressed_size(&self) -> usize {
294 MAX_UNCOMPRESSED_SIZE
295 }
296
297 pub fn max_compressed_size(&self) -> usize {
298 MAX_COMPRESSED_SIZE
299 }
300
301 pub fn max_compression_ratio(&self) -> u64 {
302 MAX_COMPRESSION_RATIO
303 }
304}
305
306impl Default for ByteStorage {
307 fn default() -> Self {
308 Self::new(None)
309 }
310}
311
312#[cfg(all(
313 test,
314 feature = "compression",
315 feature = "checksum",
316 feature = "messagepack"
317))]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn envelope_embeds_canonical_checksum() {
323 let data = b"DRY-guard payload";
324 let envelope = StorageEnvelope::new(data, "test".to_string()).unwrap();
325 assert_eq!(envelope.checksum, crate::checksum::checksum(data));
326 }
327
328 #[test]
329 fn test_storage_envelope_roundtrip() {
330 let data = b"Hello, World! This is test data for compression.".to_vec();
331 let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
332 let extracted = envelope.extract().unwrap();
333 assert_eq!(data, extracted);
334 }
335
336 #[test]
337 fn test_compression_works() {
338 let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
340 assert!(envelope.compressed_data.len() < data.len());
341 }
342
343 #[test]
344 fn test_checksum_validation() {
345 let mut envelope = StorageEnvelope::new(b"test", "test".to_string()).unwrap();
346 envelope.checksum[0] = !envelope.checksum[0];
348 assert!(matches!(
351 envelope.extract(),
352 Err(ByteStorageError::ChecksumMismatch)
353 ));
354 }
355
356 #[test]
357 fn test_raw_persistence_roundtrip() {
358 let storage = ByteStorage::new(None);
359 let test_data = b"test data for persistence";
360
361 let stored = storage.store(test_data, None).unwrap();
362 let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
363 assert_eq!(test_data, retrieved_data.as_slice());
364 assert_eq!("msgpack", format);
365 }
366
367 #[test]
368 fn test_size_limits_input() {
369 let storage = ByteStorage::new(None);
370
371 let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
373
374 let result = storage.store(&large_data, None);
375 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
376 }
377
378 #[test]
379 fn test_size_limits_envelope() {
380 let max_data = vec![0u8; MAX_UNCOMPRESSED_SIZE];
382 let envelope_result = StorageEnvelope::new(&max_data, "test".to_string());
383
384 assert!(envelope_result.is_ok());
386 }
387
388 #[test]
389 fn test_compression_ratio_bomb_protection() {
390 let malicious_envelope = StorageEnvelope {
392 compressed_data: vec![0u8; 1000], checksum: [0u8; 8], original_size: 200 * 1024 * 1024, format: "test".to_string(),
396 };
397
398 let result = malicious_envelope.extract();
399 assert!(matches!(result, Err(ByteStorageError::DecompressionBomb)));
400 }
401
402 #[test]
407 fn test_decompression_bomb_zero_compressed_size() {
408 let malicious_envelope = StorageEnvelope {
410 compressed_data: vec![], checksum: [0u8; 8],
412 original_size: 1000, format: "test".to_string(),
414 };
415
416 let result = malicious_envelope.extract();
417 assert!(
418 matches!(result, Err(ByteStorageError::DecompressionBomb)),
419 "Zero compressed size should be rejected as decompression bomb"
420 );
421 }
422
423 #[test]
424 fn test_decompression_bomb_extreme_ratio() {
425 let malicious_envelope = StorageEnvelope {
429 compressed_data: vec![0u8; 1], checksum: [0u8; 8],
431 original_size: 2000, format: "test".to_string(),
433 };
434
435 let result = malicious_envelope.extract();
436 assert!(
437 matches!(result, Err(ByteStorageError::DecompressionBomb)),
438 "Extreme ratio should be rejected as bomb: {:?}",
439 result
440 );
441 }
442
443 #[test]
444 fn test_decompression_u32_max_original_size() {
445 let malicious_envelope = StorageEnvelope {
449 compressed_data: vec![0u8; 1000],
450 checksum: [0u8; 8],
451 original_size: u32::MAX, format: "test".to_string(),
453 };
454
455 let result = malicious_envelope.extract();
456 assert!(
457 matches!(result, Err(ByteStorageError::InputTooLarge)),
458 "u32::MAX should be rejected as InputTooLarge (exceeds 512MB limit): {:?}",
459 result
460 );
461 }
462
463 #[test]
464 fn test_decompression_exactly_at_threshold() {
465 let envelope = StorageEnvelope {
469 compressed_data: vec![0u8; 100], checksum: [0u8; 8],
471 original_size: 100_000, format: "test".to_string(),
473 };
474
475 let result = envelope.extract();
476 assert!(
479 !matches!(result, Err(ByteStorageError::DecompressionBomb)),
480 "Exactly 1000:1 ratio should pass bomb check: {:?}",
481 result
482 );
483 assert!(
484 result.is_err(),
485 "Invalid data should still fail after ratio check"
486 );
487 }
488
489 #[test]
490 fn test_decompression_just_over_threshold() {
491 let malicious_envelope = StorageEnvelope {
493 compressed_data: vec![0u8; 100], checksum: [0u8; 8],
495 original_size: 100_001, format: "test".to_string(),
497 };
498
499 let result = malicious_envelope.extract();
500 assert!(
501 matches!(result, Err(ByteStorageError::DecompressionBomb)),
502 "Just over 1000:1 ratio should be rejected as bomb"
503 );
504 }
505
506 #[test]
507 fn test_decompression_bomb_integer_boundary() {
508 let envelope = StorageEnvelope {
515 compressed_data: vec![0u8; 1_000_000], checksum: [0u8; 8],
517 original_size: 1_000_000_000, format: "test".to_string(),
519 };
520
521 let result = envelope.extract();
523 assert!(
524 matches!(result, Err(ByteStorageError::InputTooLarge)),
525 "Should fail size check before ratio check: {:?}",
526 result
527 );
528 }
529
530 #[test]
531 fn test_envelope_size_validation() {
532 let storage = ByteStorage::new(None);
533
534 let oversized_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
536
537 let result = storage.retrieve(&oversized_envelope);
538 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
539 }
540
541 #[test]
542 fn test_security_limits_getters() {
543 let storage = ByteStorage::new(None);
544
545 assert_eq!(storage.max_uncompressed_size(), MAX_UNCOMPRESSED_SIZE);
546 assert_eq!(storage.max_compressed_size(), MAX_COMPRESSED_SIZE);
547 assert_eq!(storage.max_compression_ratio(), 1000u64);
548 }
549
550 #[test]
551 fn test_compression_estimate_security() {
552 let storage = ByteStorage::new(None);
553
554 let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
556 let result = storage.estimate_compression(&large_data);
557 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
558 }
559
560 #[test]
561 fn test_validate_security() {
562 let storage = ByteStorage::new(None);
563
564 let large_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
566 let result = storage.validate(&large_envelope);
567 assert!(!result); }
569
570 #[test]
571 fn test_edge_case_exactly_at_limits() {
572 let storage = ByteStorage::new(None);
574 let max_size_data = vec![1u8; MAX_UNCOMPRESSED_SIZE]; let result = storage.store(&max_size_data, None);
578 assert!(result.is_ok());
579 }
580
581 #[test]
582 fn test_zero_size_edge_case() {
583 let storage = ByteStorage::new(None);
584 let empty_data = vec![];
585
586 let stored = storage.store(&empty_data, None).unwrap();
587 let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
588 assert_eq!(empty_data, retrieved_data);
589 assert_eq!("msgpack", format);
590 }
591
592 #[test]
593 fn test_metrics_collection_on_store() {
594 let storage = ByteStorage::new(None);
595 let test_data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); storage.store(&test_data, None).unwrap();
599 let metrics = storage.get_last_metrics();
600
601 assert!(metrics.compression_ratio > 0.0); }
604
605 #[test]
606 fn test_metrics_collection_on_retrieve() {
607 let storage = ByteStorage::new(None);
608 let test_data = b"test data for retrieval metrics";
609
610 let stored = storage.store(test_data, None).unwrap();
612 storage.retrieve(&stored).unwrap();
613 let metrics = storage.get_last_metrics();
614
615 assert!(metrics.compression_ratio > 0.0); }
618}
619
620#[cfg(kani)]
624mod kani_proofs {
625 use super::*;
626
627 #[kani::proof]
630 #[kani::unwind(10)] fn verify_checksum_detects_corruption() {
632 let checksum_a: [u8; 8] = kani::any();
634 let mut checksum_b = checksum_a;
635
636 let byte_index: usize = kani::any();
638 let bit_index: usize = kani::any();
639 kani::assume(byte_index < 8);
640 kani::assume(bit_index < 8);
641
642 checksum_b[byte_index] ^= 1 << bit_index;
643
644 assert_ne!(checksum_a, checksum_b);
646 }
647
648 #[kani::proof]
652 #[kani::unwind(3)]
653 fn verify_decompression_bomb_protection() {
654 let compressed_size: u64 = kani::any();
656 let original_size: u64 = kani::any();
657
658 kani::assume(compressed_size > 0 && compressed_size <= 1000);
660 kani::assume(original_size > 0);
661
662 let max_allowed = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
666
667 if let Some(max) = max_allowed {
669 let would_reject = original_size > max;
670 let exceeds_ratio = original_size > MAX_COMPRESSION_RATIO * compressed_size;
671 assert_eq!(would_reject, exceeds_ratio);
672 } else {
673 assert!(true); }
676 }
677
678 #[kani::proof]
681 #[kani::unwind(3)]
682 fn verify_input_size_limits() {
683 let size: usize = kani::any();
684
685 kani::assume(size <= MAX_UNCOMPRESSED_SIZE + 100);
687
688 let exceeds_limit = size > MAX_UNCOMPRESSED_SIZE;
690 let should_reject = size > MAX_UNCOMPRESSED_SIZE;
691
692 assert_eq!(exceeds_limit, should_reject);
693 }
694
695 #[kani::proof]
698 #[kani::unwind(3)]
699 fn verify_compressed_size_limits() {
700 let compressed_size: usize = kani::any();
701
702 kani::assume(compressed_size <= MAX_COMPRESSED_SIZE + 100);
704
705 let exceeds_limit = compressed_size > MAX_COMPRESSED_SIZE;
707 let should_reject = compressed_size > MAX_COMPRESSED_SIZE;
708
709 assert_eq!(exceeds_limit, should_reject);
710 }
711
712 #[kani::proof]
715 #[kani::unwind(3)]
716 fn verify_compression_ratio_calculation_safety() {
717 let original_size: u64 = kani::any();
718 let compressed_size: u64 = kani::any();
719
720 kani::assume(compressed_size > 0);
722 kani::assume(compressed_size <= 10000);
723 kani::assume(original_size <= 100_000_000); let result = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
727
728 if let Some(max_allowed) = result {
730 let is_bomb = original_size > max_allowed;
732
733 if original_size <= max_allowed {
736 assert!(!is_bomb);
737 } else {
738 assert!(is_bomb);
739 }
740 }
741
742 }
744}