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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum IndexInsertOutcome {
40 Inserted,
41 Skipped,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum IndexRemoveOutcome {
50 Removed,
51 Skipped,
52}
53
54#[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#[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 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#[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#[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
373const 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#[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}