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
388    #[error("index entry missing key count")]
389    MissingLength,
390
391    #[error("index entry key count exceeds limit")]
392    TooManyKeys { count: usize },
393
394    #[error("index entry length does not match key count")]
395    LengthMismatch,
396
397    #[error("index entry contains invalid key bytes")]
398    InvalidKey,
399
400    #[error("index entry contains duplicate key")]
401    DuplicateKey,
402
403    #[error("index entry contains zero keys")]
404    EmptyEntry,
405
406    #[error("unique index entry contains {keys} keys")]
407    NonUniqueEntry { keys: usize },
408}
409
410impl IndexEntryCorruption {
411    #[must_use]
412    pub const fn message(&self) -> &'static str {
413        match self {
414            Self::TooLarge { .. } => "corrupted index entry: exceeds max size",
415            Self::MissingLength => "corrupted index entry: missing key count",
416            Self::TooManyKeys { .. } => "corrupted index entry: key count exceeds limit",
417            Self::LengthMismatch => "corrupted index entry: length mismatch",
418            Self::InvalidKey => "corrupted index entry: invalid key bytes",
419            Self::DuplicateKey => "corrupted index entry: duplicate key",
420            Self::EmptyEntry => "corrupted index entry: empty entry",
421            Self::NonUniqueEntry { .. } => "corrupted index entry: non-unique entry",
422        }
423    }
424}
425
426#[derive(Debug, ThisError)]
427pub enum IndexEntryEncodeError {
428    #[error("index entry exceeds max keys: {keys} (limit {MAX_INDEX_ENTRY_KEYS})")]
429    TooManyKeys { keys: usize },
430}
431
432impl IndexEntryEncodeError {
433    #[must_use]
434    pub const fn keys(&self) -> usize {
435        match self {
436            Self::TooManyKeys { keys } => *keys,
437        }
438    }
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
442pub struct RawIndexEntry(Vec<u8>);
443
444impl RawIndexEntry {
445    pub fn try_from_entry(entry: &IndexEntry) -> Result<Self, IndexEntryEncodeError> {
446        let keys = entry.len();
447        if keys > MAX_INDEX_ENTRY_KEYS {
448            return Err(IndexEntryEncodeError::TooManyKeys { keys });
449        }
450
451        let mut out = Vec::with_capacity(INDEX_ENTRY_LEN_BYTES + (keys * Key::STORED_SIZE));
452        let count = u32::try_from(keys).map_err(|_| IndexEntryEncodeError::TooManyKeys { keys })?;
453        out.extend_from_slice(&count.to_be_bytes());
454        for key in entry.iter_keys() {
455            out.extend_from_slice(&key.to_bytes());
456        }
457
458        Ok(Self(out))
459    }
460
461    pub fn try_decode(&self) -> Result<IndexEntry, IndexEntryCorruption> {
462        let bytes = self.0.as_slice();
463        if bytes.len() > MAX_INDEX_ENTRY_BYTES as usize {
464            return Err(IndexEntryCorruption::TooLarge { len: bytes.len() });
465        }
466        if bytes.len() < INDEX_ENTRY_LEN_BYTES {
467            return Err(IndexEntryCorruption::MissingLength);
468        }
469
470        let mut len_buf = [0u8; INDEX_ENTRY_LEN_BYTES];
471        len_buf.copy_from_slice(&bytes[..INDEX_ENTRY_LEN_BYTES]);
472        let count = u32::from_be_bytes(len_buf) as usize;
473        if count == 0 {
474            return Err(IndexEntryCorruption::EmptyEntry);
475        }
476        if count > MAX_INDEX_ENTRY_KEYS {
477            return Err(IndexEntryCorruption::TooManyKeys { count });
478        }
479
480        let expected = INDEX_ENTRY_LEN_BYTES
481            .checked_add(
482                count
483                    .checked_mul(Key::STORED_SIZE)
484                    .ok_or(IndexEntryCorruption::LengthMismatch)?,
485            )
486            .ok_or(IndexEntryCorruption::LengthMismatch)?;
487        if bytes.len() != expected {
488            return Err(IndexEntryCorruption::LengthMismatch);
489        }
490
491        let mut keys = BTreeSet::new();
492        let mut offset = INDEX_ENTRY_LEN_BYTES;
493        for _ in 0..count {
494            let end = offset + Key::STORED_SIZE;
495            let key = Key::try_from_bytes(&bytes[offset..end])
496                .map_err(|_| IndexEntryCorruption::InvalidKey)?;
497            if !keys.insert(key) {
498                return Err(IndexEntryCorruption::DuplicateKey);
499            }
500            offset = end;
501        }
502
503        Ok(IndexEntry { keys })
504    }
505
506    #[must_use]
507    pub fn as_bytes(&self) -> &[u8] {
508        &self.0
509    }
510
511    #[must_use]
512    pub const fn len(&self) -> usize {
513        self.0.len()
514    }
515
516    #[must_use]
517    pub const fn is_empty(&self) -> bool {
518        self.0.is_empty()
519    }
520}
521
522impl Storable for RawIndexEntry {
523    fn to_bytes(&self) -> Cow<'_, [u8]> {
524        Cow::Borrowed(&self.0)
525    }
526
527    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
528        Self(bytes.into_owned())
529    }
530
531    fn into_bytes(self) -> Vec<u8> {
532        self.0
533    }
534
535    const BOUND: Bound = Bound::Bounded {
536        max_size: MAX_INDEX_ENTRY_BYTES,
537        is_fixed_size: false,
538    };
539}
540
541#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
542pub struct IndexEntry {
543    keys: BTreeSet<Key>,
544}
545
546impl IndexEntry {
547    #[must_use]
548    pub fn new(key: Key) -> Self {
549        let mut keys = BTreeSet::new();
550        keys.insert(key);
551
552        Self { keys }
553    }
554
555    pub fn insert_key(&mut self, key: Key) {
556        self.keys.insert(key);
557    }
558
559    pub fn remove_key(&mut self, key: &Key) {
560        self.keys.remove(key);
561    }
562
563    #[must_use]
564    pub fn contains(&self, key: &Key) -> bool {
565        self.keys.contains(key)
566    }
567
568    #[must_use]
569    pub fn is_empty(&self) -> bool {
570        self.keys.is_empty()
571    }
572
573    #[must_use]
574    pub fn len(&self) -> usize {
575        self.keys.len()
576    }
577
578    pub fn iter_keys(&self) -> impl Iterator<Item = Key> + '_ {
579        self.keys.iter().copied()
580    }
581
582    #[must_use]
583    pub fn single_key(&self) -> Option<Key> {
584        if self.keys.len() == 1 {
585            self.keys.iter().copied().next()
586        } else {
587            None
588        }
589    }
590}
591
592///
593/// TESTS
594///
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599    use crate::traits::Storable;
600    use std::borrow::Cow;
601
602    #[test]
603    fn index_key_rejects_undersized_bytes() {
604        let buf = vec![0u8; IndexKey::STORED_SIZE as usize - 1];
605        let raw = RawIndexKey::from_bytes(Cow::Borrowed(&buf));
606        let err = IndexKey::try_from_raw(&raw).unwrap_err();
607        assert!(
608            err.contains("corrupted"),
609            "expected corruption error, got: {err}"
610        );
611    }
612
613    #[test]
614    fn index_key_rejects_oversized_bytes() {
615        let buf = vec![0u8; IndexKey::STORED_SIZE as usize + 1];
616        let raw = RawIndexKey::from_bytes(Cow::Borrowed(&buf));
617        let err = IndexKey::try_from_raw(&raw).unwrap_err();
618        assert!(
619            err.contains("corrupted"),
620            "expected corruption error, got: {err}"
621        );
622    }
623
624    #[test]
625    #[allow(clippy::cast_possible_truncation)]
626    fn index_key_rejects_len_over_max() {
627        let key = IndexKey::empty(IndexId::max_storable());
628        let mut raw = key.to_raw();
629        let len_offset = IndexName::STORED_SIZE as usize;
630        raw.0[len_offset] = (MAX_INDEX_FIELDS as u8) + 1;
631        let err = IndexKey::try_from_raw(&raw).unwrap_err();
632        assert!(
633            err.contains("corrupted"),
634            "expected corruption error, got: {err}"
635        );
636    }
637
638    #[test]
639    fn index_key_rejects_invalid_index_name() {
640        let key = IndexKey::empty(IndexId::max_storable());
641        let mut raw = key.to_raw();
642        raw.0[0] = 0;
643        raw.0[1] = 0;
644        let err = IndexKey::try_from_raw(&raw).unwrap_err();
645        assert!(
646            err.contains("corrupted"),
647            "expected corruption error, got: {err}"
648        );
649    }
650
651    #[test]
652    fn index_key_rejects_fingerprint_padding() {
653        let key = IndexKey::empty(IndexId::max_storable());
654        let mut raw = key.to_raw();
655        let values_offset = IndexName::STORED_SIZE as usize + 1;
656        raw.0[values_offset] = 1;
657        let err = IndexKey::try_from_raw(&raw).unwrap_err();
658        assert!(
659            err.contains("corrupted"),
660            "expected corruption error, got: {err}"
661        );
662    }
663
664    #[test]
665    #[expect(clippy::large_types_passed_by_value)]
666    fn index_key_ordering_matches_bytes() {
667        fn make_key(index_id: IndexId, len: u8, first: u8, second: u8) -> IndexKey {
668            let mut values = [[0u8; 16]; MAX_INDEX_FIELDS];
669            values[0] = [first; 16];
670            values[1] = [second; 16];
671            IndexKey {
672                index_id,
673                len,
674                values,
675            }
676        }
677
678        let entity = EntityName::from_static("entity");
679        let idx_a = IndexId(IndexName::from_parts(&entity, &["a"]));
680        let idx_b = IndexId(IndexName::from_parts(&entity, &["b"]));
681
682        let keys = vec![
683            make_key(idx_a, 1, 1, 0),
684            make_key(idx_a, 2, 1, 2),
685            make_key(idx_a, 1, 2, 0),
686            make_key(idx_b, 1, 0, 0),
687        ];
688
689        let mut sorted_by_ord = keys.clone();
690        sorted_by_ord.sort();
691
692        let mut sorted_by_bytes = keys;
693        sorted_by_bytes.sort_by(|a, b| a.to_raw().as_bytes().cmp(b.to_raw().as_bytes()));
694
695        assert_eq!(
696            sorted_by_ord, sorted_by_bytes,
697            "IndexKey Ord and byte ordering diverged"
698        );
699    }
700
701    #[test]
702    fn raw_index_entry_round_trip() {
703        let mut entry = IndexEntry::new(Key::Int(1));
704        entry.insert_key(Key::Uint(2));
705
706        let raw = RawIndexEntry::try_from_entry(&entry).expect("encode index entry");
707        let decoded = raw.try_decode().expect("decode index entry");
708
709        assert_eq!(decoded.len(), entry.len());
710        assert!(decoded.contains(&Key::Int(1)));
711        assert!(decoded.contains(&Key::Uint(2)));
712    }
713
714    #[test]
715    fn raw_index_entry_roundtrip_via_bytes() {
716        let mut entry = IndexEntry::new(Key::Int(9));
717        entry.insert_key(Key::Uint(10));
718
719        let raw = RawIndexEntry::try_from_entry(&entry).expect("encode index entry");
720        let encoded = Storable::to_bytes(&raw);
721        let raw = RawIndexEntry::from_bytes(encoded);
722        let decoded = raw.try_decode().expect("decode index entry");
723
724        assert_eq!(decoded.len(), entry.len());
725        assert!(decoded.contains(&Key::Int(9)));
726        assert!(decoded.contains(&Key::Uint(10)));
727    }
728
729    #[test]
730    fn raw_index_entry_rejects_empty() {
731        let raw = RawIndexEntry(vec![0, 0, 0, 0]);
732        assert!(matches!(
733            raw.try_decode(),
734            Err(IndexEntryCorruption::EmptyEntry)
735        ));
736    }
737
738    #[test]
739    fn raw_index_entry_rejects_truncated_payload() {
740        let key = Key::Int(1);
741        let mut bytes = Vec::new();
742        bytes.extend_from_slice(&1u32.to_be_bytes());
743        bytes.extend_from_slice(&key.to_bytes());
744        bytes.truncate(bytes.len() - 1);
745        let raw = RawIndexEntry(bytes);
746        assert!(matches!(
747            raw.try_decode(),
748            Err(IndexEntryCorruption::LengthMismatch)
749        ));
750    }
751
752    #[test]
753    fn raw_index_entry_rejects_oversized_payload() {
754        let bytes = vec![0u8; MAX_INDEX_ENTRY_BYTES as usize + 1];
755        let raw = RawIndexEntry(bytes);
756        assert!(matches!(
757            raw.try_decode(),
758            Err(IndexEntryCorruption::TooLarge { .. })
759        ));
760    }
761
762    #[test]
763    #[expect(clippy::cast_possible_truncation)]
764    fn raw_index_entry_rejects_corrupted_length_field() {
765        let count = (MAX_INDEX_ENTRY_KEYS + 1) as u32;
766        let raw = RawIndexEntry(count.to_be_bytes().to_vec());
767        assert!(matches!(
768            raw.try_decode(),
769            Err(IndexEntryCorruption::TooManyKeys { .. })
770        ));
771    }
772
773    #[test]
774    fn raw_index_entry_rejects_duplicate_keys() {
775        let key = Key::Int(1);
776        let mut bytes = Vec::new();
777        bytes.extend_from_slice(&2u32.to_be_bytes());
778        bytes.extend_from_slice(&key.to_bytes());
779        bytes.extend_from_slice(&key.to_bytes());
780        let raw = RawIndexEntry(bytes);
781        assert!(matches!(
782            raw.try_decode(),
783            Err(IndexEntryCorruption::DuplicateKey)
784        ));
785    }
786
787    #[test]
788    #[expect(clippy::cast_possible_truncation)]
789    fn index_key_decode_fuzz_roundtrip_is_canonical() {
790        const RUNS: u64 = 1_000;
791
792        let mut seed = 0xBADC_0FFE_u64;
793        for _ in 0..RUNS {
794            let mut bytes = [0u8; IndexKey::STORED_SIZE as usize];
795            for b in &mut bytes {
796                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
797                *b = (seed >> 24) as u8;
798            }
799
800            let raw = RawIndexKey(bytes);
801            if let Ok(decoded) = IndexKey::try_from_raw(&raw) {
802                let re = decoded.to_raw();
803                assert_eq!(
804                    raw.as_bytes(),
805                    re.as_bytes(),
806                    "decoded IndexKey must be canonical"
807                );
808            }
809        }
810    }
811
812    #[test]
813    #[expect(clippy::cast_possible_truncation)]
814    fn raw_index_entry_decode_fuzz_does_not_panic() {
815        const RUNS: u64 = 1_000;
816        const MAX_LEN: usize = 256;
817
818        let mut seed = 0xA5A5_5A5A_u64;
819        for _ in 0..RUNS {
820            seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
821            let len = (seed as usize) % MAX_LEN;
822
823            let mut bytes = vec![0u8; len];
824            for b in &mut bytes {
825                seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
826                *b = (seed >> 24) as u8;
827            }
828
829            let raw = RawIndexEntry(bytes);
830            let _ = raw.try_decode();
831        }
832    }
833}