// Copyright 2026 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use crate::{DATA_USAGE_CACHE_KEY_FORMAT, DataUsageCacheSource, DataUsageScanPlanDigest}; use std::collections::BTreeSet; use uuid::Uuid; pub const MAX_SEGMENT_INVALIDATION_ENTRIES: usize = 4; pub const MAX_SEGMENT_INVALIDATION_BYTES: usize = 128; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SegmentInvalidationError { EntryLimit, ByteLimit, InvalidProof, InvalidKey, UnknownProducer, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SegmentInvalidationProducer { Put, Delete, DeleteMarker, Multipart, Replication, Tier, DirectoryObject, } impl SegmentInvalidationProducer { pub const REQUIRED: [Self; 7] = [ Self::Put, Self::Delete, Self::DeleteMarker, Self::Multipart, Self::Replication, Self::Tier, Self::DirectoryObject, ]; } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SegmentInvalidationProducerIdentity { PutObject, DeleteObject, DeleteMarker, CompleteMultipartUpload, Replication, TierTransition, TierExpiration, DirectoryObject, Unknown, TestFixture, } impl SegmentInvalidationProducerIdentity { pub const REQUIRED_PRODUCTION: [Self; 8] = [ Self::PutObject, Self::DeleteObject, Self::DeleteMarker, Self::CompleteMultipartUpload, Self::Replication, Self::TierTransition, Self::TierExpiration, Self::DirectoryObject, ]; pub fn producer(self) -> Option { match self { Self::PutObject => Some(SegmentInvalidationProducer::Put), Self::DeleteObject => Some(SegmentInvalidationProducer::Delete), Self::DeleteMarker => Some(SegmentInvalidationProducer::DeleteMarker), Self::CompleteMultipartUpload => Some(SegmentInvalidationProducer::Multipart), Self::Replication => Some(SegmentInvalidationProducer::Replication), Self::TierTransition | Self::TierExpiration => Some(SegmentInvalidationProducer::Tier), Self::DirectoryObject => Some(SegmentInvalidationProducer::DirectoryObject), Self::Unknown | Self::TestFixture => None, } } } pub fn complete_segment_invalidation_producers( identities: I, ) -> Result, SegmentInvalidationError> where I: IntoIterator, { let mut covered_identities = BTreeSet::new(); let mut producers = BTreeSet::new(); for identity in identities { let Some(producer) = identity.producer() else { return Err(SegmentInvalidationError::UnknownProducer); }; covered_identities.insert(identity); producers.insert(producer); } if SegmentInvalidationProducerIdentity::REQUIRED_PRODUCTION .iter() .all(|identity| covered_identities.contains(identity)) && SegmentInvalidationProducer::REQUIRED .iter() .all(|producer| producers.contains(producer)) { Ok(producers) } else { Err(SegmentInvalidationError::InvalidProof) } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SegmentInvalidationDomain { LocalSingleSet, DistributedEc, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct SegmentInvalidationEnvelope { pub source: DataUsageCacheSource, pub bucket_incarnation: Uuid, pub key_format: u16, pub baseline_scan_plan_digest: DataUsageScanPlanDigest, pub process_epoch: String, pub generation_start: u64, pub generation_end: u64, pub restart_gap: bool, pub overflow: bool, pub producers: BTreeSet, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct SegmentInvalidationProof { pub source: DataUsageCacheSource, pub bucket_incarnation: Uuid, pub key_format: u16, pub baseline_scan_plan_digest: DataUsageScanPlanDigest, pub process_epoch: String, pub generation_start: u64, pub generation_end: u64, pub durable_producer_identity: bool, pub invalidation_domain: SegmentInvalidationDomain, pub distributed_ec_invalidation: bool, pub cold_zero_walk_oracle: bool, } pub fn admit_segment_invalidation( envelope: &SegmentInvalidationEnvelope, proof: &SegmentInvalidationProof, keys: I, ) -> Result, SegmentInvalidationError> where I: IntoIterator, K: AsRef, { validate_segment_invalidation_proof(envelope, proof)?; segment_invalidation_top_level_entries(keys) } fn validate_segment_invalidation_proof( envelope: &SegmentInvalidationEnvelope, proof: &SegmentInvalidationProof, ) -> Result<(), SegmentInvalidationError> { if envelope.source != proof.source || envelope.bucket_incarnation.is_nil() || envelope.bucket_incarnation != proof.bucket_incarnation || envelope.key_format != DATA_USAGE_CACHE_KEY_FORMAT || envelope.key_format != proof.key_format || envelope.baseline_scan_plan_digest != proof.baseline_scan_plan_digest || envelope.process_epoch.is_empty() || envelope.process_epoch != proof.process_epoch || envelope.generation_start != proof.generation_start || envelope.generation_end != proof.generation_end || !proof.durable_producer_identity || !proof.cold_zero_walk_oracle || (proof.invalidation_domain == SegmentInvalidationDomain::DistributedEc && !proof.distributed_ec_invalidation) || envelope.generation_start == 0 || envelope.generation_end < envelope.generation_start || proof.generation_start == 0 || proof.generation_end < proof.generation_start || envelope.restart_gap || envelope.overflow || !SegmentInvalidationProducer::REQUIRED .iter() .all(|producer| envelope.producers.contains(producer)) { return Err(SegmentInvalidationError::InvalidProof); } Ok(()) } fn segment_invalidation_top_level_entries(keys: I) -> Result, SegmentInvalidationError> where I: IntoIterator, K: AsRef, { let mut segments = BTreeSet::new(); let mut bytes = 0usize; for key in keys { let segment = top_level_segment(key.as_ref())?; if segments.contains(segment) { continue; } if segments.len() == MAX_SEGMENT_INVALIDATION_ENTRIES { return Err(SegmentInvalidationError::EntryLimit); } if segment.len() > MAX_SEGMENT_INVALIDATION_BYTES - bytes { return Err(SegmentInvalidationError::ByteLimit); } bytes += segment.len(); segments.insert(segment.to_string()); } Ok(segments) } fn top_level_segment(key: &str) -> Result<&str, SegmentInvalidationError> { if key.is_empty() || key.starts_with('/') || key.contains(['\\', '\0']) || key.split('/').any(|part| matches!(part, "" | "." | "..")) { return Err(SegmentInvalidationError::InvalidKey); } Ok(key.split('/').next().expect("validated nonempty key")) } #[cfg(test)] mod tests { use super::*; fn producers() -> BTreeSet { complete_segment_invalidation_producers(SegmentInvalidationProducerIdentity::REQUIRED_PRODUCTION) .expect("production producer matrix should be complete") } fn envelope() -> SegmentInvalidationEnvelope { SegmentInvalidationEnvelope { source: DataUsageCacheSource::new(2, 3), bucket_incarnation: Uuid::from_u128(0x12345678123456781234567812345678), key_format: DATA_USAGE_CACHE_KEY_FORMAT, baseline_scan_plan_digest: DataUsageScanPlanDigest([9; 32]), process_epoch: "epoch-a".to_string(), generation_start: 11, generation_end: 13, restart_gap: false, overflow: false, producers: producers(), } } fn proof() -> SegmentInvalidationProof { let envelope = envelope(); SegmentInvalidationProof { source: envelope.source, bucket_incarnation: envelope.bucket_incarnation, key_format: envelope.key_format, baseline_scan_plan_digest: envelope.baseline_scan_plan_digest, process_epoch: envelope.process_epoch, generation_start: envelope.generation_start, generation_end: envelope.generation_end, durable_producer_identity: true, invalidation_domain: SegmentInvalidationDomain::LocalSingleSet, distributed_ec_invalidation: false, cold_zero_walk_oracle: true, } } #[test] fn segment_invalidation_admits_only_complete_identity_proof() { let envelope = envelope(); let proof = proof(); assert_eq!( admit_segment_invalidation(&envelope, &proof, ["hot/one", "hot/two", "archive/delete-marker"]), Ok(BTreeSet::from(["archive".to_string(), "hot".to_string()])) ); let mut wrong_source = envelope.clone(); wrong_source.source = DataUsageCacheSource::new(2, 4); assert_eq!( admit_segment_invalidation(&wrong_source, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut missing_incarnation = envelope.clone(); missing_incarnation.bucket_incarnation = Uuid::nil(); assert_eq!( admit_segment_invalidation(&missing_incarnation, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut wrong_key_format = envelope.clone(); wrong_key_format.key_format = DATA_USAGE_CACHE_KEY_FORMAT.saturating_add(1); assert_eq!( admit_segment_invalidation(&wrong_key_format, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut wrong_baseline = envelope.clone(); wrong_baseline.baseline_scan_plan_digest = DataUsageScanPlanDigest([8; 32]); assert_eq!( admit_segment_invalidation(&wrong_baseline, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut wrong_epoch = envelope.clone(); wrong_epoch.process_epoch = "epoch-b".to_string(); assert_eq!( admit_segment_invalidation(&wrong_epoch, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut wrong_generation_start = proof.clone(); wrong_generation_start.generation_start = wrong_generation_start.generation_start.saturating_sub(1); assert_eq!( admit_segment_invalidation(&envelope, &wrong_generation_start, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut wrong_generation_end = proof.clone(); wrong_generation_end.generation_end = wrong_generation_end.generation_end.saturating_add(1); assert_eq!( admit_segment_invalidation(&envelope, &wrong_generation_end, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut no_durable_identity = proof.clone(); no_durable_identity.durable_producer_identity = false; assert_eq!( admit_segment_invalidation(&envelope, &no_durable_identity, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut restart_gap = envelope.clone(); restart_gap.restart_gap = true; assert_eq!( admit_segment_invalidation(&restart_gap, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut overflow = envelope.clone(); overflow.overflow = true; assert_eq!( admit_segment_invalidation(&overflow, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut generation_gap = envelope.clone(); generation_gap.generation_end = generation_gap.generation_start - 1; assert_eq!( admit_segment_invalidation(&generation_gap, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut missing_producer = envelope.clone(); missing_producer.producers.remove(&SegmentInvalidationProducer::Replication); assert_eq!( admit_segment_invalidation(&missing_producer, &proof, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut missing_zero_walk_oracle = proof.clone(); missing_zero_walk_oracle.cold_zero_walk_oracle = false; assert_eq!( admit_segment_invalidation(&envelope, &missing_zero_walk_oracle, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut distributed_without_invalidation = proof; distributed_without_invalidation.invalidation_domain = SegmentInvalidationDomain::DistributedEc; assert_eq!( admit_segment_invalidation(&envelope, &distributed_without_invalidation, ["hot/one"]), Err(SegmentInvalidationError::InvalidProof) ); let mut distributed_with_invalidation = distributed_without_invalidation; distributed_with_invalidation.distributed_ec_invalidation = true; assert_eq!( admit_segment_invalidation(&envelope, &distributed_with_invalidation, ["hot/one"]), Ok(BTreeSet::from(["hot".to_string()])) ); } #[test] fn segment_invalidation_producer_identities_must_be_known_and_complete() { assert_eq!( complete_segment_invalidation_producers(SegmentInvalidationProducerIdentity::REQUIRED_PRODUCTION), Ok(SegmentInvalidationProducer::REQUIRED.into_iter().collect()) ); assert_eq!( complete_segment_invalidation_producers([ SegmentInvalidationProducerIdentity::PutObject, SegmentInvalidationProducerIdentity::DeleteObject, SegmentInvalidationProducerIdentity::DeleteMarker, SegmentInvalidationProducerIdentity::CompleteMultipartUpload, SegmentInvalidationProducerIdentity::Replication, SegmentInvalidationProducerIdentity::TierTransition, SegmentInvalidationProducerIdentity::DirectoryObject, SegmentInvalidationProducerIdentity::Unknown, ]), Err(SegmentInvalidationError::UnknownProducer) ); assert_eq!( complete_segment_invalidation_producers([ SegmentInvalidationProducerIdentity::PutObject, SegmentInvalidationProducerIdentity::DeleteObject, SegmentInvalidationProducerIdentity::DeleteMarker, SegmentInvalidationProducerIdentity::CompleteMultipartUpload, SegmentInvalidationProducerIdentity::Replication, SegmentInvalidationProducerIdentity::TierTransition, SegmentInvalidationProducerIdentity::DirectoryObject, SegmentInvalidationProducerIdentity::TestFixture, ]), Err(SegmentInvalidationError::UnknownProducer) ); assert_eq!( complete_segment_invalidation_producers([ SegmentInvalidationProducerIdentity::PutObject, SegmentInvalidationProducerIdentity::DeleteObject, SegmentInvalidationProducerIdentity::DeleteMarker, SegmentInvalidationProducerIdentity::Replication, SegmentInvalidationProducerIdentity::TierTransition, SegmentInvalidationProducerIdentity::DirectoryObject, ]), Err(SegmentInvalidationError::InvalidProof) ); assert_eq!( complete_segment_invalidation_producers([ SegmentInvalidationProducerIdentity::PutObject, SegmentInvalidationProducerIdentity::DeleteObject, SegmentInvalidationProducerIdentity::DeleteMarker, SegmentInvalidationProducerIdentity::CompleteMultipartUpload, SegmentInvalidationProducerIdentity::Replication, SegmentInvalidationProducerIdentity::TierTransition, SegmentInvalidationProducerIdentity::DirectoryObject, ]), Err(SegmentInvalidationError::InvalidProof) ); } #[test] fn segment_invalidation_entries_are_bounded_and_key_checked() { let envelope = envelope(); let proof = proof(); assert_eq!( admit_segment_invalidation(&envelope, &proof, ["hot/one", "hot/two"]), Ok(BTreeSet::from(["hot".to_string()])) ); assert_eq!( admit_segment_invalidation(&envelope, &proof, ["a", "b", "c", "d"]) .expect("entry boundary") .len(), MAX_SEGMENT_INVALIDATION_ENTRIES ); assert_eq!( admit_segment_invalidation(&envelope, &proof, ["a", "b", "c", "d", "e"]), Err(SegmentInvalidationError::EntryLimit) ); let exact = "x".repeat(MAX_SEGMENT_INVALIDATION_BYTES); assert!(admit_segment_invalidation(&envelope, &proof, [&exact]).is_ok()); assert_eq!( admit_segment_invalidation(&envelope, &proof, [&exact, "y"]), Err(SegmentInvalidationError::ByteLimit) ); let oversized = "x".repeat(MAX_SEGMENT_INVALIDATION_BYTES + 1); assert_eq!( admit_segment_invalidation(&envelope, &proof, [&oversized]), Err(SegmentInvalidationError::ByteLimit) ); for key in ["", "/hot", "hot/../cold", "hot//one", "hot\\one", "hot/\0"] { assert_eq!( admit_segment_invalidation(&envelope, &proof, [key]), Err(SegmentInvalidationError::InvalidKey) ); } } }