#[cfg(feature = "compression")]
use lz4_flex;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum ByteStorageError {
#[error("input exceeds maximum size")]
InputTooLarge,
#[error("decompression ratio exceeds safety limit")]
DecompressionBomb,
#[error("integrity check failed")]
ChecksumMismatch,
#[error("compression failed")]
CompressionFailed,
#[error("decompression failed")]
DecompressionFailed,
#[error("size validation failed")]
SizeValidationFailed,
#[error("serialization failed: {0}")]
SerializationFailed(String),
#[error("deserialization failed: {0}")]
DeserializationFailed(String),
}
const MAX_UNCOMPRESSED_SIZE: usize = 512 * 1024 * 1024; const MAX_COMPRESSED_SIZE: usize = 512 * 1024 * 1024; const MAX_COMPRESSION_RATIO: u64 = 1000;
#[derive(Serialize, Deserialize)]
pub struct StorageEnvelope {
#[serde(with = "serde_bytes")]
pub compressed_data: Vec<u8>,
pub checksum: [u8; 8],
pub original_size: u32,
pub format: String,
}
impl StorageEnvelope {
#[cfg(all(feature = "compression", feature = "checksum"))]
pub fn new(data: &[u8], format: String) -> Result<Self, ByteStorageError> {
if data.len() > MAX_UNCOMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let original_size = data.len() as u32;
let compressed_data = lz4_flex::compress(data);
if compressed_data.len() > MAX_COMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let checksum = crate::checksum::checksum(data);
Ok(StorageEnvelope {
compressed_data,
checksum,
original_size,
format,
})
}
#[cfg(all(feature = "compression", feature = "checksum"))]
pub fn extract(&self) -> Result<Vec<u8>, ByteStorageError> {
#[cfg(test)]
crate::read_allocation_probe::reserve_first_control(
&self.compressed_data,
self.original_size,
);
check_decompression_bound(self.compressed_data.len(), self.original_size)?;
let decompressed = lz4_flex::decompress(&self.compressed_data, self.original_size as usize)
.map_err(|_| ByteStorageError::DecompressionFailed)?;
if !crate::checksum::verify_checksum(&decompressed, &self.checksum) {
return Err(ByteStorageError::ChecksumMismatch);
}
if decompressed.len() != self.original_size as usize {
return Err(ByteStorageError::SizeValidationFailed);
}
Ok(decompressed)
}
}
#[cfg(all(feature = "compression", feature = "checksum"))]
fn check_decompression_bound(
compressed_len: usize,
original_size: u32,
) -> Result<(), ByteStorageError> {
if compressed_len > MAX_COMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
if original_size as usize > MAX_UNCOMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let compressed_size = compressed_len as u64;
if compressed_size == 0 {
return Err(ByteStorageError::DecompressionBomb);
}
let max_allowed_original = MAX_COMPRESSION_RATIO
.checked_mul(compressed_size)
.ok_or(ByteStorageError::DecompressionBomb)?;
if (original_size as u64) > max_allowed_original {
return Err(ByteStorageError::DecompressionBomb);
}
Ok(())
}
pub struct ByteStorage {
default_format: String,
}
impl ByteStorage {
pub fn new(default_format: Option<String>) -> Self {
ByteStorage {
default_format: default_format.unwrap_or_else(|| "msgpack".to_string()),
}
}
#[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
pub fn store(&self, data: &[u8], format: Option<String>) -> Result<Vec<u8>, ByteStorageError> {
if data.len() > MAX_UNCOMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let format = format.unwrap_or_else(|| self.default_format.clone());
let envelope = StorageEnvelope::new(data, format)?;
let envelope_bytes = rmp_serde::to_vec(&envelope)
.map_err(|e| ByteStorageError::SerializationFailed(e.to_string()))?;
if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
Ok(envelope_bytes)
}
#[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
pub fn retrieve(&self, envelope_bytes: &[u8]) -> Result<(Vec<u8>, String), ByteStorageError> {
if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let envelope = decode_envelope(envelope_bytes)?;
let data = envelope.extract()?;
Ok((data, envelope.format))
}
#[cfg(feature = "compression")]
pub fn estimate_compression(&self, data: &[u8]) -> Result<f64, ByteStorageError> {
if data.len() > MAX_UNCOMPRESSED_SIZE {
return Err(ByteStorageError::InputTooLarge);
}
let compressed = lz4_flex::compress(data);
Ok(data.len() as f64 / compressed.len() as f64)
}
#[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
pub fn validate(&self, envelope_bytes: &[u8]) -> bool {
if envelope_bytes.len() > MAX_COMPRESSED_SIZE {
return false; }
match decode_envelope(envelope_bytes) {
Ok(envelope) => envelope.extract().is_ok(),
Err(_) => false,
}
}
pub fn max_uncompressed_size(&self) -> usize {
MAX_UNCOMPRESSED_SIZE
}
pub fn max_compressed_size(&self) -> usize {
MAX_COMPRESSED_SIZE
}
pub fn max_compression_ratio(&self) -> u64 {
MAX_COMPRESSION_RATIO
}
}
impl Default for ByteStorage {
fn default() -> Self {
Self::new(None)
}
}
#[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
fn decode_envelope(envelope_bytes: &[u8]) -> Result<StorageEnvelope, ByteStorageError> {
const MAX_DEPTH: usize = 100;
crate::check_msgpack_structure(envelope_bytes, MAX_DEPTH).map_err(|what| {
ByteStorageError::DeserializationFailed(format!("decode pre-scan: {what}"))
})?;
rmp_serde::from_slice(envelope_bytes)
.map_err(|e| ByteStorageError::DeserializationFailed(e.to_string()))
}
#[cfg(all(
test,
feature = "compression",
feature = "checksum",
feature = "messagepack"
))]
mod tests {
use super::*;
#[test]
fn envelope_embeds_canonical_checksum() {
let data = b"DRY-guard payload";
let envelope = StorageEnvelope::new(data, "test".to_string()).unwrap();
assert_eq!(envelope.checksum, crate::checksum::checksum(data));
}
#[test]
fn test_storage_envelope_roundtrip() {
let data = b"Hello, World! This is test data for compression.".to_vec();
let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
let extracted = envelope.extract().unwrap();
assert_eq!(data, extracted);
}
#[test]
fn test_compression_works() {
let data = b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_vec(); let envelope = StorageEnvelope::new(&data, "test".to_string()).unwrap();
assert!(envelope.compressed_data.len() < data.len());
}
#[test]
fn test_checksum_validation() {
let mut envelope = StorageEnvelope::new(b"test", "test".to_string()).unwrap();
envelope.checksum[0] = !envelope.checksum[0];
assert!(matches!(
envelope.extract(),
Err(ByteStorageError::ChecksumMismatch)
));
}
#[test]
fn test_raw_persistence_roundtrip() {
let storage = ByteStorage::new(None);
let test_data = b"test data for persistence";
let stored = storage.store(test_data, None).unwrap();
let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
assert_eq!(test_data, retrieved_data.as_slice());
assert_eq!("msgpack", format);
}
#[test]
fn test_size_limits_input() {
let storage = ByteStorage::new(None);
let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
let result = storage.store(&large_data, None);
assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
}
#[test]
fn test_size_limits_envelope() {
let max_data = vec![0u8; MAX_UNCOMPRESSED_SIZE];
let envelope_result = StorageEnvelope::new(&max_data, "test".to_string());
assert!(envelope_result.is_ok());
}
#[test]
fn test_compression_ratio_bomb_protection() {
let malicious_envelope = StorageEnvelope {
compressed_data: vec![0u8; 1000], checksum: [0u8; 8], original_size: 200 * 1024 * 1024, format: "test".to_string(),
};
let result = malicious_envelope.extract();
assert!(matches!(result, Err(ByteStorageError::DecompressionBomb)));
}
#[test]
fn test_decompression_bomb_zero_compressed_size() {
let malicious_envelope = StorageEnvelope {
compressed_data: vec![], checksum: [0u8; 8],
original_size: 1000, format: "test".to_string(),
};
let result = malicious_envelope.extract();
assert!(
matches!(result, Err(ByteStorageError::DecompressionBomb)),
"Zero compressed size should be rejected as decompression bomb"
);
}
#[test]
fn test_decompression_bomb_extreme_ratio() {
let malicious_envelope = StorageEnvelope {
compressed_data: vec![0u8; 1], checksum: [0u8; 8],
original_size: 2000, format: "test".to_string(),
};
let result = malicious_envelope.extract();
assert!(
matches!(result, Err(ByteStorageError::DecompressionBomb)),
"Extreme ratio should be rejected as bomb: {:?}",
result
);
}
#[test]
fn test_decompression_u32_max_original_size() {
let malicious_envelope = StorageEnvelope {
compressed_data: vec![0u8; 1000],
checksum: [0u8; 8],
original_size: u32::MAX, format: "test".to_string(),
};
let result = malicious_envelope.extract();
assert!(
matches!(result, Err(ByteStorageError::InputTooLarge)),
"u32::MAX should be rejected as InputTooLarge (exceeds 512MB limit): {:?}",
result
);
}
#[test]
fn test_decompression_exactly_at_threshold() {
let envelope = StorageEnvelope {
compressed_data: vec![0u8; 100], checksum: [0u8; 8],
original_size: 100_000, format: "test".to_string(),
};
let result = envelope.extract();
assert!(
!matches!(result, Err(ByteStorageError::DecompressionBomb)),
"Exactly 1000:1 ratio should pass bomb check: {:?}",
result
);
assert!(
result.is_err(),
"Invalid data should still fail after ratio check"
);
}
#[test]
fn test_decompression_just_over_threshold() {
let malicious_envelope = StorageEnvelope {
compressed_data: vec![0u8; 100], checksum: [0u8; 8],
original_size: 100_001, format: "test".to_string(),
};
let result = malicious_envelope.extract();
assert!(
matches!(result, Err(ByteStorageError::DecompressionBomb)),
"Just over 1000:1 ratio should be rejected as bomb"
);
}
#[test]
fn test_decompression_bomb_integer_boundary() {
let envelope = StorageEnvelope {
compressed_data: vec![0u8; 1_000_000], checksum: [0u8; 8],
original_size: 1_000_000_000, format: "test".to_string(),
};
let result = envelope.extract();
assert!(
matches!(result, Err(ByteStorageError::InputTooLarge)),
"Should fail size check before ratio check: {:?}",
result
);
}
#[test]
fn test_extract_rejects_oversized_compressed_data() {
let envelope = StorageEnvelope {
compressed_data: vec![0u8; MAX_COMPRESSED_SIZE + 1],
checksum: [0u8; 8],
original_size: 1,
format: "test".to_string(),
};
assert_eq!(envelope.extract(), Err(ByteStorageError::InputTooLarge));
}
#[test]
fn test_envelope_size_validation() {
let storage = ByteStorage::new(None);
let oversized_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
let result = storage.retrieve(&oversized_envelope);
assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
}
#[test]
fn test_security_limits_getters() {
let storage = ByteStorage::new(None);
assert_eq!(storage.max_uncompressed_size(), MAX_UNCOMPRESSED_SIZE);
assert_eq!(storage.max_compressed_size(), MAX_COMPRESSED_SIZE);
assert_eq!(storage.max_compression_ratio(), 1000u64);
}
#[test]
fn test_compression_estimate_security() {
let storage = ByteStorage::new(None);
let large_data = vec![0u8; MAX_UNCOMPRESSED_SIZE + 1];
let result = storage.estimate_compression(&large_data);
assert!(matches!(result, Err(ByteStorageError::InputTooLarge)));
}
#[test]
fn test_validate_security() {
let storage = ByteStorage::new(None);
let large_envelope = vec![0u8; MAX_COMPRESSED_SIZE + 1];
let result = storage.validate(&large_envelope);
assert!(!result); }
#[test]
fn test_edge_case_exactly_at_limits() {
let storage = ByteStorage::new(None);
let max_size_data = vec![1u8; MAX_UNCOMPRESSED_SIZE];
let result = storage.store(&max_size_data, None);
assert!(result.is_ok());
}
#[test]
fn test_zero_size_edge_case() {
let storage = ByteStorage::new(None);
let empty_data = vec![];
let stored = storage.store(&empty_data, None).unwrap();
let (retrieved_data, format) = storage.retrieve(&stored).unwrap();
assert_eq!(empty_data, retrieved_data);
assert_eq!("msgpack", format);
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(10)] fn verify_checksum_detects_corruption() {
let checksum_a: [u8; 8] = kani::any();
let mut checksum_b = checksum_a;
let byte_index: usize = kani::any();
let bit_index: usize = kani::any();
kani::assume(byte_index < 8);
kani::assume(bit_index < 8);
checksum_b[byte_index] ^= 1 << bit_index;
assert_ne!(checksum_a, checksum_b);
}
const PINNED_MAX_COMPRESSED: u64 = 512 * 1024 * 1024;
const PINNED_MAX_UNCOMPRESSED: u64 = 512 * 1024 * 1024;
const PINNED_MAX_RATIO: u64 = 1000;
#[kani::proof]
fn verify_decompression_bound_size_caps() {
let compressed_len: usize = kani::any();
let original_size: u32 = kani::any();
let over_cap = compressed_len as u64 > PINNED_MAX_COMPRESSED
|| original_size as u64 > PINNED_MAX_UNCOMPRESSED;
let result = check_decompression_bound(compressed_len, original_size);
assert_eq!(
over_cap,
matches!(result, Err(ByteStorageError::InputTooLarge))
);
}
#[kani::proof]
fn verify_decompression_bound_ratio() {
let compressed_len: usize = kani::any();
let original_size: u32 = kani::any();
kani::assume(compressed_len as u64 <= PINNED_MAX_COMPRESSED);
kani::assume(original_size as u64 <= PINNED_MAX_UNCOMPRESSED);
let is_bomb =
compressed_len == 0 || original_size as u64 > PINNED_MAX_RATIO * compressed_len as u64;
let result = check_decompression_bound(compressed_len, original_size);
if is_bomb {
assert!(matches!(result, Err(ByteStorageError::DecompressionBomb)));
} else {
assert!(result.is_ok());
}
}
}