use super::CompressionError;
use crate::atp::manifest::{
CompressionMetadata, CompressionPolicy, HashPoint, TransformOrder, TransformProofPolicy,
TransformType,
};
pub struct CompressionValidator;
impl CompressionValidator {
pub fn validate_compression_metadata(
metadata: &CompressionMetadata,
policy: &CompressionPolicy,
proof_policy: Option<&TransformProofPolicy>,
) -> Result<(), CompressionError> {
if metadata.algorithm != policy.algorithm {
return Err(CompressionError::PolicyViolation(
"compression metadata algorithm doesn't match policy".to_string(),
));
}
if metadata.level != policy.level {
return Err(CompressionError::PolicyViolation(
"compression metadata level doesn't match policy".to_string(),
));
}
if metadata.compression_ratio <= 0.0 || metadata.compression_ratio > 1.5 {
return Err(CompressionError::PolicyViolation(
"invalid compression ratio".to_string(),
));
}
let computed_ratio = metadata.compressed_size as f32 / metadata.original_size as f32;
let ratio_diff = (computed_ratio - metadata.compression_ratio).abs();
if ratio_diff > 0.01 {
return Err(CompressionError::PolicyViolation(
"compression ratio doesn't match computed ratio".to_string(),
));
}
if let Some(proof) = proof_policy {
Self::validate_against_proof_policy(metadata, proof)?;
}
Ok(())
}
fn validate_against_proof_policy(
metadata: &CompressionMetadata,
proof_policy: &TransformProofPolicy,
) -> Result<(), CompressionError> {
let is_potentially_lossy = Self::is_potentially_lossy_algorithm(metadata.algorithm);
if is_potentially_lossy && !proof_policy.allow_lossy_transforms {
return Err(CompressionError::PolicyViolation(
"lossy compression algorithm not allowed by proof policy".to_string(),
));
}
if let Some(max_ratio) = proof_policy.max_compression_ratio {
if metadata.compression_ratio > max_ratio {
return Err(CompressionError::PolicyViolation(
"compression ratio exceeds policy maximum".to_string(),
));
}
}
if proof_policy.require_deterministic_transforms {
Self::validate_deterministic_compression(metadata)?;
}
Ok(())
}
fn is_potentially_lossy_algorithm(
algorithm: crate::atp::manifest::CompressionAlgorithm,
) -> bool {
use crate::atp::manifest::CompressionAlgorithm;
match algorithm {
CompressionAlgorithm::None => false,
CompressionAlgorithm::Lz4 => false,
CompressionAlgorithm::Gzip => false,
CompressionAlgorithm::Brotli => false,
}
}
fn validate_deterministic_compression(
metadata: &CompressionMetadata,
) -> Result<(), CompressionError> {
use crate::atp::manifest::CompressionAlgorithm;
let is_deterministic = match metadata.algorithm {
CompressionAlgorithm::None => true,
CompressionAlgorithm::Lz4 => true, CompressionAlgorithm::Gzip => true, CompressionAlgorithm::Brotli => true, };
if !is_deterministic {
return Err(CompressionError::PolicyViolation(
"non-deterministic compression algorithm used".to_string(),
));
}
Ok(())
}
pub fn validate_transform_order_position(
transform_order: &TransformOrder,
has_compression: bool,
) -> Result<(), CompressionError> {
let compression_in_order = transform_order
.transforms
.contains(&TransformType::Compression);
if has_compression != compression_in_order {
return Err(CompressionError::TransformOrderViolation(
"compression presence doesn't match transform order".to_string(),
));
}
if !has_compression {
return Ok(()); }
let compression_pos = transform_order
.transforms
.iter()
.position(|&t| t == TransformType::Compression)
.unwrap();
if let Some(chunking_pos) = transform_order
.transforms
.iter()
.position(|&t| t == TransformType::Chunking)
{
if compression_pos <= chunking_pos {
return Err(CompressionError::TransformOrderViolation(
"compression must come after chunking".to_string(),
));
}
}
if let Some(encryption_pos) = transform_order
.transforms
.iter()
.position(|&t| t == TransformType::Encryption)
{
if compression_pos >= encryption_pos {
return Err(CompressionError::TransformOrderViolation(
"compression must come before encryption".to_string(),
));
}
}
Ok(())
}
pub fn validate_hash_point_consistency(
transform_order: &TransformOrder,
has_compression: bool,
) -> Result<(), CompressionError> {
if !has_compression {
if matches!(transform_order.hash_point, HashPoint::PostCompression) {
return Err(CompressionError::TransformOrderViolation(
"post-compression hash point without compression".to_string(),
));
}
return Ok(());
}
match transform_order.hash_point {
HashPoint::Plaintext => Ok(()),
HashPoint::PostCompression => Ok(()),
HashPoint::Ciphertext => {
let has_encryption = transform_order
.transforms
.contains(&TransformType::Encryption);
if !has_encryption {
return Err(CompressionError::TransformOrderViolation(
"ciphertext hash point without encryption".to_string(),
));
}
Ok(())
}
HashPoint::MultiPoint => Ok(()), }
}
pub fn validate_compression_bomb_protection(
original_size: u64,
compressed_size: u64,
max_expansion_ratio: f32,
) -> Result<(), CompressionError> {
if compressed_size > original_size {
let expansion_ratio = compressed_size as f32 / original_size as f32;
if expansion_ratio > max_expansion_ratio {
return Err(CompressionError::CompressionBomb);
}
}
if original_size > 0 {
let compression_ratio = compressed_size as f32 / original_size as f32;
if compression_ratio < 0.001 {
return Err(CompressionError::CompressionBomb);
}
}
Ok(())
}
}
pub struct CompressionProofBuilder;
impl CompressionProofBuilder {
pub fn build_compression_proof(
original_hash: [u8; 32],
compressed_hash: [u8; 32],
metadata: &CompressionMetadata,
policy: &CompressionPolicy,
) -> CompressionProof {
CompressionProof {
original_hash,
compressed_hash,
algorithm: metadata.algorithm,
level: metadata.level,
original_size: metadata.original_size,
compressed_size: metadata.compressed_size,
compression_ratio: metadata.compression_ratio,
policy_hash: Self::compute_policy_hash(policy),
}
}
fn compute_policy_hash(policy: &CompressionPolicy) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update([policy.algorithm as u8]);
hasher.update([policy.level]);
hasher.update(policy.min_size_threshold.to_be_bytes());
for kind in &policy.apply_to_kinds {
hasher.update([*kind as u8]);
}
hasher.finalize().into()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CompressionProof {
pub original_hash: [u8; 32],
pub compressed_hash: [u8; 32],
pub algorithm: crate::atp::manifest::CompressionAlgorithm,
pub level: u8,
pub original_size: u64,
pub compressed_size: u64,
pub compression_ratio: f32,
pub policy_hash: [u8; 32],
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atp::manifest::{CompressionAlgorithm, ObjectKind};
#[test]
fn test_compression_metadata_validation() {
let metadata = CompressionMetadata {
algorithm: CompressionAlgorithm::Lz4,
level: 1,
original_size: 1000,
compressed_size: 600,
compression_ratio: 0.6,
};
let policy = CompressionPolicy {
algorithm: CompressionAlgorithm::Lz4,
level: 1,
min_size_threshold: 100,
apply_to_kinds: vec![ObjectKind::FileObject],
};
assert!(
CompressionValidator::validate_compression_metadata(&metadata, &policy, None).is_ok()
);
}
#[test]
fn test_compression_metadata_algorithm_mismatch() {
let metadata = CompressionMetadata {
algorithm: CompressionAlgorithm::Gzip, level: 1,
original_size: 1000,
compressed_size: 600,
compression_ratio: 0.6,
};
let policy = CompressionPolicy {
algorithm: CompressionAlgorithm::Lz4,
level: 1,
min_size_threshold: 100,
apply_to_kinds: vec![ObjectKind::FileObject],
};
assert!(matches!(
CompressionValidator::validate_compression_metadata(&metadata, &policy, None),
Err(CompressionError::PolicyViolation(_))
));
}
#[test]
fn test_compression_bomb_detection() {
assert!(CompressionValidator::validate_compression_bomb_protection(1000, 600, 2.0).is_ok());
assert!(matches!(
CompressionValidator::validate_compression_bomb_protection(1000000, 100, 2.0),
Err(CompressionError::CompressionBomb)
));
assert!(matches!(
CompressionValidator::validate_compression_bomb_protection(1000, 3000, 2.0),
Err(CompressionError::CompressionBomb)
));
}
#[test]
fn test_transform_order_validation() {
use crate::atp::manifest::{PrivacyLevel, VerificationBoundary, VerificationLevel};
let valid_order = TransformOrder {
transforms: vec![
TransformType::Chunking,
TransformType::Compression,
TransformType::Encryption,
],
hash_point: HashPoint::PostCompression,
verification_boundary: VerificationBoundary {
relay_verifiable: VerificationLevel::TransferIntegrity,
mailbox_verifiable: VerificationLevel::ContentHash,
e2e_verification_required: true,
privacy_level: PrivacyLevel::MetadataVisible,
},
};
assert!(
CompressionValidator::validate_transform_order_position(&valid_order, true).is_ok()
);
let invalid_order = TransformOrder {
transforms: vec![TransformType::Compression, TransformType::Chunking],
hash_point: HashPoint::PostCompression,
verification_boundary: VerificationBoundary {
relay_verifiable: VerificationLevel::TransferIntegrity,
mailbox_verifiable: VerificationLevel::ContentHash,
e2e_verification_required: true,
privacy_level: PrivacyLevel::MetadataVisible,
},
};
assert!(matches!(
CompressionValidator::validate_transform_order_position(&invalid_order, true),
Err(CompressionError::TransformOrderViolation(_))
));
}
}