Skip to main content

cuckoo_clock/
associated_data.rs

1use std::{borrow::Borrow, fmt::Display};
2
3use crate::{config::CuckooConfiguration, data_block::DataBlock};
4
5/// Error type for all [`AssociatedData`] access.
6#[derive(Debug)]
7pub enum AccessError {
8    /// Error due to requesting a field that is available only if a feature is enabled in
9    /// [`crate::config::CuckooConfiguration`].
10    FeatureNotEnabled(String),
11}
12
13impl Display for AccessError {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        match self {
16            AccessError::FeatureNotEnabled(feature) => {
17                f.write_str(&format!("Feature ({feature}) not enabled."))
18            }
19        }
20    }
21}
22
23impl std::error::Error for AccessError {
24    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
25        None
26    }
27}
28
29/// Provides access to data associated with an item in the filter.
30///
31/// All data is associated by a fingerprint, meaning that collisions (false positives) will also
32/// affect the associated data - it might not be related exactly to the requested item, but just to
33/// another item that shared the same fingerprint.
34///
35/// This data is a copy of the data in the filter, meaning it will not be updated when filter data
36/// is changed and can be freely moved around.
37///
38/// # Examples
39///
40/// ```
41/// use cuckoo_clock::{CuckooFilter, config::{CuckooConfiguration, LruConfig, TtlConfig}};
42///
43/// let filter = CuckooFilter::new_random(
44///     CuckooConfiguration::builder(100_000)
45///         .with_lru(LruConfig::default())
46///         .with_ttl(TtlConfig {
47///             ttl: 10.try_into()?,
48///             ttl_bits: 8.try_into()?,
49///         })
50///         .build()?
51/// );
52/// filter.insert("example_data");
53/// let data = filter.get_associated_data("example_data").unwrap();
54///
55/// assert_eq!(data.get_stored_ttl_value()?, 10);
56/// // `get_associated_data` also increases LRU counter - it is counted as an access
57/// assert_eq!(data.get_lru_counter()?, 2);
58///
59/// # Ok::<(), Box<dyn std::error::Error>>(())
60/// ```
61pub struct AssociatedData {
62    data: Box<[u8]>,
63    configuration: CuckooConfiguration,
64}
65
66impl AssociatedData {
67    pub(crate) fn new<T: Borrow<[u8]>>(
68        data: DataBlock<T>,
69        configuration: CuckooConfiguration,
70    ) -> Self {
71        Self {
72            data: data.inner().into(),
73            configuration,
74        }
75    }
76
77    /// Returns the fingerprint for this item.
78    ///
79    /// Generally fingerprint is not very useful on its own, depending on the hasher used for
80    /// [`crate::CuckooFilter`].
81    #[must_use]
82    pub fn get_fingerprint(&self) -> u32 {
83        DataBlock::from(&self.data[..])
84            .get_fingerprint(&self.configuration)
85            .data()
86    }
87
88    /// Returns the LRU counter for this item.
89    pub fn get_lru_counter(&self) -> Result<u32, AccessError> {
90        Ok(DataBlock::from(&self.data[..]).get_lru_counter(
91            self.configuration
92                .lru_field_config
93                .as_ref()
94                .ok_or(AccessError::FeatureNotEnabled("LRU".to_string()))?,
95        ))
96    }
97
98    /// Returns the generic counter for this item.
99    pub fn get_counter(&self) -> Result<u32, AccessError> {
100        Ok(DataBlock::from(&self.data[..]).get_counter(
101            self.configuration
102                .counter_field_config
103                .as_ref()
104                .ok_or(AccessError::FeatureNotEnabled("Counter".to_string()))?,
105        ))
106    }
107
108    /// Returns the stored TTL value for this item.
109    ///
110    /// This is not a time to live in seconds. This is just a TTL counter, that is decremented by 1
111    /// each time [`crate::CuckooFilter::scan_and_update_full`] is called, until it reaches 0.
112    pub fn get_stored_ttl_value(&self) -> Result<u32, AccessError> {
113        Ok(DataBlock::from(&self.data[..]).get_ttl(
114            self.configuration
115                .ttl_field_config
116                .as_ref()
117                .ok_or(AccessError::FeatureNotEnabled("TTL".to_string()))?,
118        ))
119    }
120}
121
122#[cfg(test)]
123#[expect(clippy::unwrap_used)]
124mod tests {
125    use crate::{
126        Fingerprint,
127        config::{LruConfig, TtlConfig},
128    };
129
130    use super::*;
131
132    #[test]
133    #[allow(unused_assignments)]
134    fn basic_fingerprint_access() {
135        for bit_count in 1..=32 {
136            let config = CuckooConfiguration::builder(1000)
137                .fingerprint_bits(bit_count.try_into().unwrap())
138                .build()
139                .unwrap();
140
141            let mut data = [0u8; 4];
142            let mut data_block = DataBlock::from(&mut data[..]);
143            data_block.store_fingerprint(&Fingerprint::new(1, 1), &config);
144
145            let associated_data = AssociatedData::new(data_block, config);
146            assert_eq!(associated_data.get_fingerprint(), 1);
147
148            // Associated data should be a snapshot
149            // Further modifications should not change it
150            data[0] = 100;
151            assert_eq!(associated_data.get_fingerprint(), 1);
152        }
153    }
154
155    #[test]
156    fn disabled_features() {
157        // No additional features
158        let config = CuckooConfiguration::builder(1000)
159            .fingerprint_bits(8.try_into().unwrap())
160            .build()
161            .unwrap();
162
163        let data = [0u8; 4];
164        let data_block = DataBlock::from(&data[..]);
165        let associated_data = AssociatedData::new(data_block, config);
166
167        let Err(AccessError::FeatureNotEnabled(feature_name)) =
168            associated_data.get_stored_ttl_value()
169        else {
170            panic!("TTL not enabled error should be returned!");
171        };
172        assert_eq!(feature_name, "TTL");
173
174        let Err(AccessError::FeatureNotEnabled(feature_name)) = associated_data.get_lru_counter()
175        else {
176            panic!("LRU not enabled error should be returned!");
177        };
178        assert_eq!(feature_name, "LRU");
179
180        let Err(AccessError::FeatureNotEnabled(feature_name)) = associated_data.get_counter()
181        else {
182            panic!("Counter not enabled error should be returned!");
183        };
184        assert_eq!(feature_name, "Counter");
185    }
186
187    #[test]
188    #[allow(unused_assignments)]
189    fn ttl_access() {
190        for bit_count in 1..=32 {
191            // No additional features
192            let config = CuckooConfiguration::builder(1000)
193                .fingerprint_bits(5.try_into().unwrap())
194                .with_ttl(TtlConfig {
195                    ttl: 10.try_into().unwrap(),
196                    ttl_bits: bit_count.try_into().unwrap(),
197                })
198                .build()
199                .unwrap();
200
201            let mut data = [0u8; 5];
202            let mut data_block = DataBlock::from(&mut data[..]);
203            data_block.set_ttl(config.ttl_field_config.as_ref().unwrap(), 1);
204            let associated_data = AssociatedData::new(data_block, config);
205
206            assert_eq!(associated_data.get_stored_ttl_value().unwrap(), 1);
207
208            // Associated data should be a snapshot
209            // Further modifications should not change it
210            data[0] = 7;
211            assert_eq!(associated_data.get_stored_ttl_value().unwrap(), 1);
212        }
213    }
214
215    #[test]
216    #[allow(unused_assignments)]
217    fn lru_counter_access() {
218        for bit_count in 1..=32 {
219            // No additional features
220            let config = CuckooConfiguration::builder(1000)
221                .fingerprint_bits(5.try_into().unwrap())
222                .with_lru(LruConfig {
223                    counter_bits: bit_count.try_into().unwrap(),
224                    ..Default::default()
225                })
226                .build()
227                .unwrap();
228
229            let mut data = [0u8; 5];
230            let mut data_block = DataBlock::from(&mut data[..]);
231            data_block.inc_lru_counter(config.lru_field_config.as_ref().unwrap());
232            let associated_data = AssociatedData::new(data_block, config);
233
234            assert_eq!(associated_data.get_lru_counter().unwrap(), 1);
235
236            // Associated data should be a snapshot
237            // Further modifications should not change it
238            data[0] = 7;
239            assert_eq!(associated_data.get_lru_counter().unwrap(), 1);
240        }
241    }
242}