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 #[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#[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}