icydb_core/db/store/
key.rs1use 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#[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 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 #[must_use]
49 pub const fn key(&self) -> Key {
50 self.key
51 }
52
53 #[must_use]
55 pub const fn entity_name(&self) -> &EntityName {
56 &self.entity
57 }
58
59 #[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 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#[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#[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}