icydb_core/db/store/
key.rs

1use crate::{db::identity::EntityName, key::Key, traits::Storable};
2use canic_cdk::structures::storable::Bound;
3use std::{
4    borrow::Cow,
5    fmt::{self, Display},
6};
7
8///
9/// DataKey
10///
11
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13pub struct DataKey {
14    entity: EntityName,
15    key: Key,
16}
17
18impl DataKey {
19    #[allow(clippy::cast_possible_truncation)]
20    pub const STORED_SIZE: u32 = EntityName::STORED_SIZE + Key::STORED_SIZE as u32;
21
22    #[must_use]
23    /// Build a data key for the given entity type and primary key.
24    pub fn new<E: crate::traits::EntityKind>(key: impl Into<Key>) -> Self {
25        Self {
26            entity: EntityName::from_static(E::ENTITY_NAME),
27            key: key.into(),
28        }
29    }
30
31    #[must_use]
32    pub const fn lower_bound<E: crate::traits::EntityKind>() -> Self {
33        Self {
34            entity: EntityName::from_static(E::ENTITY_NAME),
35            key: Key::lower_bound(),
36        }
37    }
38
39    #[must_use]
40    pub const fn upper_bound<E: crate::traits::EntityKind>() -> Self {
41        Self {
42            entity: EntityName::from_static(E::ENTITY_NAME),
43            key: Key::upper_bound(),
44        }
45    }
46
47    /// Return the primary key component of this data key.
48    #[must_use]
49    pub const fn key(&self) -> Key {
50        self.key
51    }
52
53    /// Entity name (stable, compile-time constant per entity type).
54    #[must_use]
55    pub const fn entity_name(&self) -> &EntityName {
56        &self.entity
57    }
58
59    /// Compute the on-disk size used by a single data entry from its value length.
60    /// Includes the bounded `DataKey` size and the value bytes.
61    #[must_use]
62    pub const fn entry_size_bytes(value_len: u64) -> u64 {
63        Self::STORED_SIZE as u64 + value_len
64    }
65
66    #[must_use]
67    /// Max sentinel key for sizing.
68    pub fn max_storable() -> Self {
69        Self {
70            entity: EntityName::max_storable(),
71            key: Key::max_storable(),
72        }
73    }
74
75    #[must_use]
76    pub fn to_raw(&self) -> RawDataKey {
77        let mut buf = [0u8; Self::STORED_SIZE as usize];
78
79        buf[0] = self.entity.len;
80        let entity_end = EntityName::STORED_SIZE_USIZE;
81        buf[1..entity_end].copy_from_slice(&self.entity.bytes);
82
83        let key_bytes = self.key.to_bytes();
84        debug_assert_eq!(
85            key_bytes.len(),
86            Key::STORED_SIZE,
87            "Key serialization must be exactly fixed-size"
88        );
89        let key_offset = EntityName::STORED_SIZE_USIZE;
90        buf[key_offset..key_offset + Key::STORED_SIZE].copy_from_slice(&key_bytes);
91
92        RawDataKey(buf)
93    }
94
95    pub fn try_from_raw(raw: &RawDataKey) -> Result<Self, &'static str> {
96        let bytes = &raw.0;
97
98        let mut offset = 0;
99        let entity = EntityName::from_bytes(&bytes[offset..offset + EntityName::STORED_SIZE_USIZE])
100            .map_err(|_| "corrupted DataKey: invalid EntityName bytes")?;
101        offset += EntityName::STORED_SIZE_USIZE;
102
103        let key = Key::try_from_bytes(&bytes[offset..offset + Key::STORED_SIZE])
104            .map_err(|_| "corrupted DataKey: invalid Key bytes")?;
105
106        Ok(Self { entity, key })
107    }
108}
109
110impl Display for DataKey {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "#{} ({})", self.entity, self.key)
113    }
114}
115
116impl From<DataKey> for Key {
117    fn from(key: DataKey) -> Self {
118        key.key()
119    }
120}
121
122///
123/// RawDataKey
124///
125
126#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
127pub struct RawDataKey([u8; DataKey::STORED_SIZE as usize]);
128
129impl RawDataKey {
130    #[must_use]
131    pub const fn as_bytes(&self) -> &[u8; DataKey::STORED_SIZE as usize] {
132        &self.0
133    }
134}
135
136impl Storable for RawDataKey {
137    fn to_bytes(&self) -> Cow<'_, [u8]> {
138        Cow::Borrowed(&self.0)
139    }
140
141    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
142        let mut out = [0u8; DataKey::STORED_SIZE as usize];
143        if bytes.len() == out.len() {
144            out.copy_from_slice(bytes.as_ref());
145        }
146        Self(out)
147    }
148
149    fn into_bytes(self) -> Vec<u8> {
150        self.0.to_vec()
151    }
152
153    const BOUND: Bound = Bound::Bounded {
154        max_size: DataKey::STORED_SIZE,
155        is_fixed_size: true,
156    };
157}
158
159///
160/// TESTS
161///
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use std::borrow::Cow;
167
168    #[test]
169    fn data_key_is_exactly_fixed_size() {
170        let data_key = DataKey::max_storable();
171        let size = data_key.to_raw().as_bytes().len();
172
173        assert_eq!(
174            size,
175            DataKey::STORED_SIZE as usize,
176            "DataKey must serialize to exactly STORED_SIZE bytes"
177        );
178    }
179
180    #[test]
181    fn data_key_ordering_matches_bytes() {
182        let keys = vec![
183            DataKey {
184                entity: EntityName::from_static("a"),
185                key: Key::Int(0),
186            },
187            DataKey {
188                entity: EntityName::from_static("aa"),
189                key: Key::Int(0),
190            },
191            DataKey {
192                entity: EntityName::from_static("b"),
193                key: Key::Int(0),
194            },
195            DataKey {
196                entity: EntityName::from_static("a"),
197                key: Key::Uint(1),
198            },
199        ];
200
201        let mut sorted_by_ord = keys.clone();
202        sorted_by_ord.sort();
203
204        let mut sorted_by_bytes = keys;
205        sorted_by_bytes.sort_by(|a, b| a.to_raw().as_bytes().cmp(b.to_raw().as_bytes()));
206
207        assert_eq!(
208            sorted_by_ord, sorted_by_bytes,
209            "DataKey Ord and byte ordering diverged"
210        );
211    }
212
213    #[test]
214    fn data_key_rejects_undersized_bytes() {
215        let buf = vec![0u8; DataKey::STORED_SIZE as usize - 1];
216        let raw = RawDataKey::from_bytes(Cow::Borrowed(&buf));
217        let err = DataKey::try_from_raw(&raw).unwrap_err();
218        assert!(
219            err.contains("corrupted"),
220            "expected corruption error, got: {err}"
221        );
222    }
223
224    #[test]
225    fn data_key_rejects_oversized_bytes() {
226        let buf = vec![0u8; DataKey::STORED_SIZE as usize + 1];
227        let raw = RawDataKey::from_bytes(Cow::Borrowed(&buf));
228        let err = DataKey::try_from_raw(&raw).unwrap_err();
229        assert!(
230            err.contains("corrupted"),
231            "expected corruption error, got: {err}"
232        );
233    }
234
235    #[test]
236    fn data_key_rejects_invalid_entity_len() {
237        let mut raw = DataKey::max_storable().to_raw();
238        raw.0[0] = 0;
239        assert!(DataKey::try_from_raw(&raw).is_err());
240    }
241
242    #[test]
243    fn data_key_rejects_non_ascii_entity_bytes() {
244        let data_key = DataKey {
245            entity: EntityName::from_static("a"),
246            key: Key::Int(1),
247        };
248        let mut raw = data_key.to_raw();
249        raw.0[1] = 0xFF;
250        assert!(DataKey::try_from_raw(&raw).is_err());
251    }
252
253    #[test]
254    fn data_key_rejects_entity_padding() {
255        let data_key = DataKey {
256            entity: EntityName::from_static("user"),
257            key: Key::Int(1),
258        };
259        let mut raw = data_key.to_raw();
260        let padding_offset = 1 + data_key.entity.len();
261        raw.0[padding_offset] = b'x';
262        assert!(DataKey::try_from_raw(&raw).is_err());
263    }
264
265    #[test]
266    #[allow(clippy::cast_possible_truncation)]
267    fn data_key_decode_fuzz_roundtrip_is_canonical() {
268        const RUNS: u64 = 1_000;
269
270        let mut seed = 0xDEAD_BEEF_u64;
271        for _ in 0..RUNS {
272            let mut bytes = [0u8; DataKey::STORED_SIZE as usize];
273            for b in &mut bytes {
274                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
275                *b = (seed >> 24) as u8;
276            }
277
278            let raw = RawDataKey(bytes);
279            if let Ok(decoded) = DataKey::try_from_raw(&raw) {
280                let re = decoded.to_raw();
281                assert_eq!(
282                    raw.as_bytes(),
283                    re.as_bytes(),
284                    "decoded DataKey must be canonical"
285                );
286            }
287        }
288    }
289}