Skip to main content

icydb_core/db/store/
index.rs

1use crate::{
2    MAX_INDEX_FIELDS,
3    db::store::{DataKey, EntityName, IndexName, StoreRegistry},
4    prelude::*,
5    traits::Storable,
6};
7use candid::CandidType;
8use canic_cdk::structures::{BTreeMap, DefaultMemoryImpl, memory::VirtualMemory, storable::Bound};
9use derive_more::{Deref, DerefMut, Display};
10use serde::{Deserialize, Serialize};
11use std::{borrow::Cow, collections::BTreeSet};
12use thiserror::Error as ThisError;
13
14///
15/// IndexStoreRegistry
16///
17
18#[derive(Deref, DerefMut)]
19pub struct IndexStoreRegistry(StoreRegistry<IndexStore>);
20
21impl IndexStoreRegistry {
22    #[must_use]
23    pub fn new() -> Self {
24        Self(StoreRegistry::new())
25    }
26}
27
28impl Default for IndexStoreRegistry {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34///
35/// IndexInsertOutcome
36///
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum IndexInsertOutcome {
40    Inserted,
41    Skipped,
42}
43
44///
45/// IndexRemoveOutcome
46///
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum IndexRemoveOutcome {
50    Removed,
51    Skipped,
52}
53
54///
55/// IndexInsertError
56///
57
58#[derive(Debug, ThisError)]
59pub enum IndexInsertError {
60    #[error("unique index violation")]
61    UniqueViolation,
62    #[error("index entry corrupted: {0}")]
63    CorruptedEntry(#[from] IndexEntryCorruption),
64    #[error("index entry exceeds max keys: {keys} (limit {MAX_INDEX_ENTRY_KEYS})")]
65    EntryTooLarge { keys: usize },
66}
67
68///
69/// IndexStore
70///
71
72#[derive(Deref, DerefMut)]
73pub struct IndexStore(BTreeMap<RawIndexKey, RawIndexEntry, VirtualMemory<DefaultMemoryImpl>>);
74
75impl IndexStore {
76    #[must_use]
77    pub fn init(memory: VirtualMemory<DefaultMemoryImpl>) -> Self {
78        Self(BTreeMap::init(memory))
79    }
80
81    pub fn insert_index_entry<E: EntityKind>(
82        &mut self,
83        entity: &E,
84        index: &IndexModel,
85    ) -> Result<IndexInsertOutcome, IndexInsertError> {
86        let Some(index_key) = IndexKey::new(entity, index) else {
87            return Ok(IndexInsertOutcome::Skipped);
88        };
89        let raw_key = index_key.to_raw();
90        let key = entity.key();
91
92        if let Some(raw_entry) = self.get(&raw_key) {
93            let mut entry = raw_entry.try_decode()?;
94            if index.unique {
95                if entry.len() > 1 {
96                    return Err(IndexEntryCorruption::NonUniqueEntry { keys: entry.len() }.into());
97                }
98                if entry.contains(&key) {
99                    return Ok(IndexInsertOutcome::Skipped);
100                }
101                if !entry.is_empty() {
102                    return Err(IndexInsertError::UniqueViolation);
103                }
104                let entry = IndexEntry::new(key);
105                let raw_entry = RawIndexEntry::try_from_entry(&entry)
106                    .map_err(|err| IndexInsertError::EntryTooLarge { keys: err.keys() })?;
107                self.insert(raw_key, raw_entry);
108            } else {
109                entry.insert_key(key);
110                let raw_entry = RawIndexEntry::try_from_entry(&entry)
111                    .map_err(|err| IndexInsertError::EntryTooLarge { keys: err.keys() })?;
112                self.insert(raw_key, raw_entry);
113            }
114        } else {
115            let entry = IndexEntry::new(key);
116            let raw_entry = RawIndexEntry::try_from_entry(&entry)
117                .map_err(|err| IndexInsertError::EntryTooLarge { keys: err.keys() })?;
118            self.insert(raw_key, raw_entry);
119        }
120
121        Ok(IndexInsertOutcome::Inserted)
122    }
123
124    pub fn remove_index_entry<E: EntityKind>(
125        &mut self,
126        entity: &E,
127        index: &IndexModel,
128    ) -> Result<IndexRemoveOutcome, IndexEntryCorruption> {
129        let Some(index_key) = IndexKey::new(entity, index) else {
130            return Ok(IndexRemoveOutcome::Skipped);
131        };
132        let raw_key = index_key.to_raw();
133
134        if let Some(raw_entry) = self.get(&raw_key) {
135            let mut entry = raw_entry.try_decode()?;
136            entry.remove_key(&entity.key());
137            if entry.is_empty() {
138                self.remove(&raw_key);
139            } else {
140                let raw_entry = RawIndexEntry::try_from_entry(&entry)
141                    .map_err(|err| IndexEntryCorruption::TooManyKeys { count: err.keys() })?;
142                self.insert(raw_key, raw_entry);
143            }
144            return Ok(IndexRemoveOutcome::Removed);
145        }
146
147        Ok(IndexRemoveOutcome::Skipped)
148    }
149
150    pub fn resolve_data_values<E: EntityKind>(
151        &self,
152        index: &IndexModel,
153        prefix: &[Value],
154    ) -> Result<Vec<DataKey>, &'static str> {
155        let mut out = Vec::new();
156        let index_id = IndexId::new::<E>(index);
157
158        let Some(fps) = prefix
159            .iter()
160            .map(Value::to_index_fingerprint)
161            .collect::<Option<Vec<_>>>()
162        else {
163            return Ok(out);
164        };
165
166        let (start, end) = IndexKey::bounds_for_prefix(index_id, index.fields.len(), &fps);
167        let start_raw = start.to_raw();
168        let end_raw = end.to_raw();
169
170        for entry in self.range(start_raw..=end_raw) {
171            let _ = IndexKey::try_from_raw(entry.key())?;
172            let decoded = entry.value().try_decode().map_err(|err| err.message())?;
173            out.extend(decoded.iter_keys().map(|k| DataKey::new::<E>(k)));
174        }
175
176        Ok(out)
177    }
178
179    /// Sum of bytes used by all index entries.
180    pub fn memory_bytes(&self) -> u64 {
181        self.iter()
182            .map(|entry| u64::from(IndexKey::STORED_SIZE) + entry.value().len() as u64)
183            .sum()
184    }
185}
186
187///
188/// IndexId
189///
190
191#[derive(Clone, Copy, Debug, Display, Eq, Hash, Ord, PartialEq, PartialOrd)]
192pub struct IndexId(pub IndexName);
193
194impl IndexId {
195    #[must_use]
196    pub fn new<E: EntityKind>(index: &IndexModel) -> Self {
197        let entity = EntityName::from_static(E::ENTITY_NAME);
198        Self(IndexName::from_parts(&entity, index.fields))
199    }
200
201    #[must_use]
202    pub const fn max_storable() -> Self {
203        Self(IndexName::max_storable())
204    }
205}
206
207///
208/// IndexKey
209/// (FIXED-SIZE, MANUAL)
210///
211
212#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
213pub struct IndexKey {
214    index_id: IndexId,
215    len: u8,
216    values: [[u8; 16]; MAX_INDEX_FIELDS],
217}
218
219impl IndexKey {
220    #[allow(clippy::cast_possible_truncation)]
221    pub const STORED_SIZE: u32 = IndexName::STORED_SIZE + 1 + (MAX_INDEX_FIELDS as u32 * 16);
222
223    pub fn new<E: EntityKind>(entity: &E, index: &IndexModel) -> Option<Self> {
224        if index.fields.len() > MAX_INDEX_FIELDS {
225            return None;
226        }
227
228        let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
229        let mut len = 0usize;
230
231        for field in index.fields {
232            let value = entity.get_value(field)?;
233            let fp = value.to_index_fingerprint()?;
234            values[len] = fp;
235            len += 1;
236        }
237
238        #[allow(clippy::cast_possible_truncation)]
239        Some(Self {
240            index_id: IndexId::new::<E>(index),
241            len: len as u8,
242            values,
243        })
244    }
245
246    #[must_use]
247    pub const fn empty(index_id: IndexId) -> Self {
248        Self {
249            index_id,
250            len: 0,
251            values: [[0u8; 16]; MAX_INDEX_FIELDS],
252        }
253    }
254
255    #[must_use]
256    #[allow(clippy::cast_possible_truncation)]
257    pub fn bounds_for_prefix(
258        index_id: IndexId,
259        index_len: usize,
260        prefix: &[[u8; 16]],
261    ) -> (Self, Self) {
262        let mut start = Self::empty(index_id);
263        let mut end = Self::empty(index_id);
264
265        for (i, fp) in prefix.iter().enumerate() {
266            start.values[i] = *fp;
267            end.values[i] = *fp;
268        }
269
270        start.len = index_len as u8;
271        end.len = start.len;
272
273        for value in end.values.iter_mut().take(index_len).skip(prefix.len()) {
274            *value = [0xFF; 16];
275        }
276
277        (start, end)
278    }
279
280    #[must_use]
281    pub fn to_raw(&self) -> RawIndexKey {
282        let mut buf = [0u8; Self::STORED_SIZE as usize];
283
284        let name_bytes = self.index_id.0.to_bytes();
285        buf[..name_bytes.len()].copy_from_slice(&name_bytes);
286
287        let mut offset = IndexName::STORED_SIZE as usize;
288        buf[offset] = self.len;
289        offset += 1;
290
291        for value in &self.values {
292            buf[offset..offset + 16].copy_from_slice(value);
293            offset += 16;
294        }
295
296        RawIndexKey(buf)
297    }
298
299    pub fn try_from_raw(raw: &RawIndexKey) -> Result<Self, &'static str> {
300        let bytes = &raw.0;
301        if bytes.len() != Self::STORED_SIZE as usize {
302            return Err("corrupted IndexKey: invalid size");
303        }
304
305        let mut offset = 0;
306
307        let index_name =
308            IndexName::from_bytes(&bytes[offset..offset + IndexName::STORED_SIZE as usize])
309                .map_err(|_| "corrupted IndexKey: invalid IndexName bytes")?;
310        offset += IndexName::STORED_SIZE as usize;
311
312        let len = bytes[offset];
313        offset += 1;
314
315        if len as usize > MAX_INDEX_FIELDS {
316            return Err("corrupted IndexKey: invalid index length");
317        }
318
319        let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
320        for value in &mut values {
321            value.copy_from_slice(&bytes[offset..offset + 16]);
322            offset += 16;
323        }
324
325        let len_usize = len as usize;
326        for value in values.iter().skip(len_usize) {
327            if value.iter().any(|&b| b != 0) {
328                return Err("corrupted IndexKey: non-zero fingerprint padding");
329            }
330        }
331
332        Ok(Self {
333            index_id: IndexId(index_name),
334            len,
335            values,
336        })
337    }
338}
339
340#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
341pub struct RawIndexKey([u8; IndexKey::STORED_SIZE as usize]);
342
343impl RawIndexKey {
344    #[must_use]
345    pub const fn as_bytes(&self) -> &[u8; IndexKey::STORED_SIZE as usize] {
346        &self.0
347    }
348}
349
350impl Storable for RawIndexKey {
351    fn to_bytes(&self) -> Cow<'_, [u8]> {
352        Cow::Borrowed(&self.0)
353    }
354
355    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
356        let mut out = [0u8; IndexKey::STORED_SIZE as usize];
357        if bytes.len() == out.len() {
358            out.copy_from_slice(bytes.as_ref());
359        }
360        Self(out)
361    }
362
363    fn into_bytes(self) -> Vec<u8> {
364        self.0.to_vec()
365    }
366
367    const BOUND: Bound = Bound::Bounded {
368        max_size: IndexKey::STORED_SIZE,
369        is_fixed_size: true,
370    };
371}
372
373///
374/// IndexEntry (VALUE, RAW + BOUNDED)
375///
376
377const INDEX_ENTRY_LEN_BYTES: usize = 4;
378pub const MAX_INDEX_ENTRY_KEYS: usize = 65_535;
379#[allow(clippy::cast_possible_truncation)]
380pub const MAX_INDEX_ENTRY_BYTES: u32 =
381    (INDEX_ENTRY_LEN_BYTES + (MAX_INDEX_ENTRY_KEYS * Key::STORED_SIZE)) as u32;
382
383#[derive(Debug, ThisError)]
384pub enum IndexEntryCorruption {
385    #[error("index entry exceeds max size")]
386    TooLarge { len: usize },
387    #[error("index entry missing key count")]
388    MissingLength,
389    #[error("index entry key count exceeds limit")]
390    TooManyKeys { count: usize },
391    #[error("index entry length does not match key count")]
392    LengthMismatch,
393    #[error("index entry contains invalid key bytes")]
394    InvalidKey,
395    #[error("index entry contains duplicate key")]
396    DuplicateKey,
397    #[error("index entry contains zero keys")]
398    EmptyEntry,
399    #[error("unique index entry contains {keys} keys")]
400    NonUniqueEntry { keys: usize },
401}
402
403impl IndexEntryCorruption {
404    #[must_use]
405    pub const fn message(&self) -> &'static str {
406        match self {
407            Self::TooLarge { .. } => "corrupted index entry: exceeds max size",
408            Self::MissingLength => "corrupted index entry: missing key count",
409            Self::TooManyKeys { .. } => "corrupted index entry: key count exceeds limit",
410            Self::LengthMismatch => "corrupted index entry: length mismatch",
411            Self::InvalidKey => "corrupted index entry: invalid key bytes",
412            Self::DuplicateKey => "corrupted index entry: duplicate key",
413            Self::EmptyEntry => "corrupted index entry: empty entry",
414            Self::NonUniqueEntry { .. } => "corrupted index entry: non-unique entry",
415        }
416    }
417}
418
419#[derive(Debug, ThisError)]
420pub enum IndexEntryEncodeError {
421    #[error("index entry exceeds max keys: {keys} (limit {MAX_INDEX_ENTRY_KEYS})")]
422    TooManyKeys { keys: usize },
423}
424
425impl IndexEntryEncodeError {
426    #[must_use]
427    pub const fn keys(&self) -> usize {
428        match self {
429            Self::TooManyKeys { keys } => *keys,
430        }
431    }
432}
433
434#[derive(Clone, Debug, Eq, PartialEq)]
435pub struct RawIndexEntry(Vec<u8>);
436
437impl RawIndexEntry {
438    pub fn try_from_entry(entry: &IndexEntry) -> Result<Self, IndexEntryEncodeError> {
439        let keys = entry.len();
440        if keys > MAX_INDEX_ENTRY_KEYS {
441            return Err(IndexEntryEncodeError::TooManyKeys { keys });
442        }
443
444        let mut out = Vec::with_capacity(INDEX_ENTRY_LEN_BYTES + (keys * Key::STORED_SIZE));
445        let count = u32::try_from(keys).map_err(|_| IndexEntryEncodeError::TooManyKeys { keys })?;
446        out.extend_from_slice(&count.to_be_bytes());
447        for key in entry.iter_keys() {
448            out.extend_from_slice(&key.to_bytes());
449        }
450
451        Ok(Self(out))
452    }
453
454    pub fn try_decode(&self) -> Result<IndexEntry, IndexEntryCorruption> {
455        let bytes = self.0.as_slice();
456        if bytes.len() > MAX_INDEX_ENTRY_BYTES as usize {
457            return Err(IndexEntryCorruption::TooLarge { len: bytes.len() });
458        }
459        if bytes.len() < INDEX_ENTRY_LEN_BYTES {
460            return Err(IndexEntryCorruption::MissingLength);
461        }
462
463        let mut len_buf = [0u8; INDEX_ENTRY_LEN_BYTES];
464        len_buf.copy_from_slice(&bytes[..INDEX_ENTRY_LEN_BYTES]);
465        let count = u32::from_be_bytes(len_buf) as usize;
466        if count == 0 {
467            return Err(IndexEntryCorruption::EmptyEntry);
468        }
469        if count > MAX_INDEX_ENTRY_KEYS {
470            return Err(IndexEntryCorruption::TooManyKeys { count });
471        }
472
473        let expected = INDEX_ENTRY_LEN_BYTES
474            .checked_add(
475                count
476                    .checked_mul(Key::STORED_SIZE)
477                    .ok_or(IndexEntryCorruption::LengthMismatch)?,
478            )
479            .ok_or(IndexEntryCorruption::LengthMismatch)?;
480        if bytes.len() != expected {
481            return Err(IndexEntryCorruption::LengthMismatch);
482        }
483
484        let mut keys = BTreeSet::new();
485        let mut offset = INDEX_ENTRY_LEN_BYTES;
486        for _ in 0..count {
487            let end = offset + Key::STORED_SIZE;
488            let key = Key::try_from_bytes(&bytes[offset..end])
489                .map_err(|_| IndexEntryCorruption::InvalidKey)?;
490            if !keys.insert(key) {
491                return Err(IndexEntryCorruption::DuplicateKey);
492            }
493            offset = end;
494        }
495
496        Ok(IndexEntry { keys })
497    }
498
499    #[must_use]
500    pub fn as_bytes(&self) -> &[u8] {
501        &self.0
502    }
503
504    #[must_use]
505    pub const fn len(&self) -> usize {
506        self.0.len()
507    }
508
509    #[must_use]
510    pub const fn is_empty(&self) -> bool {
511        self.0.is_empty()
512    }
513}
514
515impl Storable for RawIndexEntry {
516    fn to_bytes(&self) -> Cow<'_, [u8]> {
517        Cow::Borrowed(&self.0)
518    }
519
520    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
521        Self(bytes.into_owned())
522    }
523
524    fn into_bytes(self) -> Vec<u8> {
525        self.0
526    }
527
528    const BOUND: Bound = Bound::Bounded {
529        max_size: MAX_INDEX_ENTRY_BYTES,
530        is_fixed_size: false,
531    };
532}
533
534#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
535pub struct IndexEntry {
536    keys: BTreeSet<Key>,
537}
538
539impl IndexEntry {
540    #[must_use]
541    pub fn new(key: Key) -> Self {
542        let mut keys = BTreeSet::new();
543        keys.insert(key);
544
545        Self { keys }
546    }
547
548    pub fn insert_key(&mut self, key: Key) {
549        self.keys.insert(key);
550    }
551
552    pub fn remove_key(&mut self, key: &Key) {
553        self.keys.remove(key);
554    }
555
556    #[must_use]
557    pub fn contains(&self, key: &Key) -> bool {
558        self.keys.contains(key)
559    }
560
561    #[must_use]
562    pub fn is_empty(&self) -> bool {
563        self.keys.is_empty()
564    }
565
566    #[must_use]
567    pub fn len(&self) -> usize {
568        self.keys.len()
569    }
570
571    pub fn iter_keys(&self) -> impl Iterator<Item = Key> + '_ {
572        self.keys.iter().copied()
573    }
574
575    #[must_use]
576    pub fn single_key(&self) -> Option<Key> {
577        if self.keys.len() == 1 {
578            self.keys.iter().copied().next()
579        } else {
580            None
581        }
582    }
583}
584
585///
586/// TESTS
587///
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592    use crate::traits::Storable;
593    use std::borrow::Cow;
594
595    #[test]
596    fn index_key_rejects_undersized_bytes() {
597        let buf = vec![0u8; IndexKey::STORED_SIZE as usize - 1];
598        let raw = RawIndexKey::from_bytes(Cow::Borrowed(&buf));
599        let err = IndexKey::try_from_raw(&raw).unwrap_err();
600        assert!(
601            err.contains("corrupted"),
602            "expected corruption error, got: {err}"
603        );
604    }
605
606    #[test]
607    fn index_key_rejects_oversized_bytes() {
608        let buf = vec![0u8; IndexKey::STORED_SIZE as usize + 1];
609        let raw = RawIndexKey::from_bytes(Cow::Borrowed(&buf));
610        let err = IndexKey::try_from_raw(&raw).unwrap_err();
611        assert!(
612            err.contains("corrupted"),
613            "expected corruption error, got: {err}"
614        );
615    }
616
617    #[test]
618    #[allow(clippy::cast_possible_truncation)]
619    fn index_key_rejects_len_over_max() {
620        let key = IndexKey::empty(IndexId::max_storable());
621        let mut raw = key.to_raw();
622        let len_offset = IndexName::STORED_SIZE as usize;
623        raw.0[len_offset] = (MAX_INDEX_FIELDS as u8) + 1;
624        let err = IndexKey::try_from_raw(&raw).unwrap_err();
625        assert!(
626            err.contains("corrupted"),
627            "expected corruption error, got: {err}"
628        );
629    }
630
631    #[test]
632    fn index_key_rejects_invalid_index_name() {
633        let key = IndexKey::empty(IndexId::max_storable());
634        let mut raw = key.to_raw();
635        raw.0[0] = 0;
636        raw.0[1] = 0;
637        let err = IndexKey::try_from_raw(&raw).unwrap_err();
638        assert!(
639            err.contains("corrupted"),
640            "expected corruption error, got: {err}"
641        );
642    }
643
644    #[test]
645    fn index_key_rejects_fingerprint_padding() {
646        let key = IndexKey::empty(IndexId::max_storable());
647        let mut raw = key.to_raw();
648        let values_offset = IndexName::STORED_SIZE as usize + 1;
649        raw.0[values_offset] = 1;
650        let err = IndexKey::try_from_raw(&raw).unwrap_err();
651        assert!(
652            err.contains("corrupted"),
653            "expected corruption error, got: {err}"
654        );
655    }
656
657    #[test]
658    #[expect(clippy::large_types_passed_by_value)]
659    fn index_key_ordering_matches_bytes() {
660        fn make_key(index_id: IndexId, len: u8, first: u8, second: u8) -> IndexKey {
661            let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
662            values[0] = [first; 16];
663            values[1] = [second; 16];
664            IndexKey {
665                index_id,
666                len,
667                values,
668            }
669        }
670
671        let entity = EntityName::from_static("entity");
672        let idx_a = IndexId(IndexName::from_parts(&entity, &["a"]));
673        let idx_b = IndexId(IndexName::from_parts(&entity, &["b"]));
674
675        let keys = vec![
676            make_key(idx_a, 1, 1, 0),
677            make_key(idx_a, 2, 1, 2),
678            make_key(idx_a, 1, 2, 0),
679            make_key(idx_b, 1, 0, 0),
680        ];
681
682        let mut sorted_by_ord = keys.clone();
683        sorted_by_ord.sort();
684
685        let mut sorted_by_bytes = keys;
686        sorted_by_bytes.sort_by(|a, b| a.to_raw().as_bytes().cmp(b.to_raw().as_bytes()));
687
688        assert_eq!(
689            sorted_by_ord, sorted_by_bytes,
690            "IndexKey Ord and byte ordering diverged"
691        );
692    }
693
694    #[test]
695    fn raw_index_entry_round_trip() {
696        let mut entry = IndexEntry::new(Key::Int(1));
697        entry.insert_key(Key::Uint(2));
698
699        let raw = RawIndexEntry::try_from_entry(&entry).expect("encode index entry");
700        let decoded = raw.try_decode().expect("decode index entry");
701
702        assert_eq!(decoded.len(), entry.len());
703        assert!(decoded.contains(&Key::Int(1)));
704        assert!(decoded.contains(&Key::Uint(2)));
705    }
706
707    #[test]
708    fn raw_index_entry_roundtrip_via_bytes() {
709        let mut entry = IndexEntry::new(Key::Int(9));
710        entry.insert_key(Key::Uint(10));
711
712        let raw = RawIndexEntry::try_from_entry(&entry).expect("encode index entry");
713        let encoded = Storable::to_bytes(&raw);
714        let raw = RawIndexEntry::from_bytes(encoded);
715        let decoded = raw.try_decode().expect("decode index entry");
716
717        assert_eq!(decoded.len(), entry.len());
718        assert!(decoded.contains(&Key::Int(9)));
719        assert!(decoded.contains(&Key::Uint(10)));
720    }
721
722    #[test]
723    fn raw_index_entry_rejects_empty() {
724        let raw = RawIndexEntry(vec![0, 0, 0, 0]);
725        assert!(matches!(
726            raw.try_decode(),
727            Err(IndexEntryCorruption::EmptyEntry)
728        ));
729    }
730
731    #[test]
732    fn raw_index_entry_rejects_truncated_payload() {
733        let key = Key::Int(1);
734        let mut bytes = Vec::new();
735        bytes.extend_from_slice(&1u32.to_be_bytes());
736        bytes.extend_from_slice(&key.to_bytes());
737        bytes.truncate(bytes.len() - 1);
738        let raw = RawIndexEntry(bytes);
739        assert!(matches!(
740            raw.try_decode(),
741            Err(IndexEntryCorruption::LengthMismatch)
742        ));
743    }
744
745    #[test]
746    fn raw_index_entry_rejects_oversized_payload() {
747        let bytes = vec![0u8; MAX_INDEX_ENTRY_BYTES as usize + 1];
748        let raw = RawIndexEntry(bytes);
749        assert!(matches!(
750            raw.try_decode(),
751            Err(IndexEntryCorruption::TooLarge { .. })
752        ));
753    }
754
755    #[test]
756    #[expect(clippy::cast_possible_truncation)]
757    fn raw_index_entry_rejects_corrupted_length_field() {
758        let count = (MAX_INDEX_ENTRY_KEYS + 1) as u32;
759        let raw = RawIndexEntry(count.to_be_bytes().to_vec());
760        assert!(matches!(
761            raw.try_decode(),
762            Err(IndexEntryCorruption::TooManyKeys { .. })
763        ));
764    }
765
766    #[test]
767    fn raw_index_entry_rejects_duplicate_keys() {
768        let key = Key::Int(1);
769        let mut bytes = Vec::new();
770        bytes.extend_from_slice(&2u32.to_be_bytes());
771        bytes.extend_from_slice(&key.to_bytes());
772        bytes.extend_from_slice(&key.to_bytes());
773        let raw = RawIndexEntry(bytes);
774        assert!(matches!(
775            raw.try_decode(),
776            Err(IndexEntryCorruption::DuplicateKey)
777        ));
778    }
779
780    #[test]
781    #[expect(clippy::cast_possible_truncation)]
782    fn index_key_decode_fuzz_roundtrip_is_canonical() {
783        const RUNS: u64 = 1_000;
784
785        let mut seed = 0xBADC_0FFE_u64;
786        for _ in 0..RUNS {
787            let mut bytes = [0u8; IndexKey::STORED_SIZE as usize];
788            for b in &mut bytes {
789                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
790                *b = (seed >> 24) as u8;
791            }
792
793            let raw = RawIndexKey(bytes);
794            if let Ok(decoded) = IndexKey::try_from_raw(&raw) {
795                let re = decoded.to_raw();
796                assert_eq!(
797                    raw.as_bytes(),
798                    re.as_bytes(),
799                    "decoded IndexKey must be canonical"
800                );
801            }
802        }
803    }
804
805    #[test]
806    #[expect(clippy::cast_possible_truncation)]
807    fn raw_index_entry_decode_fuzz_does_not_panic() {
808        const RUNS: u64 = 1_000;
809        const MAX_LEN: usize = 256;
810
811        let mut seed = 0xA5A5_5A5A_u64;
812        for _ in 0..RUNS {
813            seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
814            let len = (seed as usize) % MAX_LEN;
815
816            let mut bytes = vec![0u8; len];
817            for b in &mut bytes {
818                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
819                *b = (seed >> 24) as u8;
820            }
821
822            let raw = RawIndexEntry(bytes);
823            let _ = raw.try_decode();
824        }
825    }
826}