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 #[serde(with = "serde_bytes")]
62 pub compressed_data: Vec<u8>,
63 pub checksum: [u8; 8],
65 pub original_size: u32,
67 pub format: String,
69}
70
71impl StorageEnvelope {
72 #[cfg(all(feature = "compression", feature = "checksum"))]
78 pub fn new(data: &[u8], format: String) -> Result<Self, ByteStorageError> {
79 if data.len() > MAX_UNCOMPRESSED_SIZE {
81 return Err(ByteStorageError::InputTooLarge);
82 }
83
84 let original_size = data.len() as u32;
85
86 let compressed_data = lz4_flex::compress(data);
88
89 if compressed_data.len() > MAX_COMPRESSED_SIZE {
91 return Err(ByteStorageError::InputTooLarge);
92 }
93
94 let checksum = crate::checksum::checksum(data);
96
97 Ok(StorageEnvelope {
98 compressed_data,
99 checksum,
100 original_size,
101 format,
102 })
103 }
104
105 #[cfg(all(feature = "compression", feature = "checksum"))]
107 pub fn extract(&self) -> Result<Vec<u8>, ByteStorageError> {
108 if self.compressed_data.len() > MAX_COMPRESSED_SIZE {
110 return Err(ByteStorageError::InputTooLarge);
111 }
112
113 if self.original_size as usize > MAX_UNCOMPRESSED_SIZE {
114 return Err(ByteStorageError::InputTooLarge);
115 }
116
117 let compressed_size = self.compressed_data.len() as u64;
120
121 if compressed_size == 0 {
123 return Err(ByteStorageError::DecompressionBomb);
124 }
125
126 let max_allowed_original = MAX_COMPRESSION_RATIO
128 .checked_mul(compressed_size)
129 .ok_or(ByteStorageError::DecompressionBomb)?;
130
131 if (self.original_size as u64) > max_allowed_original {
133 return Err(ByteStorageError::DecompressionBomb);
134 }
135
136 let decompressed = lz4_flex::decompress(&self.compressed_data, self.original_size as usize)
138 .map_err(|_| ByteStorageError::DecompressionFailed)?;
139
140 if !crate::checksum::verify_checksum(&decompressed, &self.checksum) {
143 return Err(ByteStorageError::ChecksumMismatch);
144 }
145
146 if decompressed.len() != self.original_size as usize {
148 return Err(ByteStorageError::SizeValidationFailed);
149 }
150
151 Ok(decompressed)
152 }
153}
154
155pub struct ByteStorage {
158 default_format: String,
159 last_metrics: Arc<Mutex<OperationMetrics>>,
161}
162
163impl ByteStorage {
164 pub fn new(default_format: Option<String>) -> Self {
166 ByteStorage {
167 default_format: default_format.unwrap_or_else(|| "msgpack".to_string()),
168 last_metrics: Arc::new(Mutex::new(OperationMetrics::new())),
169 }
170 }
171
172 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
176 pub fn store(&self, data: &[u8], format: Option<String>) -> Result<Vec<u8>, ByteStorageError> {
177 if data.len() > MAX_UNCOMPRESSED_SIZE {
179 return Err(ByteStorageError::InputTooLarge);
180 }
181
182 let format = format.unwrap_or_else(|| self.default_format.clone());
183
184 #[cfg(not(target_arch = "wasm32"))]
186 let compression_start = Instant::now();
187 let original_size = data.len();
188
189 let envelope = StorageEnvelope::new(data, format)?;
190
191 #[cfg(not(target_arch = "wasm32"))]
192 let compression_micros = compression_start.elapsed().as_micros() as u64;
193 #[cfg(target_arch = "wasm32")]
194 let compression_micros = 0u64;
195 let compressed_size = envelope.compressed_data.len();
196
197 let envelope_bytes = rmp_serde::to_vec(&envelope)
199 .map_err(|e| ByteStorageError::SerializationFailed(e.to_string()))?;
200
201 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
203 return Err(ByteStorageError::InputTooLarge);
204 }
205
206 if let Ok(mut metrics) = self.last_metrics.lock() {
208 *metrics = OperationMetrics::new().with_compression(
209 compression_micros,
210 original_size,
211 compressed_size,
212 );
213 }
214
215 Ok(envelope_bytes)
216 }
217
218 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
222 pub fn retrieve(&self, envelope_bytes: &[u8]) -> Result<(Vec<u8>, String), ByteStorageError> {
223 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
225 return Err(ByteStorageError::InputTooLarge);
226 }
227
228 let envelope: StorageEnvelope = rmp_serde::from_slice(envelope_bytes)
230 .map_err(|e| ByteStorageError::DeserializationFailed(e.to_string()))?;
231
232 #[cfg(not(target_arch = "wasm32"))]
234 let decompress_start = Instant::now();
235
236 let data = envelope.extract()?;
238
239 #[cfg(not(target_arch = "wasm32"))]
240 let decompress_micros = decompress_start.elapsed().as_micros() as u64;
241 #[cfg(target_arch = "wasm32")]
242 let decompress_micros = 0u64;
243
244 let compressed_size = envelope.compressed_data.len();
246 let original_size = envelope.original_size as usize;
247
248 if let Ok(mut metrics) = self.last_metrics.lock() {
250 *metrics = OperationMetrics::new().with_compression(
251 decompress_micros,
252 original_size,
253 compressed_size,
254 );
255 }
256
257 Ok((data, envelope.format))
258 }
259
260 #[cfg(feature = "compression")]
262 pub fn estimate_compression(&self, data: &[u8]) -> Result<f64, ByteStorageError> {
263 if data.len() > MAX_UNCOMPRESSED_SIZE {
265 return Err(ByteStorageError::InputTooLarge);
266 }
267
268 let compressed = lz4_flex::compress(data);
269
270 Ok(data.len() as f64 / compressed.len() as f64)
271 }
272
273 #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
275 pub fn validate(&self, envelope_bytes: &[u8]) -> bool {
276 if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
278 return false; }
280
281 match rmp_serde::from_slice::<StorageEnvelope>(envelope_bytes) {
282 Ok(envelope) => envelope.extract().is_ok(),
283 Err(_) => false,
284 }
285 }
286
287 pub fn get_last_metrics(&self) -> OperationMetrics {
291 self.last_metrics
292 .lock()
293 .map(|metrics| metrics.clone())
294 .unwrap_or_else(|_| OperationMetrics::new())
295 }
296
297 pub fn max_uncompressed_size(&self) -> usize {
299 MAX_UNCOMPRESSED_SIZE
300 }
301
302 pub fn max_compressed_size(&self) -> usize {
303 MAX_COMPRESSED_SIZE
304 }
305
306 pub fn max_compression_ratio(&self) -> u64 {
307 MAX_COMPRESSION_RATIO
308 }
309}
310
311impl Default for ByteStorage {
312 fn default() -> Self {
313 Self::new(None)
314 }
315}
316
317#[cfg(all(
318 test,
319 feature = "compression",
320 feature = "checksum",
321 feature = "messagepack"
322))]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn envelope_embeds_canonical_checksum() {
328 let data = b"DRY-guard payload";
329 let envelope = StorageEnvelope::new(data, "test".to_string()).unwrap();
330 assert_eq!(envelope.checksum, crate::checksum::checksum(data));
331 }
332
333 #[test]
334 fn test_storage_envelope_roundtrip() {
335 let data = b"Hello, World! This is test data for compression.".to_vec();
336 let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
337 let extracted = envelope.extract().unwrap();
338 assert_eq!(data, extracted);
339 }
340
341 #[test]
342 fn test_compression_works() {
343 let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
345 assert!(envelope.compressed_data.len() < data.len());
346 }
347
348 #[test]
349 fn test_checksum_validation() {
350 let mut envelope = StorageEnvelope::new(b"test", "test".to_string()).unwrap();
351 envelope.checksum[0] = !envelope.checksum[0];
353 assert!(matches!(
356 envelope.extract(),
357 Err(ByteStorageError::ChecksumMismatch)
358 ));
359 }
360
361 #[test]
362 fn test_raw_persistence_roundtrip() {
363 let storage = ByteStorage::new(None);
364 let test_data = b"test data for persistence";
365
366 let stored = storage.store(test_data, None).unwrap();
367 let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
368 assert_eq!(test_data, retrieved_data.as_slice());
369 assert_eq!("msgpack", format);
370 }
371
372 #[test]
373 fn test_size_limits_input() {
374 let storage = ByteStorage::new(None);
375
376 let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
378
379 let result = storage.store(&large_data, None);
380 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
381 }
382
383 #[test]
384 fn test_size_limits_envelope() {
385 let max_data = vec![0u8; MAX_UNCOMPRESSED_SIZE];
387 let envelope_result = StorageEnvelope::new(&max_data, "test".to_string());
388
389 assert!(envelope_result.is_ok());
391 }
392
393 #[test]
394 fn test_compression_ratio_bomb_protection() {
395 let malicious_envelope = StorageEnvelope {
397 compressed_data: vec![0u8; 1000], checksum: [0u8; 8], original_size: 200 * 1024 * 1024, format: "test".to_string(),
401 };
402
403 let result = malicious_envelope.extract();
404 assert!(matches!(result, Err(ByteStorageError::DecompressionBomb)));
405 }
406
407 #[test]
412 fn test_decompression_bomb_zero_compressed_size() {
413 let malicious_envelope = StorageEnvelope {
415 compressed_data: vec![], checksum: [0u8; 8],
417 original_size: 1000, format: "test".to_string(),
419 };
420
421 let result = malicious_envelope.extract();
422 assert!(
423 matches!(result, Err(ByteStorageError::DecompressionBomb)),
424 "Zero compressed size should be rejected as decompression bomb"
425 );
426 }
427
428 #[test]
429 fn test_decompression_bomb_extreme_ratio() {
430 let malicious_envelope = StorageEnvelope {
434 compressed_data: vec![0u8; 1], checksum: [0u8; 8],
436 original_size: 2000, format: "test".to_string(),
438 };
439
440 let result = malicious_envelope.extract();
441 assert!(
442 matches!(result, Err(ByteStorageError::DecompressionBomb)),
443 "Extreme ratio should be rejected as bomb: {:?}",
444 result
445 );
446 }
447
448 #[test]
449 fn test_decompression_u32_max_original_size() {
450 let malicious_envelope = StorageEnvelope {
454 compressed_data: vec![0u8; 1000],
455 checksum: [0u8; 8],
456 original_size: u32::MAX, format: "test".to_string(),
458 };
459
460 let result = malicious_envelope.extract();
461 assert!(
462 matches!(result, Err(ByteStorageError::InputTooLarge)),
463 "u32::MAX should be rejected as InputTooLarge (exceeds 512MB limit): {:?}",
464 result
465 );
466 }
467
468 #[test]
469 fn test_decompression_exactly_at_threshold() {
470 let envelope = StorageEnvelope {
474 compressed_data: vec![0u8; 100], checksum: [0u8; 8],
476 original_size: 100_000, format: "test".to_string(),
478 };
479
480 let result = envelope.extract();
481 assert!(
484 !matches!(result, Err(ByteStorageError::DecompressionBomb)),
485 "Exactly 1000:1 ratio should pass bomb check: {:?}",
486 result
487 );
488 assert!(
489 result.is_err(),
490 "Invalid data should still fail after ratio check"
491 );
492 }
493
494 #[test]
495 fn test_decompression_just_over_threshold() {
496 let malicious_envelope = StorageEnvelope {
498 compressed_data: vec![0u8; 100], checksum: [0u8; 8],
500 original_size: 100_001, format: "test".to_string(),
502 };
503
504 let result = malicious_envelope.extract();
505 assert!(
506 matches!(result, Err(ByteStorageError::DecompressionBomb)),
507 "Just over 1000:1 ratio should be rejected as bomb"
508 );
509 }
510
511 #[test]
512 fn test_decompression_bomb_integer_boundary() {
513 let envelope = StorageEnvelope {
520 compressed_data: vec![0u8; 1_000_000], checksum: [0u8; 8],
522 original_size: 1_000_000_000, format: "test".to_string(),
524 };
525
526 let result = envelope.extract();
528 assert!(
529 matches!(result, Err(ByteStorageError::InputTooLarge)),
530 "Should fail size check before ratio check: {:?}",
531 result
532 );
533 }
534
535 #[test]
536 fn test_envelope_size_validation() {
537 let storage = ByteStorage::new(None);
538
539 let oversized_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
541
542 let result = storage.retrieve(&oversized_envelope);
543 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
544 }
545
546 #[test]
547 fn test_security_limits_getters() {
548 let storage = ByteStorage::new(None);
549
550 assert_eq!(storage.max_uncompressed_size(), MAX_UNCOMPRESSED_SIZE);
551 assert_eq!(storage.max_compressed_size(), MAX_COMPRESSED_SIZE);
552 assert_eq!(storage.max_compression_ratio(), 1000u64);
553 }
554
555 #[test]
556 fn test_compression_estimate_security() {
557 let storage = ByteStorage::new(None);
558
559 let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
561 let result = storage.estimate_compression(&large_data);
562 assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
563 }
564
565 #[test]
566 fn test_validate_security() {
567 let storage = ByteStorage::new(None);
568
569 let large_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
571 let result = storage.validate(&large_envelope);
572 assert!(!result); }
574
575 #[test]
576 fn test_edge_case_exactly_at_limits() {
577 let storage = ByteStorage::new(None);
579 let max_size_data = vec![1u8; MAX_UNCOMPRESSED_SIZE]; let result = storage.store(&max_size_data, None);
583 assert!(result.is_ok());
584 }
585
586 #[test]
587 fn test_zero_size_edge_case() {
588 let storage = ByteStorage::new(None);
589 let empty_data = vec![];
590
591 let stored = storage.store(&empty_data, None).unwrap();
592 let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
593 assert_eq!(empty_data, retrieved_data);
594 assert_eq!("msgpack", format);
595 }
596
597 #[test]
598 fn test_metrics_collection_on_store() {
599 let storage = ByteStorage::new(None);
600 let test_data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); storage.store(&test_data, None).unwrap();
604 let metrics = storage.get_last_metrics();
605
606 assert!(metrics.compression_ratio > 0.0); }
609
610 #[test]
611 fn test_metrics_collection_on_retrieve() {
612 let storage = ByteStorage::new(None);
613 let test_data = b"test data for retrieval metrics";
614
615 let stored = storage.store(test_data, None).unwrap();
617 storage.retrieve(&stored).unwrap();
618 let metrics = storage.get_last_metrics();
619
620 assert!(metrics.compression_ratio > 0.0); }
623}
624
625#[cfg(kani)]
629mod kani_proofs {
630 use super::*;
631
632 #[kani::proof]
635 #[kani::unwind(10)] fn verify_checksum_detects_corruption() {
637 let checksum_a: [u8; 8] = kani::any();
639 let mut checksum_b = checksum_a;
640
641 let byte_index: usize = kani::any();
643 let bit_index: usize = kani::any();
644 kani::assume(byte_index < 8);
645 kani::assume(bit_index < 8);
646
647 checksum_b[byte_index] ^= 1 << bit_index;
648
649 assert_ne!(checksum_a, checksum_b);
651 }
652
653 #[kani::proof]
657 #[kani::unwind(3)]
658 fn verify_decompression_bomb_protection() {
659 let compressed_size: u64 = kani::any();
661 let original_size: u64 = kani::any();
662
663 kani::assume(compressed_size > 0 && compressed_size <= 1000);
665 kani::assume(original_size > 0);
666
667 let max_allowed = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
671
672 if let Some(max) = max_allowed {
674 let would_reject = original_size > max;
675 let exceeds_ratio = original_size > MAX_COMPRESSION_RATIO * compressed_size;
676 assert_eq!(would_reject, exceeds_ratio);
677 } else {
678 assert!(true); }
681 }
682
683 #[kani::proof]
686 #[kani::unwind(3)]
687 fn verify_input_size_limits() {
688 let size: usize = kani::any();
689
690 kani::assume(size <= MAX_UNCOMPRESSED_SIZE + 100);
692
693 let exceeds_limit = size > MAX_UNCOMPRESSED_SIZE;
695 let should_reject = size > MAX_UNCOMPRESSED_SIZE;
696
697 assert_eq!(exceeds_limit, should_reject);
698 }
699
700 #[kani::proof]
703 #[kani::unwind(3)]
704 fn verify_compressed_size_limits() {
705 let compressed_size: usize = kani::any();
706
707 kani::assume(compressed_size <= MAX_COMPRESSED_SIZE + 100);
709
710 let exceeds_limit = compressed_size > MAX_COMPRESSED_SIZE;
712 let should_reject = compressed_size > MAX_COMPRESSED_SIZE;
713
714 assert_eq!(exceeds_limit, should_reject);
715 }
716
717 #[kani::proof]
720 #[kani::unwind(3)]
721 fn verify_compression_ratio_calculation_safety() {
722 let original_size: u64 = kani::any();
723 let compressed_size: u64 = kani::any();
724
725 kani::assume(compressed_size > 0);
727 kani::assume(compressed_size <= 10000);
728 kani::assume(original_size <= 100_000_000); let result = MAX_COMPRESSION_RATIO.checked_mul(compressed_size);
732
733 if let Some(max_allowed) = result {
735 let is_bomb = original_size > max_allowed;
737
738 if original_size <= max_allowed {
741 assert!(!is_bomb);
742 } else {
743 assert!(is_bomb);
744 }
745 }
746
747 }
749}